'use client';

import { useCallback, useState } from 'react';
import Cropper, { type Area } from 'react-easy-crop';
import { Loader2 } from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { toastError } from '@/lib/api/toast-error';

export interface ImageCropperDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  /** The picked file (raw, pre-crop). Cropper renders an object URL. */
  file: File | null;
  /** Aspect ratio for the crop frame. 1 = square (avatars), 4 = wide
   *  banners. Defaults to 1. */
  aspect?: number;
  /** Output JPEG quality 0-1. Default 0.85. Ignored for PNG. */
  outputQuality?: number;
  /** Output width in pixels (height derived from aspect). Default 512. */
  outputWidth?: number;
  /** Output format. 'auto' (default) preserves alpha: PNG output when
   *  the source is PNG/GIF/WebP/AVIF, JPEG otherwise. Override to force
   *  a specific format. */
  outputFormat?: 'auto' | 'jpeg' | 'png';
  /** Async upload handler - receives the cropped Blob. Cropper closes on
   *  success; toasts on error. */
  onUpload: (blob: Blob) => Promise<void>;
  /** Dialog title. Default "Crop image". */
  title?: string;
}

/**
 * MIME types that carry an alpha channel. A PNG source dropped through a
 * JPEG-output canvas composites transparent pixels against black on
 * export - destroying the design intent of any logo with transparency.
 * Default to PNG output whenever the source could have had alpha.
 */
const ALPHA_CAPABLE_MIME = new Set(['image/png', 'image/gif', 'image/webp', 'image/avif']);

/**
 * Reusable crop-then-upload modal. Renders a draggable/zoomable crop
 * frame over the picked file, then writes the cropped pixels to a
 * Canvas, exports as JPEG, and hands the Blob to the caller.
 *
 * Used for: profile avatars, port logos, brochure covers - anywhere a
 * square or fixed-aspect image needs to land in storage.
 */
export function ImageCropperDialog({
  open,
  onOpenChange,
  file,
  aspect = 1,
  outputQuality = 0.85,
  outputWidth = 512,
  outputFormat = 'auto',
  onUpload,
  title = 'Crop image',
}: ImageCropperDialogProps) {
  const [crop, setCrop] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  const [pixels, setPixels] = useState<Area | null>(null);
  const [uploading, setUploading] = useState(false);

  const objectUrl = file ? URL.createObjectURL(file) : null;

  const onCropComplete = useCallback((_area: Area, areaPixels: Area) => {
    setPixels(areaPixels);
  }, []);

  async function handleUpload() {
    if (!objectUrl || !pixels || !file) return;
    setUploading(true);
    try {
      const resolvedFormat: 'jpeg' | 'png' =
        outputFormat === 'auto'
          ? ALPHA_CAPABLE_MIME.has(file.type)
            ? 'png'
            : 'jpeg'
          : outputFormat;
      const blob = await renderCrop(
        objectUrl,
        pixels,
        aspect,
        outputWidth,
        outputQuality,
        resolvedFormat,
      );
      await onUpload(blob);
      onOpenChange(false);
    } catch (err) {
      toastError(err);
    } finally {
      setUploading(false);
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    }
  }

  function handleClose(o: boolean) {
    if (!o && objectUrl) URL.revokeObjectURL(objectUrl);
    onOpenChange(o);
  }

  return (
    <Dialog open={open} onOpenChange={handleClose}>
      <DialogContent className="sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>{title}</DialogTitle>
        </DialogHeader>

        {objectUrl && (
          <>
            <div className="relative h-72 w-full bg-muted rounded-md overflow-hidden">
              <Cropper
                image={objectUrl}
                crop={crop}
                zoom={zoom}
                aspect={aspect}
                onCropChange={setCrop}
                onZoomChange={setZoom}
                onCropComplete={onCropComplete}
                cropShape={aspect === 1 ? 'round' : 'rect'}
                showGrid={false}
              />
            </div>

            <div className="space-y-2 pt-2">
              <Label htmlFor="cropper-zoom" className="text-xs text-muted-foreground">
                Zoom
              </Label>
              <input
                id="cropper-zoom"
                type="range"
                min={1}
                max={3}
                step={0.05}
                value={zoom}
                onChange={(e) => setZoom(Number(e.target.value))}
                className="w-full"
              />
            </div>
          </>
        )}

        <DialogFooter>
          <Button variant="outline" onClick={() => handleClose(false)} disabled={uploading}>
            Cancel
          </Button>
          <Button onClick={handleUpload} disabled={uploading || !pixels}>
            {uploading ? (
              <>
                <Loader2 className="mr-1.5 h-4 w-4 animate-spin" aria-hidden />
                Uploading…
              </>
            ) : (
              'Save'
            )}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

/**
 * Crop the source image at `pixels`, resize to `outputWidth`×
 * (outputWidth/aspect), and return as a JPEG or PNG Blob. Async because
 * <img> needs an onload event before we can drawImage. PNG preserves
 * alpha for logos; JPEG is smaller for photos / backgrounds.
 */
async function renderCrop(
  src: string,
  pixels: Area,
  aspect: number,
  outputWidth: number,
  quality: number,
  format: 'jpeg' | 'png',
): Promise<Blob> {
  const image = await loadImage(src);
  const canvas = document.createElement('canvas');
  const outputHeight = Math.round(outputWidth / aspect);
  canvas.width = outputWidth;
  canvas.height = outputHeight;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Canvas 2d context unavailable');
  ctx.drawImage(
    image,
    pixels.x,
    pixels.y,
    pixels.width,
    pixels.height,
    0,
    0,
    outputWidth,
    outputHeight,
  );
  const mime = format === 'png' ? 'image/png' : 'image/jpeg';
  return await new Promise<Blob>((resolve, reject) => {
    canvas.toBlob(
      (b) => (b ? resolve(b) : reject(new Error('Canvas toBlob returned null'))),
      mime,
      // toBlob's quality param applies to JPEG/WebP only; PNG ignores it.
      format === 'jpeg' ? quality : undefined,
    );
  });
}

function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => resolve(img);
    img.onerror = (e) => reject(e);
    img.src = src;
  });
}
