'use client';

import {
  isSupportAttachmentAllowed,
  isSupportAttachmentImage,
  SUPPORT_ATTACHMENT_MAX_BYTES,
  SUPPORT_ATTACHMENT_MAX_PER_MESSAGE,
} from '@edr/types';
import { useCallback, useEffect, useRef, useState } from 'react';

/** A file staged in the composer, not yet sent. */
export interface PendingAttachment {
  /** Local-only id; the server id doesn't exist until the message is sent. */
  id: string;
  file: File;
  /** Object URL, images only. Revoked when the entry goes away. */
  previewUrl?: string;
}

let nextId = 0;

/**
 * Staging area for files being attached to a message.
 *
 * Files are held client-side until send, then posted alongside the text in one
 * multipart request — there's no upload-then-reference step, so nothing to
 * garbage-collect if the agent changes their mind.
 *
 * Object URLs for image previews are revoked on removal and unmount; without
 * that, pasting screenshots into a long-lived chat page leaks the full bytes of
 * every image for the life of the tab.
 */
export function useAttachmentDraft(onReject?: (reason: string) => void) {
  const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
  const rejectRef = useRef(onReject);
  rejectRef.current = onReject;

  // Read from a ref in the unmount cleanup so it doesn't re-run (and revoke
  // still-live URLs) on every change to the list.
  const attachmentsRef = useRef(attachments);
  attachmentsRef.current = attachments;
  useEffect(
    () => () => {
      for (const a of attachmentsRef.current) {
        if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
      }
    },
    [],
  );

  const add = useCallback((files: File[]) => {
    if (files.length === 0) return;
    setAttachments((current) => {
      const accepted: PendingAttachment[] = [];
      for (const file of files) {
        if (current.length + accepted.length >= SUPPORT_ATTACHMENT_MAX_PER_MESSAGE) {
          rejectRef.current?.(`Up to ${SUPPORT_ATTACHMENT_MAX_PER_MESSAGE} files per message.`);
          break;
        }
        if (!isSupportAttachmentAllowed(file.type)) {
          rejectRef.current?.(`${file.name}: that file type isn't supported.`);
          continue;
        }
        if (file.size > SUPPORT_ATTACHMENT_MAX_BYTES) {
          rejectRef.current?.(
            `${file.name} is over the ${SUPPORT_ATTACHMENT_MAX_BYTES / (1024 * 1024)}MB limit.`,
          );
          continue;
        }
        accepted.push({
          id: `pending-${nextId++}`,
          file,
          previewUrl: isSupportAttachmentImage(file.type) ? URL.createObjectURL(file) : undefined,
        });
      }
      return accepted.length ? [...current, ...accepted] : current;
    });
  }, []);

  const remove = useCallback((id: string) => {
    setAttachments((current) => {
      const target = current.find((a) => a.id === id);
      if (target?.previewUrl) URL.revokeObjectURL(target.previewUrl);
      return current.filter((a) => a.id !== id);
    });
  }, []);

  const clear = useCallback(() => {
    setAttachments((current) => {
      for (const a of current) {
        if (a.previewUrl) URL.revokeObjectURL(a.previewUrl);
      }
      return [];
    });
  }, []);

  /**
   * Pull files off a paste. Returns true if anything was taken, so the caller
   * can suppress the default paste — otherwise pasting a screenshot also drops
   * its filename (or nothing) into the textarea.
   *
   * Copying an image in most apps puts BOTH the bitmap and some text/html on
   * the clipboard, so check for files first and only then let the text through.
   */
  const addFromPaste = useCallback(
    (clipboard: DataTransfer | null): boolean => {
      const files = Array.from(clipboard?.files ?? []);
      if (files.length === 0) return false;
      add(files);
      return true;
    },
    [add],
  );

  return {
    attachments,
    files: attachments.map((a) => a.file),
    add,
    addFromPaste,
    remove,
    clear,
  };
}
