<?php

namespace App\Http\Controllers\Admin;

use App\Jobs\Crawl\IndexDocumentJob;
use App\Models\Agent;
use App\Models\Document;
use App\Models\Source;
use App\Services\Parsers\ParserRegistry;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class UploadController
{
    public function __construct(private ParserRegistry $parsers) {}

    public function store(Request $request, Agent $agent): RedirectResponse
    {
        $request->user()->can('update', $agent) || abort(403);

        $request->validate([
            'files' => ['required', 'array', 'max:10'],
            'files.*' => ['file', 'max:51200'], // 50 MB
        ]);

        $source = Source::create([
            'agent_id' => $agent->id,
            'type' => 'file',
            'status' => 'crawling',
            'config' => ['filenames' => collect($request->file('files'))->map(fn ($f) => $f->getClientOriginalName())->all()],
        ]);

        $createdDocs = 0;
        /** @var array<int, string> $unsupported  filenames with no parser */
        $unsupported = [];
        /** @var array<int, string> $parseErrors  filename: reason */
        $parseErrors = [];
        /** @var array<int, string> $empty  filename returned 0 text */
        $empty = [];

        foreach ($request->file('files') as $file) {
            $name = (string) $file->getClientOriginalName();
            $extension = strtolower((string) $file->getClientOriginalExtension());
            $parser = $this->parsers->for($extension);

            if ($parser === null) {
                $unsupported[] = $name;

                continue;
            }

            // Parser exceptions (corrupted PDF, scanned-only PDF with
            // no text layer, encrypted DOCX, malformed CSV) used to
            // bubble out as 500 errors and abort the entire batch.
            // Catch per-file so one bad upload doesn't sink the rest;
            // record the reason on the source so the admin can see
            // exactly which file broke.
            try {
                $bytes = (string) file_get_contents($file->getRealPath());
                $segments = $parser->parse($bytes, $name);
            } catch (\Throwable $e) {
                Log::warning('upload.parser_failed', [
                    'agent_id' => $agent->id,
                    'source_id' => $source->id,
                    'filename' => $name,
                    'extension' => $extension,
                    'error' => $e->getMessage(),
                ]);
                $parseErrors[] = $name.': '.Str::limit($e->getMessage(), 160);

                continue;
            }

            // Drop blank segments before creating Document rows. Some
            // PDF parsers return empty pages for the title page or a
            // page that only contains an image — those would produce
            // a Document with no extractable text and the indexing job
            // would silently early-return on chunks=[], leaving an
            // orphan document with zero chunks.
            $segments = array_values(array_filter(
                array_map(static fn ($s) => trim((string) $s), $segments),
                static fn (string $s) => $s !== '',
            ));

            if ($segments === []) {
                $empty[] = $name;

                continue;
            }

            foreach ($segments as $i => $text) {
                $document = Document::create([
                    'source_id' => $source->id,
                    'agent_id' => $agent->id,
                    'url' => null,
                    'title' => $name.($i > 0 ? " (segment {$i})" : ''),
                    'content_hash' => hash('sha256', $text),
                    'text_path' => null,
                    'lang' => null,
                    'fetched_at' => now(),
                ]);
                $createdDocs++;
                IndexDocumentJob::dispatch($document->id, $text)->onQueue('index');
            }
        }

        $issues = [];
        if ($unsupported !== []) {
            $issues[] = 'Unsupported file type: '.implode(', ', $unsupported);
        }
        if ($empty !== []) {
            $issues[] = 'No extractable text: '.implode(', ', $empty)
                .' (likely a scanned PDF with no text layer — OCR not yet supported).';
        }
        foreach ($parseErrors as $err) {
            $issues[] = $err;
        }

        $source->forceFill([
            'status' => $createdDocs > 0 ? 'indexed' : 'failed',
            'last_synced_at' => now(),
            'error' => $issues === [] ? null : Str::limit(implode(' | ', $issues), 480),
        ])->save();

        // The "searchable in ~60s" note matters because Cloudflare Vectorize
        // has eventual consistency on metadata-filtered queries: even after
        // a successful upsert, agent_id-filtered searches return 0 hits
        // for ~30-60s while the metadata index propagates. Without the
        // copy the admin uploads a file, immediately tests the chatbot,
        // gets "I don't know", and assumes indexing is broken.
        $msg = "Uploaded — {$createdDocs} segments queued for indexing. The agent can answer questions about these files within ~60 seconds.";
        if ($issues !== []) {
            // Surface per-file warnings even on partial success so the
            // admin doesn't think a corrupted file silently uploaded.
            $msg .= ' Issues: '.implode(' | ', $issues);

            return back()->with($createdDocs > 0 ? 'warning' : 'error', $msg);
        }

        return back()->with('success', $msg);
    }
}
