import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useReactAt } from "i18n-auto-extractor/react";
import { motion } from "framer-motion";

import { useSettingsStore, useUiStore, useVideoStore } from "@/hooks/stores";
import Card from "@components/Card";
import { ConfirmDialog } from "@components/ConfirmDialog";
import TextArea from "@components/TextArea";
import notifications from "@/notifications";
import { eventMatchesShortcut } from "@/utils/shortcuts";

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

type OcrStatus = "idle" | "selecting" | "processing" | "result";

type TesseractWorker = {
  recognize: (image: HTMLCanvasElement, options?: unknown, output?: unknown) => Promise<{
    data: {
      text?: string;
      blocks?: Array<{
        paragraphs: Array<{
          lines: Array<{
            text: string;
            bbox: { x0: number };
            words: Array<{ text: string; bbox: { x0: number; x1: number } }>;
          }>;
        }>;
      }>;
    };
  }>;
  terminate: () => Promise<unknown>;
};

async function loadTesseract() {
  const { createWorker } = await import("tesseract.js");
  return createWorker;
}

let workerPromise: Promise<TesseractWorker> | null = null;
let cleanupTimer: ReturnType<typeof setTimeout> | null = null;

async function initWorker() {
  const createWorker = await loadTesseract();
  return createWorker("eng", 1) as Promise<TesseractWorker>;
}

async function terminateWorker() {
  if (!workerPromise) return;
  try {
    const worker = await workerPromise;
    await worker.terminate();
  } catch {
    // Ignore termination errors.
  }
  workerPromise = null;
}

function getWorker() {
  if (cleanupTimer) {
    clearTimeout(cleanupTimer);
    cleanupTimer = null;
  }

  if (!workerPromise) {
    workerPromise = initWorker().catch(err => {
      workerPromise = null;
      throw err;
    });
  }

  // Auto-terminate worker after inactivity to reduce memory footprint.
  cleanupTimer = setTimeout(() => {
    void terminateWorker();
    cleanupTimer = null;
  }, 60_000);

  return workerPromise;
}

async function performOcr(canvas: HTMLCanvasElement): Promise<string> {
  const worker = await getWorker();
  const { data } = await worker.recognize(canvas, {}, { text: true, blocks: true });
  const lines = data.blocks?.flatMap(b => b.paragraphs.flatMap(p => p.lines)) ?? [];

  if (lines.length === 0) return (data.text || "").trim();

  // Estimate character width from OCR words so left indentation is preserved.
  let totalCharWidth = 0;
  let samples = 0;
  for (const line of lines) {
    for (const word of line.words) {
      const len = word.text.trim().length;
      if (len > 0) {
        totalCharWidth += (word.bbox.x1 - word.bbox.x0) / len;
        samples++;
      }
    }
  }

  if (samples === 0) return (data.text || "").trim();

  const charWidth = totalCharWidth / samples;
  const minX = Math.min(...lines.map(l => l.bbox.x0));

  return lines
    .map(line => {
      const indent = Math.round((line.bbox.x0 - minX) / charWidth);
      return " ".repeat(indent) + line.text.trim();
    })
    .join("\n")
    .trim();
}

function captureRegion(videoEl: HTMLVideoElement, rect: Rect): HTMLCanvasElement {
  const canvas = document.createElement("canvas");
  canvas.width = rect.width;
  canvas.height = rect.height;
  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error("Failed to acquire 2D canvas context");
  ctx.drawImage(videoEl, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height);
  return canvas;
}

function getVideoDisplayRect(videoElement: HTMLVideoElement, videoWidth: number, videoHeight: number) {
  const videoRect = videoElement.getBoundingClientRect();
  const elementAspectRatio = videoRect.width / videoRect.height;
  const streamAspectRatio = videoWidth / videoHeight;

  let effectiveWidth = videoRect.width;
  let effectiveHeight = videoRect.height;
  let offsetX = 0;
  let offsetY = 0;

  if (elementAspectRatio > streamAspectRatio) {
    effectiveWidth = videoRect.height * streamAspectRatio;
    offsetX = (videoRect.width - effectiveWidth) / 2;
  } else if (elementAspectRatio < streamAspectRatio) {
    effectiveHeight = videoRect.width / streamAspectRatio;
    offsetY = (videoRect.height - effectiveHeight) / 2;
  }

  return { videoRect, effectiveWidth, effectiveHeight, offsetX, offsetY };
}

interface OcrOverlayProps {
  videoRef: React.RefObject<HTMLVideoElement>;
  containerRef: React.RefObject<HTMLDivElement>;
}

export default function OcrOverlay({ videoRef, containerRef }: OcrOverlayProps) {
  const { width: videoWidth, height: videoHeight } = useVideoStore();
  const isOcrMode = useUiStore(state => state.isOcrMode);
  const setOcrMode = useUiStore(state => state.setOcrMode);
  const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
  const ocrShortcutEnabled = useSettingsStore(state => state.ocrShortcutEnabled);
  const ocrShortcut = useSettingsStore(state => state.ocrShortcut);
  const { $at } = useReactAt();

  const mountedRef = useRef(true);
  const resultRef = useRef<HTMLTextAreaElement>(null);
  const [status, setStatus] = useState<OcrStatus>("idle");
  const [selectionStart, setSelectionStart] = useState<{ x: number; y: number } | null>(null);
  const [selectionRect, setSelectionRect] = useState<Rect | null>(null);
  const [ocrResult, setOcrResult] = useState("");
  const [isClosing, setIsClosing] = useState(false);

  useEffect(() => {
    return () => {
      mountedRef.current = false;
    };
  }, []);

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (!ocrShortcutEnabled) return;
      if (!eventMatchesShortcut(e, ocrShortcut)) return;
      const activeElement = document.activeElement as HTMLElement | null;
      const isEditable =
        !!activeElement
        && (activeElement.tagName === "INPUT"
          || activeElement.tagName === "TEXTAREA"
          || activeElement.isContentEditable);
      if (isEditable) return;
      if (videoWidth === 0 || videoHeight === 0) return;
      e.preventDefault();
      e.stopPropagation();
      setOcrMode(!isOcrMode);
    };
    document.addEventListener("keydown", handleKeyDown, { capture: true });
    return () => document.removeEventListener("keydown", handleKeyDown, { capture: true });
  }, [isOcrMode, ocrShortcut, ocrShortcutEnabled, setOcrMode, videoWidth, videoHeight]);

  const closeOverlay = useCallback(() => {
    if (status === "processing" || status === "result") {
      setIsClosing(true);
      setSelectionRect(null);
      setSelectionStart(null);
      setTimeout(() => setOcrMode(false), 200);
      return;
    }
    setOcrMode(false);
  }, [setOcrMode, status]);

  useEffect(() => {
    if (!isOcrMode) {
      setStatus("idle");
      setSelectionRect(null);
      setSelectionStart(null);
      setOcrResult("");
      setIsClosing(false);
    }
  }, [isOcrMode]);

  useEffect(() => {
    if (!isOcrMode) return;
    if (status === "processing" || status === "result") {
      setDisableVideoFocusTrap(true);
      return () => setDisableVideoFocusTrap(false);
    }
  }, [isOcrMode, setDisableVideoFocusTrap, status]);

  useEffect(() => {
    if (!isOcrMode) return;
    if (status === "processing" || status === "result") return;
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        e.preventDefault();
        e.stopPropagation();
        setOcrMode(false);
      }
    };
    document.addEventListener("keydown", handleKeyDown, { capture: true });
    return () => document.removeEventListener("keydown", handleKeyDown, { capture: true });
  }, [isOcrMode, setOcrMode, status]);

  useEffect(() => {
    if (!isOcrMode) return;
    if (status !== "result") return;
    const handleCopy = () => {
      notifications.success($at("Copied"), { duration: 4000 });
      closeOverlay();
    };
    document.addEventListener("copy", handleCopy);
    return () => document.removeEventListener("copy", handleCopy);
  }, [closeOverlay, isOcrMode, status, $at]);

  useEffect(() => {
    if (status === "result" && resultRef.current) {
      resultRef.current.focus();
      resultRef.current.select();
    }
  }, [status]);

  const toVideoCoords = useCallback((clientX: number, clientY: number) => {
    const videoElement = videoRef.current;
    if (!videoElement) return { x: 0, y: 0 };
    const { videoRect, effectiveWidth, effectiveHeight, offsetX, offsetY } = getVideoDisplayRect(
      videoElement,
      videoWidth,
      videoHeight,
    );
    const relX = clientX - videoRect.left - offsetX;
    const relY = clientY - videoRect.top - offsetY;
    const scaleX = videoWidth / effectiveWidth;
    const scaleY = videoHeight / effectiveHeight;
    return {
      x: Math.max(0, Math.min(videoWidth, Math.round(relX * scaleX))),
      y: Math.max(0, Math.min(videoHeight, Math.round(relY * scaleY))),
    };
  }, [videoHeight, videoRef, videoWidth]);

  const finishSelection = useCallback(async () => {
    const videoElement = videoRef.current;
    if (status !== "selecting") return;
    if (!videoElement || !selectionRect) {
      setStatus("idle");
      setSelectionStart(null);
      setSelectionRect(null);
      return;
    }
    if (selectionRect.width < 10 || selectionRect.height < 10) {
      setStatus("idle");
      setSelectionStart(null);
      setSelectionRect(null);
      return;
    }

    setStatus("processing");
    try {
      const canvas = captureRegion(videoElement, selectionRect);
      const text = await performOcr(canvas);
      canvas.width = 0;
      canvas.height = 0;

      if (!mountedRef.current) return;
      if (text) {
        setOcrResult(text);
        setStatus("result");
      } else {
        notifications.error($at("No text detected"));
        closeOverlay();
      }
    } catch (error) {
      if (!mountedRef.current) return;
      console.error("OCR failed:", error);
      notifications.error($at("OCR failed"));
      closeOverlay();
    }
  }, [closeOverlay, selectionRect, status, videoRef, $at]);

  const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
    if (status === "processing" || status === "result") return;
    if (!videoRef.current || videoWidth === 0 || videoHeight === 0) return;
    e.preventDefault();
    e.stopPropagation();
    e.currentTarget.setPointerCapture(e.pointerId);
    const coords = toVideoCoords(e.clientX, e.clientY);
    setSelectionStart(coords);
    setSelectionRect(null);
    setStatus("selecting");
  }, [status, toVideoCoords, videoHeight, videoRef, videoWidth]);

  const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
    if (status !== "selecting" || !selectionStart) return;
    e.preventDefault();
    e.stopPropagation();
    const coords = toVideoCoords(e.clientX, e.clientY);
    const x = Math.min(selectionStart.x, coords.x);
    const y = Math.min(selectionStart.y, coords.y);
    const width = Math.abs(coords.x - selectionStart.x);
    const height = Math.abs(coords.y - selectionStart.y);
    setSelectionRect({ x, y, width, height });
  }, [selectionStart, status, toVideoCoords]);

  const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
    if (status !== "selecting") return;
    e.preventDefault();
    e.stopPropagation();
    e.currentTarget.releasePointerCapture(e.pointerId);
    void finishSelection();
  }, [finishSelection, status]);

  const selectionStyle = useMemo(() => {
    const videoElement = videoRef.current;
    const containerElement = containerRef.current;
    if (!selectionRect || !videoElement || !containerElement || videoWidth === 0 || videoHeight === 0) {
      return undefined;
    }

    const { videoRect, effectiveWidth, effectiveHeight, offsetX, offsetY } = getVideoDisplayRect(
      videoElement,
      videoWidth,
      videoHeight,
    );
    const containerRect = containerElement.getBoundingClientRect();
    const baseX = videoRect.left - containerRect.left + offsetX;
    const baseY = videoRect.top - containerRect.top + offsetY;

    return {
      left: `${baseX + (selectionRect.x / videoWidth) * effectiveWidth}px`,
      top: `${baseY + (selectionRect.y / videoHeight) * effectiveHeight}px`,
      width: `${(selectionRect.width / videoWidth) * effectiveWidth}px`,
      height: `${(selectionRect.height / videoHeight) * effectiveHeight}px`,
    };
  }, [containerRef, selectionRect, videoHeight, videoRef, videoWidth]);

  if (!isOcrMode) return null;

  return (
    <>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        transition={{ duration: 0.15 }}
        className="absolute inset-0 z-20 touch-none"
        style={{ cursor: status === "result" ? "default" : "crosshair" }}
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
      >
        <div className="fixed inset-0 bg-black/20" />

        {status === "idle" && (
          <div className="pointer-events-none absolute inset-x-0 top-4 flex justify-center">
            <div className="rounded-md bg-black/70 px-3 py-1.5 text-xs font-medium text-white">
              {$at("Drag to select text area")}
            </div>
          </div>
        )}

        {selectionRect && selectionStyle && status !== "result" && (
          <div
            className="absolute border-2 border-dashed border-blue-400 bg-blue-400/10"
            style={selectionStyle}
          >
            {selectionRect.width >= 10 && selectionRect.height >= 10 && (
              <Card className="absolute right-0 -bottom-6 w-auto px-1.5 py-0.5 text-[10px] font-medium tabular-nums dark:text-white">
                {selectionRect.width} &times; {selectionRect.height}
              </Card>
            )}
          </div>
        )}
      </motion.div>

      <ConfirmDialog
        open={(status === "processing" || status === "result") && !isClosing}
        onClose={closeOverlay}
        title={status === "result" ? $at("Copy text") : $at("Recognizing text...")}
        description={status === "result" ? $at("Review the recognized text before copying.") : $at("Please wait while OCR is running.")}
        confirmText={$at("Copy text")}
        onConfirm={() => {
          if (status !== "result") return;
          if (navigator.clipboard?.writeText && window.isSecureContext) {
            navigator.clipboard.writeText(ocrResult).then(() => {
              notifications.success($at("Copied"), { duration: 4000 });
              closeOverlay();
            }).catch(() => {
              if (!resultRef.current) return;
              resultRef.current.focus();
              resultRef.current.select();
              document.execCommand("copy");
            });
            return;
          }

          if (!resultRef.current) return;
          resultRef.current.focus();
          resultRef.current.select();
          document.execCommand("copy");
        }}
        isConfirming={status === "processing"}
      >
        {status === "processing" ? (
          <div className="mt-2 space-y-2">
            <div className="h-4 w-full animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
            <div className="h-4 w-3/4 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
            <div className="h-4 w-5/6 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
          </div>
        ) : (
          <div className="mt-2">
            <TextArea
              ref={resultRef}
              value={ocrResult}
              readOnly
              rows={Math.min(10, ocrResult.split("\n").length + 1)}
            />
          </div>
        )}
      </ConfirmDialog>
    </>
  );
}
