"use client";
import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useDropzone } from "react-dropzone";
import { useRouter } from "next/navigation";
import { UploadCloud, Loader2, CheckCircle2, AlertCircle, Copy, X, Check, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import { parseDroppedFilename } from "@/lib/jav/dropParser";
import { lookupActressesByNames, type ActressLookupResult } from "@/app/actions/actressLookup";
import { IngestPreviewDialog, type ActressDecisions, type IngestPreviewFile } from "./IngestPreviewDialog";
import { CollisionReviewDialog, type CollisionReviewItem, type CollisionDecision } from "./CollisionReviewDialog";

interface FailedFile {
  filename: string;
  reason: string;
}

interface DuplicateFile {
  filename: string;
  code: string | null;
  /** Metadata the client sent up alongside this re-upload — these are
   *  the merges that the dedup branch in ingestFile applies to the
   *  existing row (actress links via INSERT OR IGNORE, autoAssign tag,
   *  autoAssign collection). Surfaced in the summary so the user can
   *  tell whether anything actually changed for a "duplicate". */
  mergedActresses: string[];
  mergedTag: string | null;
  mergedCollectionId: number | null;
}

interface UploadState {
  total: number;
  done: number;
  duplicates: DuplicateFile[];
  failed: FailedFile[];
  finished: boolean;
}

interface PendingJob {
  image: File;
  nfo?: File;
  parsed: ReturnType<typeof parseDroppedFilename>;
  /** Stable id used to correlate jobs with collision-review decisions on
   *  re-submit. Composed from filename + size to survive React re-renders. */
  jobId: string;
}

export function DropZone({
  compact = false,
  autoAssign,
}: {
  compact?: boolean;
  autoAssign?: { tagName?: string; collectionId?: number };
}) {
  const router = useRouter();
  const [state, setState] = useState<UploadState | null>(null);
  const [copied, setCopied] = useState(false);
  const [preview, setPreview] = useState<{
    jobs: PendingJob[];
    files: IngestPreviewFile[];
    lookup: ActressLookupResult[];
  } | null>(null);
  const [collisionReview, setCollisionReview] = useState<{
    items: CollisionReviewItem[];
    jobsById: Record<string, PendingJob>;
    actressDecisions?: ActressDecisions;
    linkedActressNames?: Record<string, string>;
  } | null>(null);

  const runJobs = useCallback(async (
    jobs: PendingJob[],
    actressDecisions?: ActressDecisions,
    linkedActressNames?: Record<string, string>,
    collisionDecisions?: Record<string, CollisionDecision>,
  ) => {
    let done = 0;
    const duplicates: DuplicateFile[] = [];
    const failed: FailedFile[] = [];
    const collisions: CollisionReviewItem[] = [];
    setState({ total: jobs.length, done: 0, duplicates: [], failed: [], finished: false });

    const CONCURRENCY = 6;

    const acceptedActressesFor = (job: PendingJob): string[] => {
      if (!actressDecisions || job.parsed.actresses.length === 0) return [];
      const perFile = actressDecisions[job.parsed.original] ?? {};
      return job.parsed.actresses.flatMap((n) => {
        const d = perFile[n.toLowerCase()];
        if (d === "link") return [linkedActressNames?.[n.toLowerCase()] ?? n];
        if (d === "create") return [n];
        return [];
      });
    };

    const buildBody = (job: PendingJob, accepted: string[]): FormData => {
      const fd = new FormData();
      fd.append("file", job.image);
      if (job.nfo) fd.append("nfo", job.nfo);
      if (autoAssign?.tagName) fd.append("autoTag", autoAssign.tagName);
      if (autoAssign?.collectionId != null) fd.append("autoCollection", String(autoAssign.collectionId));
      if (job.parsed.targetFilename !== job.image.name) {
        fd.append("targetFilename", job.parsed.targetFilename);
      }
      if (accepted.length > 0) fd.append("actressNames", JSON.stringify(accepted));
      const collisionDecision = collisionDecisions?.[job.jobId];
      if (collisionDecision) fd.append("onCollision", collisionDecision);
      return fd;
    };

    const runOne = async (job: PendingJob): Promise<void> => {
      const accepted = acceptedActressesFor(job);
      try {
        const res = await fetch("/api/upload", { method: "POST", body: buildBody(job, accepted) });
        if (!res.ok) {
          let reason = `HTTP ${res.status}`;
          try { const j = await res.json(); if (j?.error) reason = String(j.error); } catch {}
          failed.push({ filename: job.image.name, reason });
        } else {
          const data = await res.json();
          if (data.collision) {
            collisions.push({
              jobId: job.jobId,
              filename: job.image.name,
              code: data.code ?? null,
              ...data.collision,
            });
          } else if (data.duplicate) {
            duplicates.push({
              filename: job.image.name,
              code: data.code ?? null,
              mergedActresses: accepted,
              mergedTag: autoAssign?.tagName ?? null,
              mergedCollectionId: autoAssign?.collectionId ?? null,
            });
          }
        }
      } catch (e) {
        failed.push({ filename: job.image.name, reason: (e as Error).message || "network error" });
      }
      done++;
      setState({ total: jobs.length, done, duplicates: duplicates.slice(), failed: failed.slice(), finished: false });
    };

    let cursor = 0;
    const workers = Array.from({ length: Math.min(CONCURRENCY, jobs.length) }, async () => {
      while (true) {
        const idx = cursor++;
        if (idx >= jobs.length) return;
        await runOne(jobs[idx]);
      }
    });
    await Promise.all(workers);

    if (collisions.length > 0 && !collisionDecisions) {
      // First pass surfaced code collisions — pause and let the user
      // decide. Decisions are funnelled back through runJobs with the
      // colliding subset.
      const jobsById: Record<string, PendingJob> = {};
      for (const j of jobs) jobsById[j.jobId] = j;
      setCollisionReview({ items: collisions, jobsById, actressDecisions, linkedActressNames });
      // Mark the run finished for the non-colliding jobs; the resolution
      // pass will produce a fresh state when it begins.
      setState({ total: jobs.length, done, duplicates, failed, finished: true });
      return;
    }

    setState({ total: jobs.length, done, duplicates, failed, finished: true });
    router.refresh();
  }, [autoAssign, router]);

  const onDrop = useCallback(async (accepted: File[]) => {
    if (accepted.length === 0) return;

    const byBase = new Map<string, { image?: File; nfo?: File }>();
    for (const f of accepted) {
      const base = f.name.replace(/\.[^.]+$/, "").toLowerCase();
      const slot = byBase.get(base) ?? {};
      const ext = f.name.toLowerCase().match(/\.([^.]+)$/)?.[1];
      if (ext === "nfo" || ext === "xml") slot.nfo = f;
      else slot.image = f;
      byBase.set(base, slot);
    }

    const jobs: PendingJob[] = Array.from(byBase.values())
      .filter((j) => j.image)
      .map((j) => ({
        image: j.image!,
        nfo: j.nfo,
        parsed: parseDroppedFilename(j.image!.name),
        jobId: `${j.image!.name}::${j.image!.size}`,
      }));

    if (jobs.length === 0) return;

    const filesWithActresses = jobs.filter((j) => j.parsed.actresses.length > 0);
    if (filesWithActresses.length === 0) {
      await runJobs(jobs);
      return;
    }

    const uniqueNames = Array.from(
      new Set(filesWithActresses.flatMap((j) => j.parsed.actresses.map((s) => s.toLowerCase()))),
    );
    const displayNames = uniqueNames.map((lc) => {
      const found = filesWithActresses.flatMap((j) => j.parsed.actresses).find((n) => n.toLowerCase() === lc);
      return found ?? lc;
    });
    const lookup = await lookupActressesByNames(displayNames);

    setPreview({
      jobs,
      files: jobs.map((j) => ({
        original: j.parsed.original,
        targetFilename: j.parsed.targetFilename,
        code: j.parsed.code,
        actresses: j.parsed.actresses,
      })),
      lookup,
    });
  }, [runJobs]);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      "image/png": [".png"],
      "image/jpeg": [".jpg", ".jpeg"],
      "image/webp": [".webp"],
      "application/xml": [".nfo", ".xml"],
      "text/xml": [".nfo", ".xml"],
    },
  });

  function exportFailures() {
    if (!state || state.failed.length === 0) return;
    const lines = ["filename\treason", ...state.failed.map((f) => `${f.filename}\t${f.reason.replace(/\t/g, " ")}`)];
    const blob = new Blob([lines.join("\n") + "\n"], { type: "text/tab-separated-values;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    const stamp = new Date().toISOString().replace(/[:.]/g, "-");
    a.download = `pinkudex-failed-imports-${stamp}.tsv`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  }

  async function copyFailures() {
    if (!state || state.failed.length === 0) return;
    const text = state.failed.map((f) => `${f.filename}\t${f.reason}`).join("\n");
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    } catch {
      // Fallback: select-all in a textarea
      const ta = document.createElement("textarea");
      ta.value = text;
      ta.style.position = "fixed";
      ta.style.opacity = "0";
      document.body.appendChild(ta);
      ta.select();
      document.execCommand("copy");
      ta.remove();
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    }
  }

  const inProgress = state && !state.finished;
  // "added" = brand new rows in the DB. Duplicates are excluded because
  // they hit an existing row by SHA — no insert happened.
  const dupCount = state?.duplicates.length ?? 0;
  const addedCount = state ? state.done - state.failed.length - dupCount : 0;
  const progressPct = state && state.total > 0 ? Math.round((state.done / state.total) * 100) : 0;
  const [showDuplicates, setShowDuplicates] = useState(true);

  return (
    <>
      <div
        {...getRootProps()}
        className={cn(
          "relative cursor-pointer rounded-2xl border-2 border-dashed transition-all overflow-hidden group",
          compact ? "px-4 py-3 flex items-center gap-3" : "px-12 py-7 flex flex-col items-center gap-3 text-center",
          isDragActive
            ? "border-[var(--color-cyan)] bg-[color-mix(in_oklch,var(--color-cyan)_8%,transparent)] shadow-[var(--shadow-glow-cyan)]"
            : "border-[var(--color-glass-border-strong)] hover:border-[var(--color-cyan)] hover:bg-[var(--color-glass)]",
        )}
      >
        <input {...getInputProps()} />
        {state ? (
          <>
            {inProgress ? (
              <Loader2 className={cn("text-[var(--color-cyan)] animate-spin", compact ? "w-5 h-5" : "w-8 h-8")} />
            ) : state.failed.length > 0 ? (
              <AlertCircle className={cn("text-[var(--color-coral)]", compact ? "w-5 h-5" : "w-8 h-8")} />
            ) : (
              <CheckCircle2 className={cn("text-[var(--color-mint)]", compact ? "w-5 h-5" : "w-8 h-8")} />
            )}
            <div className={compact ? "text-sm" : ""}>
              <div className="font-medium">{state.done} / {state.total} processed</div>
              <div className="text-xs text-[var(--color-fg-muted)] mt-0.5 flex items-center gap-2 justify-center flex-wrap">
                <span className="text-[var(--color-mint)]">{addedCount} added</span>
                {dupCount > 0 && <span>· {dupCount} already in library</span>}
                {state.failed.length > 0 && <span className="text-[var(--color-coral)]">· {state.failed.length} failed</span>}
              </div>
            </div>
          </>
        ) : (
          <>
            <UploadCloud className={cn("text-[var(--color-fg-dim)] group-hover:text-[var(--color-cyan)] transition-colors", compact ? "w-5 h-5" : "w-10 h-10")} />
            <div>
              <div className={cn("font-medium", compact ? "text-sm" : "text-base")}>
                {isDragActive ? "Drop To Import" : "Drop Covers Here"}
              </div>
              {!compact && (
                <div className="text-sm text-[var(--color-fg-muted)] mt-1">
                  JPG, PNG, WEBP — drop a matching <span className="font-mono">.nfo</span> alongside to auto-fill metadata.
                </div>
              )}
            </div>
          </>
        )}

        {state && (
          <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-glass-border)]">
            <div
              className="h-full bg-[var(--color-cyan)] transition-all"
              style={{ width: `${progressPct}%` }}
            />
          </div>
        )}
      </div>

      {state && state.finished && (
        <ImportSummaryModal
          addedCount={addedCount}
          duplicates={state.duplicates}
          failed={state.failed}
          showDuplicates={showDuplicates}
          onToggleDuplicates={() => setShowDuplicates((s) => !s)}
          copied={copied}
          onCopyFailures={copyFailures}
          onExportFailures={exportFailures}
          onClose={() => setState(null)}
        />
      )}

      {preview && (
        <IngestPreviewDialog
          files={preview.files}
          lookup={preview.lookup}
          onCancel={() => setPreview(null)}
          onSkipActresses={() => {
            const jobs = preview.jobs;
            setPreview(null);
            void runJobs(jobs);
          }}
          onConfirm={(decisions) => {
            const jobs = preview.jobs;
            const linkedNames = Object.fromEntries(
              preview.lookup
                .filter((r) => r.match)
                .map((r) => [r.name.toLowerCase(), r.match!.name]),
            );
            setPreview(null);
            void runJobs(jobs, decisions, linkedNames);
          }}
        />
      )}

      {collisionReview && (
        <CollisionReviewDialog
          items={collisionReview.items}
          onCancel={() => setCollisionReview(null)}
          onConfirm={(decisions) => {
            const cr = collisionReview;
            setCollisionReview(null);
            const replays = cr.items.map((it) => cr.jobsById[it.jobId]).filter((j): j is PendingJob => !!j);
            void runJobs(replays, cr.actressDecisions, cr.linkedActressNames, decisions);
          }}
        />
      )}
    </>
  );
}

function ImportSummaryModal({
  addedCount,
  duplicates,
  failed,
  showDuplicates,
  onToggleDuplicates,
  copied,
  onCopyFailures,
  onExportFailures,
  onClose,
}: {
  addedCount: number;
  duplicates: DuplicateFile[];
  failed: FailedFile[];
  showDuplicates: boolean;
  onToggleDuplicates: () => void;
  copied: boolean;
  onCopyFailures: () => void;
  onExportFailures: () => void;
  onClose: () => void;
}) {
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (typeof document === "undefined") return null;
  const dupCount = duplicates.length;
  const dupsWithMerges = duplicates.filter((d) =>
    d.mergedActresses.length > 0 || d.mergedTag != null || d.mergedCollectionId != null,
  );
  const dupsUntouched = dupCount - dupsWithMerges.length;
  const tone = failed.length > 0 ? "error" : "ok";
  const total = addedCount + dupCount + failed.length;

  // One-line plain-English narrative explaining what just happened. Helps
  // when the count summary alone is ambiguous (e.g. "1 imported · 1 dup"
  // could either mean a fresh insert plus a separate duplicate, or a
  // single file that was deduped).
  const narrative = (() => {
    const parts: string[] = [];
    if (addedCount > 0) parts.push(`Added ${addedCount} new cover${addedCount === 1 ? "" : "s"} to the library.`);
    if (dupCount > 0) {
      if (dupsWithMerges.length > 0 && dupsUntouched > 0) {
        parts.push(`${dupCount} file${dupCount === 1 ? "" : "s"} matched existing covers — no new rows were inserted; ${dupsWithMerges.length} had fresh metadata merged into the existing entry.`);
      } else if (dupsWithMerges.length > 0) {
        parts.push(`${dupCount} file${dupCount === 1 ? " was" : "s were"} already in the library — no new rows were inserted, but the new metadata you picked was merged into the existing entr${dupCount === 1 ? "y" : "ies"}.`);
      } else {
        parts.push(`${dupCount} file${dupCount === 1 ? " was" : "s were"} already in the library — no new rows were inserted and there was no extra metadata to merge.`);
      }
    }
    if (failed.length > 0) parts.push(`${failed.length} file${failed.length === 1 ? "" : "s"} failed.`);
    if (parts.length === 0) parts.push(`Nothing was processed.`);
    return parts.join(" ");
  })();

  return createPortal(
    <div
      className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
      onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div className="relative w-[min(95vw,720px)] max-h-[85vh] flex flex-col rounded-2xl border border-[var(--color-glass-border-strong)] bg-[var(--color-bg-0)] shadow-2xl">
        <div className="px-5 py-4 border-b border-[var(--color-glass-border)] flex items-start justify-between gap-3">
          <div className="flex items-start gap-3 min-w-0">
            {tone === "ok" ? (
              <CheckCircle2 className="w-6 h-6 text-[var(--color-mint)] shrink-0 mt-0.5" />
            ) : (
              <AlertCircle className="w-6 h-6 text-[var(--color-coral)] shrink-0 mt-0.5" />
            )}
            <div className="min-w-0">
              <div className="text-base font-medium">Import complete · {total} file{total === 1 ? "" : "s"}</div>
              <div className="text-xs text-[var(--color-fg-muted)] mt-0.5 flex items-center gap-2 flex-wrap">
                <span className="text-[var(--color-mint)] font-medium">{addedCount} added</span>
                {dupCount > 0 && (
                  <button
                    type="button"
                    onClick={onToggleDuplicates}
                    className="hover:text-[var(--color-fg)] underline-offset-2 hover:underline"
                    title="Toggle duplicate list"
                  >
                    · {dupCount} already in library
                  </button>
                )}
                {failed.length > 0 && (
                  <span className="text-[var(--color-coral)] font-medium">· {failed.length} failed</span>
                )}
              </div>
              <p className="text-xs text-[var(--color-fg-dim)] mt-2 leading-relaxed">{narrative}</p>
            </div>
          </div>
          <button
            type="button"
            onClick={onClose}
            className="text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] shrink-0"
            title="Dismiss"
          >
            <X className="w-4 h-4" />
          </button>
        </div>

        {(failed.length > 0 || (showDuplicates && dupCount > 0)) && (
          <div className="overflow-y-auto px-5 py-3 space-y-3">
            {failed.length > 0 && (
              <section>
                <div className="flex items-center justify-between mb-2">
                  <div className="text-[11px] uppercase tracking-wider font-mono text-[var(--color-fg-muted)]">
                    Failed ({failed.length})
                  </div>
                  <div className="flex items-center gap-2">
                    <button
                      type="button"
                      onClick={onCopyFailures}
                      className="flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-md border border-[var(--color-glass-border)] hover:bg-[var(--color-glass)] text-[var(--color-fg-dim)] hover:text-[var(--color-fg)]"
                      title="Copy failed filenames + reasons to clipboard"
                    >
                      {copied ? <Check className="w-3.5 h-3.5 text-[var(--color-mint)]" /> : <Copy className="w-3.5 h-3.5" />}
                      {copied ? "Copied" : "Copy"}
                    </button>
                    <button
                      type="button"
                      onClick={onExportFailures}
                      className="flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-md border border-[var(--color-glass-border)] hover:bg-[var(--color-glass)] text-[var(--color-fg-dim)] hover:text-[var(--color-fg)]"
                      title="Download failed list as a .tsv file"
                    >
                      <Download className="w-3.5 h-3.5" />
                      Export
                    </button>
                  </div>
                </div>
                <div className="max-h-60 overflow-y-auto text-xs font-mono space-y-0.5 rounded-md border border-[var(--color-glass-border)] p-2">
                  {failed.map((f) => (
                    <div key={f.filename} className="flex items-baseline gap-2 text-[var(--color-fg-dim)]">
                      <AlertCircle className="w-3 h-3 text-[var(--color-coral)] shrink-0 translate-y-0.5" />
                      <span className="truncate text-[var(--color-fg)]">{f.filename}</span>
                      <span className="text-[var(--color-fg-muted)] truncate">— {f.reason}</span>
                    </div>
                  ))}
                </div>
              </section>
            )}

            {showDuplicates && dupCount > 0 && (
              <section>
                <div className="text-[11px] uppercase tracking-wider font-mono text-[var(--color-fg-muted)] mb-2">
                  Already in library ({dupCount})
                </div>
                <div className="max-h-72 overflow-y-auto text-xs space-y-1 rounded-md border border-[var(--color-glass-border)] p-2">
                  {duplicates.map((d) => {
                    const merges: string[] = [];
                    if (d.mergedActresses.length > 0) {
                      merges.push(`linked actress${d.mergedActresses.length === 1 ? "" : "es"}: ${d.mergedActresses.join(", ")}`);
                    }
                    if (d.mergedTag) merges.push(`tag: ${d.mergedTag}`);
                    if (d.mergedCollectionId != null) merges.push(`collection #${d.mergedCollectionId}`);
                    return (
                      <div key={d.filename} className="flex items-start gap-2 font-mono text-[var(--color-fg-dim)]">
                        <span className="text-[10px] uppercase tracking-wider text-[var(--color-cyan)] shrink-0 w-20 truncate translate-y-0.5" title={d.code ?? "no code"}>
                          {d.code ?? "—"}
                        </span>
                        <div className="min-w-0 flex-1">
                          <div className="truncate text-[var(--color-fg)]">{d.filename}</div>
                          {merges.length > 0 ? (
                            <div className="text-[var(--color-mint)]/80 text-[11px] mt-0.5">
                              + {merges.join(" · ")}
                            </div>
                          ) : (
                            <div className="text-[var(--color-fg-muted)] text-[11px] mt-0.5 italic">
                              no new metadata to merge
                            </div>
                          )}
                        </div>
                      </div>
                    );
                  })}
                </div>
                <p className="text-[11px] text-[var(--color-fg-muted)] mt-2 leading-relaxed">
                  These files were already imported earlier (matched by SHA-256 of the bytes). The cover row was kept as-is; any new actress / tag / collection picks shown above were merged into the existing entry via <span className="font-mono">INSERT OR IGNORE</span>, so previous links are untouched.
                </p>
              </section>
            )}
          </div>
        )}

        <div className="px-5 py-3 border-t border-[var(--color-glass-border)] flex items-center justify-end">
          <button
            type="button"
            onClick={onClose}
            className="text-sm px-4 py-1.5 rounded-lg bg-[var(--color-cyan)]/20 text-[var(--color-cyan)] ring-1 ring-[var(--color-cyan)]/50 hover:bg-[var(--color-cyan)]/30"
          >
            Close
          </button>
        </div>
      </div>
    </div>,
    document.body,
  );
}
