import { type AttachmentHandle, downloadAndDecryptAttachment } from '@chat-app/shared/chat';
import { useEffect, useState } from 'react';

import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
import { supabase } from '../lib/supabase';
import { AlertIcon, SpinnerIcon } from './icons';
import { Lightbox } from './Lightbox';
import { ViewOnceImage } from './ViewOnceImage';

interface Props {
  handle: AttachmentHandle;
  /** True iff the local user sent this message. View-once images use this
   *  to suppress the burn (senders never burn their own attachment) and to
   *  pick the right preview chrome. Defaults to false so existing callers
   *  (e.g. MediaFilesDrawer) keep working without modification. */
  mine?: boolean;
}

// Max inline-preview dimension. Full-resolution stays available for the
// lightbox. Animated formats (gif/webp/apng) are passed through untouched
// so animation isn't lost; everything else is downscaled to this box.
const THUMB_MAX_DIM = 640;
const ANIMATED_MIME = /^image\/(gif|apng|webp)/;

async function makeThumbnail(blob: Blob): Promise<Blob | null> {
  if (ANIMATED_MIME.test(blob.type)) return null;
  if (typeof createImageBitmap !== 'function') return null;
  if (typeof OffscreenCanvas !== 'function') return null;
  try {
    const bitmap = await createImageBitmap(blob);
    const largest = Math.max(bitmap.width, bitmap.height);
    if (largest <= THUMB_MAX_DIM) {
      bitmap.close();
      return null;
    }
    const scale = THUMB_MAX_DIM / largest;
    const w = Math.max(1, Math.round(bitmap.width * scale));
    const h = Math.max(1, Math.round(bitmap.height * scale));
    const canvas = new OffscreenCanvas(w, h);
    const ctx = canvas.getContext('2d');
    if (!ctx) {
      bitmap.close();
      return null;
    }
    ctx.drawImage(bitmap, 0, 0, w, h);
    bitmap.close();
    return await canvas.convertToBlob({ type: 'image/webp', quality: 0.8 });
  } catch {
    return null;
  }
}

export function AttachmentImage({ handle, mine = false }: Props) {
  const [fullUrl, setFullUrl] = useState<string | null>(null);
  const [thumbUrl, setThumbUrl] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [lightboxOpen, setLightboxOpen] = useState(false);

  useEffect(() => {
    let cancelled = false;
    const created: string[] = [];
    setError(null);
    setFullUrl(null);
    setThumbUrl(null);

    const take = (blob: Blob): string => {
      const u = URL.createObjectURL(blob);
      created.push(u);
      return u;
    };

    // OPFS cache → decrypt → generate thumbnail for inline display.
    // Lightbox swaps to the full blob when opened.
    void (async () => {
      const cached = await getCachedAttachment(handle.id);
      let blob: Blob;
      if (cached) {
        blob = cached;
      } else {
        try {
          blob = await downloadAndDecryptAttachment({ client: supabase, handle });
          void putCachedAttachment(handle.id, blob);
        } catch (err: unknown) {
          if (!cancelled) {
            setError(err instanceof Error ? err.message : 'download failed');
          }
          return;
        }
      }
      if (cancelled) return;
      const full = take(blob);
      setFullUrl(full);
      const thumb = await makeThumbnail(blob);
      if (cancelled) return;
      if (thumb) {
        setThumbUrl(take(thumb));
      }
    })();

    return () => {
      cancelled = true;
      for (const u of created) URL.revokeObjectURL(u);
    };
  }, [handle.id, handle.storagePath, handle.keyB64, handle.nonceB64]);

  const blobUrl = thumbUrl ?? fullUrl;

  if (error) {
    return (
      <div className="mt-2 inline-flex items-center gap-2 rounded-lg border border-rose-500/20 bg-rose-500/10 px-3 py-2 text-xs text-rose-200">
        <AlertIcon className="h-4 w-4" />
        <span>{error}</span>
      </div>
    );
  }

  if (!blobUrl) {
    return (
      <div className="mt-2 flex h-28 w-28 items-center justify-center rounded-lg border border-white/10 bg-ink-800/60">
        <SpinnerIcon className="h-5 w-5 text-brand-400" />
      </div>
    );
  }

  // View-once attachments swap the standard image preview for the ViewOnceImage
  // chrome (locked card → fullscreen lightbox + burn for recipient, normal
  // image with badge for sender). We still kick off the decrypt above so the
  // fullscreen lightbox has the decoded blob ready the moment the recipient
  // taps. `handle.viewedAt` is undefined in the current renderer (it lives on
  // the public attachment row, not in the encrypted payload) — the component
  // treats undefined as "not yet burned" and uses the RPC response to flip
  // the state locally after the recipient opens.
  if (handle.viewOnce) {
    return (
      <ViewOnceImage
        attachmentId={handle.id}
        viewedAt={handle.viewedAt ?? null}
        isSender={mine}
        src={blobUrl}
      />
    );
  }

  return (
    <>
      <button
        type="button"
        onClick={() => setLightboxOpen(true)}
        aria-label="Bild öffnen"
        className="mt-2 block w-fit max-w-full cursor-pointer overflow-hidden rounded-lg border border-white/10 bg-ink-800/40 transition hover:border-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/40"
      >
        <img
          src={blobUrl}
          alt="attachment"
          loading="lazy"
          decoding="async"
          className="block h-auto max-h-80 w-auto max-w-full object-contain"
        />
      </button>
      {lightboxOpen && fullUrl && <Lightbox url={fullUrl} onClose={() => setLightboxOpen(false)} />}
    </>
  );
}

// Lightbox extracted to ./Lightbox.tsx so the settings avatar preview and
// any future surface can reuse the same dialog without duplication.
