<?php

namespace App\Console\Commands;

use App\Services\OcrService;
use App\Synapse\Agents\ExtractDsarEvidenceAgent;
use App\Synapse\Agents\FilterDsarPagesAgent;
use App\Synapse\Agents\VerifyDsarDocumentAgent;
use App\Synapse\ZipExpander;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Laravel\Ai\Exceptions\RateLimitedException;
use Laravel\Ai\Files;
use Smalot\PdfParser\Config as PdfParserConfig;
use Smalot\PdfParser\Parser as PdfParser;

/**
 * Runs a batch of local files through the same verify → filter → extract stages as
 * ExtractEvidenceFromAttachmentJob / ExtractEvidenceFromDocumentJob, without a DSAR/document
 * DB record and without persisting anything — a dev tool for tuning the Synapse prompts
 * against a folder of sample records before they're wired into a real case.
 */
class TestEvidenceExtraction extends Command
{
    protected $signature = 'evidence:test
                            {paths* : One or more file or directory paths to test}
                            {--out= : Directory to write per-file JSON results (default: storage/app/evidence-tests/<timestamp>)}
                            {--no-save : Print results to console only, do not write JSON files}
                            {--ext=pdf,jpg,jpeg,png,webp : Comma-separated extensions to include when a path is a directory or zip}
                            {--skip-verify : Skip the Stage 1 "is this a DSAR document" check and always extract}
                            {--limit=0 : Stop after N files (0 = no limit)}
                            {--ocr : For scanned PDFs (no text layer), run real OCR (tesseract) synchronously in-process instead of the vision-model fallback — tests the actual OCR pipeline}
                            {--ocr-max-pages=0 : Cap OCR to the first N pages of a scanned doc (0 = no limit). OCR is slow — one subprocess call per page}';

    protected $description = 'Run a batch of local files through the evidence-extraction pipeline (verify, filter, extract) for testing/tuning, without touching the database.';

    private const LARGE_DOC_THRESHOLD = 40;

    private const MIN_TEXT_CHARS = 80;

    private const CHUNK_SIZE = 25;

    private ZipExpander $zipExpander;

    public function __construct()
    {
        parent::__construct();
        $this->zipExpander = new ZipExpander();
    }

    public function handle(): int
    {
        try {
            return $this->runBatch();
        } finally {
            $this->zipExpander->cleanup();
        }
    }

    private function runBatch(): int
    {
        $files = $this->collectFiles();

        if (empty($files)) {
            $this->error('No matching files found.');
            return self::FAILURE;
        }

        $limit = (int) $this->option('limit');
        if ($limit > 0) {
            $files = array_slice($files, 0, $limit);
        }

        $outDir = $this->option('no-save') ? null : $this->resolveOutDir();
        if ($outDir) {
            File::ensureDirectoryExists($outDir);
            $this->info("Writing per-file results to {$outDir}");
        }

        $this->info('Testing ' . count($files) . ' file(s)...');
        $this->newLine();

        $rows   = [];
        $totals = ['items' => 0, 'high' => 0, 'medium' => 0, 'low' => 0, 'errors' => 0];

        foreach ($files as $i => $path) {
            $n = $i + 1;
            $this->line("[{$n}/" . count($files) . "] " . basename($path));

            $result = $this->processFile($path);

            if ($result['error']) {
                $totals['errors']++;
                $this->error('    ' . $result['error']);
            } else {
                $counts = $result['severity_counts'];
                $totals['items']  += $result['item_count'];
                $totals['high']   += $counts['high'];
                $totals['medium'] += $counts['medium'];
                $totals['low']    += $counts['low'];

                $verdict = $result['skipped']
                    ? "skipped ({$result['skip_reason']})"
                    : "{$result['item_count']} item(s)";
                $this->line("    pages={$result['page_count']} large=" . ($result['is_large'] ? 'yes' : 'no')
                    . " -> {$verdict} [" . round($result['elapsed_ms']) . 'ms]');
            }

            $rows[] = [
                basename($path),
                $result['page_count'],
                $result['is_large'] ? 'yes' : 'no',
                $result['error'] ? 'error' : ($result['skipped'] ? 'skipped' : 'ok'),
                $result['item_count'],
                $result['severity_counts']['high'],
                $result['severity_counts']['medium'],
                $result['severity_counts']['low'],
                round($result['elapsed_ms']) . 'ms',
            ];

            if ($outDir) {
                $name = pathinfo($path, PATHINFO_FILENAME) . '.json';
                File::put($outDir . DIRECTORY_SEPARATOR . $name, json_encode($result, JSON_PRETTY_PRINT));
            }
        }

        $this->newLine();
        $this->table(
            ['File', 'Pages', 'Large', 'Status', 'Items', 'High', 'Med', 'Low', 'Time'],
            $rows
        );

        $this->newLine();
        $this->info("Total: {$totals['items']} item(s) — {$totals['high']} high, {$totals['medium']} medium, {$totals['low']} low across "
            . count($files) . " file(s), {$totals['errors']} error(s).");

        return self::SUCCESS;
    }

    /** @return string[] */
    private function collectFiles(): array
    {
        $extensions = array_map(fn ($ext) => strtolower(trim($ext, ". \t\n\r\0\x0B")), explode(',', (string) $this->option('ext')));
        $found      = [];

        foreach ($this->argument('paths') as $path) {
            if (is_dir($path)) {
                foreach (File::allFiles($path) as $file) {
                    $ext = strtolower($file->getExtension());
                    if ($ext === 'zip') {
                        $found = array_merge($found, $this->expandZipMatching($file->getPathname(), $extensions));
                    } elseif (in_array($ext, $extensions, true)) {
                        $found[] = $file->getPathname();
                    }
                }
            } elseif (is_file($path)) {
                if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'zip') {
                    $found = array_merge($found, $this->expandZipMatching($path, $extensions));
                } else {
                    $found[] = $path;
                }
            } else {
                $this->warn("Path not found, skipping: {$path}");
            }
        }

        sort($found);
        return array_values(array_unique($found));
    }

    /** @return string[] extracted member paths matching $extensions */
    private function expandZipMatching(string $zipPath, array $extensions): array
    {
        $this->line('    unzipping ' . basename($zipPath) . '...');

        try {
            $members = $this->zipExpander->expand($zipPath);
        } catch (\Throwable $e) {
            $this->warn('    could not open zip, skipping: ' . $e->getMessage());
            return [];
        }

        $matched = array_values(array_filter(
            array_map(fn ($m) => $m['path'], $members),
            fn ($path) => in_array(strtolower(pathinfo($path, PATHINFO_EXTENSION)), $extensions, true)
        ));

        $this->line('    found ' . count($matched) . ' matching file(s) inside ' . basename($zipPath) . ' (' . count($members) . ' total).');

        return $matched;
    }

    private function resolveOutDir(): string
    {
        $out = $this->option('out');
        if ($out) {
            return rtrim($out, '/\\');
        }

        return storage_path('app/evidence-tests/' . now()->format('Y-m-d_His'));
    }

    private function processFile(string $path): array
    {
        $start = microtime(true);
        $mime  = File::mimeType($path) ?: 'application/octet-stream';
        $size  = File::size($path);

        $result = [
            'file'            => basename($path),
            'path'            => $path,
            'mime'            => $mime,
            'size'            => $size,
            'page_count'      => 0,
            'is_large'        => false,
            'skipped'         => false,
            'skip_reason'     => null,
            'verify'          => null,
            'filter'          => null,
            'ocr'             => null,
            'items'           => [],
            'item_count'      => 0,
            'severity_counts' => ['high' => 0, 'medium' => 0, 'low' => 0],
            'error'           => null,
            'elapsed_ms'      => 0,
        ];

        try {
            // Stage 0: junk pre-filter (mirrors the production small-image skip).
            if (str_starts_with($mime, 'image/') && $size > 0 && $size < config('synapse.min_image_evidence_bytes', 50000)) {
                $result['skipped']     = true;
                $result['skip_reason'] = "small image ({$size} bytes) — likely signature/logo";
                $result['elapsed_ms']  = (microtime(true) - $start) * 1000;
                return $result;
            }

            $isPdf       = str_contains($mime, 'pdf');
            $perPageText = $isPdf ? $this->parsePageText($path) : [];
            $pageCount   = count($perPageText);
            $isLarge     = $pageCount >= self::LARGE_DOC_THRESHOLD;

            $result['page_count'] = $pageCount;
            $result['is_large']   = $isLarge;

            $file = $this->loadFile($path, $mime);

            // ── Stage 1: verify ─────────────────────────────────────────────
            if (!$isLarge && !$this->option('skip-verify')) {
                $verify     = (new VerifyDsarDocumentAgent)->prompt(
                    'Filename: ' . basename($path) . "\nMIME type: {$mime}",
                    attachments: [$file]
                );
                $verifyData = $verify->structured;
                $result['verify'] = $verifyData;

                if (!$verifyData['is_dsar_document'] && in_array($verifyData['confidence'], ['high', 'medium'], true)) {
                    $result['skipped']     = true;
                    $result['skip_reason'] = "verify: not a DSAR document ({$verifyData['confidence']} confidence)";
                    $result['elapsed_ms']  = (microtime(true) - $start) * 1000;
                    return $result;
                }
            }

            // ── Stage 2: filter pages ───────────────────────────────────────
            $relevantPageNums = [];
            $filterData       = null;

            if ($isLarge) {
                foreach ($perPageText as $i => $text) {
                    if (mb_strlen(trim($text)) >= self::MIN_TEXT_CHARS) {
                        $relevantPageNums[] = $i + 1;
                    }
                }
                $result['filter'] = [
                    'method'      => 'heuristic',
                    'total_pages' => $pageCount,
                    'relevant'    => count($relevantPageNums),
                    'excluded'    => $pageCount - count($relevantPageNums),
                ];
            } else {
                $filter     = (new FilterDsarPagesAgent)->prompt('Filename: ' . basename($path), attachments: [$file]);
                $filterData = $filter->structured;
                $result['filter'] = $filterData;

                $relevantPageNums = array_values(array_filter(
                    array_map('intval', explode(',', $filterData['relevant_pages_csv'] ?? '')),
                    fn ($n) => $n > 0
                ));
            }

            $isScanned = false;
            if (empty($relevantPageNums)) {
                if ($isLarge) {
                    // No text layer on any page of a large doc — treat as scanned and fall
                    // back to vision extraction (this test command doesn't run the async OCR
                    // pipeline that production uses for exact page numbers/bounding boxes).
                    $isScanned = true;
                } else {
                    $result['skipped']     = true;
                    $result['skip_reason'] = 'no relevant pages found';
                    $result['elapsed_ms']  = (microtime(true) - $start) * 1000;
                    return $result;
                }
            }

            // ── Stage 3: extract evidence ────────────────────────────────────
            $items = [];

            if ($isScanned) {
                if ($this->option('ocr')) {
                    $ocrResult = $this->runOcr($path, $pageCount, basename($path));
                    $result['ocr'] = $ocrResult;

                    if (isset($ocrResult['error'])) {
                        $result['error']      = $ocrResult['error'];
                        $result['elapsed_ms'] = (microtime(true) - $start) * 1000;
                        return $result;
                    }

                    $items = $ocrResult['items'];
                } elseif ($pageCount > (int) config('ocr.vision_max_pages', 80)) {
                    $result['error'] = "{$pageCount}-page scanned doc exceeds vision_max_pages "
                        . '(' . config('ocr.vision_max_pages', 80) . '); production would route this through '
                        . 'the async OCR pipeline. Pass --ocr to test real OCR extraction instead of vision.';
                    $result['elapsed_ms'] = (microtime(true) - $start) * 1000;
                    return $result;
                } else {
                    $prompt  = 'Filename: ' . basename($path) . "\n"
                        . "This document is scanned (image-only, {$pageCount} pages) with no extractable text layer. "
                        . 'Read the pages visually and extract all evidence, noting the page number where each quote appears.';
                    $extract = (new ExtractDsarEvidenceAgent)->prompt($prompt, attachments: [$file]);
                    $items   = json_decode($extract->structured['items_json'] ?? '[]', true) ?? [];
                }
            } elseif ($isLarge) {
                foreach (array_chunk($relevantPageNums, self::CHUNK_SIZE) as $chunkIndex => $chunkPages) {
                    $chunkText = $this->buildChunkText($chunkPages, $perPageText);
                    $prompt    = 'Filename: ' . basename($path) . ' (chunk ' . ($chunkIndex + 1) . ')' . "\n"
                        . 'Pages in this chunk: ' . implode(', ', $chunkPages) . "\n\n" . $chunkText;
                    $extract   = (new ExtractDsarEvidenceAgent)->prompt($prompt);
                    $items     = array_merge($items, json_decode($extract->structured['items_json'] ?? '[]', true) ?? []);
                }
            } else {
                $relevantPages = implode(', ', $relevantPageNums);
                $excludedNotes = trim($filterData['exclusion_notes'] ?? '');
                $prompt        = 'Filename: ' . basename($path) . "\n"
                    . ($relevantPages !== '' ? "Focus your analysis on pages: {$relevantPages}.\n" : '')
                    . ($excludedNotes !== '' ? "Skip these diagnostic-imagery pages: {$excludedNotes}.\n" : '');
                $extract       = (new ExtractDsarEvidenceAgent)->prompt($prompt, attachments: [$file]);
                $items         = json_decode($extract->structured['items_json'] ?? '[]', true) ?? [];
            }

            $items = array_values(array_filter($items, fn ($item) => !empty($item['quote']) && !empty($item['category'])));

            $result['items']      = $items;
            $result['item_count'] = count($items);
            foreach ($items as $item) {
                $severity = $item['severity'] ?? 'medium';
                if (isset($result['severity_counts'][$severity])) {
                    $result['severity_counts'][$severity]++;
                }
            }
        } catch (RateLimitedException $e) {
            $result['error'] = 'Rate limited: ' . $e->getMessage();
        } catch (\Throwable $e) {
            $result['error'] = $e->getMessage();
        }

        $result['elapsed_ms'] = (microtime(true) - $start) * 1000;
        return $result;
    }

    /** @return string[] per-page text, empty array if not a PDF or parsing failed */
    private function parsePageText(string $path): array
    {
        try {
            $config = new PdfParserConfig();
            $config->setRetainImageContent(false);
            $config->setDecodeMemoryLimit(100 * 1024 * 1024);

            $pdf = (new PdfParser([], $config))->parseFile($path);
            return array_map(fn ($page) => $page->getText(), $pdf->getPages());
        } catch (\Throwable $e) {
            $this->warn('    PDF text parse failed: ' . $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);
    }

    /**
     * Runs the real OCR pipeline (OcrService — tesseract + a rasterizer) synchronously,
     * in-process, over a scanned PDF's pages, then feeds the OCR'd text through the same
     * chunked extraction as production's OcrExtractEvidenceJob. No queue/worker needed — each
     * page's OCR call already blocks until tesseract finishes, this just runs every page in
     * one command invocation instead of across several async job dispatches.
     *
     * @return array{error?: string, pages_ocrd?: int, pages_with_text?: int, page_stats?: array, items?: array}
     */
    private function runOcr(string $path, int $pageCount, string $filename): array
    {
        /** @var OcrService $ocr */
        $ocr = app(OcrService::class);

        if (!$ocr->isAvailable()) {
            return [
                'error' => 'OCR tooling unavailable — install tesseract plus a rasterizer '
                    . "(poppler-utils' pdftoppm, or ghostscript) and check config/ocr.php binaries.",
            ];
        }

        $maxPages = (int) $this->option('ocr-max-pages');
        $lastPage = $maxPages > 0 ? min($pageCount, $maxPages) : $pageCount;

        $this->line("    running OCR on pages 1-{$lastPage} of {$pageCount}...");

        $perPage   = [];
        $pageStats = [];
        for ($page = 1; $page <= $lastPage; $page++) {
            $data           = $ocr->ocrPageData($path, $page);
            $perPage[$page] = $data;
            $pageStats[]    = [
                'page'  => $page,
                'chars' => mb_strlen(trim($data['text'])),
                'words' => count($data['words']),
            ];
        }

        $pagesWithText = array_keys(array_filter($perPage, fn ($p) => trim($p['text']) !== ''));

        $this->line('    OCR done — ' . count($pagesWithText) . "/{$lastPage} page(s) had readable text; extracting...");

        $items = [];
        foreach (array_chunk($pagesWithText, self::CHUNK_SIZE) as $chunkPages) {
            $chunkText = $this->buildOcrChunkText($chunkPages, $perPage);
            $prompt    = "Filename: {$filename} (scanned, OCR'd)\n"
                . 'Pages in this chunk: ' . implode(', ', $chunkPages) . "\n\n" . $chunkText;
            $extract   = (new ExtractDsarEvidenceAgent)->prompt($prompt);
            $items     = array_merge($items, json_decode($extract->structured['items_json'] ?? '[]', true) ?? []);
        }

        return [
            'pages_ocrd'      => $lastPage,
            'pages_with_text' => count($pagesWithText),
            'page_stats'      => $pageStats,
            'items'           => $items,
        ];
    }

    /** @param array<int,array{text:string,words:array}> $perPage */
    private function buildOcrChunkText(array $pageNums, array $perPage): string
    {
        $parts = [];
        foreach ($pageNums as $page) {
            $text = $perPage[$page]['text'] ?? '';
            if (trim($text) !== '') {
                $parts[] = "=== PAGE {$page} ===\n" . trim($text);
            }
        }
        return implode("\n\n", $parts);
    }

    private function loadFile(string $path, string $mime): Files\Document|Files\Image
    {
        return str_starts_with($mime, 'image/')
            ? Files\Image::fromPath($path)
            : Files\Document::fromPath($path);
    }
}
