<?php

declare(strict_types=1);

namespace App\Module\Integration\Infrastructure\Service;

use App\Module\Integration\Domain\Entity\GiteaConfiguration;
use App\Module\Integration\Domain\Exception\GiteaApiException;
use App\Module\Integration\Domain\Repository\GiteaConfigurationRepositoryInterface;
use App\Module\ProjectManagement\Domain\Entity\Project;
use App\Module\ProjectManagement\Domain\Entity\Task;
use App\Shared\Infrastructure\Service\TokenEncryptor;
use Symfony\Component\String\Slugger\AsciiSlugger;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

final readonly class GiteaApiService
{
    private SluggerInterface $slugger;

    public function __construct(
        private HttpClientInterface $httpClient,
        private GiteaConfigurationRepositoryInterface $configRepository,
        private TokenEncryptor $tokenEncryptor,
    ) {
        $this->slugger = new AsciiSlugger('fr');
    }

    public function testConnection(): bool
    {
        try {
            $this->request('GET', '/api/v1/version');

            return true;
        } catch (GiteaApiException) {
            return false;
        }
    }

    /**
     * @return array<array{full_name: string, name: string, owner: array{login: string}}>
     */
    public function listRepositories(): array
    {
        $result = [];
        $page   = 1;

        do {
            $data = $this->request('GET', '/api/v1/repos/search', [
                'query' => ['page' => $page, 'limit' => 50],
            ]);
            $result = array_merge($result, $data['data'] ?? []);
            ++$page;
        } while (!empty($data['data']) && 50 === count($data['data']));

        return $result;
    }

    public function getDefaultBranch(Project $project): string
    {
        $this->assertProjectHasRepo($project);
        $data = $this->request('GET', sprintf(
            '/api/v1/repos/%s/%s',
            $project->getGiteaOwner(),
            $project->getGiteaRepo(),
        ));

        return $data['default_branch'] ?? 'main';
    }

    public function createBranch(Project $project, Task $task, string $type, string $baseBranch): string
    {
        $this->assertProjectHasRepo($project);
        $branchName = $this->generateBranchName($task, $type);

        $this->request('POST', sprintf(
            '/api/v1/repos/%s/%s/branches',
            $project->getGiteaOwner(),
            $project->getGiteaRepo(),
        ), [
            'json' => [
                'new_branch_name' => $branchName,
                'old_branch_name' => $baseBranch,
            ],
        ]);

        return $branchName;
    }

    public function generateBranchName(Task $task, string $type): string
    {
        $project = $task->getProject();
        if (null === $project) {
            throw new GiteaApiException('Task has no project.');
        }

        $slug = $this->slugger->slug($task->getTitle())->lower()->truncate(50)->toString();
        $slug = rtrim($slug, '-');

        return sprintf('%s/%s-%d-%s', $type, $project->getCode(), $task->getNumber(), $slug);
    }

    /**
     * @return array<array{name: string, commit: array}>
     */
    public function listBranches(Project $project, string $taskCode): array
    {
        $this->assertProjectHasRepo($project);

        $allBranches = [];
        $page        = 1;

        do {
            $pageBranches = $this->request('GET', sprintf(
                '/api/v1/repos/%s/%s/branches',
                $project->getGiteaOwner(),
                $project->getGiteaRepo(),
            ), [
                'query' => ['page' => $page, 'limit' => 50],
            ]);
            $allBranches = array_merge($allBranches, $pageBranches);
            ++$page;
        } while (!empty($pageBranches) && 50 === count($pageBranches));

        $regex = sprintf('#^[^/]+/%s($|-.+)#', preg_quote($taskCode, '#'));

        return array_values(array_filter(
            $allBranches,
            static fn (array $branch): bool => 1 === preg_match($regex, $branch['name']),
        ));
    }

    /**
     * @return array<array{sha: string, commit: array{message: string, author: array}, created: string}>
     */
    public function listBranchCommits(Project $project, string $branch): array
    {
        $this->assertProjectHasRepo($project);

        $defaultBranch = $this->getDefaultBranch($project);

        $data = $this->request('GET', sprintf(
            '/api/v1/repos/%s/%s/compare/%s...%s',
            $project->getGiteaOwner(),
            $project->getGiteaRepo(),
            $defaultBranch,
            urlencode($branch),
        ));

        return $data['commits'] ?? [];
    }

    /**
     * @return array<array{number: int, title: string, state: string, head: array, user: array, merged: bool}>
     */
    public function listPullRequests(Project $project, string $taskCode): array
    {
        $this->assertProjectHasRepo($project);

        $branches = $this->listBranches($project, $taskCode);
        $prs      = [];

        foreach ($branches as $branch) {
            $branchPrs = $this->request('GET', sprintf(
                '/api/v1/repos/%s/%s/pulls',
                $project->getGiteaOwner(),
                $project->getGiteaRepo(),
            ), [
                'query' => ['state' => 'all', 'head' => $branch['name']],
            ]);
            $prs = array_merge($prs, $branchPrs);
        }

        // Fetch CI status for each PR
        foreach ($prs as &$pr) {
            $sha = $pr['head']['sha'] ?? null;
            if (null !== $sha) {
                try {
                    $pr['ci_statuses'] = $this->request('GET', sprintf(
                        '/api/v1/repos/%s/%s/commits/%s/statuses',
                        $project->getGiteaOwner(),
                        $project->getGiteaRepo(),
                        $sha,
                    ));
                } catch (GiteaApiException) {
                    $pr['ci_statuses'] = [];
                }
            }
        }

        return $prs;
    }

    private function getConfiguration(): GiteaConfiguration
    {
        $config = $this->configRepository->findSingleton();
        if (null === $config) {
            throw new GiteaApiException('Gitea is not configured.');
        }

        return $config;
    }

    private function getDecryptedToken(GiteaConfiguration $config): string
    {
        $encrypted = $config->getEncryptedToken();
        if (null === $encrypted) {
            throw new GiteaApiException('Gitea token is not set.');
        }

        try {
            return $this->tokenEncryptor->decrypt($encrypted);
        } catch (Throwable $e) {
            throw new GiteaApiException('Failed to decrypt Gitea token: '.$e->getMessage(), 0, $e);
        }
    }

    private function extractGiteaError(HttpExceptionInterface $e): string
    {
        try {
            $body = $e->getResponse()->getContent(false);
            $data = json_decode($body, true);

            if (is_array($data)) {
                return $data['message'] ?? $data['error'] ?? $body;
            }

            return $body ?: 'Unknown Gitea error';
        } catch (ExceptionInterface) {
            return 'Gitea API error (HTTP '.$e->getResponse()->getStatusCode().')';
        }
    }

    private function assertProjectHasRepo(Project $project): void
    {
        if (!$project->hasGiteaRepo()) {
            throw new GiteaApiException('Project has no Gitea repository configured.');
        }
    }

    /**
     * @param array<string, mixed> $options
     */
    private function request(string $method, string $path, array $options = []): array
    {
        $config = $this->getConfiguration();
        $token  = $this->getDecryptedToken($config);

        $options['headers'] = array_merge($options['headers'] ?? [], [
            'Authorization' => 'token '.$token,
            'Accept'        => 'application/json',
        ]);
        $options['timeout'] = 10;

        try {
            $response = $this->httpClient->request($method, rtrim($config->getUrl(), '/').$path, $options);

            return $response->toArray();
        } catch (HttpExceptionInterface $e) {
            $message = $this->extractGiteaError($e);

            throw new GiteaApiException($message, $e->getResponse()->getStatusCode(), $e);
        } catch (ExceptionInterface $e) {
            throw new GiteaApiException('Gitea API error: '.$e->getMessage(), 0, $e);
        }
    }
}
