"use client";

import * as React from "react";
import { ImagePlus, Paperclip, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
import { ticketsTexts as tx } from "@/lib/i18n/tickets";
import {
  ACCEPTED_IMAGE_TYPES,
  ATTACHMENT_ACCEPT_ATTR,
  MAX_ATTACHMENTS,
  MAX_ATTACHMENT_BYTES,
  MAX_IMAGE_DIMENSION,
  MAX_STORED_BYTES,
  formatBytes,
} from "@/lib/validation/tickets";
import type { TicketAttachmentDTO } from "./types";

function fill(template: string, values: Record<string, string>): string {
  return template.replace(/\{(\w+)\}/g, (_m, key: string) => values[key] ?? "");
}

function dataUrlBytes(dataUrl: string): number {
  const comma = dataUrl.indexOf(",");
  const b64 = comma >= 0 ? dataUrl.slice(comma + 1) : "";
  const pad = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
  return Math.max(0, Math.floor((b64.length * 3) / 4) - pad);
}

function readDataUrl(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result ?? ""));
    reader.onerror = () => reject(new Error("read failed"));
    reader.readAsDataURL(file);
  });
}

type Decoded = {
  source: CanvasImageSource;
  width: number;
  height: number;
  cleanup: () => void;
};

async function decodeImage(file: File): Promise<Decoded> {
  if (typeof createImageBitmap === "function") {
    try {
      const bitmap = await createImageBitmap(file);
      return {
        source: bitmap,
        width: bitmap.width,
        height: bitmap.height,
        cleanup: () => bitmap.close?.(),
      };
    } catch {
      // fall through to the <img> path
    }
  }
  const url = URL.createObjectURL(file);
  const img = await new Promise<HTMLImageElement>((resolve, reject) => {
    const el = new window.Image();
    el.onload = () => resolve(el);
    el.onerror = () => reject(new Error("decode failed"));
    el.src = url;
  });
  return {
    source: img,
    width: img.naturalWidth || img.width,
    height: img.naturalHeight || img.height,
    cleanup: () => URL.revokeObjectURL(url),
  };
}

/**
 * Re-encodes an image so the stored base64 stays small — Server Actions cap
 * request bodies at 1 MB, well under the 3 × 2 MB the user is allowed to pick.
 */
async function prepare(file: File): Promise<TicketAttachmentDTO> {
  // Animated GIFs cannot survive a canvas round-trip, so they pass through as-is.
  if (file.type === "image/gif") {
    if (file.size > MAX_STORED_BYTES) {
      throw new Error(fill(tx.attachmentGifTooLarge, { name: file.name }));
    }
    const raw = await readDataUrl(file);
    return { name: file.name, type: "image/gif", size: dataUrlBytes(raw), dataUrl: raw };
  }

  const decoded = await decodeImage(file);
  try {
    const longest = Math.max(decoded.width, decoded.height);
    if (file.size <= MAX_STORED_BYTES && longest <= MAX_IMAGE_DIMENSION) {
      const raw = await readDataUrl(file);
      return { name: file.name, type: file.type, size: dataUrlBytes(raw), dataUrl: raw };
    }

    const scale = Math.min(1, MAX_IMAGE_DIMENSION / Math.max(1, longest));
    const width = Math.max(1, Math.round(decoded.width * scale));
    const height = Math.max(1, Math.round(decoded.height * scale));

    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("no canvas context");
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, width, height);
    ctx.drawImage(decoded.source, 0, 0, width, height);

    let quality = 0.82;
    let out = canvas.toDataURL("image/jpeg", quality);
    while (dataUrlBytes(out) > MAX_STORED_BYTES && quality > 0.35) {
      quality -= 0.15;
      out = canvas.toDataURL("image/jpeg", quality);
    }

    const baseName = file.name.replace(/\.[^./\\]+$/, "");
    return {
      name: `${baseName || "image"}.jpg`,
      type: "image/jpeg",
      size: dataUrlBytes(out),
      dataUrl: out,
    };
  } finally {
    decoded.cleanup();
  }
}

export function AttachmentPicker({
  name = "attachments",
  value,
  onChange,
  disabled,
  className,
  compact = false,
}: {
  name?: string;
  value: TicketAttachmentDTO[];
  onChange: (next: TicketAttachmentDTO[]) => void;
  disabled?: boolean;
  className?: string;
  compact?: boolean;
}) {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const [busy, setBusy] = React.useState(false);
  const [errors, setErrors] = React.useState<string[]>([]);
  const full = value.length >= MAX_ATTACHMENTS;

  async function handleFiles(list: FileList | null) {
    if (!list || list.length === 0) return;
    setBusy(true);
    const problems: string[] = [];
    const accepted: TicketAttachmentDTO[] = [];

    for (const file of Array.from(list)) {
      if (value.length + accepted.length >= MAX_ATTACHMENTS) {
        problems.push(tx.attachmentsFull);
        break;
      }
      if (!(ACCEPTED_IMAGE_TYPES as readonly string[]).includes(file.type)) {
        problems.push(fill(tx.attachmentWrongType, { name: file.name }));
        continue;
      }
      if (file.size > MAX_ATTACHMENT_BYTES) {
        problems.push(fill(tx.attachmentTooLarge, { name: file.name }));
        continue;
      }
      try {
        accepted.push(await prepare(file));
      } catch (error) {
        problems.push(
          error instanceof Error && error.message
            ? error.message
            : fill(tx.attachmentUnreadable, { name: file.name }),
        );
      }
    }

    if (accepted.length) onChange([...value, ...accepted].slice(0, MAX_ATTACHMENTS));
    setErrors(problems);
    setBusy(false);
    if (inputRef.current) inputRef.current.value = "";
  }

  function remove(index: number) {
    onChange(value.filter((_, i) => i !== index));
    setErrors([]);
  }

  return (
    <div className={cn("space-y-2.5", className)}>
      <input type="hidden" name={name} value={JSON.stringify(value)} />
      <input
        ref={inputRef}
        type="file"
        accept={ATTACHMENT_ACCEPT_ATTR}
        multiple
        className="hidden"
        disabled={disabled || busy || full}
        onChange={(event) => void handleFiles(event.target.files)}
      />

      <div className="flex flex-wrap items-center gap-2">
        <Button
          type="button"
          variant="outline"
          size={compact ? "sm" : "default"}
          className="gap-2 rounded-lg"
          disabled={disabled || busy || full}
          onClick={() => inputRef.current?.click()}
        >
          {busy ? <Spinner className="size-4" /> : <ImagePlus className="size-4" />}
          {busy ? tx.attachmentProcessing : tx.attachmentsAdd}
        </Button>
        <span className="text-xs text-muted-foreground">
          {full ? tx.attachmentsFull : tx.attachmentsHint}
        </span>
      </div>

      {value.length > 0 ? (
        <ul className="flex flex-wrap gap-2">
          {value.map((file, index) => (
            <li
              key={`${file.name}-${index}`}
              className="group relative w-28 overflow-hidden rounded-xl border bg-card"
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={file.dataUrl}
                alt={file.name}
                className="h-20 w-full object-cover"
              />
              <div className="flex items-center gap-1 px-2 py-1.5">
                <Paperclip className="size-3 shrink-0 text-muted-foreground" />
                <span className="truncate font-mono text-[10px] text-muted-foreground">
                  {formatBytes(file.size)}
                </span>
              </div>
              <button
                type="button"
                aria-label={tx.attachmentRemove}
                title={tx.attachmentRemove}
                disabled={disabled}
                onClick={() => remove(index)}
                className="absolute top-1 right-1 flex size-6 items-center justify-center rounded-full bg-black/60 text-white transition-colors hover:bg-black/80"
              >
                <X className="size-3.5" />
              </button>
            </li>
          ))}
        </ul>
      ) : null}

      {errors.length > 0 ? (
        <ul className="space-y-1">
          {errors.map((message, index) => (
            <li key={index} className="text-xs text-destructive">
              {message}
            </li>
          ))}
        </ul>
      ) : null}
    </div>
  );
}

export default AttachmentPicker;
