'use client';

/**
 * usePaneDrop — widen the composer's file-drop target to the whole chat pane.
 *
 * The composer's own drag surface (`data-wails-dropzone` div) is a small
 * target easy to miss. When a pane hosts one conversation at a time,
 * dropping a file anywhere over the chat should attach it — the ChatGPT /
 * Telegram / Slack model. This hook implements that WITHOUT the composer
 * needing to own the pane's DOM:
 *
 *  - It listens for drag events at the WINDOW level (capture) and gates on
 *    `dataTransfer.types.includes('Files')` — so a text/selection drag never
 *    triggers it.
 *  - The overlay is positioned over the composer's nearest POSITIONED
 *    ancestor (the chat pane, normally `position: relative`). If none is
 *    found it falls back to the viewport. A ResizeObserver + scroll/resize
 *    listeners keep the rect fresh while a drag is in flight.
 *  - The drop routes through the SAME `onFiles` (the composer's validated
 *    `attachFiles`) as the paperclip / paste / composer-drop paths — no
 *    second attach lane.
 *
 * Enabled only when `dropScope === 'pane'` and attaching is on; otherwise the
 * hook is inert and returns a null overlay (the composer keeps its own
 * surface-scoped drop).
 */

import { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

import { cn } from '@djangocfg/ui-core/lib';

export interface UsePaneDropOptions {
  /** Master switch — only bind when the host asked for pane scope + attach on. */
  enabled: boolean;
  /** The composer root — used to find the pane (nearest positioned ancestor). */
  composerRef: React.RefObject<HTMLElement | null>;
  /** Funnel dropped files through the composer's validated attach pipeline. */
  onFiles: (files: File[]) => void;
  /** Overlay caption. Defaults to a plain English string. */
  label?: string;
}

export interface UsePaneDropReturn {
  /** Portal overlay node (or null). Render it anywhere — it portals to body. */
  paneOverlay: React.ReactNode;
}

/** Walk up to the nearest ancestor establishing a positioning context. */
function nearestPositionedAncestor(el: HTMLElement | null): HTMLElement | null {
  let node = el?.parentElement ?? null;
  while (node) {
    const pos = getComputedStyle(node).position;
    if (pos === 'relative' || pos === 'absolute' || pos === 'fixed' || pos === 'sticky') {
      return node;
    }
    node = node.parentElement;
  }
  return null;
}

export function usePaneDrop(options: UsePaneDropOptions): UsePaneDropReturn {
  const { enabled, composerRef, onFiles, label = 'Drop files here' } = options;

  const [dragging, setDragging] = useState(false);
  const [rect, setRect] = useState<DOMRect | null>(null);
  // Counter of enter/leave — a single boolean flickers as the cursor crosses
  // child element boundaries (each generates leave→enter). Depth hits 0 only
  // when the pointer truly left the window.
  const depth = useRef(0);
  const paneRef = useRef<HTMLElement | null>(null);

  const measure = useCallback(() => {
    const pane = paneRef.current;
    setRect(pane ? pane.getBoundingClientRect() : null);
  }, []);

  useEffect(() => {
    if (!enabled) {
      // Ensure a scope flip mid-drag can't leave a stuck overlay.
      depth.current = 0;
      setDragging(false);
      return;
    }

    const hasFiles = (e: DragEvent) =>
      Array.from(e.dataTransfer?.types ?? []).includes('Files');

    const onDragEnter = (e: DragEvent) => {
      if (!hasFiles(e)) return;
      // Resolve the pane lazily on the first enter (the composer is mounted by now).
      paneRef.current = nearestPositionedAncestor(composerRef.current);
      depth.current += 1;
      if (!dragging) {
        measure();
        setDragging(true);
      }
    };
    const onDragOver = (e: DragEvent) => {
      if (!hasFiles(e)) return;
      // Allow the drop (else the browser rejects it and onDrop never fires).
      e.preventDefault();
      if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
    };
    const onDragLeave = (e: DragEvent) => {
      if (!hasFiles(e)) return;
      depth.current = Math.max(0, depth.current - 1);
      if (depth.current === 0) setDragging(false);
    };
    const onDrop = (e: DragEvent) => {
      if (!hasFiles(e)) return;
      e.preventDefault();
      depth.current = 0;
      setDragging(false);
      const files = Array.from(e.dataTransfer?.files ?? []);
      if (files.length > 0) onFiles(files);
    };

    // Native-shell drag signal. WKWebView / WebView2 intercept an OS file drag
    // natively and NEVER emit DOM dragenter/leave, so the listeners above can't
    // light the overlay. The native side instead fans out a `cmdop:native-drag`
    // window event (see cmdop transport `native-drop.ts`) as the drag enters /
    // leaves; we drive the overlay off that. The actual files still arrive via
    // the native `window.__cmdopFileDrop` → attachFiles path, not a DOM drop.
    const onNativeDrag = (e: Event) => {
      const active = !!(e as CustomEvent<{ active?: boolean }>).detail?.active;
      if (active) {
        paneRef.current = nearestPositionedAncestor(composerRef.current);
        measure();
        setDragging(true);
      } else {
        depth.current = 0;
        setDragging(false);
      }
    };

    // Capture phase so we see the drag before any inner handler stops it.
    window.addEventListener('dragenter', onDragEnter, true);
    window.addEventListener('dragover', onDragOver, true);
    window.addEventListener('dragleave', onDragLeave, true);
    window.addEventListener('drop', onDrop, true);
    window.addEventListener('cmdop:native-drag', onNativeDrag);
    return () => {
      window.removeEventListener('dragenter', onDragEnter, true);
      window.removeEventListener('dragover', onDragOver, true);
      window.removeEventListener('dragleave', onDragLeave, true);
      window.removeEventListener('drop', onDrop, true);
      window.removeEventListener('cmdop:native-drag', onNativeDrag);
    };
  }, [enabled, composerRef, onFiles, measure, dragging]);

  // Keep the overlay aligned with the pane while a drag is in flight.
  useEffect(() => {
    if (!dragging) return;
    const onChange = () => measure();
    window.addEventListener('scroll', onChange, true);
    window.addEventListener('resize', onChange);
    const ro = paneRef.current ? new ResizeObserver(onChange) : null;
    if (ro && paneRef.current) ro.observe(paneRef.current);
    return () => {
      window.removeEventListener('scroll', onChange, true);
      window.removeEventListener('resize', onChange);
      ro?.disconnect();
    };
  }, [dragging, measure]);

  if (!enabled || !dragging || typeof document === 'undefined') {
    return { paneOverlay: null };
  }

  // Cover the pane rect (or the viewport if we couldn't find a positioned
  // ancestor). `pointer-events-none` so the underlying drop target still
  // receives the drop event — the overlay is a pure visual affordance.
  const style: React.CSSProperties = rect
    ? { position: 'fixed', top: rect.top, left: rect.left, width: rect.width, height: rect.height }
    : { position: 'fixed', inset: 0 };

  const overlay = (
    <div
      aria-hidden
      className="pointer-events-none z-50"
      style={style}
    >
      <div
        className={cn(
          'absolute inset-2 flex flex-col items-center justify-center gap-3 rounded-2xl',
          'border-2 border-dashed border-primary/50 bg-primary/5 backdrop-blur-[2px]',
          'transition-opacity duration-150',
        )}
      >
        <div className="flex h-14 w-14 items-center justify-center rounded-full bg-primary/10 ring-1 ring-primary/30">
          <svg
            width="26" height="26" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"
            className="text-primary"
          >
            <path d="M12 3v12" />
            <path d="m7 10 5 5 5-5" />
            <path d="M5 21h14" />
          </svg>
        </div>
        <span className="text-sm font-medium text-foreground/90">{label}</span>
      </div>
    </div>
  );

  return { paneOverlay: createPortal(overlay, document.body) };
}
