import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { qk } from "../lib/queryKeys";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
  Crop,
  Eye,
  EyeOff,
  Pause,
  Play,
  Scissors,
  SplitSquareHorizontal,
  Loader2,
  X,
} from "lucide-react";
import clsx from "clsx";
import Modal from "./Modal";
import { notify } from "../lib/notifications";
import { api, type DownloadJob, type EditSpec } from "../lib/api";
import { inputCls, btnCls } from "./ui/form";

// mm:ss.d timecode <-> seconds.
function tc(s: number): string {
  if (!isFinite(s) || s < 0) s = 0;
  const m = Math.floor(s / 60);
  return `${m}:${(s - m * 60).toFixed(1).padStart(4, "0")}`;
}
function parseTc(v: string): number | null {
  v = v.trim();
  if (!v) return null;
  if (v.includes(":")) {
    const [m, s] = v.split(":");
    const mm = Number(m),
      ss = Number(s);
    if (!isFinite(mm) || !isFinite(ss)) return null;
    return mm * 60 + ss;
  }
  const n = Number(v);
  return isFinite(n) ? n : null;
}

type Seg = { id: number; start: number; end: number; keep: boolean };
type Frac = { x: number; y: number; w: number; h: number };
type Drag = { type: "seek" } | { type: "boundary"; bi: number };

const numCls =
  "w-24 rounded-md bg-surface border border-border px-2 py-1 text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-accent/40";
const MIN_SEG = 0.1;

export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClose: () => void }) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const videoRef = useRef<HTMLVideoElement>(null);
  const trackRef = useRef<HTMLDivElement>(null);
  const sid = useRef(1);
  const nid = () => sid.current++;

  const srcDur = job.asset?.duration_s ?? 0;
  const [duration, setDuration] = useState(srcDur);
  const [current, setCurrent] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [trackW, setTrackW] = useState(0);

  // The cut-list: contiguous segments covering [0, duration]. Starts as the whole video, kept.
  const [segments, setSegments] = useState<Seg[]>([{ id: 0, start: 0, end: srcDur, keep: true }]);
  const [selId, setSelId] = useState(0);
  const [output, setOutput] = useState<"separate" | "join">("separate");
  const [cropOn, setCropOn] = useState(false);
  const [crop, setCrop] = useState<Frac>({ x: 0.1, y: 0.1, w: 0.8, h: 0.8 });
  const [accurate, setAccurate] = useState(true);
  const [name, setName] = useState("");

  const fileUrl = useMemo(() => api.downloadFileUrl(job.id), [job.id]);
  const aspect =
    (job.asset?.width && job.asset?.height ? job.asset.width / job.asset.height : 0) || 16 / 9;
  const sb = useQuery({
    queryKey: qk.storyboard(job.id),
    queryFn: () => api.downloadStoryboard(job.id),
    staleTime: Infinity,
    retry: false,
  });
  const strip = sb.data?.available ? sb.data : null;
  const defaultName = job.display_name || job.asset?.title || "";

  // Measure the track for aspect-correct filmstrip tiling.
  useEffect(() => {
    const el = trackRef.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(() => setTrackW(el.clientWidth));
    ro.observe(el);
    setTrackW(el.clientWidth);
    return () => ro.disconnect();
  }, []);

  const onLoaded = () => {
    const v = videoRef.current;
    if (!v) return;
    const d = v.duration && isFinite(v.duration) ? v.duration : srcDur;
    setDuration(d);
    // Reconcile the still-pristine full-length segment to the REAL decoded duration. The initial
    // segment ends at the (integer, often-rounded) metadata `srcDur`; once the media loads we know
    // the true length (e.g. 213.44 vs 213). Without this the untouched last segment stays short, so
    // the no-op guard (isFullSingle: end >= duration) reads false and enables a "Create" that files a
    // clip of an unmodified video and drops its tail. Only touches the untouched segment (end===srcDur).
    setSegments((s) => (s.length === 1 && s[0]!.end === srcDur ? [{ ...s[0]!, end: d }] : s));
  };
  const onTimeUpdate = () => {
    const v = videoRef.current;
    if (v) setCurrent(v.currentTime);
  };
  const seek = (s: number) => {
    const v = videoRef.current;
    if (v) v.currentTime = Math.max(0, Math.min(duration, s));
  };
  const playPause = () => {
    const v = videoRef.current;
    if (!v) return;
    v.paused ? v.play() : v.pause();
  };

  // --- cut-list mutations ---
  const boundaries = useMemo(() => segments.slice(0, -1).map((s) => s.end), [segments]);
  const splitAtPlayhead = () => {
    const time = current;
    setSegments((segs) => {
      const i = segs.findIndex((s) => time > s.start + MIN_SEG && time < s.end - MIN_SEG);
      if (i < 0) return segs;
      const s = segs[i]!; // i >= 0 from findIndex, so segs[i] exists
      const b: Seg = { id: nid(), start: time, end: s.end, keep: s.keep };
      const next = [...segs.slice(0, i), { ...s, end: time }, b, ...segs.slice(i + 1)];
      return next;
    });
  };
  const moveBoundary = (bi: number, time: number) =>
    setSegments((segs) => {
      if (bi < 0 || bi >= segs.length - 1) return segs;
      // bi is in [0, length-2] here, so bi and bi+1 both index existing segments.
      const lo = segs[bi]!.start + MIN_SEG;
      const hi = segs[bi + 1]!.end - MIN_SEG;
      const tt = Math.max(lo, Math.min(hi, time));
      const copy = [...segs];
      copy[bi] = { ...copy[bi]!, end: tt };
      copy[bi + 1] = { ...copy[bi + 1]!, start: tt };
      return copy;
    });
  const deleteBoundary = (bi: number) =>
    setSegments((segs) => {
      if (segs.length < 2) return segs;
      const merged = { ...segs[bi]!, end: segs[bi + 1]!.end };
      return [...segs.slice(0, bi), merged, ...segs.slice(bi + 2)];
    });
  const toggleKeep = (id: number) =>
    setSegments((segs) => segs.map((s) => (s.id === id ? { ...s, keep: !s.keep } : s)));

  // --- timeline drag (seek / boundary) + hover preview ---
  const [drag, setDrag] = useState<Drag | null>(null);
  const [hoverX, setHoverX] = useState<number | null>(null);
  const timeAt = useCallback(
    (clientX: number) => {
      const el = trackRef.current;
      if (!el) return 0;
      const r = el.getBoundingClientRect();
      return Math.max(0, Math.min(1, (clientX - r.left) / r.width)) * duration;
    },
    [duration],
  );
  useEffect(() => {
    if (!drag) return;
    const move = (e: PointerEvent) => {
      const tt = timeAt(e.clientX);
      if (drag.type === "seek") {
        setCurrent(tt);
        seek(tt);
      } else moveBoundary(drag.bi, tt);
    };
    const up = () => setDrag(null);
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    return () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
  }, [drag, timeAt]);

  // --- crop drag ---
  const overlayRef = useRef<HTMLDivElement>(null);
  const cropDrag = useRef<null | { mode: "move" | "resize"; px: number; py: number; orig: Frac }>(
    null,
  );
  const onCropDown = (e: React.PointerEvent, mode: "move" | "resize") => {
    e.preventDefault();
    e.stopPropagation();
    (e.target as Element).setPointerCapture?.(e.pointerId);
    cropDrag.current = { mode, px: e.clientX, py: e.clientY, orig: { ...crop } };
  };
  useEffect(() => {
    const move = (e: PointerEvent) => {
      const d = cropDrag.current;
      const el = overlayRef.current;
      if (!d || !el) return;
      const r = el.getBoundingClientRect();
      const dx = (e.clientX - d.px) / r.width;
      const dy = (e.clientY - d.py) / r.height;
      if (d.mode === "move")
        setCrop({
          ...d.orig,
          x: Math.max(0, Math.min(1 - d.orig.w, d.orig.x + dx)),
          y: Math.max(0, Math.min(1 - d.orig.h, d.orig.y + dy)),
        });
      else
        setCrop({
          ...d.orig,
          w: Math.max(0.05, Math.min(1 - d.orig.x, d.orig.w + dx)),
          h: Math.max(0.05, Math.min(1 - d.orig.y, d.orig.h + dy)),
        });
    };
    const up = () => (cropDrag.current = null);
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
    return () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
  }, []);

  // --- filmstrip tile helpers (percentage sprite positioning; aspect-correct cells) ---
  const pct = (v: number) => `${(duration ? (v / duration) * 100 : 0).toFixed(3)}%`;
  const tileStyle = (idx: number): React.CSSProperties => {
    const cols = strip?.cols ?? 12;
    const rows = strip?.rows ?? 1;
    const i = Math.max(0, Math.min((strip?.count ?? 1) - 1, idx));
    const col = i % cols;
    const row = Math.floor(i / cols);
    return {
      backgroundImage: `url(${api.storyboardImageUrl(job.id)})`,
      backgroundSize: `${cols * 100}% ${rows * 100}%`,
      backgroundPosition: `${cols > 1 ? (col / (cols - 1)) * 100 : 0}% ${rows > 1 ? (row / (rows - 1)) * 100 : 0}%`,
    };
  };
  const stripCells = strip && trackW ? Math.max(1, Math.floor(trackW / (44 * aspect))) : 0;
  const hoverTime = hoverX != null ? timeAt(hoverX) : null;
  // Clamp the hover-scrub popover inside the track so it never overflows the modal (which would
  // add a horizontal scrollbar): center it on the cursor, but keep both edges within [0, trackW].
  const HOVER_W = 148;
  const hoverLeft =
    hoverX != null
      ? Math.max(
          0,
          Math.min(
            Math.max(0, (trackW || HOVER_W) - HOVER_W),
            hoverX - (trackRef.current?.getBoundingClientRect().left ?? 0) - HOVER_W / 2,
          ),
        )
      : 0;

  // --- create ---
  const kept = segments.filter((s) => s.keep);
  const keptDur = kept.reduce((a, s) => a + (s.end - s.start), 0);
  const isFullSingle =
    segments.length === 1 &&
    kept.length === 1 &&
    kept[0]!.start <= 0.01 &&
    kept[0]!.end >= duration - 0.01;
  const valid = kept.length >= 1 && keptDur >= MIN_SEG && (cropOn || !isFullSingle);
  const willJoin = output === "join" && kept.length > 1;
  const reencode = cropOn || accurate;

  const cropPx = (): { x: number; y: number; w: number; h: number } | undefined => {
    const v = videoRef.current;
    const vw = v?.videoWidth || job.asset?.width || 0;
    const vh = v?.videoHeight || job.asset?.height || 0;
    if (!cropOn || !vw || !vh) return undefined;
    const even = (n: number) => Math.max(2, Math.round(n / 2) * 2);
    return {
      x: even(crop.x * vw),
      y: even(crop.y * vh),
      w: even(crop.w * vw),
      h: even(crop.h * vh),
    };
  };

  const create = useMutation({
    mutationFn: async () => {
      const cp = cropPx();
      const base = name.trim() || defaultName;
      if (willJoin) {
        const spec: EditSpec = {
          segments: kept.map((s) => ({ start_s: +s.start.toFixed(3), end_s: +s.end.toFixed(3) })),
          accurate,
        };
        if (cp) spec.crop = cp;
        await api.enqueueEdit({
          source_job_id: job.id,
          edit_spec: spec,
          display_name: name.trim() || undefined,
        });
        return { files: 1 };
      }
      const N = kept.length;
      for (let i = 0; i < N; i++) {
        const s = kept[i]!; // i < N === kept.length, so kept[i] exists
        const spec: EditSpec = { trim: { start_s: +s.start.toFixed(3), end_s: +s.end.toFixed(3) } };
        if (cp) spec.crop = cp;
        else spec.accurate = accurate;
        const dn = N > 1 ? `${base} (${i + 1}/${N})` : name.trim() || undefined;
        await api.enqueueEdit({ source_job_id: job.id, edit_spec: spec, display_name: dn });
      }
      return { files: N };
    },
    onSuccess: ({ files }) => {
      qc.invalidateQueries({ queryKey: qk.downloads() });
      qc.invalidateQueries({ queryKey: qk.downloadUsage() });
      notify({ level: "success", message: t("editor.created", { count: files }) });
      onClose();
    },
    onError: () => notify({ level: "error", message: t("editor.failed") }),
  });

  const sel = segments.find((s) => s.id === selId) ?? segments[0];
  const selIdx = segments.findIndex((s) => s.id === sel?.id);

  return (
    <Modal title={t("editor.title", { name: defaultName })} onClose={onClose} maxWidth="max-w-3xl">
      <div className="space-y-3">
        {/* video + crop overlay */}
        <div className="relative bg-black rounded-lg overflow-hidden select-none">
          <video
            ref={videoRef}
            src={fileUrl}
            onLoadedMetadata={onLoaded}
            onTimeUpdate={onTimeUpdate}
            onPlay={() => setPlaying(true)}
            onPause={() => setPlaying(false)}
            className="w-full max-h-[42vh] mx-auto block"
            playsInline
          />
          {cropOn && (
            <div ref={overlayRef} className="absolute inset-0">
              <div
                className="absolute border-2 border-accent cursor-move"
                style={{
                  left: `${crop.x * 100}%`,
                  top: `${crop.y * 100}%`,
                  width: `${crop.w * 100}%`,
                  height: `${crop.h * 100}%`,
                  boxShadow: "0 0 0 9999px rgba(0,0,0,0.5)",
                }}
                onPointerDown={(e) => onCropDown(e, "move")}
              >
                <div
                  className="absolute -right-1.5 -bottom-1.5 w-4 h-4 rounded-sm bg-accent cursor-nwse-resize"
                  onPointerDown={(e) => onCropDown(e, "resize")}
                />
              </div>
            </div>
          )}
        </div>

        {/* transport */}
        <div className="flex items-center gap-3">
          <button
            onClick={playPause}
            className="p-2 rounded-lg bg-surface hover:bg-card transition"
            title={t("editor.playPause")}
          >
            {playing ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
          </button>
          <div className="text-xs text-muted tabular-nums">
            {tc(current)} / {tc(duration)}
          </div>
          <div className="flex-1" />
          <button
            onClick={splitAtPlayhead}
            className={clsx(btnCls, "bg-surface hover:bg-card inline-flex items-center gap-1.5")}
          >
            <SplitSquareHorizontal className="w-4 h-4" /> {t("editor.splitHere")}
          </button>
        </div>

        {/* timeline: filmstrip + segments (keep/drop tint) + markers + playhead + hover preview */}
        <div className="relative">
          <div
            ref={trackRef}
            className="relative h-14 rounded-lg overflow-hidden bg-surface border border-border cursor-pointer"
            onPointerDown={(e) => {
              setDrag({ type: "seek" });
              seek(timeAt(e.clientX));
            }}
            onPointerMove={(e) => setHoverX(e.clientX)}
            onPointerLeave={() => setHoverX(null)}
          >
            {/* aspect-correct filmstrip */}
            {stripCells > 0 && (
              <div className="absolute inset-0 flex pointer-events-none">
                {Array.from({ length: stripCells }).map((_, i) => (
                  <div
                    key={i}
                    className="h-full flex-1"
                    style={tileStyle(
                      Math.round(((i + 0.5) / stripCells) * ((strip!.count ?? 1) - 1)),
                    )}
                  />
                ))}
              </div>
            )}
            {/* segment tint: dropped = dimmed + hatched */}
            {segments.map((s) => (
              <div
                key={s.id}
                className={clsx(
                  "absolute inset-y-0 border-r border-black/40 last:border-r-0",
                  s.keep ? "bg-accent/10" : "bg-black/60",
                  s.id === selId && "ring-2 ring-inset ring-white/70",
                )}
                style={{
                  left: pct(s.start),
                  width: pct(s.end - s.start),
                  ...(s.keep
                    ? {}
                    : {
                        backgroundImage:
                          "repeating-linear-gradient(45deg, rgba(0,0,0,0.55) 0 6px, rgba(0,0,0,0.25) 6px 12px)",
                      }),
                }}
                onPointerDown={() => setSelId(s.id)}
                title={s.keep ? t("editor.keptSeg") : t("editor.droppedSeg")}
              >
                {/* keep/drop toggle */}
                <button
                  onPointerDown={(e) => {
                    e.stopPropagation();
                    toggleKeep(s.id);
                  }}
                  className={clsx(
                    "absolute top-1 left-1 p-0.5 rounded bg-black/50 backdrop-blur-sm",
                    s.keep ? "text-accent" : "text-muted",
                  )}
                  title={s.keep ? t("editor.drop") : t("editor.keep")}
                >
                  {s.keep ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
                </button>
              </div>
            ))}
            {/* draggable boundary markers */}
            {boundaries.map((b, bi) => (
              <div
                key={bi}
                className="absolute inset-y-0 -ml-1.5 w-3 flex items-center justify-center z-base"
                style={{ left: pct(b) }}
              >
                <div
                  className="w-1 h-full bg-white/90 cursor-ew-resize rounded"
                  onPointerDown={(e) => {
                    e.stopPropagation();
                    setDrag({ type: "boundary", bi });
                  }}
                />
                <button
                  onPointerDown={(e) => {
                    e.stopPropagation();
                    deleteBoundary(bi);
                  }}
                  className="absolute -top-0 -right-2 p-0.5 rounded-full bg-black/70 text-muted hover:text-red-400"
                  title={t("editor.removeCut")}
                >
                  <X className="w-3 h-3" />
                </button>
              </div>
            ))}
            {/* playhead */}
            <div
              className="absolute inset-y-0 w-0.5 bg-white pointer-events-none z-menu"
              style={{ left: pct(current) }}
            />
          </div>

          {/* hover-scrub thumbnail */}
          {strip && hoverTime != null && (
            <div
              className="absolute bottom-full mb-2 pointer-events-none z-chrome rounded-md overflow-hidden border border-border shadow-xl bg-black"
              style={{ left: hoverLeft, width: HOVER_W }}
            >
              <div
                style={{
                  width: HOVER_W,
                  height: HOVER_W / aspect,
                  ...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))),
                }}
              />
              <div className="text-[10px] text-center text-muted py-0.5 tabular-nums">
                {tc(hoverTime)}
              </div>
            </div>
          )}
        </div>

        {/* selected-segment editor */}
        {sel && (
          <div className="flex flex-wrap items-center gap-2 text-sm rounded-lg glass-card px-3 py-2">
            <span className="font-medium">{t("editor.segment", { n: selIdx + 1 })}</span>
            <button
              onClick={() => toggleKeep(sel.id)}
              className={clsx(
                "inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium",
                sel.keep ? "bg-accent/15 text-accent" : "bg-surface text-muted",
              )}
            >
              {sel.keep ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
              {sel.keep ? t("editor.kept") : t("editor.dropped")}
            </button>
            <div className="flex-1" />
            <label className="flex items-center gap-1 text-muted">
              {t("editor.in")}
              <input
                defaultValue={tc(sel.start)}
                key={`s-${sel.id}-${sel.start}`}
                disabled={selIdx === 0}
                onBlur={(e) => {
                  const v = parseTc(e.target.value);
                  if (v != null && selIdx > 0) moveBoundary(selIdx - 1, v);
                }}
                className={numCls}
              />
            </label>
            <label className="flex items-center gap-1 text-muted">
              {t("editor.out")}
              <input
                defaultValue={tc(sel.end)}
                key={`e-${sel.id}-${sel.end}`}
                disabled={selIdx === segments.length - 1}
                onBlur={(e) => {
                  const v = parseTc(e.target.value);
                  if (v != null && selIdx < segments.length - 1) moveBoundary(selIdx, v);
                }}
                className={numCls}
              />
            </label>
            <span className="text-xs text-muted tabular-nums w-16 text-right">
              {tc(sel.end - sel.start)}
            </span>
          </div>
        )}

        {/* output mode + summary */}
        <div className="flex flex-wrap items-center gap-2 text-sm">
          <span className="text-muted">{t("editor.output.label")}:</span>
          {(["separate", "join"] as const).map((m) => (
            <button
              key={m}
              onClick={() => setOutput(m)}
              disabled={m === "join" && kept.length < 2}
              className={clsx(
                "px-3 py-1.5 rounded-lg border transition disabled:opacity-40",
                output === m
                  ? "border-accent bg-accent/10 text-accent"
                  : "border-border text-muted hover:text-fg",
              )}
            >
              {t(`editor.output.${m}`)}
            </button>
          ))}
          <div className="flex-1" />
          <span className="text-xs text-muted">
            {t("editor.keptSummary", { n: kept.length, dur: tc(keptDur) })}
          </span>
        </div>

        {/* crop + precision */}
        <div className="grid sm:grid-cols-2 gap-3">
          <button
            onClick={() => setCropOn((v) => !v)}
            className={clsx(
              "flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition",
              cropOn
                ? "border-accent text-accent bg-accent/10"
                : "border-border text-muted hover:text-fg",
            )}
          >
            <Crop className="w-4 h-4" /> {cropOn ? t("editor.cropOn") : t("editor.cropOff")}
          </button>
          {!cropOn ? (
            <div className="flex gap-2">
              {(["accurate", "fast"] as const).map((m) => {
                const on = (m === "accurate") === accurate;
                return (
                  <button
                    key={m}
                    onClick={() => setAccurate(m === "accurate")}
                    className={clsx(
                      "flex-1 px-3 py-2 rounded-lg border text-left transition",
                      on ? "border-accent bg-accent/10" : "border-border hover:border-muted",
                    )}
                    title={t(`editor.${m}.hint`)}
                  >
                    <div className={clsx("text-sm font-medium", on ? "text-accent" : "text-fg")}>
                      {t(`editor.${m}.label`)}
                    </div>
                  </button>
                );
              })}
            </div>
          ) : (
            <div className="flex items-center px-3 text-xs text-muted">
              {t("editor.cropReencode")}
            </div>
          )}
        </div>

        <div>
          <label className="block text-sm font-medium mb-1">{t("editor.nameLabel")}</label>
          <input
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder={defaultName}
            className={inputCls}
          />
        </div>

        <div className="flex items-center justify-between pt-1">
          <span className="text-xs text-muted">
            {reencode ? t("editor.willReencode") : t("editor.willCopy")}
          </span>
          <div className="flex gap-2">
            <button
              onClick={onClose}
              className={clsx(btnCls, "text-muted hover:text-fg hover:bg-surface")}
            >
              {t("common.cancel")}
            </button>
            <button
              onClick={() => create.mutate()}
              disabled={!valid || create.isPending}
              className={clsx(
                btnCls,
                "bg-accent text-accent-fg hover:opacity-90 inline-flex items-center gap-1.5",
              )}
            >
              {create.isPending ? (
                <Loader2 className="w-4 h-4 animate-spin" />
              ) : (
                <Scissors className="w-4 h-4" />
              )}
              {willJoin
                ? t("editor.createJoin")
                : kept.length > 1
                  ? t("editor.createN", { count: kept.length })
                  : t("editor.create")}
            </button>
          </div>
        </div>
      </div>
    </Modal>
  );
}
