<?php

namespace Modules\Shared\Bnfexpress;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Modules\Shared\Bnfexpress\Exceptions\BnfexpressApiException;
use Modules\Shared\Bnfexpress\Support\BnfexpressSignature;

/**
 * Signed HTTP client for bnfexpress's admin APIs — EV FAQs, agent
 * instruction versions, and read-only EV chat history. Backend-to-backend
 * auth only (no user session/JWT): every request is signed per
 * BnfexpressSignature (config('services.bnfexpress')).
 */
class BnfexpressAdminClient
{
    private const AGENT = 'ev';

    private readonly string $baseUrl;

    private readonly string $clientId;

    private readonly string $secret;

    /**
     * @param  array<string, mixed>|null  $config
     */
    public function __construct(?array $config = null)
    {
        $config ??= (array) config('services.bnfexpress');

        $this->baseUrl = rtrim((string) ($config['ai_api_url'] ?? ''), '/');
        $this->clientId = (string) ($config['client_id'] ?? '');
        $this->secret = (string) ($config['client_secret'] ?? '');
    }

    // --- FAQs ---------------------------------------------------------

    /**
     * @return array<string, mixed>
     */
    public function listFaqs(?string $q = null, string $search = 'normal', ?int $limit = null, ?int $offset = null): array
    {
        return $this->request('GET', '/admin/faqs', query: array_filter([
            'agent' => self::AGENT,
            'q' => $q,
            // Ignored by bnfexpress when q is empty, but only sent when q is set.
            'search' => ($q !== null && $q !== '') ? $search : null,
            'limit' => $limit,
            'offset' => $offset,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function getFaq(int|string $id): array
    {
        return $this->request('GET', "/admin/faqs/{$id}");
    }

    /**
     * @param  array<string, mixed>  $metadata
     * @return array<string, mixed>
     */
    public function createFaq(string $content, array $metadata = []): array
    {
        return $this->request('POST', '/admin/faqs', body: [
            'content' => $content,
            'agent' => self::AGENT,
            'metadata' => $metadata,
        ]);
    }

    /**
     * @param  array<string, mixed>|null  $metadata
     * @return array<string, mixed>
     */
    public function updateFaq(int|string $id, ?string $content = null, ?array $metadata = null): array
    {
        return $this->request('PATCH', "/admin/faqs/{$id}", body: array_filter([
            'content' => $content,
            'metadata' => $metadata,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function deleteFaq(int|string $id): array
    {
        return $this->request('DELETE', "/admin/faqs/{$id}");
    }

    // --- Agent instructions --------------------------------------------

    /**
     * @return array<string, mixed>
     */
    public function listInstructions(?int $limit = null, ?int $offset = null): array
    {
        return $this->request('GET', '/admin/agent-instructions', query: array_filter([
            'agent' => self::AGENT,
            'limit' => $limit,
            'offset' => $offset,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function getActiveInstruction(): array
    {
        return $this->request('GET', '/admin/agent-instructions/active', query: [
            'agent' => self::AGENT,
        ]);
    }

    /**
     * Publishes a new instruction version. Setting $activate (default true)
     * deactivates the previously active version automatically, server-side.
     *
     * @return array<string, mixed>
     */
    public function publishInstruction(string $content, bool $activate = true): array
    {
        return $this->request('POST', '/admin/agent-instructions', body: [
            'agent' => self::AGENT,
            'content' => $content,
            'activate' => $activate,
        ]);
    }

    /**
     * Rolls back to an older instruction version.
     *
     * @return array<string, mixed>
     */
    public function activateInstruction(int|string $id): array
    {
        return $this->request('POST', "/admin/agent-instructions/{$id}/activate");
    }

    // --- EV chat history (read-only) ------------------------------------

    /**
     * @return array{total: int, limit: int, offset: int, sessions: list<array<string, mixed>>}
     */
    public function listSessions(?int $limit = null, ?int $offset = null): array
    {
        return $this->request('GET', '/admin/ev/history', query: array_filter([
            'limit' => $limit,
            'offset' => $offset,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function getSessionTranscript(string $userId, string $sessionId): array
    {
        return $this->request('GET', "/admin/ev/history/{$userId}/{$sessionId}");
    }

    // --- Suggestions ------------------------------------------------------

    /**
     * @return array<string, mixed>
     */
    public function listSuggestions(?string $q = null, ?int $limit = null, ?int $offset = null): array
    {
        return $this->request('GET', '/admin/suggestions', query: array_filter([
            'q' => $q,
            'limit' => $limit,
            'offset' => $offset,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function getSuggestion(int|string $id): array
    {
        return $this->request('GET', "/admin/suggestions/{$id}");
    }

    /**
     * @return array<string, mixed>
     */
    public function createSuggestion(string $textDisplay, string $lang, ?string $intent = null, int $weight = 0, string $source = 'admin'): array
    {
        return $this->request('POST', '/admin/suggestions', body: [
            'text_display' => $textDisplay,
            'lang' => $lang,
            'intent' => $intent,
            'weight' => $weight,
            'source' => $source,
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    public function updateSuggestion(int|string $id, ?string $textDisplay = null, ?string $lang = null, ?string $intent = null, ?int $weight = null, ?string $source = null): array
    {
        return $this->request('PATCH', "/admin/suggestions/{$id}", body: array_filter([
            'text_display' => $textDisplay,
            'lang' => $lang,
            'intent' => $intent,
            'weight' => $weight,
            'source' => $source,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function deleteSuggestion(int|string $id): array
    {
        return $this->request('DELETE', "/admin/suggestions/{$id}");
    }

    /**
     * @param  list<array{text: string, lang: string, intent?: string|null}>  $items
     * @return array{created: int, skipped: int, trie_rebuilt: bool}
     */
    public function batchCreateSuggestions(array $items): array
    {
        return $this->request('POST', '/admin/suggestions/batch', body: ['items' => $items]);
    }

    /**
     * @param  list<int|string>  $ids
     * @return array{deleted: int, skipped: int}
     */
    public function batchDeleteSuggestions(array $ids): array
    {
        return $this->request('DELETE', '/admin/suggestions/batch', body: ['ids' => $ids]);
    }

    // --- Suggestion misses --------------------------------------------------

    /**
     * @return array<int, array<string, mixed>>
     */
    public function listSuggestionMisses(?bool $wasUsed = null, ?int $limit = null, ?int $offset = null): array
    {
        return $this->request('GET', '/admin/suggestion-misses', query: array_filter([
            'was_used' => $wasUsed,
            'limit' => $limit,
            'offset' => $offset,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @return array<string, mixed>
     */
    public function dismissSuggestionMiss(int|string $id): array
    {
        return $this->request('DELETE', "/admin/suggestion-misses/{$id}");
    }

    /**
     * @param  list<int|string>  $missIds
     * @return array{created: int, skipped: int, trie_rebuilt: bool}
     */
    public function promoteSuggestionMisses(array $missIds, ?string $lang = null, ?string $intent = null): array
    {
        return $this->request('POST', '/admin/suggestion-misses/promote', body: array_filter([
            'miss_ids' => $missIds,
            'lang' => $lang,
            'intent' => $intent,
        ], fn (mixed $value): bool => $value !== null));
    }

    /**
     * @param  list<int|string>  $missIds
     * @return array{deleted: int, skipped: int}
     */
    public function batchDeleteSuggestionMisses(array $missIds): array
    {
        return $this->request('DELETE', '/admin/suggestion-misses/batch', body: ['miss_ids' => $missIds]);
    }

    // --- Suggestion sync/embeddings ------------------------------------------

    /**
     * @return array{job_id: string}
     */
    public function syncSuggestions(): array
    {
        return $this->request('POST', '/admin/suggestions/sync');
    }

    /**
     * @return array{status: string, result: mixed}
     */
    public function getSuggestionSyncStatus(string $jobId): array
    {
        return $this->request('GET', "/admin/suggestions/sync/{$jobId}");
    }

    /**
     * @return array<string, mixed>
     */
    public function syncOneSuggestion(int|string $id): array
    {
        return $this->request('POST', "/admin/suggestions/{$id}/sync");
    }

    /**
     * @return array<string, mixed>
     */
    public function reloadSuggestionIndex(): array
    {
        return $this->request('POST', '/admin/suggestions/reload-index');
    }

    /**
     * @return array<string, mixed>
     */
    public function deleteSuggestionEmbedding(int|string $id): array
    {
        return $this->request('DELETE', "/admin/suggestions/{$id}/embedding");
    }

    // --- Request plumbing ------------------------------------------------

    /**
     * @param  array<string, mixed>  $query
     * @param  array<string, mixed>|null  $body
     * @return array<string, mixed>
     */
    private function request(string $method, string $path, array $query = [], ?array $body = null): array
    {
        // Signed over exactly these bytes — must match what's actually sent,
        // so it's built once and reused for both the signature and the body.
        $rawBody = $body !== null ? json_encode($body, JSON_THROW_ON_ERROR) : '';

        $headers = BnfexpressSignature::headers($method, $path, $rawBody, $this->clientId, $this->secret);

        $pending = Http::baseUrl($this->baseUrl)->withHeaders($headers);

        try {
            $response = match ($method) {
                'GET' => $pending->get($path, $query),
                // DELETE with a body (e.g. batchDeleteSuggestions) must send it the same
                // way POST/PATCH do — $query is never used as delete()'s $data here, that
                // param means something else (a JSON body) than what its name implies.
                'DELETE' => $body !== null
                    ? $pending->withBody($rawBody, 'application/json')->delete($path)
                    : $pending->delete($path),
                'POST' => $pending->withBody($rawBody, 'application/json')->post($path),
                'PATCH' => $pending->withBody($rawBody, 'application/json')->patch($path),
                default => throw new \InvalidArgumentException("Unsupported HTTP method [{$method}]."),
            };
        } catch (ConnectionException $exception) {
            throw new BnfexpressApiException($exception->getMessage());
        }

        if (! $response->successful()) {
            throw new BnfexpressApiException($this->errorMessage($response), $response->status());
        }

        return (array) $response->json();
    }

    /**
     * bnfexpress's {"detail": "..."} is usually a plain string, but FastAPI's
     * own request-validation failures (422s) return `detail` as a list of
     * {loc, msg, type} objects instead — casting that straight to string
     * produces the literal, useless "Array" (with a PHP warning). Handle
     * both shapes.
     */
    private function errorMessage(Response $response): string
    {
        $detail = $response->json('detail');

        if (is_string($detail)) {
            return $detail;
        }

        if (is_array($detail)) {
            return implode(' ', array_map(
                fn (mixed $item): string => is_array($item) ? (string) ($item['msg'] ?? json_encode($item)) : (string) $item,
                $detail,
            ));
        }

        return "bnfexpress request failed with status {$response->status()}.";
    }
}
