<?php

namespace App\Livewire\KnowledgeBase;

use App\Models\KbChunk;
use App\Models\KbDocument;
use App\Models\KbQueryLog;
use App\Traits\AuthorizesWorkspaceActions;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Component;

/**
 * Knowledge Base Insights — turns the silent backend RAG into a visible,
 * sellable "AI Knowledge Agent" feature.
 *
 * Renders four things from `kb_query_logs`:
 *   1. KPI cards   — questions answered, deflection rate, avg confidence, cost
 *   2. Knowledge gaps — was_fallback=true entries the admin should fix
 *   3. Top sources — which KB documents the agent actually relies on
 *   4. Recent answers — feed of Q&A pairs with admin thumbs for tuning
 *
 * The whole page is read-only over the logs except for the admin-side
 * thumbs feedback (markHelpful), used internally to tune which answers
 * the agent gets right vs wrong.
 *
 * Customer-facing thumbs (rendered in widget / email) and an "Add to KB"
 * one-click flow come in Phase 1 — the UI exposes the buttons now but
 * they only update internal state.
 */
class Insights extends Component
{
    use AuthorizesWorkspaceActions;

    /** Time window for every chart and table. */
    public int $rangeDays = 30;

    /** Filter the recent feed and top-questions list to a single channel. */
    public ?string $channelFilter = null;

    /** When true, the recent-answers feed only shows was_fallback=true. */
    public bool $onlyGaps = false;

    public function mount(): void
    {
        if (! $this->authorizeWorkspaceAction('view')) {
            return;
        }
    }

    public function setRange(int $days): void
    {
        $this->rangeDays = in_array($days, [7, 30, 90], true) ? $days : 30;
    }

    public function setChannel(?string $channel): void
    {
        $this->channelFilter = $channel;
    }

    public function toggleOnlyGaps(): void
    {
        $this->onlyGaps = !$this->onlyGaps;
    }

    /**
     * Admin tuning thumbs. Marks an answer helpful/unhelpful from inside
     * the dashboard so the admin can curate which past answers were
     * actually good — customer-facing thumbs (widget / email) update the
     * same column via the public feedback endpoint coming in Phase 1.
     */
    public function markHelpful(int $id, bool $helpful): void
    {
        if (! $this->authorizeWorkspaceAction('manage')) {
            return;
        }

        KbQueryLog::where('id', $id)->update(['was_helpful' => $helpful]);
    }

    /** Bounded query — all charts pull from this same scope. */
    protected function baseQuery()
    {
        $query = KbQueryLog::where('created_at', '>=', now()->subDays($this->rangeDays));
        if ($this->channelFilter) {
            $query->where('channel', $this->channelFilter);
        }
        return $query;
    }

    #[Computed]
    public function stats(): array
    {
        $row = (clone $this->baseQuery())
            ->selectRaw('COUNT(*) as total')
            ->selectRaw('SUM(CASE WHEN was_fallback = 0 THEN 1 ELSE 0 END) as answered')
            ->selectRaw('SUM(CASE WHEN was_fallback = 1 THEN 1 ELSE 0 END) as fallbacks')
            ->selectRaw('AVG(confidence) as avg_confidence')
            ->selectRaw('SUM(cost_usd) as total_cost')
            ->selectRaw('SUM(CASE WHEN was_helpful = 1 THEN 1 ELSE 0 END) as helpful')
            ->selectRaw('SUM(CASE WHEN was_helpful = 0 THEN 1 ELSE 0 END) as unhelpful')
            ->first();

        $total = (int) ($row->total ?? 0);
        $answered = (int) ($row->answered ?? 0);
        $fallbacks = (int) ($row->fallbacks ?? 0);

        return [
            'total' => $total,
            'answered' => $answered,
            'fallbacks' => $fallbacks,
            // Deflection rate = % of questions where the KB-grounded answer
            // had enough confidence that we did NOT show "I don't know".
            // This is the headline number admins care about.
            'deflection_rate' => $total > 0 ? round(($answered / $total) * 100, 1) : 0.0,
            'fallback_rate' => $total > 0 ? round(($fallbacks / $total) * 100, 1) : 0.0,
            'avg_confidence' => $row->avg_confidence ? round((float) $row->avg_confidence, 1) : 0.0,
            'total_cost' => round((float) ($row->total_cost ?? 0), 4),
            'avg_cost' => $total > 0 ? round(((float) ($row->total_cost ?? 0)) / $total, 6) : 0.0,
            'helpful' => (int) ($row->helpful ?? 0),
            'unhelpful' => (int) ($row->unhelpful ?? 0),
            // Conservative hours-saved estimate at 90 seconds per deflected
            // ticket — undersells the impact on purpose so demos don't
            // look implausible.
            'hours_saved' => round(($answered * 90) / 3600, 1),
        ];
    }

    /**
     * Knowledge gaps — questions where the agent fell back to "I don't
     * know" because no KB chunk passed the confidence threshold. These
     * are the highest-value fix targets: each one is a customer the
     * agent failed.
     */
    #[Computed]
    public function knowledgeGaps()
    {
        return (clone $this->baseQuery())
            ->where('was_fallback', true)
            ->latest()
            ->limit(20)
            ->get(['id', 'question', 'answer', 'channel', 'confidence', 'created_at']);
    }

    /**
     * Top sources — which KB documents are actually doing work.
     * Useful for pruning dead-weight content and for marketing copy
     * like "this doc has answered 247 questions this month".
     */
    #[Computed]
    public function topSources(): array
    {
        // cited_chunk_ids is JSON. Pulling all logs and aggregating in PHP
        // is cleaner than vendor-specific JSON functions and keeps this
        // working on MySQL 5.7 / MariaDB / SQLite at the same time.
        $logs = (clone $this->baseQuery())
            ->where('was_fallback', false)
            ->whereNotNull('cited_chunk_ids')
            ->get(['cited_chunk_ids']);

        $chunkCounts = [];
        foreach ($logs as $log) {
            foreach (($log->cited_chunk_ids ?? []) as $chunkId) {
                $chunkCounts[$chunkId] = ($chunkCounts[$chunkId] ?? 0) + 1;
            }
        }

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

        // Resolve chunks → documents → aggregate per-document totals.
        $chunks = KbChunk::with('document')
            ->whereIn('id', array_keys($chunkCounts))
            ->get();

        $docTotals = [];
        foreach ($chunks as $chunk) {
            $docId = $chunk->document_id;
            if (!isset($docTotals[$docId])) {
                $docTotals[$docId] = [
                    'document_id' => $docId,
                    'title' => $chunk->document?->title ?? 'Deleted source',
                    'source_url' => $chunk->document?->source_url,
                    'type' => $chunk->document?->type,
                    'citations' => 0,
                ];
            }
            $docTotals[$docId]['citations'] += $chunkCounts[$chunk->id] ?? 0;
        }

        usort($docTotals, fn($a, $b) => $b['citations'] <=> $a['citations']);

        return array_slice(array_values($docTotals), 0, 10);
    }

    /**
     * Most-asked questions — clusters of semantically similar questions
     * from the current window. Useful for spotting recurring requests
     * the team can shortcut with a Q&A, an FAQ page update, or product
     * fix. Clustered locally because DB-side JSON/regex grouping breaks
     * portability across MySQL/MariaDB/SQLite.
     */
    #[Computed]
    public function topAskedQuestions(): array
    {
        $logs = (clone $this->baseQuery())
            ->where('was_fallback', false)
            ->latest()
            ->limit(500)
            ->get(['id', 'question', 'created_at']);

        return $this->clusterQuestions($logs, 8);
    }

    /**
     * Clustered knowledge gaps — same idea but for was_fallback=true.
     * A single cluster of 15 "what is your refund policy" variants
     * matters far more than 15 unrelated one-off failures, so this
     * panel rolls them up to surface the real fix targets.
     */
    #[Computed]
    public function clusteredGaps(): array
    {
        $logs = (clone $this->baseQuery())
            ->where('was_fallback', true)
            ->latest()
            ->limit(500)
            ->get(['id', 'question', 'created_at']);

        return $this->clusterQuestions($logs, 5);
    }

    /**
     * Cheap semantic-ish clustering on question text. Two questions
     * cluster together when their "signature" matches: a sorted set of
     * the 5 most-significant tokens (length > 3, English stopwords
     * removed). "What's your refund policy?" and "What is the refund
     * policy?" both signature to "policy refund". Good enough for the
     * dashboard surface without needing embeddings on every render.
     *
     * Returns up to $limit clusters sorted by count desc, each with:
     *   ['signature', 'representative', 'count', 'last_asked']
     */
    private function clusterQuestions($logs, int $limit): array
    {
        static $stopwords = [
            'what','when','where','which','whose','whom','that','this','these','those',
            'have','having','does','doing','will','would','should','could','about','from',
            'with','your','their','there','they','them','then','than','were','been','being',
            'into','onto','upon','also','some','many','much','very','just','only','more',
            'most','need','want','tell','know','make','help','please','hello','thanks',
            'thank','okay','yeah','yes',
        ];
        $stopwordsFlip = array_flip($stopwords);

        $clusters = [];
        foreach ($logs as $log) {
            $normalized = mb_strtolower(trim((string) $log->question));
            $normalized = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $normalized);
            $tokens = preg_split('/\s+/', (string) $normalized, -1, PREG_SPLIT_NO_EMPTY);
            $significant = [];
            foreach ($tokens as $t) {
                if (mb_strlen($t) <= 3) continue;
                if (isset($stopwordsFlip[$t])) continue;
                $significant[$t] = true;
                if (count($significant) >= 5) break;
            }
            if (empty($significant)) {
                // Empty signature = highly generic question ("hi", "?"). Skip
                // — it has no actionable cluster and would dominate the
                // panel with junk.
                continue;
            }
            $keys = array_keys($significant);
            sort($keys);
            $signature = implode(' ', $keys);

            if (!isset($clusters[$signature])) {
                $clusters[$signature] = [
                    'signature' => $signature,
                    'representative' => $log->question,
                    'count' => 0,
                    'last_asked' => $log->created_at,
                ];
            }
            $clusters[$signature]['count']++;
            if ($log->created_at > $clusters[$signature]['last_asked']) {
                $clusters[$signature]['last_asked'] = $log->created_at;
                $clusters[$signature]['representative'] = $log->question;
            }
        }

        usort($clusters, fn($a, $b) => $b['count'] <=> $a['count']);

        return array_slice(array_values($clusters), 0, $limit);
    }

    /**
     * Recent answers feed — the live tail of what the agent has been
     * saying. Limited because this can grow fast on busy workspaces.
     */
    #[Computed]
    public function recentLogs()
    {
        $query = (clone $this->baseQuery())->latest();
        if ($this->onlyGaps) {
            $query->where('was_fallback', true);
        }
        return $query->limit(25)->get();
    }

    /**
     * Channels surfaced in the filter pill row. Only shows channels that
     * have actually been used in this window — keeps the row tight on
     * workspaces that only use one or two channels.
     */
    #[Computed]
    public function availableChannels(): array
    {
        return KbQueryLog::where('created_at', '>=', now()->subDays($this->rangeDays))
            ->whereNotNull('channel')
            ->distinct()
            ->pluck('channel')
            ->filter()
            ->values()
            ->toArray();
    }

    /**
     * Lets the page warn "No KB content yet" instead of just showing
     * empty charts on a fresh install.
     */
    #[Computed]
    public function hasKbContent(): bool
    {
        return KbDocument::where('status', 'ready')->exists();
    }

    public function render()
    {
        return view('livewire.knowledge-base.insights');
    }
}
