<?php

namespace App\Jobs\Synapse;

use App\Models\Document\Document;
use App\Models\Dsar\Dsar;
use App\Models\Dsar\DsarEvidenceItem;
use App\Models\Synapse\SynapseRun;
use App\Synapse\Agents\ExtractDsarEvidenceAgent;
use App\Synapse\Agents\FilterDsarPagesAgent;
use App\Synapse\Agents\VerifyDsarDocumentAgent;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Laravel\Ai\Exceptions\RateLimitedException;
use Laravel\Ai\Files;
use Smalot\PdfParser\Parser as PdfParser;

class ExtractEvidenceFromDocumentJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 3;
    public int $timeout = 600;
    public int $backoff = 60;

    /** Documents above this page count skip AI verify/filter and use text chunking. */
    private const LARGE_DOC_THRESHOLD = 40;

    /** Max pages of text sent to the extraction agent per call. */
    private const CHUNK_SIZE = 25;

    /** Pages whose extracted text is shorter than this are treated as image/blank. */
    private const MIN_TEXT_CHARS = 80;

    public function __construct(
        public readonly int $documentId,
    ) {}

    public function handle(): void
    {
        $document = Document::with(['owner', 'type'])->find($this->documentId);

        if (!$document) {
            Log::warning("ExtractEvidenceFromDocumentJob: document {$this->documentId} not found.");
            return;
        }

        $document->update(['evidence_status' => 'processing']);

        $storagePath = $document->owner->getDocumentStoragePath() . $document->file_name;
        $solicitorId = $document->solicitor_id;
        $dsarId      = $document->owner instanceof Dsar ? $document->owner->id : null;

        try {
            $isPdf = $document->file_type === 'application/pdf';

            // Parse per-page text for PDFs so we know the page count and can chunk large docs.
            $perPageText = $isPdf ? $this->parsePageText($storagePath) : [];
            $pageCount   = count($perPageText);
            $isLarge     = $pageCount >= self::LARGE_DOC_THRESHOLD;

            Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — pages={$pageCount}, large=" . ($isLarge ? 'yes' : 'no') . ".");

            // ── Stage 1: Verify ───────────────────────────────────────────────
            if ($isLarge) {
                // Large docs are almost certainly real records — skip the AI verify step.
            } else {
                $file       = $this->loadFile($document, $storagePath);
                $verify     = (new VerifyDsarDocumentAgent)->prompt(
                    "Filename: {$document->file_name}\n"
                        . "Document type: " . ($document->type?->display_name ?? 'Unknown') . "\n"
                        . "MIME type: {$document->file_type}",
                    attachments: [$file]
                );
                $verifyData = $verify->structured;

                Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — is_dsar_document={$verifyData['is_dsar_document']}, confidence={$verifyData['confidence']}.");

                if (!$verifyData['is_dsar_document'] && in_array($verifyData['confidence'], ['high', 'medium'], true)) {
                    $document->update(['evidence_status' => 'skipped']);
                    return;
                }
            }

            // ── Stage 2: Filter pages ─────────────────────────────────────────
            if ($isLarge) {
                // Heuristic filter: skip pages with very little text (images, ECGs, blanks).
                $relevantPageNums = [];
                $excludedPages    = [];
                foreach ($perPageText as $i => $text) {
                    $pageNum = $i + 1;
                    if (mb_strlen(trim($text)) >= self::MIN_TEXT_CHARS) {
                        $relevantPageNums[] = $pageNum;
                    } else {
                        $excludedPages[] = $pageNum;
                    }
                }
                $filterRecord = [
                    'method'      => 'heuristic',
                    'total_pages' => $pageCount,
                    'relevant'    => count($relevantPageNums),
                    'excluded'    => count($excludedPages),
                ];
                $this->recordSynapseRun('dsar_page_filter', $document, $filterRecord, $solicitorId);
                Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — heuristic filter: {$filterRecord['relevant']} relevant, {$filterRecord['excluded']} excluded.");
            } else {
                $file       = $file ?? $this->loadFile($document, $storagePath);
                $filter     = (new FilterDsarPagesAgent)->prompt(
                    "Filename: {$document->file_name}",
                    attachments: [$file]
                );
                $filterData = $filter->structured;
                $this->recordSynapseRun('dsar_page_filter', $document, $filterData, $solicitorId);

                $relevantPageNums = array_values(array_filter(
                    array_map('intval', explode(',', $filterData['relevant_pages_csv'] ?? '')),
                    fn($n) => $n > 0
                ));
                Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — AI filter: " . count($relevantPageNums) . " relevant of {$filterData['total_pages']}.");
            }

            if (empty($relevantPageNums)) {
                $document->update(['evidence_status' => 'skipped']);
                return;
            }

            // ── Stage 3: Extract evidence ─────────────────────────────────────
            $totalItems = 0;

            if ($isLarge) {
                $chunks = array_chunk($relevantPageNums, self::CHUNK_SIZE);
                foreach ($chunks as $chunkIndex => $chunkPages) {
                    $chunkText = $this->buildChunkText($chunkPages, $perPageText);
                    $prompt    = "Filename: {$document->file_name} (chunk " . ($chunkIndex + 1) . " of " . count($chunks) . ")\n"
                        . "Pages in this chunk: " . implode(', ', $chunkPages) . "\n\n"
                        . $chunkText;
                    $extract     = (new ExtractDsarEvidenceAgent)->prompt($prompt);
                    $extractData = $extract->structured;
                    $items       = json_decode($extractData['items_json'] ?? '[]', true) ?? [];
                    $totalItems += $this->saveItems($items, $document->id, $solicitorId, $dsarId);
                    Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — chunk {$chunkIndex} extracted " . count($items) . " items.");
                }
            } else {
                $file          = $file ?? $this->loadFile($document, $storagePath);
                $relevantPages = implode(', ', $relevantPageNums);
                $excludedNotes = trim($filterData['exclusion_notes'] ?? '');
                $prompt        = "Filename: {$document->file_name}\n"
                    . "Focus your analysis on pages: {$relevantPages}.\n"
                    . ($excludedNotes ? "Skip these diagnostic-imagery pages: {$excludedNotes}.\n" : '');
                $extract     = (new ExtractDsarEvidenceAgent)->prompt($prompt, attachments: [$file]);
                $extractData = $extract->structured;
                $items       = json_decode($extractData['items_json'] ?? '[]', true) ?? [];
                $totalItems += $this->saveItems($items, $document->id, $solicitorId, $dsarId);
            }

            $this->recordSynapseRun('dsar_evidence_extractor', $document, ['item_count' => $totalItems], $solicitorId);
            $document->update(['evidence_status' => 'completed']);
            Log::info("ExtractEvidenceFromDocumentJob: document {$this->documentId} — done, {$totalItems} evidence items.");

        } catch (RateLimitedException $e) {
            $delay = min(60 * $this->attempts(), 600);
            Log::warning("ExtractEvidenceFromDocumentJob: rate limited for document {$this->documentId}, releasing in {$delay}s.");
            $this->release($delay);
        } catch (\Throwable $e) {
            $document->update(['evidence_status' => 'failed']);
            Log::error("ExtractEvidenceFromDocumentJob: failed for document {$this->documentId}.", ['error' => $e->getMessage()]);
            report($e);
            $this->fail($e);
        }
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private function parsePageText(string $storagePath): array
    {
        try {
            $contents = Storage::disk('public')->get($storagePath);
            $parser   = new PdfParser();
            $pdf      = $parser->parseContent($contents);
            return array_map(fn($page) => $page->getText(), $pdf->getPages());
        } catch (\Throwable $e) {
            Log::warning("ExtractEvidenceFromDocumentJob: per-page parse failed for document {$this->documentId}: {$e->getMessage()}");
            return [];
        }
    }

    private function buildChunkText(array $pageNums, array $perPageText): string
    {
        $parts = [];
        foreach ($pageNums as $pageNum) {
            $text = $perPageText[$pageNum - 1] ?? '';
            if (trim($text) !== '') {
                $parts[] = "=== PAGE {$pageNum} ===\n" . trim($text);
            }
        }
        return implode("\n\n", $parts);
    }

    private function saveItems(array $items, int $documentId, ?int $solicitorId, ?int $dsarId): int
    {
        $count = 0;
        foreach ($items as $item) {
            if (empty($item['quote']) || empty($item['category'])) {
                continue;
            }
            DsarEvidenceItem::create([
                'solicitor_id' => $solicitorId,
                'document_id'  => $documentId,
                'dsar_id'      => $dsarId,
                'page'         => ($item['page'] ?? 0) ?: null,
                'quote'        => $item['quote'],
                'category'     => $item['category'],
                'severity'     => $item['severity'] ?? 'medium',
                'comment'      => $item['comment'] ?? '',
                'source'       => 'ai',
            ]);
            $count++;
        }
        return $count;
    }

    private function loadFile(Document $document, string $storagePath): Files\Document|Files\Image
    {
        $isImage = str_starts_with($document->file_type ?? '', 'image/');
        return $isImage
            ? Files\Image::fromStorage($storagePath, disk: 'public')
            : Files\Document::fromStorage($storagePath, disk: 'public');
    }

    private function recordSynapseRun(string $agentType, Document $document, array $result, ?int $solicitorId): void
    {
        if (!config('synapse.record_runs', true)) {
            return;
        }
        SynapseRun::create([
            'solicitor_id' => $solicitorId,
            'agent_type'   => $agentType,
            'subject_type' => Document::class,
            'subject_id'   => $document->id,
            'status'       => 'completed',
            'result'       => $result,
        ]);
    }
}
