'use client';

import { useRef, useState, useEffect } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { Camera, Loader2, RotateCcw, AlertTriangle, CheckCircle2, Save } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { DatePicker } from '@/components/ui/date-picker';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { useUIStore } from '@/stores/ui-store';
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { EXPENSE_CATEGORIES, PAYMENT_METHODS } from '@/lib/constants';
import { runTesseract } from '@/lib/ocr/tesseract-client';

// Lazy-loaded compression - the worker bundle isn't on the critical path,
// and most users won't reach this code without first granting camera/file
// access, by which point the module is already paged in.
async function compressReceiptIfHeavy(file: File): Promise<File> {
  // Only compress raster images > ~1 MB. PDFs, SVGs, and small files pass
  // through untouched. Magic-byte check via mime type - the caller is the
  // file picker which trusts the picker output already.
  if (!file.type.startsWith('image/') || file.type === 'image/svg+xml') return file;
  if (file.size < 1 * 1024 * 1024) return file;
  const { default: imageCompression } = await import('browser-image-compression');
  try {
    const compressed = await imageCompression(file, {
      maxSizeMB: 0.5, // ~500 KB target - plenty of resolution for OCR
      maxWidthOrHeight: 2000, // tesseract.js's sweet spot for receipt text
      useWebWorker: true, // off the main thread; UI stays responsive
      // Auto-rotate to EXIF orientation, strip metadata. Phones often
      // store the rotation as EXIF rather than rotating pixels; without
      // this the receipt comes out sideways and OCR confidence tanks.
      preserveExif: false,
      fileType: file.type === 'image/png' ? 'image/jpeg' : file.type,
      initialQuality: 0.85,
    });
    // Browser-image-compression typings always say `File`, but in some
    // runtimes the value comes through as a plain Blob. Belt-and-suspenders:
    // wrap in a File so downstream FormData uses the original filename.
    const blob = compressed as unknown as Blob;
    if (typeof File !== 'undefined' && blob instanceof File) return blob;
    return new File([blob], file.name, { type: blob.type });
  } catch {
    // Fall back to the original - we don't want a corner-case compression
    // bug to block the user from saving an expense.
    return file;
  }
}

// ─── Types ────────────────────────────────────────────────────────────────────

interface ParsedReceipt {
  establishment: string | null;
  date: string | null;
  amount: number | null;
  currency: string | null;
  lineItems: Array<{ description: string; amount: number }>;
  confidence: number;
}

type ScanState =
  | { kind: 'idle' }
  | { kind: 'processing'; engine: 'tesseract' | 'ai' }
  | {
      kind: 'verify';
      parsed: ParsedReceipt;
      source: 'ai' | 'tesseract' | 'manual';
      reason?: string;
      providerError?: string;
    }
  | { kind: 'saving' }
  | { kind: 'saved'; expenseId: string }
  | { kind: 'error'; message: string };

interface ScanResp {
  data: {
    parsed: ParsedReceipt;
    source: 'ai' | 'manual';
    reason?: string;
    provider?: string;
    model?: string;
    providerError?: string;
  };
}

// ─── Form ─────────────────────────────────────────────────────────────────────

interface VerifyFormProps {
  parsed: ParsedReceipt;
  imagePreview: string;
  imageFile: File;
  source: 'ai' | 'tesseract' | 'manual';
  reason?: string;
  providerError?: string;
  onSubmit: (input: {
    establishmentName: string;
    amount: string;
    currency: string;
    expenseDate: string;
    category: string;
    paymentMethod: string;
    description: string;
    file: File;
  }) => void;
  onRetake: () => void;
  saving: boolean;
}

const TODAY = () => new Date().toISOString().slice(0, 10);

function VerifyForm({
  parsed,
  imagePreview,
  imageFile,
  source,
  reason: _reason,
  providerError,
  onSubmit,
  onRetake,
  saving,
}: VerifyFormProps) {
  const [establishmentName, setEstablishment] = useState(parsed.establishment ?? '');
  const [amount, setAmount] = useState(parsed.amount != null ? String(parsed.amount) : '');
  const [currency, setCurrency] = useState((parsed.currency ?? 'USD').toUpperCase());
  const [expenseDate, setExpenseDate] = useState(parsed.date ?? TODAY());
  const [category, setCategory] = useState<string>('other');
  const [paymentMethod, setPaymentMethod] = useState<string>('credit_card');
  const [description, setDescription] = useState('');

  const lowConfidence = source !== 'manual' && parsed.confidence < 0.6;
  const noOcr = source === 'manual';
  const engineLabel = source === 'ai' ? 'AI' : source === 'tesseract' ? 'on-device OCR' : 'manual';

  const banner = noOcr ? (
    <div className="flex items-start gap-2 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900">
      <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
      <div>
        <p className="font-medium">Manual entry mode</p>
        <p className="text-xs mt-0.5">
          {providerError
            ? `We couldn't read the receipt automatically: ${providerError}.`
            : "We couldn't read the receipt automatically."}{' '}
          Fill in the details below to save the expense with the photo attached.
        </p>
      </div>
    </div>
  ) : lowConfidence ? (
    <div className="flex items-start gap-2 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900">
      <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
      <div>
        <p className="font-medium">Low-confidence read - please double-check the fields</p>
        <p className="text-xs mt-0.5">
          {engineLabel} returned {Math.round(parsed.confidence * 100)}% confidence.
        </p>
      </div>
    </div>
  ) : (
    <div className="flex items-start gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm text-emerald-900">
      <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
      <div>
        <p className="font-medium">Receipt parsed - confirm the fields and save</p>
        <p className="text-xs mt-0.5">
          {engineLabel} · {Math.round(parsed.confidence * 100)}% confidence.
        </p>
      </div>
    </div>
  );

  return (
    <form
      className="flex flex-col gap-4"
      onSubmit={(e) => {
        e.preventDefault();
        onSubmit({
          establishmentName,
          amount,
          currency,
          expenseDate,
          category,
          paymentMethod,
          description,
          file: imageFile,
        });
      }}
    >
      {banner}

      <div className="overflow-hidden rounded-lg border border-border">
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={imagePreview}
          alt="Receipt preview"
          className="block w-full max-h-[40vh] object-contain bg-muted"
        />
      </div>

      <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
        <div className="space-y-1.5 sm:col-span-2">
          <Label htmlFor="establishmentName">Vendor / establishment</Label>
          <Input
            id="establishmentName"
            value={establishmentName}
            onChange={(e) => setEstablishment(e.target.value)}
            placeholder="e.g. Marina Fuel Station"
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="amount">Amount</Label>
          <Input
            id="amount"
            type="number"
            step="0.01"
            min="0"
            inputMode="decimal"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
            required
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="currency">Currency</Label>
          <Input
            id="currency"
            value={currency}
            onChange={(e) => setCurrency(e.target.value.toUpperCase())}
            maxLength={3}
            required
          />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="expenseDate">Date</Label>
          <DatePicker id="expenseDate" value={expenseDate} onChange={setExpenseDate} />
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="category">Category</Label>
          <Select value={category} onValueChange={setCategory}>
            <SelectTrigger id="category">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {EXPENSE_CATEGORIES.map((c) => (
                <SelectItem key={c} value={c} className="capitalize">
                  {c.replace(/_/g, ' ')}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <div className="space-y-1.5">
          <Label htmlFor="paymentMethod">Payment method</Label>
          <Select value={paymentMethod} onValueChange={setPaymentMethod}>
            <SelectTrigger id="paymentMethod">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {PAYMENT_METHODS.map((p) => (
                <SelectItem key={p} value={p} className="capitalize">
                  {p.replace(/_/g, ' ')}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <div className="space-y-1.5 sm:col-span-2">
          <Label htmlFor="description">Notes (optional)</Label>
          <Textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            rows={2}
          />
        </div>
      </div>

      <div className="flex flex-col gap-2 sm:flex-row">
        <Button
          type="submit"
          disabled={saving || !amount}
          className="h-12 text-base sm:flex-1"
          data-testid="scan-save"
        >
          {saving ? (
            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
          ) : (
            <Save className="mr-2 h-4 w-4" />
          )}
          Save expense
        </Button>
        <Button
          type="button"
          variant="outline"
          onClick={onRetake}
          disabled={saving}
          className="h-12 text-base"
        >
          <RotateCcw className="mr-2 h-4 w-4" />
          Retake
        </Button>
      </div>
    </form>
  );
}

// ─── Shell ────────────────────────────────────────────────────────────────────

interface ScanShellProps {
  /** Per-port brand logo resolved server-side by the page wrapper.
   *  Null hides the logo block - we never fall back to another tenant's
   *  imagery. */
  logoUrl?: string | null;
  portName?: string | null;
  /** CM-6: when true, skip ALL parsing - open an empty form for manual entry. */
  manualEntry?: boolean;
}

export function ScanShell({ logoUrl, portName, manualEntry = false }: ScanShellProps = {}) {
  const router = useRouter();
  const portSlug = useUIStore((s) => s.currentPortSlug);
  const fileRef = useRef<HTMLInputElement>(null);
  const [state, setState] = useState<ScanState>({ kind: 'idle' });
  const [imagePreview, setImagePreview] = useState<string | null>(null);
  // Track the (possibly compressed) File alongside its preview URL.
  // Reading fileRef.current.files[0] during render trips the React
  // Compiler's ref-purity rule + would point at the *raw* uploaded
  // bytes (pre-compression), not the resized bytes we OCR'd.
  const [currentFile, setCurrentFile] = useState<File | null>(null);

  // Revoke blob URL on unmount.
  useEffect(() => {
    return () => {
      if (imagePreview) URL.revokeObjectURL(imagePreview);
    };
  }, [imagePreview]);

  async function handleFile(rawFile: File) {
    // Compress oversized phone photos to ~500 KB in a WebWorker BEFORE
    // we hand the bytes to tesseract or the server. Receipts from 12MP
    // cameras are usually 4-8 MB; this drops them to ~250-500 KB without
    // visible quality loss for text OCR. Mobile bandwidth + the server's
    // sharp pipeline both benefit.
    const file = await compressReceiptIfHeavy(rawFile);
    if (imagePreview) URL.revokeObjectURL(imagePreview);
    setImagePreview(URL.createObjectURL(file));
    setCurrentFile(file);

    // CM-6: manual-entry mode - the port admin disabled scanning. Skip
    // Tesseract AND the server call entirely; go straight to an empty form.
    if (manualEntry) {
      setState({
        kind: 'verify',
        parsed: {
          establishment: null,
          date: null,
          amount: null,
          currency: null,
          lineItems: [],
          confidence: 0,
        },
        source: 'manual',
        reason: 'manual-mode',
      });
      return;
    }

    setState({ kind: 'processing', engine: 'tesseract' });

    // Always run Tesseract first - it's free, on-device, and gives us a
    // baseline parse we can fall back to if the optional AI pass is off
    // or fails. The WASM bundle dynamic-imports inside `runTesseract`.
    let tesseract: Awaited<ReturnType<typeof runTesseract>> | null = null;
    try {
      tesseract = await runTesseract(file);
    } catch (err) {
      // Tesseract.js itself failed (corrupt image, OOM, etc). Don't bail -
      // give the user the manual form so they can still save the expense.
      setState({
        kind: 'verify',
        parsed: {
          establishment: null,
          date: null,
          amount: null,
          currency: null,
          lineItems: [],
          confidence: 0,
        },
        source: 'manual',
        reason: 'tesseract-error',
        providerError: err instanceof Error ? err.message : 'On-device OCR failed',
      });
      return;
    }

    // Now ask the server whether AI is enabled for this port. If it is,
    // the server runs the configured provider and returns a richer parse;
    // otherwise we keep the Tesseract result.
    setState({ kind: 'processing', engine: 'ai' });
    try {
      const fd = new FormData();
      fd.append('file', file);
      const portId = useUIStore.getState().currentPortId;
      const headers = new Headers();
      if (portId) headers.set('X-Port-Id', portId);
      const res = await fetch('/api/v1/expenses/scan-receipt', {
        method: 'POST',
        body: fd,
        credentials: 'include',
        headers,
      });
      if (!res.ok) throw new Error(`Server returned ${res.status}`);
      const body = (await res.json()) as ScanResp;

      if (body.data.source === 'ai' && body.data.parsed.confidence >= tesseract.parsed.confidence) {
        // AI did at least as well as Tesseract - prefer its result.
        setState({
          kind: 'verify',
          parsed: body.data.parsed,
          source: 'ai',
          reason: body.data.reason,
          providerError: body.data.providerError,
        });
        return;
      }

      // Either AI is disabled (`source: 'manual', reason: 'ai-disabled'`),
      // not configured, or it underperformed - fall back to Tesseract.
      setState({
        kind: 'verify',
        parsed: tesseract.parsed,
        source: 'tesseract',
        reason: body.data.reason,
        providerError: body.data.providerError,
      });
    } catch {
      // Server unreachable - still let the user verify with the Tesseract
      // result and save the expense. We don't surface the network error
      // because the local parse is usable.
      setState({
        kind: 'verify',
        parsed: tesseract.parsed,
        source: 'tesseract',
      });
    }
  }

  async function handleSubmit(input: {
    establishmentName: string;
    amount: string;
    currency: string;
    expenseDate: string;
    category: string;
    paymentMethod: string;
    description: string;
    file: File;
  }) {
    setState({ kind: 'saving' });
    try {
      // Upload the image (multipart - apiFetch wraps JSON, so use raw fetch).
      const fd = new FormData();
      fd.append('file', input.file);
      fd.append('category', 'receipt');
      const portId = useUIStore.getState().currentPortId;
      const headers = new Headers();
      if (portId) headers.set('X-Port-Id', portId);
      const upRes = await fetch('/api/v1/files/upload', {
        method: 'POST',
        body: fd,
        credentials: 'include',
        headers,
      });
      if (!upRes.ok) throw new Error(`Upload failed: ${upRes.status}`);
      const upJson = (await upRes.json()) as { data: { id: string } };

      const expense = await apiFetch<{ data: { id: string } }>(`/api/v1/expenses`, {
        method: 'POST',
        body: {
          establishmentName: input.establishmentName || undefined,
          amount: input.amount,
          currency: input.currency,
          expenseDate: input.expenseDate,
          category: input.category,
          paymentMethod: input.paymentMethod,
          description: input.description || undefined,
          receiptFileIds: [upJson.data.id],
          paymentStatus: 'unpaid',
        },
      });

      setState({ kind: 'saved', expenseId: expense.data.id });
    } catch (err) {
      setState({
        kind: 'error',
        message: err instanceof Error ? err.message : 'Save failed',
      });
    }
  }

  function reset() {
    if (imagePreview) {
      URL.revokeObjectURL(imagePreview);
      setImagePreview(null);
    }
    setCurrentFile(null);
    setState({ kind: 'idle' });
    if (fileRef.current) fileRef.current.value = '';
  }

  return (
    <main
      // pb-[max(1.5rem,env(safe-area-inset-bottom))] - mobile-pwa-auditor
      // caught that the "Save expense" button was sitting flush against
      // the home indicator on iPhone 14/15 in standalone PWA mode
      // (viewportFit:cover + statusBarStyle:default exposes the safe-
      // area inset, but the original `py-6` ignored it).
      className="mx-auto flex min-h-dvh w-full max-w-xl flex-col gap-4 px-4 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] sm:py-10"
    >
      {/* Brand header - logo centered, page title underneath. Establishes
          the standalone identity (this is the PWA home for the scanner). */}
      <header className="flex flex-col items-center gap-3">
        {logoUrl ? (
          <Image
            src={logoUrl}
            alt={portName ?? 'Logo'}
            width={64}
            height={64}
            className="rounded-full shadow-md"
            priority
            unoptimized
          />
        ) : null}
        <div className="flex w-full items-center justify-between">
          <h1 className="text-xl font-semibold">Scan a receipt</h1>
          {state.kind !== 'idle' ? (
            <Button variant="ghost" size="sm" onClick={reset}>
              Start over
            </Button>
          ) : null}
        </div>
      </header>

      {state.kind === 'idle' ? (
        <section className="flex flex-1 flex-col items-center justify-center gap-4 rounded-2xl border-2 border-dashed border-border bg-muted/20 p-8 text-center">
          <Camera className="h-12 w-12 text-muted-foreground/60" aria-hidden />
          <div>
            <p className="text-base font-medium">Tap to capture a receipt</p>
            <p className="mt-1 text-xs text-muted-foreground">
              Use your camera or pick an image from your library. We&apos;ll read it and pre-fill
              the form for you to confirm.
            </p>
          </div>
          <Button
            type="button"
            className="h-12 px-6 text-base"
            onClick={() => fileRef.current?.click()}
            data-testid="scan-capture"
          >
            <Camera className="mr-2 h-5 w-5" />
            Capture receipt
          </Button>
          <input
            ref={fileRef}
            type="file"
            accept="image/*"
            capture="environment"
            className="hidden"
            onChange={(e) => {
              const f = e.target.files?.[0];
              if (f) void handleFile(f);
            }}
          />
        </section>
      ) : null}

      {state.kind === 'processing' ? (
        <section className="flex flex-1 flex-col items-center justify-center gap-3 py-12">
          <Loader2 className="h-10 w-10 animate-spin text-brand" />
          <p className="text-sm text-muted-foreground">
            {state.engine === 'tesseract' ? 'Reading on-device…' : 'Refining with AI…'}
          </p>
        </section>
      ) : null}

      {state.kind === 'verify' && imagePreview ? (
        <VerifyForm
          parsed={state.parsed}
          imagePreview={imagePreview}
          imageFile={currentFile as File}
          source={state.source}
          reason={state.reason}
          providerError={state.providerError}
          onSubmit={handleSubmit}
          onRetake={reset}
          saving={false}
        />
      ) : null}

      {state.kind === 'saving' ? (
        <section className="flex flex-1 flex-col items-center justify-center gap-3 py-12">
          <Loader2 className="h-10 w-10 animate-spin text-brand" />
          <p className="text-sm text-muted-foreground">Saving expense…</p>
        </section>
      ) : null}

      {state.kind === 'saved' ? (
        <section className="flex flex-1 flex-col items-center justify-center gap-3 rounded-2xl border border-emerald-200 bg-emerald-50 p-8 text-center">
          <CheckCircle2 className="h-12 w-12 text-emerald-600" />
          <p className="text-base font-semibold text-emerald-900">Expense saved</p>
          <div className="flex gap-2">
            <Button onClick={reset} variant="outline" data-testid="scan-another">
              Scan another
            </Button>
            <Button
              onClick={() => router.push(`/${portSlug}/expenses/${state.expenseId}` as never)}
            >
              View expense
            </Button>
          </div>
        </section>
      ) : null}

      {state.kind === 'error' ? (
        <section
          className={cn(
            'flex flex-col items-center gap-3 rounded-2xl border border-destructive/30 bg-destructive/5 p-6 text-center',
          )}
        >
          <AlertTriangle className="h-10 w-10 text-destructive" />
          <p className="text-base font-medium text-destructive">{state.message}</p>
          <Button onClick={reset} variant="outline">
            Try again
          </Button>
        </section>
      ) : null}
    </main>
  );
}
