"use client";

import { useMemo, useRef, useState } from "react";
import JSZip from "jszip";
import {
  ArrowDownToLine, Check, ChevronDown, FileImage, FolderOpen, ImagePlus,
  RotateCcw, ShieldCheck, Sparkles, Trash2, X, Zap,
} from "lucide-react";
import { SiteShell } from "@/components/site-shell";

type ItemStatus = "queued" | "converting" | "done" | "error";
type BatchItem = { id: string; file: File; status: ItemStatus; progress: number; width?: number; height?: number; output?: Blob; error?: string };
type LoadedImage = { source: CanvasImageSource; width: number; height: number; cleanup: () => void };

type LegacyFileEntry = {
  isFile: boolean;
  isDirectory: boolean;
  file?: (callback: (file: File) => void, error?: (reason: DOMException) => void) => void;
  createReader?: () => { readEntries: (callback: (entries: LegacyFileEntry[]) => void, error?: (reason: DOMException) => void) => void };
};

const browserFormats = ["image/png", "image/jpeg", "image/webp", "image/avif", "image/gif", "image/bmp", "image/svg+xml", "image/x-icon"];

function bytes(value: number) {
  if (value < 1024) return `${value} B`;
  if (value < 1024 ** 2) return `${(value / 1024).toFixed(1)} KB`;
  return `${(value / 1024 ** 2).toFixed(value > 10 * 1024 ** 2 ? 0 : 1)} MB`;
}

function outputName(name: string) {
  return `${name.replace(/\.[^/.]+$/, "") || "image"}.webp`;
}

function createItemId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}

function downloadBlob(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = name;
  anchor.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

async function imageFromBlob(blob: Blob): Promise<LoadedImage> {
  const url = URL.createObjectURL(blob);
  const image = new Image();
  image.decoding = "async";
  image.src = url;
  await image.decode();
  return { source: image, width: image.naturalWidth, height: image.naturalHeight, cleanup: () => URL.revokeObjectURL(url) };
}

async function loadImage(file: File): Promise<LoadedImage> {
  const lower = file.name.toLowerCase();
  if (lower.endsWith(".heic") || lower.endsWith(".heif") || file.type.includes("heic") || file.type.includes("heif")) {
    const { default: heic2any } = await import("heic2any");
    const result = await heic2any({ blob: file, toType: "image/png" });
    return imageFromBlob(Array.isArray(result) ? result[0] : result);
  }
  if (lower.endsWith(".tif") || lower.endsWith(".tiff") || file.type.includes("tiff")) {
    const UTIF = await import("utif");
    const buffer = await file.arrayBuffer();
    const ifds = UTIF.decode(buffer);
    if (!ifds.length) throw new Error("No image found in this TIFF file");
    UTIF.decodeImage(buffer, ifds[0]);
    const rgba = UTIF.toRGBA8(ifds[0]);
    const canvas = document.createElement("canvas");
    canvas.width = ifds[0].width;
    canvas.height = ifds[0].height;
    const context = canvas.getContext("2d");
    if (!context) throw new Error("Canvas is not available");
    context.putImageData(new ImageData(new Uint8ClampedArray(rgba), canvas.width, canvas.height), 0, 0);
    return { source: canvas, width: canvas.width, height: canvas.height, cleanup: () => undefined };
  }
  if (!browserFormats.includes(file.type) && !/\.(png|jpe?g|webp|avif|gif|bmp|svg|ico)$/i.test(file.name)) throw new Error("Unsupported image format");
  return imageFromBlob(file);
}

function canvasToWebp(canvas: HTMLCanvasElement, quality: number) {
  return new Promise<Blob>((resolve, reject) => {
    canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("This browser cannot encode WebP")), "image/webp", quality / 100);
  });
}

async function filesFromEntry(entry: LegacyFileEntry): Promise<File[]> {
  if (entry.isFile && entry.file) {
    return [await new Promise<File>((resolve, reject) => entry.file!(resolve, reject))];
  }
  if (!entry.isDirectory || !entry.createReader) return [];
  const reader = entry.createReader();
  const children: LegacyFileEntry[] = [];
  while (true) {
    const next = await new Promise<LegacyFileEntry[]>((resolve, reject) => reader.readEntries(resolve, reject));
    if (!next.length) break;
    children.push(...next);
  }
  return (await Promise.all(children.map(filesFromEntry))).flat();
}

async function filesFromDrop(dataTransfer: DataTransfer) {
  const entries = Array.from(dataTransfer.items)
    .map((item) => (item as DataTransferItem & { webkitGetAsEntry?: () => LegacyFileEntry | null }).webkitGetAsEntry?.())
    .filter((entry): entry is LegacyFileEntry => Boolean(entry));
  if (!entries.length) return Array.from(dataTransfer.files);
  return (await Promise.all(entries.map(filesFromEntry))).flat();
}

export default function Home() {
  const [items, setItems] = useState<BatchItem[]>([]);
  const [quality, setQuality] = useState(82);
  const [resize, setResize] = useState("original");
  const [customWidth, setCustomWidth] = useState(1920);
  const [running, setRunning] = useState(false);
  const [dragging, setDragging] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);
  const folderInputRef = useRef<HTMLInputElement>(null);

  const done = items.filter((item) => item.status === "done");
  const errors = items.filter((item) => item.status === "error");
  const incompleteCount = items.length - done.length;
  const originalBytes = items.reduce((total, item) => total + item.file.size, 0);
  const convertedBytes = done.reduce((total, item) => total + (item.output?.size ?? 0), 0);
  const overall = items.length ? Math.round(items.reduce((sum, item) => sum + item.progress, 0) / items.length) : 0;
  const savings = originalBytes && done.length === items.length ? Math.round((1 - convertedBytes / originalBytes) * 100) : null;
  const accepted = useMemo(() => ".png,.jpg,.jpeg,.webp,.avif,.gif,.bmp,.svg,.ico,.heic,.heif,.tif,.tiff,image/*", []);
  const qualityRatio = (quality - 30) / 70;
  const qualityFill = `calc(${qualityRatio * 100}% + ${(0.5 - qualityRatio) * 17}px)`;

  function addFiles(list: FileList | File[]) {
    const incoming = Array.from(list).filter((file) => file.type.startsWith("image/") || /\.(png|jpe?g|webp|avif|gif|bmp|svg|ico|heic|heif|tiff?)$/i.test(file.name));
    setItems((current) => [...current, ...incoming.map((file) => ({ id: `${file.name}-${file.size}-${file.lastModified}-${createItemId()}`, file, status: "queued" as const, progress: 0 }))]);
  }

  async function convertAll() {
    if (!items.length || running) return;
    const batchQuality = quality;
    const batchResize = resize;
    const batchCustomWidth = customWidth;
    setRunning(true);
    for (const target of items.filter((item) => item.status !== "done")) {
      setItems((current) => current.map((item) => item.id === target.id ? { ...item, status: "converting", progress: 18, error: undefined } : item));
      let loaded: LoadedImage | undefined;
      try {
        loaded = await loadImage(target.file);
        const requested = batchResize === "original" ? loaded.width : batchResize === "custom" ? batchCustomWidth : Number(batchResize);
        const width = Math.min(loaded.width, Math.max(1, requested));
        const height = Math.max(1, Math.round((loaded.height / loaded.width) * width));
        setItems((current) => current.map((item) => item.id === target.id ? { ...item, progress: 55, width, height } : item));
        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;
        const context = canvas.getContext("2d", { alpha: true });
        if (!context) throw new Error("Canvas is not available");
        context.imageSmoothingEnabled = true;
        context.imageSmoothingQuality = "high";
        context.drawImage(loaded.source, 0, 0, width, height);
        const output = await canvasToWebp(canvas, batchQuality);
        setItems((current) => current.map((item) => item.id === target.id ? { ...item, status: "done", progress: 100, output } : item));
      } catch (error) {
        setItems((current) => current.map((item) => item.id === target.id ? { ...item, status: "error", progress: 100, error: error instanceof Error ? error.message : "Conversion failed" } : item));
      } finally {
        loaded?.cleanup();
      }
      await new Promise((resolve) => requestAnimationFrame(resolve));
    }
    setRunning(false);
  }

  async function downloadAll() {
    if (!done.length) return;
    if (done.length === 1) {
      downloadBlob(done[0].output!, outputName(done[0].file.name));
      return;
    }
    const zip = new JSZip();
    const used = new Map<string, number>();
    done.forEach((item) => {
      let name = outputName(item.file.name);
      const count = used.get(name) ?? 0;
      used.set(name, count + 1);
      if (count) name = name.replace(/\.webp$/, `-${count + 1}.webp`);
      zip.file(name, item.output!);
    });
    downloadBlob(await zip.generateAsync({ type: "blob", compression: "STORE" }), `batchforge-${new Date().toISOString().slice(0, 10)}.zip`);
  }

  function reset() { setItems([]); setRunning(false); }

  return (
    <SiteShell>
      <section className="workspace family-container family-intro-offset">
        <div className="intro">
          <div><p className="eyebrow"><Zap size={14} fill="currentColor" /> LOCAL BATCH CONVERTER</p><h1 className="family-hero-title">Drop it. <em>WebP it.</em></h1><p className="family-hero-description">Convert individual images or entire folders to WebP in one pass.</p></div>
          <div className="format-cloud" aria-label="Supported formats"><span>PNG</span><span>JPG</span><span>HEIC</span><span>TIFF</span><span>AVIF</span><span>SVG</span><b>→ WEBP</b></div>
        </div>

        <div className="app-grid">
          <section className="main-panel">
            <div className={`dropzone ${dragging ? "is-dragging" : ""} ${items.length ? "compact" : ""}`}
              onDragEnter={(event) => { event.preventDefault(); if (!running) setDragging(true); }} onDragOver={(event) => event.preventDefault()}
              onDragLeave={(event) => { if (event.currentTarget === event.target) setDragging(false); }}
              onDrop={async (event) => { event.preventDefault(); setDragging(false); if (!running) addFiles(await filesFromDrop(event.dataTransfer)); }}
              onClick={() => { if (!running) inputRef.current?.click(); }} role="group" aria-label="Add images" aria-disabled={running}>
              <input data-testid="file-input" ref={inputRef} hidden disabled={running} multiple accept={accepted} type="file" onChange={(event) => { if (event.target.files) addFiles(event.target.files); event.target.value = ""; }} />
              <input data-testid="folder-input" ref={(node) => { folderInputRef.current = node; node?.setAttribute("webkitdirectory", ""); }} hidden disabled={running} multiple accept={accepted} type="file" onChange={(event) => { if (event.target.files) addFiles(event.target.files); event.target.value = ""; }} />
              <div className="drop-icon"><ImagePlus size={28} /></div>
              <div><strong>{dragging ? "Release to add images" : items.length ? "Add more images" : "Drop images or folders here"}</strong><span>{items.length ? "Keep building this batch" : "or choose files or a folder"}</span></div>
              <div className="browse-actions"><button disabled={running} type="button" onClick={(event) => { event.stopPropagation(); inputRef.current?.click(); }}><ImagePlus size={17} /> Browse files</button><button disabled={running} type="button" onClick={(event) => { event.stopPropagation(); folderInputRef.current?.click(); }}><FolderOpen size={17} /> Browse folder</button></div>
            </div>

            {items.length > 0 && <div className="queue-card family-card">
              <div className="queue-head"><div><h2>Conversion queue</h2><span>{items.length} {items.length === 1 ? "file" : "files"} · {bytes(originalBytes)}</span></div><button className="clear" onClick={reset} disabled={running}><Trash2 size={15} /> Clear</button></div>
              <div className="file-list">{items.map((item) => <div className="file-row" key={item.id}>
                <div className={`file-icon ${item.status}`}><FileImage size={18} /></div>
                <div className="file-info"><strong title={item.file.name}>{item.file.name}</strong><span>{bytes(item.file.size)}{item.width ? ` · ${item.width} × ${item.height}` : ""}{item.error ? ` · ${item.error}` : ""}</span>{item.status === "converting" && <i style={{ width: `${item.progress}%` }} />}</div>
                <div className={`status ${item.status}`}>{item.status === "done" && <Check size={14} />}{item.status === "error" && <X size={14} />}{item.status === "converting" ? "Converting" : item.status === "done" ? bytes(item.output?.size ?? 0) : item.status === "error" ? "Failed" : "Ready"}</div>
                {item.output && <button className="icon-button" title="Download file" aria-label={`Download ${outputName(item.file.name)}`} onClick={() => downloadBlob(item.output!, outputName(item.file.name))}><ArrowDownToLine size={17} /></button>}
                {!running && !item.output && <button className="icon-button" title="Remove file" aria-label={`Remove ${item.file.name}`} onClick={() => setItems((current) => current.filter((entry) => entry.id !== item.id))}><X size={17} /></button>}
              </div>)}</div>{running && <div className="overall"><span style={{ width: `${overall}%` }} /></div>}
            </div>}
          </section>

          <aside className="settings-card family-card">
            <div className="settings-title"><div><h2>Output settings</h2><span>WEBP</span></div><button className="reset-settings" disabled={running} aria-label="Reset output settings" title="Reset settings" onClick={() => { setQuality(82); setResize("original"); }}><RotateCcw size={17} /></button></div>
            <label className="field-label" htmlFor="quality"><span>Quality</span><b>{quality}%</b></label>
            <input id="quality" aria-label="WebP quality" disabled={running} className="range" type="range" min="30" max="100" value={quality} onChange={(event) => setQuality(Number(event.target.value))} style={{ "--value": qualityFill } as React.CSSProperties} />
            <div className="range-labels"><span>Smaller</span><span>Sharper</span></div>
            <label className="field-label second" htmlFor="max-width"><span>Max width</span></label>
            <div className="select-wrap"><select id="max-width" disabled={running} value={resize} onChange={(event) => setResize(event.target.value)}><option value="original">Keep original size</option><option value="2560">2560 px</option><option value="1920">1920 px</option><option value="1280">1280 px</option><option value="custom">Custom width</option></select><ChevronDown size={16} /></div>
            {resize === "custom" && <div className="custom-width"><input aria-label="Custom maximum width" disabled={running} type="number" min="1" max="16000" value={customWidth} onChange={(event) => setCustomWidth(Number(event.target.value))} /><span>px</span></div>}
            {done.length === items.length && done.length > 0 ? <button data-testid="download-all" className="primary success family-primary" onClick={downloadAll}><ArrowDownToLine size={18} /> Download {done.length === 1 ? "WebP" : "ZIP"}</button> : <><button data-testid="convert" className="primary family-primary" disabled={!items.length || running} onClick={convertAll}>{running ? <><span className="spinner" /> Converting {overall}%</> : <><Zap size={18} fill="currentColor" /> {errors.length > 0 && errors.length === incompleteCount ? "Retry failed" : "Convert"}</>}</button>{!running && done.length > 0 && errors.length > 0 && <button className="secondary" onClick={downloadAll}><ArrowDownToLine size={17} /> Download {done.length} successful</button>}</>}
            {savings !== null && <p className={`savings ${savings < 0 ? "negative" : ""}`}>{savings >= 0 ? `${savings}% smaller than originals` : `${Math.abs(savings)}% larger than originals`}</p>}
          </aside>
        </div>

        <div className="benefits"><div><Zap size={17} /><span><b>No batch cap</b>No set file-count limit. Practical capacity depends on device memory.</span></div><div><ShieldCheck size={17} /><span><b>100% local · nothing uploaded</b>Conversion happens inside your browser.</span></div><div><Sparkles size={17} /><span><b>Metadata removed</b>Files receive consistent WebP names.</span></div></div>
      </section>
    </SiteShell>
  );
}
