'use client';

import { useCallback, useEffect, useMemo, useRef, type RefObject } from 'react';

import { buildAcceptString, useClipboardPaste } from '../../forms/Uploader';
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
import { fileToAttachment, revokeAttachmentUrl } from './fileToAttachment';
import {
  DEFAULT_PASTE_TEXT_THRESHOLD,
  textToAttachment,
} from './textToAttachment';
import type {
  ComposerAcceptType,
  ComposerAttachConfig,
  ComposerAttachHandle,
} from './types';

const ALL_ACCEPT: ComposerAcceptType[] = ['image', 'audio', 'video', 'document'];

/** True when a file's MIME is allowed by the accepted asset categories. */
function isAcceptedType(file: File, accept: ComposerAcceptType[]): boolean {
  const mime = file.type;
  if (mime.startsWith('image/')) return accept.includes('image');
  if (mime.startsWith('audio/')) return accept.includes('audio');
  if (mime.startsWith('video/')) return accept.includes('video');
  // No / other MIME — treat as a document.
  return accept.includes('document');
}

export interface UseComposerAttachParams {
  composer: UseChatComposerReturn;
  config: ComposerAttachConfig;
  disabled?: boolean;
  /** Paste listener scope. Defaults to `document` when omitted. */
  pasteScopeRef?: RefObject<HTMLElement | null>;
}

/**
 * The unified attach pipeline. One validated path that the paperclip
 * button, the `+` menu, drag-drop and Ctrl+V paste all funnel into:
 *
 *   openPicker() / attachFiles(File[])
 *     → validate (size / type / count)
 *     → fileToAttachment()
 *     → composer.addAttachment()
 *
 * Phase 2 stops at `status:'ready'` (object-URL). The `uploadFn`
 * lifecycle lands in phase 5 — `config.uploadFn` is accepted here but
 * not yet consumed.
 *
 * Reuses `useClipboardPaste` + `buildAcceptString` from the Uploader
 * tool rather than re-implementing clipboard / mime handling.
 */
export function useComposerAttach({
  composer,
  config,
  disabled = false,
  pasteScopeRef,
}: UseComposerAttachParams): ComposerAttachHandle {
  const {
    accept = ALL_ACCEPT,
    maxFiles,
    maxSizeBytes,
    multiple = true,
    pasteEnabled = true,
    pasteTextAsChunk = true,
    pasteTextThreshold = DEFAULT_PASTE_TEXT_THRESHOLD,
    uploadFn,
    onReject,
  } = config;

  const inputRef = useRef<HTMLInputElement>(null);

  // Stable refs so the paste listener / handlers never go stale without
  // forcing the consumer to memoize anything.
  const composerRef = useRef(composer);
  composerRef.current = composer;
  const onRejectRef = useRef(onReject);
  onRejectRef.current = onReject;
  const uploadFnRef = useRef(uploadFn);
  uploadFnRef.current = uploadFn;

  const acceptString = useMemo(() => {
    // buildAcceptString expects the Uploader's AssetType — same string
    // union as ComposerAcceptType, so the cast is safe.
    return buildAcceptString(accept as Parameters<typeof buildAcceptString>[0]);
  }, [accept]);

  // Drive one file through the host `uploadFn`: flip the attachment to
  // `uploading`, stream progress, then settle on `ready` (url rewritten
  // to the remote location) or `error`. Fire-and-forget — failures are
  // surfaced on the attachment, never thrown.
  const runUpload = useCallback(
    (file: File, attachmentId: string, localUrl: string) => {
      const fn = uploadFnRef.current;
      if (!fn) return;
      const c = composerRef.current;
      c.updateAttachment(attachmentId, { status: 'uploading', progress: 0 });
      fn(file, (fraction) => {
        composerRef.current.updateAttachment(attachmentId, {
          progress: Math.max(0, Math.min(1, fraction)),
        });
      })
        .then((result) => {
          composerRef.current.updateAttachment(attachmentId, {
            status: 'ready',
            progress: 1,
            url: result.url,
            thumbnailUrl: result.thumbnailUrl,
          });
          // The remote URL replaced the object-URL — free it.
          revokeAttachmentUrl({ url: localUrl });
        })
        .catch(() => {
          composerRef.current.updateAttachment(attachmentId, { status: 'error' });
        });
    },
    [],
  );

  // The single validated entry point. Drop / paste / picker all land here.
  const attachFiles = useCallback(
    (files: File[]) => {
      if (disabled || files.length === 0) return;
      const c = composerRef.current;
      const cap = maxFiles ?? Number.POSITIVE_INFINITY;

      let slots = cap - c.attachments.length;
      for (const file of files) {
        if (slots <= 0) {
          onRejectRef.current?.(file, 'count');
          continue;
        }
        if (maxSizeBytes != null && file.size > maxSizeBytes) {
          onRejectRef.current?.(file, 'size');
          continue;
        }
        if (!isAcceptedType(file, accept)) {
          onRejectRef.current?.(file, 'type');
          continue;
        }
        const attachment = fileToAttachment(file);
        c.addAttachment(attachment);
        // With an uploadFn the attachment starts local then uploads in
        // the background; without one it stays `ready` immediately.
        if (uploadFnRef.current) {
          runUpload(file, attachment.id, attachment.url);
        }
        slots -= 1;
      }
    },
    [disabled, maxFiles, maxSizeBytes, accept, runUpload],
  );

  const openPicker = useCallback(() => {
    if (disabled) return;
    inputRef.current?.click();
  }, [disabled]);

  // Remove a staged attachment, freeing its object-URL first so a
  // discarded blob does not leak.
  const removeAttachment = useCallback((id: string) => {
    const c = composerRef.current;
    const found = c.attachments.find((a) => a.id === id);
    if (found) revokeAttachmentUrl(found);
    c.removeAttachment(id);
  }, []);

  const onInputChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const picked = e.target.files;
      if (picked && picked.length > 0) attachFiles(Array.from(picked));
      // Reset so picking the same file twice still fires `change`.
      e.target.value = '';
    },
    [attachFiles],
  );

  // Ctrl+V / Cmd+V — reuse the Uploader's clipboard resolver. It already
  // skips text fields when the clipboard carries text, so pasting an
  // image into the composer textarea attaches without hijacking typing.
  const pasteAccept = useMemo(
    () => accept.filter((a) => a !== 'document'),
    [accept],
  );
  useClipboardPaste(
    {
      enabled: pasteEnabled && !disabled,
      acceptTypes: pasteAccept.length > 0 ? pasteAccept : undefined,
      maxBytes: maxSizeBytes,
      onFiles: attachFiles,
    },
    pasteScopeRef,
  );

  // Long-text paste → "Pasted text" chunk. A separate listener from the
  // file path above: `useClipboardPaste` deliberately ignores pastes
  // that carry text into a text field (so it doesn't hijack typing), so
  // it never sees this case. We intercept here, before the browser
  // inserts the text into the textarea, and mint a `type:'text'`
  // attachment instead — the ChatGPT/Claude behaviour.
  const pasteTextEnabled = pasteEnabled && pasteTextAsChunk && !disabled;
  useEffect(() => {
    if (!pasteTextEnabled) return undefined;
    const target: HTMLElement | Document = pasteScopeRef?.current ?? document;

    const onPaste = (e: Event) => {
      const ev = e as ClipboardEvent;
      const cd = ev.clipboardData;
      if (!cd) return;
      // Only chunk pastes aimed at the composer's editable surface —
      // a paste into some unrelated input inside the scope is left alone.
      const node = ev.target;
      const el =
        node instanceof HTMLElement ? node : null;
      const isEditable =
        el != null &&
        (el instanceof HTMLTextAreaElement ||
          el instanceof HTMLInputElement ||
          el.isContentEditable);
      if (!isEditable) return;
      // A file/blob paste is the image path's job — don't steal it.
      if (Array.from(cd.items).some((i) => i.kind === 'file')) return;
      const text = cd.getData('text/plain');
      if (text.length < pasteTextThreshold) return;
      // Respect the file-count cap (text chunks share the attachment list).
      const c = composerRef.current;
      if (maxFiles != null && c.attachments.length >= maxFiles) return;
      e.preventDefault();
      c.addAttachment(textToAttachment(text));
    };

    target.addEventListener('paste', onPaste);
    return () => target.removeEventListener('paste', onPaste);
  }, [pasteTextEnabled, pasteTextThreshold, maxFiles, pasteScopeRef]);

  // On unmount, free any object-URLs still held by staged attachments
  // (drafts abandoned without sending). `composerRef` gives the latest
  // list without re-running the effect on every attach.
  useEffect(() => {
    return () => {
      for (const a of composerRef.current.attachments) {
        revokeAttachmentUrl(a);
      }
    };
  }, []);

  const atCap =
    maxFiles != null && composer.attachments.length >= maxFiles;

  return {
    openPicker,
    attachFiles,
    removeAttachment,
    disabled: disabled || atCap,
    inputProps: {
      ref: inputRef,
      type: 'file',
      accept: acceptString,
      multiple,
      hidden: true,
      onChange: onInputChange,
    },
  };
}
