<?php

namespace App\Services\Whmcs;

use App\Models\Ticket;
use App\Models\TicketReply;
use Illuminate\Support\Facades\DB;

/**
 * Case-based retrieval — given a fresh ticket, find a handful of
 * resolved-in-the-past tickets that look semantically similar, then
 * surface the WINNING staff reply from each as a few-shot example for
 * the drafter.
 *
 * Why this matters: the AI was starting cold on every ticket. With this,
 * a "MongoDB connection fail" ticket gets the AI 3 examples of how
 * Tanvir / Ayesha actually resolved similar tickets in the past — same
 * voice, same plan-aware specifics, same escalation path. Drafts get
 * much better without any model fine-tuning.
 *
 * Ranking (no embeddings — keyword overlap is enough for v1):
 *   - Subject keyword overlap with current ticket subject (highest weight)
 *   - First-customer-message keyword overlap
 *   - Same department (bonus)
 *   - Same hosting brand on customer's product (bonus, if available)
 *   - Recency decay (older tickets weighted down)
 *
 * "Winning reply" = first staff reply that came AFTER the customer's
 * first message, on a ticket that eventually CLOSED in good state. We
 * skip AI-drafted replies (author_type=ai_auto) and held-for-review
 * drafts so the few-shots are always human-authored gold.
 */
class SimilarTicketRetriever
{
    /** Skip tokens — common stop words that match too easily. */
    private const STOPWORDS = [
        'the','a','an','is','are','was','were','my','i','to','of','in','on',
        'for','this','that','it','and','or','but','please','help','need',
        'amar','ami','korte','kora','jonno','holo','hocche','hocce','na','er','ei',
        're','fwd','re:','fwd:', 'hi','hello','dear','sir',
    ];

    /**
     * Return up to $max similar resolved tickets with their winning reply.
     *
     * @return array<int, array{
     *   ticket_id:int, subject:string, dept:string, customer_msg:string,
     *   staff_reply:string, agent_name:string, resolved_at:?string, score:int
     * }>
     */
    public function findFor(Ticket $ticket, int $max = 3): array
    {
        $tokens = $this->tokenize($ticket->subject . ' ' . $this->latestCustomerSnippet($ticket));
        if (empty($tokens)) return [];

        // Pull a candidate pool: same-department, resolved, not the ticket
        // itself, with at least one human reply. Cap candidate pool at
        // 200 — we score in PHP rather than ranking in SQL since the
        // keyword scoring is non-trivial.
        $deptCandidates = Ticket::query()
            ->where('id', '!=', $ticket->id)
            ->whereIn('whmcs_status', ['Closed', 'Resolved'])
            ->whereHas('replies', fn ($q) => $q->where('author_type', 'staff')
                ->where('is_internal_note', false))
            ->when($ticket->department, fn ($q) => $q->where('department', $ticket->department))
            ->orderByDesc('whmcs_last_reply_at')
            ->limit(200)
            ->get(['id', 'subject', 'department', 'customer_name', 'whmcs_last_reply_at']);

        // If too few same-dept candidates, broaden to any resolved ticket.
        if ($deptCandidates->count() < 30) {
            $deptCandidates = Ticket::query()
                ->where('id', '!=', $ticket->id)
                ->whereIn('whmcs_status', ['Closed', 'Resolved'])
                ->whereHas('replies', fn ($q) => $q->where('author_type', 'staff')
                    ->where('is_internal_note', false))
                ->orderByDesc('whmcs_last_reply_at')
                ->limit(200)
                ->get(['id', 'subject', 'department', 'customer_name', 'whmcs_last_reply_at']);
        }

        $scored = [];
        foreach ($deptCandidates as $cand) {
            $candTokens = $this->tokenize($cand->subject);
            $overlap = count(array_intersect($tokens, $candTokens));
            if ($overlap === 0) continue;

            // Recency bonus — within 90 days = +2, within 180 = +1.
            $age = $cand->whmcs_last_reply_at
                ? now()->diffInDays($cand->whmcs_last_reply_at)
                : 9999;
            $recency = $age < 90 ? 2 : ($age < 180 ? 1 : 0);

            $deptBonus = (! empty($ticket->department) && $cand->department === $ticket->department) ? 2 : 0;

            $score = ($overlap * 3) + $recency + $deptBonus;
            $scored[] = ['cand' => $cand, 'score' => $score];
        }
        if (empty($scored)) return [];

        usort($scored, fn ($a, $b) => $b['score'] <=> $a['score']);

        // Take top N, then enrich with the winning reply text. Done in a
        // second pass to avoid loading reply bodies for tickets we'll
        // discard.
        $out = [];
        foreach (array_slice($scored, 0, $max * 2) as $row) {
            $cand = $row['cand'];
            $reply = $this->winningReplyFor($cand);
            if (! $reply) continue;
            $custMsg = $this->firstCustomerMessage($cand);

            $out[] = [
                'ticket_id'   => (int) $cand->id,
                'subject'     => (string) $cand->subject,
                'dept'        => (string) $cand->department,
                'customer_msg'=> mb_substr($custMsg, 0, 600, 'UTF-8'),
                'staff_reply' => mb_substr($reply['body'], 0, 1000, 'UTF-8'),
                'agent_name'  => (string) $reply['agent'],
                'resolved_at' => $cand->whmcs_last_reply_at ? (string) $cand->whmcs_last_reply_at : null,
                'score'       => (int) $row['score'],
            ];
            if (count($out) >= $max) break;
        }
        return $out;
    }

    /**
     * Render the few-shot examples as a markdown-ish prompt block. Empty
     * string when no good examples found — caller just omits the section.
     */
    public function buildPromptBlock(Ticket $ticket, int $max = 3): string
    {
        $examples = $this->findFor($ticket, $max);
        if (empty($examples)) return '';

        $lines = ["## Past tickets you (HostSeba staff) resolved with this kind of issue"];
        $lines[] = "Read these examples for the EXACT TONE + STRUCTURE the human staff used.";
        $lines[] = "Mimic their phrasing, level of detail, and how they handled escalation. These are";
        $lines[] = "the winning answers — your reply should sound like them, NOT like a generic AI.";

        foreach ($examples as $i => $ex) {
            $lines[] = "";
            $lines[] = "### Example " . ($i + 1) . " (ticket #{$ex['ticket_id']}, resolved by {$ex['agent_name']})";
            $lines[] = "Customer wrote:";
            $lines[] = "  \"" . trim(preg_replace("/\s+/u", ' ', $ex['customer_msg'])) . "\"";
            $lines[] = "Staff resolved with:";
            $lines[] = "  \"" . trim(preg_replace("/\s+/u", ' ', $ex['staff_reply'])) . "\"";
        }
        $lines[] = "";
        $lines[] = "Use these as REFERENCE for tone, not as a script — facts in the current ticket may differ.";
        return implode("\n", $lines);
    }

    /**
     * First staff (human) reply that came AFTER the customer's first
     * message. Excludes AI-generated rows so few-shots are gold.
     */
    private function winningReplyFor(Ticket $ticket): ?array
    {
        $firstCustomer = $ticket->replies()
            ->where('author_type', 'customer')
            ->orderBy('sent_at')->first();
        $q = $ticket->replies()
            ->where('author_type', 'staff')
            ->where('is_internal_note', false)
            ->whereNull('ai_provider')   // exclude AI auto-replies
            ->orderBy('sent_at');
        if ($firstCustomer) {
            $q->where('sent_at', '>=', $firstCustomer->sent_at);
        }
        $reply = $q->first(['body', 'author_name']);
        if (! $reply || mb_strlen(trim($reply->body), 'UTF-8') < 30) return null;
        return ['body' => (string) $reply->body, 'agent' => (string) ($reply->author_name ?: 'Staff')];
    }

    private function firstCustomerMessage(Ticket $ticket): string
    {
        $r = $ticket->replies()
            ->where('author_type', 'customer')
            ->orderBy('sent_at')
            ->first(['body']);
        return (string) ($r->body ?? '');
    }

    private function latestCustomerSnippet(Ticket $ticket): string
    {
        $r = $ticket->replies()
            ->where('author_type', 'customer')
            ->orderByDesc('sent_at')
            ->first(['body']);
        return mb_substr((string) ($r->body ?? ''), 0, 800, 'UTF-8');
    }

    /**
     * Cheap word tokeniser. Lowercases, strips punctuation, drops stop
     * words + short tokens. Good enough for keyword overlap; embeddings
     * are an upgrade we can do later if quality demands it.
     */
    private function tokenize(string $text): array
    {
        $text = strtolower($text);
        // Split on non-letter (covers Bangla + Latin since the regex uses
        // unicode \p{L}).
        if (! preg_match_all('/[\p{L}0-9]{3,}/u', $text, $m)) return [];
        $tokens = array_unique($m[0]);
        return array_values(array_diff($tokens, self::STOPWORDS));
    }
}
