import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Cropper, { type Area } from 'react-easy-crop';

import { Modal } from './Modal';

interface Props {
  /** When true, the dialog is mounted. Always pair with a `key` change on
   *  the source file so a fresh open re-runs the loader. */
  open: boolean;
  /** Source image. Object URL is created/revoked internally. */
  file: File | null;
  /** width:height ratio of the crop box. 1 for avatar, 3 for banner. */
  aspect: number;
  /** Output image dimensions in px. The crop area is scaled to this. */
  outputWidth: number;
  outputHeight: number;
  title: string;
  /** Called with the encoded blob when the user confirms. The dialog stays
   *  open until the parent flips `open` back off (typically after upload
   *  completes) — gives the parent a chance to show errors inline. */
  onConfirm: (blob: Blob) => void;
  onClose: () => void;
}

const ENCODE_QUALITY = 0.85;

// Crop dialog used by both avatar (1:1) and banner (3:1) flows. Wraps
// react-easy-crop in our standard modal chrome and runs the canvas crop
// inline on confirm so the upload helpers receive a ready-to-store Blob.
export function ImageCropDialog({
  open,
  file,
  aspect,
  outputWidth,
  outputHeight,
  title,
  onConfirm,
  onClose,
}: Props) {
  const [crop, setCrop] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  const [areaPixels, setAreaPixels] = useState<Area | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Resolve the file into an object URL exactly once per file. Revoked on
  // unmount or when the file changes so a long crop session doesn't leak.
  const objectUrl = useMemo(() => {
    if (!file) return null;
    return URL.createObjectURL(file);
  }, [file]);
  useEffect(() => {
    return () => {
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [objectUrl]);

  // Reset transient UI state every time a new file is loaded so reopens
  // don't carry over the previous session's zoom/offset.
  useEffect(() => {
    if (!file) return;
    setCrop({ x: 0, y: 0 });
    setZoom(1);
    setAreaPixels(null);
    setError(null);
  }, [file]);

  const onCropComplete = useCallback((_: Area, pixels: Area) => {
    setAreaPixels(pixels);
  }, []);

  const submitting = useRef(false);
  const handleConfirm = useCallback(async () => {
    if (submitting.current || !objectUrl || !areaPixels) return;
    submitting.current = true;
    setBusy(true);
    setError(null);
    try {
      const blob = await renderCrop({
        srcUrl: objectUrl,
        area: areaPixels,
        outputWidth,
        outputHeight,
      });
      onConfirm(blob);
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'crop failed');
    } finally {
      submitting.current = false;
      setBusy(false);
    }
  }, [objectUrl, areaPixels, outputWidth, outputHeight, onConfirm]);

  return (
    <Modal open={open} title={title} onClose={busy ? () => undefined : onClose} size="lg">
      <div className="flex flex-col gap-3 p-5">
        {/* Cropper canvas. Fixed height (320px) so the layout stays stable
            regardless of the source image dimensions; react-easy-crop fits
            the image into the box and the user pans/zooms inside. */}
        <div className="relative h-[320px] w-full overflow-hidden rounded-xl bg-black">
          {objectUrl && (
            <Cropper
              image={objectUrl}
              crop={crop}
              zoom={zoom}
              aspect={aspect}
              onCropChange={setCrop}
              onZoomChange={setZoom}
              onCropComplete={onCropComplete}
              cropShape={aspect === 1 ? 'round' : 'rect'}
              showGrid={false}
              objectFit="contain"
            />
          )}
        </div>

        <div className="flex items-center gap-3 px-1">
          <label className="text-xs font-medium text-fg-muted" htmlFor="crop-zoom">
            Zoom
          </label>
          <input
            id="crop-zoom"
            type="range"
            min={1}
            max={4}
            step={0.01}
            value={zoom}
            onChange={(e) => setZoom(Number(e.target.value))}
            className="flex-1 accent-accent"
          />
          <span className="w-10 shrink-0 text-right text-xs tabular-nums text-fg-muted">
            {Math.round(zoom * 100)}%
          </span>
        </div>

        {error && (
          <p role="alert" className="text-xs text-rose-500 dark:text-rose-300">
            {error}
          </p>
        )}

        <div className="flex justify-end gap-2 pt-1">
          <button
            type="button"
            onClick={onClose}
            disabled={busy}
            className="cursor-pointer rounded-lg border border-line bg-surface-2 px-3 py-1.5 text-sm text-fg transition hover:bg-surface disabled:cursor-not-allowed disabled:opacity-60"
          >
            Abbrechen
          </button>
          <button
            type="button"
            onClick={() => void handleConfirm()}
            disabled={busy || !areaPixels}
            className="cursor-pointer rounded-lg bg-accent px-3 py-1.5 text-sm font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-60"
          >
            {busy ? 'Speichern…' : 'Speichern'}
          </button>
        </div>
      </div>
    </Modal>
  );
}

interface RenderArgs {
  srcUrl: string;
  area: Area;
  outputWidth: number;
  outputHeight: number;
}

// Off-thread canvas would be nicer but the input is bounded (single image,
// max ~12MP after react-easy-crop's clamp) so the main-thread cost is in
// the tens of ms. Keeping it inline avoids the OffscreenCanvas + Worker
// plumbing for a one-shot operation.
async function renderCrop({
  srcUrl,
  area,
  outputWidth,
  outputHeight,
}: RenderArgs): Promise<Blob> {
  const img = await new Promise<HTMLImageElement>((resolve, reject) => {
    const i = new Image();
    i.onload = () => resolve(i);
    i.onerror = () => reject(new Error('image load failed'));
    i.src = srcUrl;
  });

  // Don't upscale beyond the source crop. A 200x200 selection stays at
  // 200x200 instead of inflating to outputWidth — saves bytes and avoids
  // the soft look of canvas-resampled enlargement.
  const targetWidth = Math.min(outputWidth, Math.round(area.width));
  const targetHeight = Math.min(outputHeight, Math.round(area.height));

  const canvas = document.createElement('canvas');
  canvas.width = targetWidth;
  canvas.height = targetHeight;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('canvas context unavailable');
  ctx.drawImage(
    img,
    area.x,
    area.y,
    area.width,
    area.height,
    0,
    0,
    targetWidth,
    targetHeight,
  );

  const blob = await new Promise<Blob | null>((resolve) =>
    canvas.toBlob(resolve, 'image/webp', ENCODE_QUALITY),
  );
  if (blob) return blob;

  const jpeg = await new Promise<Blob | null>((resolve) =>
    canvas.toBlob(resolve, 'image/jpeg', ENCODE_QUALITY),
  );
  if (!jpeg) throw new Error('canvas toBlob returned null');
  return jpeg;
}
