<?php

namespace App\Domain\Integration\Drivers\Bitbucket;

use App\Domain\Credential\Models\Credential;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Bitbucket Cloud HTTP client using Atlassian API token (Basic auth).
 *
 * Parallel surface to BitbucketIntegrationDriver (OAuth2). Required because
 * Atlassian deprecated OAuth Bearer for these credentials in 2025-2026:
 * Atlassian API tokens authenticate only via HTTP Basic (username + token).
 *
 * Workspace scope is enforced before every HTTP call against
 * `secret_data.workspace` on the credential.
 */
class BitbucketBasicAuthDriver
{
    private const API_BASE = 'https://api.bitbucket.org/2.0';

    public function readFile(Credential $credential, string $repoSlug, string $branch, string $path): string
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);
        $url = self::API_BASE."/repositories/{$repo}/src/".rawurlencode($branch).'/'.ltrim($path, '/');

        return $this->http($credential)->get($url)->throw()->body();
    }

    /**
     * @return array<int, array{path: string, line_number: int, line_content: string}>
     */
    public function searchCode(Credential $credential, string $repoSlug, string $branch, string $pattern, ?string $pathFilter = null): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);
        $workspace = $this->workspace($credential);

        $query = $pattern.' repo:'.explode('/', $repo)[1];
        if ($pathFilter !== null && $pathFilter !== '') {
            $query .= ' path:'.$pathFilter;
        }

        $response = $this->http($credential)
            ->get(self::API_BASE."/workspaces/{$workspace}/search/code", [
                'search_query' => $query,
            ])
            ->throw();

        $hits = [];
        foreach ($response->json('values', []) as $value) {
            $filePath = $value['file']['path'] ?? '';
            foreach ($value['content_matches'] ?? [] as $match) {
                foreach ($match['lines'] ?? [] as $line) {
                    $hits[] = [
                        'path' => $filePath,
                        'line_number' => (int) ($line['line'] ?? 0),
                        'line_content' => $this->joinSegments($line['segments'] ?? []),
                    ];
                }
            }
        }

        return $hits;
    }

    /**
     * @return array{pr_number: int, pr_url: string, state: string}
     */
    public function createPullRequest(Credential $credential, string $repoSlug, string $sourceBranch, string $destinationBranch, string $title, string $description): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);

        $payload = [
            'title' => $title,
            'description' => $description,
            'source' => ['branch' => ['name' => $sourceBranch]],
            'destination' => ['branch' => ['name' => $destinationBranch]],
            'close_source_branch' => false,
        ];

        $response = $this->http($credential)
            ->post(self::API_BASE."/repositories/{$repo}/pullrequests", $payload)
            ->throw()
            ->json();

        return [
            'pr_number' => (int) ($response['id'] ?? 0),
            'pr_url' => $response['links']['html']['href'] ?? '',
            'state' => (string) ($response['state'] ?? 'OPEN'),
        ];
    }

    public function commentOnPullRequest(Credential $credential, string $repoSlug, int $prId, string $body): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);

        return $this->http($credential)
            ->post(self::API_BASE."/repositories/{$repo}/pullrequests/{$prId}/comments", [
                'content' => ['raw' => $body],
            ])
            ->throw()
            ->json();
    }

    public function closePullRequest(Credential $credential, string $repoSlug, int $prId): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);

        return $this->http($credential)
            ->post(self::API_BASE."/repositories/{$repo}/pullrequests/{$prId}/decline")
            ->throw()
            ->json();
    }

    /**
     * Fetch a PR's diffstat — file list with per-file lines added/removed counts
     * plus the PR's source/destination branch metadata.
     *
     * @return array{
     *     pr_url: string,
     *     source_branch: string,
     *     destination_branch: string,
     *     state: string,
     *     files: list<array{path: string, lines_added: int, lines_removed: int, status: string}>,
     *     totals: array{files: int, lines_added: int, lines_removed: int},
     * }
     */
    public function getPullRequestDiffStat(Credential $credential, string $repoSlug, int $prId): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);

        $pr = $this->http($credential)
            ->get(self::API_BASE."/repositories/{$repo}/pullrequests/{$prId}")
            ->throw()
            ->json();

        $diffstatResponse = $this->http($credential)
            ->get(self::API_BASE."/repositories/{$repo}/pullrequests/{$prId}/diffstat", [
                'pagelen' => 100,
            ])
            ->throw()
            ->json();

        $files = [];
        $totalAdded = 0;
        $totalRemoved = 0;

        foreach ($diffstatResponse['values'] ?? [] as $entry) {
            $path = $entry['new']['path'] ?? $entry['old']['path'] ?? '';
            $linesAdded = (int) ($entry['lines_added'] ?? 0);
            $linesRemoved = (int) ($entry['lines_removed'] ?? 0);

            $files[] = [
                'path' => $path,
                'lines_added' => $linesAdded,
                'lines_removed' => $linesRemoved,
                'status' => (string) ($entry['status'] ?? 'modified'),
            ];

            $totalAdded += $linesAdded;
            $totalRemoved += $linesRemoved;
        }

        return [
            'pr_url' => (string) ($pr['links']['html']['href'] ?? ''),
            'source_branch' => (string) ($pr['source']['branch']['name'] ?? ''),
            'destination_branch' => (string) ($pr['destination']['branch']['name'] ?? ''),
            'state' => (string) ($pr['state'] ?? 'OPEN'),
            'files' => $files,
            'totals' => [
                'files' => count($files),
                'lines_added' => $totalAdded,
                'lines_removed' => $totalRemoved,
            ],
        ];
    }

    public function mergePullRequest(Credential $credential, string $repoSlug, int $prId, ?string $mergeStrategy = null): array
    {
        $repo = $this->assertWorkspaceAllowed($credential, $repoSlug);
        $payload = $mergeStrategy !== null ? ['merge_strategy' => $mergeStrategy] : [];

        return $this->http($credential)
            ->post(self::API_BASE."/repositories/{$repo}/pullrequests/{$prId}/merge", $payload)
            ->throw()
            ->json();
    }

    private function http(Credential $credential): PendingRequest
    {
        $secret = $this->secretData($credential);
        $username = (string) ($secret['username'] ?? '');
        $password = (string) ($secret['password'] ?? '');

        if ($username === '' || $password === '') {
            throw new \InvalidArgumentException('Bitbucket credential is missing username or password.');
        }

        return Http::withBasicAuth($username, $password)
            ->timeout(15)
            ->acceptJson();
    }

    private function workspace(Credential $credential): string
    {
        $workspace = (string) ($this->secretData($credential)['workspace'] ?? '');

        if ($workspace === '') {
            throw new \InvalidArgumentException('Bitbucket credential must include `workspace` in secret_data.');
        }

        return $workspace;
    }

    /**
     * @return array<string, mixed>
     */
    private function secretData(Credential $credential): array
    {
        /** @var mixed $data */
        $data = $credential->secret_data;

        return is_array($data) ? $data : [];
    }

    private function assertWorkspaceAllowed(Credential $credential, string $repoSlug): string
    {
        $workspace = $this->workspace($credential);

        if (str_contains($repoSlug, '/')) {
            [$callerWorkspace, $slug] = explode('/', $repoSlug, 2);
            if ($callerWorkspace !== $workspace) {
                throw new AccessDeniedHttpException("Repository `{$repoSlug}` is outside allowed workspace `{$workspace}`.");
            }

            return "{$workspace}/{$slug}";
        }

        return "{$workspace}/{$repoSlug}";
    }

    /**
     * @param  array<int, array{type?: string, text?: string}>  $segments
     */
    private function joinSegments(array $segments): string
    {
        return implode('', array_map(fn (array $s): string => (string) ($s['text'] ?? ''), $segments));
    }
}
