"use client";

import * as React from "react";
import Image from "next/image";
import { CircleCheckIcon, CircleXIcon, DownloadIcon, FileSpreadsheetIcon, ImageIcon, RotateCcwIcon, TriangleAlertIcon, UploadIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";

import { TONE_BOX } from "./tones";

/* ------------------------------------- Helpers ------------------------------------- */

export const formatBytes = (n: number) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(0)} KB` : `${Number((n / 1024 / 1024).toFixed(1))} MB`);

/** Friendly reason a file can't be used, checked before anything is uploaded (spec §9: never "413"). */
export function checkFile(file: File, accept: string[], maxBytes: number, kind: "csv" | "image"): string | undefined {
  const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".")).toLowerCase() : "";
  if (!accept.includes(ext)) {
    if (kind === "csv" && (ext === ".xlsx" || ext === ".xls")) return `This file is an Excel workbook (${ext}). Save it as CSV and try again.`;
    if (kind === "image" && ext === ".heic") return "This is an iPhone photo (.heic). Save it as JPG or PNG and try again.";
    return `${file.name} is not a ${accept.map((a) => a.slice(1).toUpperCase()).join(" or ")} file. Choose a ${accept.join(" or ")} file.`;
  }
  if (file.size > maxBytes) return `This file is ${formatBytes(file.size)}. Choose one under ${formatBytes(maxBytes)}.`;
  if (file.size === 0) return "This file is empty. Choose another file.";
  return undefined;
}

/** Minimal RFC 4180 CSV parser: quoted fields, escaped quotes, commas and line breaks inside quotes. */
export function parseCsv(text: string): string[][] {
  const rows: string[][] = [];
  let row: string[] = [];
  let field = "";
  let quoted = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (quoted) {
      if (c === '"' && text[i + 1] === '"') {
        field += '"';
        i++;
      } else if (c === '"') quoted = false;
      else field += c;
    } else if (c === '"') quoted = true;
    else if (c === ",") {
      row.push(field);
      field = "";
    } else if (c === "\n" || c === "\r") {
      if (c === "\r" && text[i + 1] === "\n") i++;
      row.push(field);
      if (row.some((f) => f.trim() !== "")) rows.push(row);
      row = [];
      field = "";
    } else field += c;
  }
  row.push(field);
  if (row.some((f) => f.trim() !== "")) rows.push(row);
  return rows;
}

const toCsv = (rows: (string | number)[][]) => rows.map((r) => r.map((v) => (/[",\n]/.test(String(v)) ? `"${String(v).replace(/"/g, '""')}"` : String(v))).join(",")).join("\n");

export function downloadText(filename: string, text: string, type = "text/csv") {
  const url = URL.createObjectURL(new Blob([text], { type }));
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}

/* ----------------------------------- File upload ----------------------------------- */

type Phase = { state: "idle" } | { state: "uploading"; file: File; pct: number } | { state: "ready"; file: File; note?: string; preview?: string } | { state: "failed"; file?: File; message: string };

/**
 * File upload (spec 01-actions-and-inputs §9). A drop zone with a Choose file button (the real control;
 * drag-and-drop is an extra). Type and size are checked before uploading. Uploads show progress, announced
 * at start, 50% and done. Variants: csv (template link), image (preview, or a link instead), compact (button only).
 * `upload` is the API call; it reports progress 0–100 and may return a note ("212 rows found").
 */
export function FileUpload({
  label,
  hint,
  kind,
  accept,
  maxBytes,
  compact = false,
  disabled = false,
  onTemplate,
  upload,
  onReady,
  onRemove,
  allowUrl = false,
}: {
  label: string;
  hint: string;
  kind: "csv" | "image";
  /** Extensions, e.g. [".csv"] or [".jpg", ".jpeg", ".png"]. */
  accept: string[];
  maxBytes: number;
  /** Button only, no drop zone (table toolbars). */
  compact?: boolean;
  disabled?: boolean;
  onTemplate?: () => void;
  upload: (file: File, onProgress: (pct: number) => void) => Promise<string | void>;
  onReady?: (file: File) => void;
  onRemove?: () => void;
  /** Image only: "Use a link instead". */
  allowUrl?: boolean;
}) {
  const [phase, setPhase] = React.useState<Phase>({ state: "idle" });
  const [drag, setDrag] = React.useState(false);
  const [announce, setAnnounce] = React.useState("");
  const [urlMode, setUrlMode] = React.useState(false);
  const [url, setUrl] = React.useState("");
  const [urlError, setUrlError] = React.useState("");
  const inputRef = React.useRef<HTMLInputElement>(null);
  const announced = React.useRef(new Set<string>());
  const id = React.useId();

  const start = async (file: File) => {
    const problem = checkFile(file, accept, maxBytes, kind);
    if (problem) {
      setPhase({ state: "failed", file, message: problem });
      setAnnounce(problem);
      return;
    }
    announced.current = new Set();
    setPhase({ state: "uploading", file, pct: 0 });
    setAnnounce(`Uploading ${file.name}`);
    try {
      const note = await upload(file, (pct) => {
        setPhase((p) => (p.state === "uploading" && p.file === file ? { ...p, pct } : p));
        if (pct >= 50 && !announced.current.has("50")) {
          announced.current.add("50");
          setAnnounce(`${file.name}, 50% uploaded`);
        }
      });
      const preview = kind === "image" ? URL.createObjectURL(file) : undefined;
      setPhase({ state: "ready", file, note: note || undefined, preview });
      setAnnounce(`${file.name} ready${note ? `, ${note}` : ""}`);
      onReady?.(file);
    } catch (e) {
      const message = e instanceof Error && e.message ? e.message : "Couldn't upload. Check the connection and try again.";
      setPhase({ state: "failed", file, message });
      setAnnounce(message);
    }
  };

  const remove = () => {
    if (phase.state === "ready" && phase.preview) URL.revokeObjectURL(phase.preview);
    setPhase({ state: "idle" });
    setAnnounce("File removed");
    onRemove?.();
  };

  const chooser = (
    <>
      <Button type="button" variant={compact ? "outline" : "secondary"} size={compact ? "sm" : "default"} disabled={disabled} onClick={() => inputRef.current?.click()} aria-label={`Choose ${kind === "csv" ? "CSV file" : "image"} for ${label.toLowerCase()}`}>
        <UploadIcon data-icon="inline-start" />
        {compact ? `Import ${kind === "csv" ? "CSV" : "image"}` : "Choose file"}
      </Button>
    </>
  );

  const failed = phase.state === "failed";
  const imageReady = phase.state === "ready" && !!phase.preview;
  const Icon = kind === "csv" ? FileSpreadsheetIcon : ImageIcon;

  const actions = (
    <>
      {phase.state === "failed" && phase.file && !checkFile(phase.file, accept, maxBytes, kind) ? (
        <Button variant="ghost" size="sm" onClick={() => void start(phase.file!)}>
          <RotateCcwIcon data-icon="inline-start" />
          Retry
        </Button>
      ) : null}
      {phase.state === "ready" ? (
        <Button variant="ghost" size="sm" onClick={() => inputRef.current?.click()} disabled={disabled}>
          Replace
        </Button>
      ) : null}
      {phase.state !== "uploading" ? (
        <Button variant="ghost" size="sm" className="text-status-error-text hover:text-status-error-text" onClick={remove} disabled={disabled}>
          Remove
        </Button>
      ) : null}
    </>
  );

  return (
    <div className="space-y-2">
      <div className="flex flex-wrap items-baseline justify-between gap-2">
        <span id={`${id}-label`} className="text-sm font-medium">
          {label}
        </span>
        {kind === "csv" && onTemplate ? (
          <button type="button" onClick={onTemplate} className="inline-flex cursor-pointer items-center gap-1 text-sm font-medium text-primary underline-offset-2 hover:underline">
            <DownloadIcon className="size-3.5" /> Download template
          </button>
        ) : null}
      </div>

      {compact ? (
        <div className="flex items-center gap-2">{chooser}</div>
      ) : urlMode ? (
        <div className="space-y-2">
          <div className="flex gap-2">
            <Input value={url} onChange={(e) => {
                setUrl(e.target.value);
                setUrlError("");
              }} placeholder="https://…/image.jpg" aria-label={`Image link for ${label.toLowerCase()}`} aria-invalid={!!urlError} />
            <Button
              variant="secondary"
              onClick={() => {
                try {
                  const u = new URL(url.trim());
                  if (!/^https?:$/.test(u.protocol)) throw new Error();
                  if (!/\.(jpe?g|png|webp)(\?.*)?$/i.test(u.pathname)) return setUrlError("The link must end in .jpg, .png or .webp.");
                  setPhase({ state: "ready", file: new File([], u.pathname.split("/").pop() || "image"), note: "Linked image", preview: u.toString() });
                  setUrlMode(false);
                } catch {
                  setUrlError("Enter a full link starting with https://");
                }
              }}
            >
              Use link
            </Button>
          </div>
          {urlError ? <p className="text-xs text-status-error-text">{urlError}</p> : null}
          <button type="button" className="cursor-pointer text-sm text-primary underline-offset-2 hover:underline" onClick={() => setUrlMode(false)}>
            Upload a file instead
          </button>
        </div>
      ) : phase.state !== "ready" && phase.state !== "uploading" ? (
        <div
          onDragOver={(e) => {
            e.preventDefault();
            if (!disabled) setDrag(true);
          }}
          onDragLeave={() => setDrag(false)}
          onDrop={(e) => {
            e.preventDefault();
            setDrag(false);
            const f = e.dataTransfer.files?.[0];
            if (f && !disabled) void start(f);
          }}
          className={cn(
            "flex flex-col items-center gap-2 rounded-lg border border-dashed p-6 text-center transition-colors",
            disabled ? "border-input bg-muted text-muted-foreground" : drag ? "border-solid border-ring bg-accent" : failed ? "border-[var(--color-border-input-error)]" : "border-input hover:border-[var(--color-border-strong)]",
          )}
        >
          <Icon className="size-6 text-muted-foreground" aria-hidden />
          <p className="text-sm text-muted-foreground">{drag ? "Drop to upload" : "Drag a file here or"}</p>
          {chooser}
          <p className="text-xs text-muted-foreground">{hint}</p>
          {kind === "image" && allowUrl ? (
            <button type="button" className="cursor-pointer text-xs text-primary underline-offset-2 hover:underline" onClick={() => setUrlMode(true)} disabled={disabled}>
              Use a link instead
            </button>
          ) : null}
        </div>
      ) : null}

      {/* File row */}
      {phase.state !== "idle" && !urlMode ? (
        <div className={cn("flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border px-3 py-2", failed ? "border-status-error-border bg-status-error-bg" : "border-border bg-card")}>
          {imageReady ? (
            <span className="relative size-20 shrink-0 self-start overflow-hidden rounded-md border border-border bg-muted">
              <Image src={(phase as { preview: string }).preview} alt="" fill sizes="80px" unoptimized className="object-cover" />
            </span>
          ) : (
            <Icon className="size-5 shrink-0 text-muted-foreground" aria-hidden />
          )}
          <div className="min-w-0 flex-1">
            <p className="truncate text-sm font-medium">{phase.state === "failed" && !phase.file ? "Upload failed" : (phase as { file: File }).file?.name}</p>
            {phase.state === "uploading" ? (
              <div className="mt-1 flex items-center gap-2">
                <div role="progressbar" aria-label={`Uploading ${phase.file.name}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={phase.pct} className="h-1.5 flex-1 overflow-hidden rounded-sm bg-muted">
                  <div className="h-full bg-primary transition-[width]" style={{ width: `${phase.pct}%` }} />
                </div>
                <span className="text-xs text-muted-foreground tabular-nums">{phase.pct}%</span>
              </div>
            ) : phase.state === "ready" ? (
              <p className="flex items-center gap-1 text-xs text-status-success-text">
                <CircleCheckIcon className="size-3.5" aria-hidden /> Ready{phase.file.size ? ` · ${formatBytes(phase.file.size)}` : ""}
                {phase.note ? ` · ${phase.note}` : ""}
              </p>
            ) : null}
            {/* An image preview takes the room, so its actions sit under the name. */}
            {imageReady ? <div className="mt-1.5 -ml-2.5 flex items-center">{actions}</div> : null}
          </div>
          {imageReady ? null : actions}
          {phase.state === "failed" ? (
            <p className="flex basis-full items-start gap-1 text-xs text-status-error-text">
              <CircleXIcon className="mt-px size-3.5 shrink-0" aria-hidden /> {phase.message}
            </p>
          ) : null}
        </div>
      ) : null}

      {/* Always mounted so Replace still works once the drop zone is gone. */}
      <input
        ref={inputRef}
        type="file"
        accept={accept.join(",")}
        className="hidden"
        tabIndex={-1}
        onChange={(e) => {
          const f = e.target.files?.[0];
          e.target.value = "";
          if (f) void start(f);
        }}
      />
      <p className="sr-only" aria-live="polite">
        {announce}
      </p>
    </div>
  );
}

/* --------------------------------- CSV import review --------------------------------- */

export type CsvColumn = {
  key: string;
  /** Header expected in the file (matched ignoring case and spaces). */
  header: string;
  required?: boolean;
  validate?: (value: string) => string | undefined;
};

export type CsvReviewResult = { valid: Record<string, string>[]; problems: { row: number; column: string; value: string; message: string }[]; duplicates: number[]; missingHeaders: string[]; total: number };

/** Read and check every row before anything is imported. `uniqueKey` flags repeated values (e.g. phone). */
export function reviewCsv(text: string, columns: CsvColumn[], uniqueKey?: string): CsvReviewResult {
  const rows = parseCsv(text.replace(/^﻿/, ""));
  const norm = (s: string) => s.trim().toLowerCase().replace(/[\s_-]+/g, "");
  const head = (rows[0] ?? []).map(norm);
  const idx = Object.fromEntries(columns.map((c) => [c.key, head.indexOf(norm(c.header))]));
  const missingHeaders = columns.filter((c) => c.required && idx[c.key] < 0).map((c) => c.header);
  const valid: Record<string, string>[] = [];
  const problems: CsvReviewResult["problems"] = [];
  const duplicates: number[] = [];
  const seen = new Map<string, number>();
  if (missingHeaders.length) return { valid, problems, duplicates, missingHeaders, total: Math.max(0, rows.length - 1) };
  rows.slice(1).forEach((r, i) => {
    const line = i + 2; // spreadsheet row number (header is row 1)
    const rec: Record<string, string> = {};
    let ok = true;
    for (const c of columns) {
      const v = (idx[c.key] >= 0 ? (r[idx[c.key]] ?? "") : "").trim();
      rec[c.key] = v;
      const msg = !v && c.required ? `${c.header} is missing.` : v ? c.validate?.(v) : undefined;
      if (msg) {
        problems.push({ row: line, column: c.header, value: v, message: msg });
        ok = false;
      }
    }
    if (ok && uniqueKey) {
      const k = rec[uniqueKey];
      if (seen.has(k)) {
        duplicates.push(line);
        problems.push({ row: line, column: columns.find((c) => c.key === uniqueKey)!.header, value: k, message: `Same as row ${seen.get(k)}; only the first is imported.` });
        ok = false;
      } else seen.set(k, line);
    }
    if (ok) valid.push(rec);
  });
  return { valid, problems, duplicates, missingHeaders, total: rows.length - 1 };
}

/**
 * Import review: what the file contains before anything is imported. Counts, every problem with its row,
 * column and fix, a downloadable error report, then "Import N valid rows". Rows with problems are skipped,
 * never imported half-right.
 */
export function CsvImportReview({
  result,
  noun,
  importing,
  onImport,
  onChooseAnother,
}: {
  result: CsvReviewResult;
  /** "customers", "suppliers" */
  noun: string;
  importing?: boolean;
  onImport: () => void;
  onChooseAnother: () => void;
}) {
  const badRows = new Set(result.problems.map((p) => p.row)).size;
  if (result.missingHeaders.length) {
    return (
      <div role="alert" className={cn("space-y-2 rounded-lg border p-3 text-sm", TONE_BOX.error)}>
        <p className="flex items-center gap-2 font-semibold">
          <CircleXIcon className="size-4" aria-hidden /> The file is missing {result.missingHeaders.length === 1 ? "a column" : "columns"}: {result.missingHeaders.join(", ")}
        </p>
        <p>Use the template so the columns match, then choose the file again.</p>
        <Button size="sm" variant="outline" className="bg-background" onClick={onChooseAnother}>
          Choose another file
        </Button>
      </div>
    );
  }
  return (
    <section aria-label="Import review" className="space-y-3">
      <dl className="grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-border bg-border text-sm sm:grid-cols-4">
        {[
          ["Rows in file", result.total],
          [`Ready to import`, result.valid.length],
          ["Rows with problems", badRows],
          ["Duplicates", result.duplicates.length],
        ].map(([k, v]) => (
          <div key={k as string} className="bg-card px-3 py-2.5">
            <dt className="text-xs text-muted-foreground">{k}</dt>
            <dd className="text-base font-semibold tabular-nums">{v}</dd>
          </div>
        ))}
      </dl>
      {result.problems.length ? (
        <div className={cn("space-y-2 rounded-lg border p-3", TONE_BOX.warning)}>
          <div className="flex flex-wrap items-center justify-between gap-2 text-sm">
            <p className="flex items-center gap-2 font-semibold">
              <TriangleAlertIcon className="size-4" aria-hidden /> {badRows} {badRows === 1 ? "row is" : "rows are"} skipped until fixed
            </p>
            <Button
              size="sm"
              variant="outline"
              className="bg-background"
              onClick={() => downloadText(`${noun}-import-problems.csv`, toCsv([["row", "column", "value", "problem"], ...result.problems.map((p) => [p.row, p.column, p.value, p.message])]))}
            >
              <DownloadIcon data-icon="inline-start" />
              Download error report
            </Button>
          </div>
          <div className="max-h-56 overflow-y-auto rounded-md border border-status-warning-border bg-card text-foreground">
            <Table className="text-sm">
              <TableHeader>
                <TableRow>
                  <TableHead className="w-14 text-right">Row</TableHead>
                  <TableHead>Column</TableHead>
                  <TableHead>Value</TableHead>
                  <TableHead>Problem</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {result.problems.map((p, i) => (
                  <TableRow key={i}>
                    <TableCell className="text-right tabular-nums">{p.row}</TableCell>
                    <TableCell>{p.column}</TableCell>
                    <TableCell className="max-w-40 truncate font-mono text-xs">{p.value || "(empty)"}</TableCell>
                    <TableCell className="whitespace-normal">{p.message}</TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </div>
        </div>
      ) : (
        <p className={cn("rounded-lg border px-3 py-2 text-sm", TONE_BOX.success)}>Every row is ready to import.</p>
      )}
      <div className="flex flex-wrap justify-end gap-2">
        <Button variant="outline" onClick={onChooseAnother} disabled={importing}>
          Choose another file
        </Button>
        <Button onClick={onImport} disabled={importing || result.valid.length === 0}>
          {importing ? "Importing…" : `Import ${result.valid.length} ${result.valid.length === 1 ? noun.replace(/s$/, "") : noun}`}
        </Button>
      </div>
    </section>
  );
}
