<?php

declare(strict_types=1);

namespace App\Module\Integration\Infrastructure\Service;

use App\Module\Integration\Domain\Entity\BookStackConfiguration;
use App\Module\Integration\Domain\Exception\BookStackApiException;
use App\Module\Integration\Domain\Repository\BookStackConfigurationRepositoryInterface;
use App\Shared\Infrastructure\Service\TokenEncryptor;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

final class BookStackApiService
{
    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly BookStackConfigurationRepositoryInterface $configRepository,
        private readonly TokenEncryptor $tokenEncryptor,
    ) {}

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

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

    /**
     * @return array<array{id: int, name: string}>
     */
    public function listShelves(): array
    {
        $result = [];
        $offset = 0;
        $count  = 100;

        do {
            $data = $this->request('GET', '/api/shelves', [
                'query' => ['count' => $count, 'offset' => $offset],
            ]);
            $items  = $data['data'] ?? [];
            $result = array_merge($result, $items);
            $offset += $count;
        } while (!empty($items) && $count === count($items));

        return $result;
    }

    /**
     * Search for pages and books within a specific shelf.
     *
     * Algorithm:
     * 1. Fetch the shelf data (book IDs + slugs)
     * 2. Run two search queries (one for pages, one for books)
     * 3. Filter results: pages must belong to a book on the shelf, books must be on the shelf
     *
     * @return array<array{id: int, type: string, name: string, url: string}>
     */
    public function searchInShelf(int $shelfId, string $query): array
    {
        $shelfData = $this->request('GET', sprintf('/api/shelves/%d', $shelfId));
        $books     = $shelfData['books'] ?? [];

        if (empty($books)) {
            return [];
        }

        $bookIds   = array_map(static fn (array $book): int => $book['id'], $books);
        $bookSlugs = [];
        foreach ($books as $book) {
            $bookSlugs[$book['id']] = $book['slug'] ?? '';
        }

        $config  = $this->getConfiguration();
        $baseUrl = rtrim($config->getUrl() ?? '', '/');
        $trimmed = trim($query);

        // BookStack search API accepts {type:X} for one type at a time -- run two queries
        $pageResults = $this->request('GET', '/api/search', [
            'query' => ['query' => $trimmed.' {type:page}', 'count' => 50],
        ]);
        $bookResults = $this->request('GET', '/api/search', [
            'query' => ['query' => $trimmed.' {type:book}', 'count' => 50],
        ]);

        $allResults = array_merge($pageResults['data'] ?? [], $bookResults['data'] ?? []);

        $filtered = [];
        foreach ($allResults as $item) {
            $type = $item['type'] ?? '';

            if ('page' === $type) {
                $bookId = $item['book_id'] ?? 0;
                if (in_array($bookId, $bookIds, true)) {
                    $filtered[] = [
                        'id'   => $item['id'],
                        'type' => 'page',
                        'name' => $item['name'] ?? '',
                        'url'  => $baseUrl.'/books/'.($bookSlugs[$bookId] ?? '').'/page/'.$item['slug'],
                    ];
                }
            } elseif ('book' === $type && in_array($item['id'], $bookIds, true)) {
                $filtered[] = [
                    'id'   => $item['id'],
                    'type' => 'book',
                    'name' => $item['name'] ?? '',
                    'url'  => $baseUrl.'/books/'.$item['slug'],
                ];
            }
        }

        return $filtered;
    }

    /**
     * @return array{id: int, name: string, slug: string}
     */
    public function getPage(int $id): array
    {
        return $this->request('GET', sprintf('/api/pages/%d', $id));
    }

    /**
     * @return array{id: int, name: string, slug: string}
     */
    public function getBook(int $id): array
    {
        return $this->request('GET', sprintf('/api/books/%d', $id));
    }

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

        return $config;
    }

    /**
     * @return array{tokenId: string, tokenSecret: string}
     */
    private function getDecryptedTokens(BookStackConfiguration $config): array
    {
        $encryptedId     = $config->getEncryptedTokenId();
        $encryptedSecret = $config->getEncryptedTokenSecret();

        if (null === $encryptedId || null === $encryptedSecret) {
            throw new BookStackApiException('BookStack tokens are not set.');
        }

        try {
            return [
                'tokenId'     => $this->tokenEncryptor->decrypt($encryptedId),
                'tokenSecret' => $this->tokenEncryptor->decrypt($encryptedSecret),
            ];
        } catch (Throwable $e) {
            throw new BookStackApiException('Failed to decrypt BookStack tokens: '.$e->getMessage(), 0, $e);
        }
    }

    private function extractError(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 BookStack error';
        } catch (ExceptionInterface) {
            return 'BookStack API error (HTTP '.$e->getResponse()->getStatusCode().')';
        }
    }

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

        $options['headers'] = array_merge($options['headers'] ?? [], [
            'Authorization' => sprintf('Token %s:%s', $tokens['tokenId'], $tokens['tokenSecret']),
            '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->extractError($e);

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