'use client';

import {
  type ChangeEvent,
  type ClipboardEvent,
  type KeyboardEvent,
  type RefObject,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';

import type { ChatAttachment } from '../types';
import { LIMITS } from '../constants';
import { sanitizeDraft } from '../utils';

export interface UseChatComposerOptions {
  onSubmit: (content: string, attachments: ChatAttachment[]) => void | Promise<void>;
  initialValue?: string;
  maxLength?: number;
  maxAttachments?: number;
  disabled?: boolean;
  /** 'enter' = Enter sends, Shift+Enter newline. 'cmd+enter' = Enter inserts newline, Cmd/Ctrl+Enter sends. */
  submitOn?: 'enter' | 'cmd+enter';
  history?: { enabled?: boolean; size?: number };
  onPasteFiles?: (files: File[]) => void;
  /**
   * Persist the current draft to `sessionStorage` under this key. The
   * draft is loaded once on mount (overrides `initialValue` if non-
   * empty) and rewritten on every value change. Cleared on `reset()`.
   *
   * Pass a per-conversation id to keep separate drafts per chat. Pass
   * `undefined` (default) to disable persistence — composer behaves
   * exactly as before. Plan64.
   */
  persistKey?: string;
  /**
   * Skip pre-submit draft sanitation (trim + line-ending normalise +
   * zero-width strip). Default `false` — sanitation matches what
   * ChatGPT / Claude / Telegram ship and is what consumers want 99% of
   * the time. Set to `true` for niche flows that need byte-perfect
   * passthrough (clipboard inspector, raw-prompt debug tool).
   */
  preserveExactValue?: boolean;
}

export interface UseChatComposerReturn {
  value: string;
  setValue: (next: string) => void;
  attachments: ChatAttachment[];
  addAttachment: (a: ChatAttachment) => void;
  /** Patch an existing attachment in place — used by the upload lifecycle
   *  to flip `status` / `progress` / `url` without losing list order. */
  updateAttachment: (id: string, patch: Partial<ChatAttachment>) => void;
  removeAttachment: (id: string) => void;
  isSubmitting: boolean;
  canSubmit: boolean;
  submit: () => Promise<void>;
  reset: () => void;
  focus: () => void;
  textareaRef: RefObject<HTMLTextAreaElement | null>;
  textareaProps: {
    ref: RefObject<HTMLTextAreaElement | null>;
    value: string;
    disabled: boolean;
    onChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
    onKeyDown: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
    onPaste: (e: ClipboardEvent<HTMLTextAreaElement>) => void;
  };
  recallPrevious: () => void;
  recallNext: () => void;
}

const MAX_TEXTAREA_HEIGHT = 240;

export function useChatComposer(options: UseChatComposerOptions): UseChatComposerReturn {
  const {
    onSubmit,
    initialValue = '',
    maxLength = LIMITS.messageMaxLength,
    maxAttachments = LIMITS.attachmentsMax,
    disabled = false,
    submitOn = 'enter',
    history = { enabled: true, size: LIMITS.composerHistorySize },
    onPasteFiles,
    persistKey,
    preserveExactValue = false,
  } = options;

  // Hydrate draft from sessionStorage on mount when a key is provided.
  // We read once, lazily — switching `persistKey` mid-life (e.g.
  // session change) requires a parent remount via React `key`, same
  // pattern as `<MessageList>`. Avoids accidental cross-session
  // bleed-through when the parent forgets to remount.
  const initialFromStorage = (() => {
    if (!persistKey || typeof window === 'undefined') return initialValue;
    try {
      const stored = window.sessionStorage.getItem(`chat:draft:${persistKey}`);
      return stored && stored.length > 0 ? stored : initialValue;
    } catch {
      return initialValue;
    }
  })();
  const [value, setValueState] = useState(initialFromStorage);

  // Persist on every value change. Throwaway swallow keeps quota /
  // private-mode failures from breaking the composer.
  useEffect(() => {
    if (!persistKey || typeof window === 'undefined') return;
    try {
      const k = `chat:draft:${persistKey}`;
      if (value.length > 0) window.sessionStorage.setItem(k, value);
      else window.sessionStorage.removeItem(k);
    } catch {
      /* noop — quota / disabled storage is non-fatal */
    }
  }, [value, persistKey]);
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);

  const historyRef = useRef<{ items: string[]; index: number }>({ items: [], index: -1 });

  const setValue = useCallback(
    (next: string) => {
      setValueState(next.length > maxLength ? next.slice(0, maxLength) : next);
    },
    [maxLength],
  );

  // Autosize textarea on value change.
  useEffect(() => {
    const el = textareaRef.current;
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = `${Math.min(el.scrollHeight, MAX_TEXTAREA_HEIGHT)}px`;
  }, [value]);

  const reset = useCallback(() => {
    setValueState('');
    setAttachments([]);
    historyRef.current.index = -1;
  }, []);

  const focus = useCallback(() => {
    requestAnimationFrame(() => textareaRef.current?.focus());
  }, []);

  const submit = useCallback(async () => {
    // Sanitise BEFORE the empty-guard so a draft of only whitespace
    // (`   \n\n   `) is treated as empty — saves a round trip for
    // "send" attempts that would produce nothing.
    //
    // sanitizeDraft is intentionally conservative: trim outer
    // whitespace, normalise line endings, strip zero-width chars.
    // It does NOT collapse internal whitespace or cap blank lines
    // (would break code indentation / intentional separators). See
    // utils/sanitizeDraft.ts for the full ruleset + rationale.
    //
    // The cleaned text is what reaches `onSubmit` (matches ChatGPT /
    // Claude / Telegram behaviour — the bubble shows what the user
    // last saw, sans accidental trailing whitespace). Niche callers
    // can opt out via `preserveExactValue`.
    const cleaned = preserveExactValue ? value : sanitizeDraft(value);
    if ((!cleaned && attachments.length === 0) || isSubmitting || disabled) return;
    setIsSubmitting(true);
    try {
      if (history.enabled !== false && cleaned) {
        const buf = historyRef.current.items;
        if (buf[buf.length - 1] !== cleaned) {
          buf.push(cleaned);
          if (buf.length > (history.size ?? LIMITS.composerHistorySize)) buf.shift();
        }
        historyRef.current.index = -1;
      }
      const snapshot = [...attachments];
      reset();
      await onSubmit(cleaned, snapshot);
    } finally {
      setIsSubmitting(false);
    }
  }, [value, attachments, isSubmitting, disabled, history, onSubmit, reset, preserveExactValue]);

  const addAttachment = useCallback(
    (a: ChatAttachment) => {
      setAttachments((prev) => {
        if (prev.some((p) => p.id === a.id)) {
          return prev.map((p) => (p.id === a.id ? a : p));
        }
        if (prev.length >= maxAttachments) return prev;
        return [...prev, a];
      });
    },
    [maxAttachments],
  );

  const updateAttachment = useCallback(
    (id: string, patch: Partial<ChatAttachment>) => {
      setAttachments((prev) =>
        prev.map((a) => (a.id === id ? { ...a, ...patch } : a)),
      );
    },
    [],
  );

  const removeAttachment = useCallback((id: string) => {
    setAttachments((prev) => prev.filter((a) => a.id !== id));
  }, []);

  const recallPrevious = useCallback(() => {
    const { items } = historyRef.current;
    if (!items.length) return;
    const next = historyRef.current.index < 0 ? items.length - 1 : Math.max(0, historyRef.current.index - 1);
    historyRef.current.index = next;
    setValueState(items[next] ?? '');
  }, []);

  const recallNext = useCallback(() => {
    const { items } = historyRef.current;
    if (!items.length || historyRef.current.index < 0) return;
    const next = historyRef.current.index + 1;
    if (next >= items.length) {
      historyRef.current.index = -1;
      setValueState('');
      return;
    }
    historyRef.current.index = next;
    setValueState(items[next] ?? '');
  }, []);

  const onChange = useCallback(
    (e: ChangeEvent<HTMLTextAreaElement>) => {
      setValue(e.target.value);
    },
    [setValue],
  );

  const onKeyDown = useCallback(
    (e: KeyboardEvent<HTMLTextAreaElement>) => {
      if (e.key === 'Enter') {
        const isCmd = e.metaKey || e.ctrlKey;
        const shouldSend = submitOn === 'cmd+enter' ? isCmd : !e.shiftKey;
        if (shouldSend) {
          e.preventDefault();
          void submit();
        }
        return;
      }
      if (history.enabled !== false && value === '' && attachments.length === 0) {
        if (e.key === 'ArrowUp') {
          e.preventDefault();
          recallPrevious();
        } else if (e.key === 'ArrowDown' && historyRef.current.index >= 0) {
          e.preventDefault();
          recallNext();
        }
      }
    },
    [submitOn, submit, history, value, attachments.length, recallPrevious, recallNext],
  );

  const onPaste = useCallback(
    (e: ClipboardEvent<HTMLTextAreaElement>) => {
      const files = Array.from(e.clipboardData?.files ?? []);
      if (files.length && onPasteFiles) {
        e.preventDefault();
        onPasteFiles(files);
      }
    },
    [onPasteFiles],
  );

  // canSubmit mirrors what submit() will actually do — gate the Send
  // button on the post-sanitation result so a whitespace-only draft
  // renders Send as disabled (no false-hope affordance).
  // preserveExactValue callers fall back to raw .trim() — they
  // opted out of sanitation, but we still don't enable Send on a
  // pure-whitespace draft (an empty message is rarely intentional).
  const canSubmit =
    !disabled && !isSubmitting && (
      (preserveExactValue ? value.trim().length : sanitizeDraft(value).length) > 0
      || attachments.length > 0
    );

  return {
    value,
    setValue,
    attachments,
    addAttachment,
    updateAttachment,
    removeAttachment,
    isSubmitting,
    canSubmit,
    submit,
    reset,
    focus,
    textareaRef,
    textareaProps: {
      ref: textareaRef,
      value,
      disabled,
      onChange,
      onKeyDown,
      onPaste,
    },
    recallPrevious,
    recallNext,
  };
}
