<?php

namespace App\DTOs;

class AIResponse
{
    public function __construct(
        public readonly string $content,
        public readonly int $confidence,
        public readonly string $model,
        public readonly string $provider,
        public readonly int $tokens_in,
        public readonly int $tokens_out,
        public readonly float $cost,
        public readonly int $response_time_ms,
        public readonly array $sources_used = [],
        public readonly ?string $sentiment = null,
        // -- Knowledge Agent fields. Defaults keep every existing caller
        //    working unchanged; the new behaviour only kicks in when
        //    AIManager actually populates them.
        public readonly bool $was_fallback = false,
        public readonly ?string $fallback_message = null,
        public readonly ?string $original_content = null,
        /**
         * Numbered citations matching the [1], [2], ... markers the LLM
         * was instructed to emit inline. Each entry has:
         *   ['index' => int, 'title' => string, 'url' => ?string, 'document_id' => ?int]
         * Channel renderers expand these into a clickable "Sources:"
         * footer via appendCitationsFooter().
         */
        public readonly array $cited_sources = [],
        /**
         * Opaque signed token consumed by the public thumbs feedback
         * endpoint. Set when this reply has a `kb_query_logs` row that
         * the customer is allowed to rate (i.e. a real customer-facing
         * answer, not a playground call). null = no thumbs UI should be
         * shown.
         */
        public readonly ?string $feedback_token = null,
    ) {}

    public function toArray(): array
    {
        return [
            'content' => $this->content,
            'confidence' => $this->confidence,
            'model' => $this->model,
            'provider' => $this->provider,
            'tokens_in' => $this->tokens_in,
            'tokens_out' => $this->tokens_out,
            'cost' => $this->cost,
            'response_time_ms' => $this->response_time_ms,
            'sources_used' => $this->sources_used,
            'sentiment' => $this->sentiment,
            'was_fallback' => $this->was_fallback,
            'fallback_message' => $this->fallback_message,
            'original_content' => $this->original_content,
            'cited_sources' => $this->cited_sources,
            'feedback_token' => $this->feedback_token,
        ];
    }

    /**
     * Check if AI confidence meets the given threshold.
     */
    public function meetsThreshold(int $threshold): bool
    {
        return $this->confidence >= $threshold;
    }

    /**
     * The text that should actually be shown to the customer.
     *
     * Three layers, applied in order:
     *   1. If the workspace's fallback substitution fired, return the
     *      canned fallback message instead of the (likely hallucinated)
     *      LLM output. `content` already holds the fallback in this
     *      case; `original_content` keeps the raw LLM answer for
     *      auditing.
     *   2. Otherwise, append a numbered "Sources:" footer expanding the
     *      [1], [2] markers the LLM emitted inline, formatted for the
     *      given channel ('plain', 'html', 'markdown').
     *   3. If there are no citations, return content untouched.
     *
     * Channel renderers (widget, email, WhatsApp, Instagram, FB) should
     * call this instead of accessing `content` directly when sending to
     * customers. Approval-mode review UIs still want raw `content` so
     * the admin can see exactly what the LLM said.
     */
    public function customerContent(string $format = 'plain'): string
    {
        if ($this->was_fallback) {
            return $this->content; // already substituted by AIManager
        }

        if (empty($this->cited_sources)) {
            return $this->content;
        }

        return $this->content . self::appendCitationsFooter($this->content, $this->cited_sources, $format);
    }

    /**
     * Build a "Sources:" footer string that only lists citations the
     * answer actually references.
     *
     * Parses the answer text for `[N]` markers, intersects against the
     * supplied $citedSources, and renders the matched subset. This
     * matters because retrieval may return five chunks but the LLM
     * might only cite two — we shouldn't pad the footer with sources
     * the customer can't see referenced.
     *
     * Returns a string starting with "\n\n" so callers can append
     * unconditionally (or get an empty string when nothing is cited).
     *
     * @param  array  $citedSources  Array of ['index', 'title', 'url'].
     */
    public static function appendCitationsFooter(string $content, array $citedSources, string $format = 'plain'): string
    {
        if (empty($citedSources)) {
            return '';
        }

        // Pull [N] indices the LLM actually mentioned in its answer.
        preg_match_all('/\[(\d+)\]/', $content, $matches);
        $referencedIndices = array_map('intval', $matches[1] ?? []);

        if (empty($referencedIndices)) {
            return ''; // LLM ignored the citation instruction — don't fake it
        }

        $referenced = array_unique($referencedIndices);
        sort($referenced);

        $sourcesByIndex = [];
        foreach ($citedSources as $src) {
            if (isset($src['index'])) {
                $sourcesByIndex[(int) $src['index']] = $src;
            }
        }

        $lines = [];
        foreach ($referenced as $idx) {
            $src = $sourcesByIndex[$idx] ?? null;
            if (!$src) {
                continue; // hallucinated citation — skip
            }
            $title = $src['title'] ?? 'Source';
            $url = $src['url'] ?? null;
            $lines[] = match ($format) {
                'html' => $url
                    ? "  <li>[{$idx}] <a href=\"" . htmlspecialchars($url, ENT_QUOTES) . '" target="_blank" rel="noopener">' . htmlspecialchars($title) . '</a></li>'
                    : "  <li>[{$idx}] " . htmlspecialchars($title) . '</li>',
                'markdown' => $url ? "[{$idx}] [{$title}]({$url})" : "[{$idx}] {$title}",
                default => $url ? "[{$idx}] {$title} — {$url}" : "[{$idx}] {$title}",
            };
        }

        if (empty($lines)) {
            return '';
        }

        return match ($format) {
            'html' => "\n\n<p><strong>Sources:</strong></p>\n<ul>\n" . implode("\n", $lines) . "\n</ul>",
            'markdown' => "\n\n**Sources:**\n" . implode("\n", $lines),
            default => "\n\nSources:\n" . implode("\n", $lines),
        };
    }
}
