import { defineTrait } from '../define.js';

/**
 * Declarative drop zone — wires the host into the HTML5 drag-and-drop
 * pipeline so a sibling `draggable` / `drag-ghost` source (or a native
 * file drag from the OS) can land on it.
 *
 * Lifecycle:
 *   dragenter → set data-drop-target-over, dispatch `drop-enter`
 *   dragover  → conditionally call preventDefault() per the
 *               `data-drop-target-accepts` filter (calling preventDefault
 *               is the platform's own "this target accepts" signal —
 *               without it the browser shows a reject cursor + the
 *               subsequent `drop` event never fires)
 *   dragleave → clear data-drop-target-over, dispatch `drop-leave`
 *   drop      → dispatch `drop-receive` with { dataTransfer, draggable };
 *               consumer is responsible for processing the payload
 *
 * Accept filter (`data-drop-target-accepts`):
 *   ""        → accepts anything
 *   "files"   → only file drops (e.dataTransfer.types.includes('Files'))
 *   "<csv>"   → only the listed MIME types (image/png, application/json…)
 *
 * When a payload is rejected the trait does NOT call preventDefault, so
 * the cursor shows reject. It also sets `data-drop-target-rejected`
 * with a short reason ("type" | "files-only") so consumers can style
 * the rejection feedback (red flash, alert toast, etc.) and dispatches
 * a `drop-rejected` event so reactive code can respond.
 */

const FILES_TYPE = 'Files';

function parseAcceptList(raw) {
  if (raw == null) return null; // attribute absent → accept anything
  const trimmed = String(raw).trim();
  if (trimmed === '') return null; // empty string → accept anything

  // Special token: only file drops.
  if (trimmed.toLowerCase() === 'files') return { mode: 'files' };

  // Otherwise: csv MIME list. Split, trim, lowercase for case-insensitive
  // compare against dataTransfer.types (which are already lowercase per spec).
  const list = trimmed
    .split(',')
    .map((s) => s.trim().toLowerCase())
    .filter(Boolean);

  if (list.length === 0) return null;
  return { mode: 'mime', list };
}

function evaluateAccept(dataTransfer, parsed) {
  // null / no constraint → accept anything.
  if (!parsed) return { ok: true };
  if (!dataTransfer) return { ok: false, reason: 'no-data' };

  // dataTransfer.types is a DOMStringList (also indexable like an array).
  // Coerce to a real array for set-membership checks.
  const types = Array.from(dataTransfer.types || []);

  if (parsed.mode === 'files') {
    return types.includes(FILES_TYPE)
      ? { ok: true }
      : { ok: false, reason: 'files-only' };
  }

  if (parsed.mode === 'mime') {
    // dataTransfer carries 'Files' for OS-level file drags AND the file's
    // MIME type once the file objects are inspected. We accept either:
    // a literal type match, or a 'Files' drag whose first item matches.
    const lowerTypes = types.map((t) => t.toLowerCase());

    // Direct MIME type match (e.g., 'text/plain' from a text drag).
    if (parsed.list.some((m) => lowerTypes.includes(m))) {
      return { ok: true };
    }

    // OS file drag: inspect the items list for file-kind entries.
    if (lowerTypes.includes(FILES_TYPE.toLowerCase()) && dataTransfer.items) {
      const items = Array.from(dataTransfer.items);
      const fileItem = items.find((it) => it.kind === 'file');
      if (fileItem && parsed.list.includes(fileItem.type.toLowerCase())) {
        return { ok: true };
      }
    }

    return { ok: false, reason: 'type' };
  }

  return { ok: true };
}

export const dropTarget = defineTrait({
  name: 'drop-target',
  category: 'motion-positioning',
  description: 'Declarative drop zone: hit-testing, accept-reject, drag-over feedback',
  attributes: ['data-drop-target-active', 'data-drop-target-over', 'data-drop-target-rejected'],
  events: ['drop-enter', 'drop-leave', 'drop-receive', 'drop-rejected'],
  config: ['data-drop-target-accepts'],
  setup({ host }) {
    // dragenter/dragleave fire for descendants too — track depth so we
    // only flip data-drop-target-over off when the pointer truly leaves
    // the host's subtree, not when it crosses into a child element.
    let enterDepth = 0;

    function isDisabled() {
      return host.hasAttribute('disabled');
    }

    function readAccepts() {
      return parseAcceptList(host.getAttribute('data-drop-target-accepts'));
    }

    function clearOver() {
      enterDepth = 0;
      host.removeAttribute('data-drop-target-over');
      host.removeAttribute('data-drop-target-rejected');
    }

    function onDragEnter(e) {
      if (isDisabled()) return;
      enterDepth++;
      if (enterDepth === 1) {
        host.setAttribute('data-drop-target-over', '');
        host.dispatchEvent(new CustomEvent('drop-enter', {
          bubbles: true,
          detail: { dataTransfer: e.dataTransfer },
        }));
      }
    }

    function onDragOver(e) {
      if (isDisabled()) return;

      const parsed = readAccepts();
      const verdict = evaluateAccept(e.dataTransfer, parsed);

      if (verdict.ok) {
        // preventDefault on dragover is the platform contract: "this target
        // accepts this payload". Without it, the drop event never fires
        // and the cursor shows the reject icon.
        e.preventDefault();
        if (host.hasAttribute('data-drop-target-rejected')) {
          host.removeAttribute('data-drop-target-rejected');
        }
      } else {
        // No preventDefault — cursor shows reject. Surface the reason so
        // CSS can theme the rejection (red flash, etc.).
        if (host.getAttribute('data-drop-target-rejected') !== verdict.reason) {
          host.setAttribute('data-drop-target-rejected', verdict.reason);
          host.dispatchEvent(new CustomEvent('drop-rejected', {
            bubbles: true,
            detail: { reason: verdict.reason, dataTransfer: e.dataTransfer },
          }));
        }
      }
    }

    function onDragLeave() {
      if (isDisabled()) return;
      enterDepth = Math.max(0, enterDepth - 1);
      if (enterDepth === 0) {
        host.removeAttribute('data-drop-target-over');
        host.removeAttribute('data-drop-target-rejected');
        host.dispatchEvent(new CustomEvent('drop-leave', { bubbles: true }));
      }
    }

    function onDrop(e) {
      if (isDisabled()) return;

      const parsed = readAccepts();
      const verdict = evaluateAccept(e.dataTransfer, parsed);

      if (!verdict.ok) {
        // Defensive — drop should not have fired (no preventDefault on
        // dragover would have suppressed it), but a programmatic dispatch
        // can still land here. Treat as rejection.
        host.setAttribute('data-drop-target-rejected', verdict.reason);
        host.dispatchEvent(new CustomEvent('drop-rejected', {
          bubbles: true,
          detail: { reason: verdict.reason, dataTransfer: e.dataTransfer },
        }));
        clearOver();
        return;
      }

      // preventDefault on the drop itself stops the browser's default
      // behavior (e.g., navigating to a dropped file's URL).
      e.preventDefault();

      // The 'draggable' detail field is best-effort — only populated when
      // the drag started from another element in the same document, since
      // OS file drags carry no source element.
      const draggable = e.dataTransfer?.types?.includes?.('text/x-source-id')
        ? document.getElementById(e.dataTransfer.getData('text/x-source-id'))
        : null;

      host.dispatchEvent(new CustomEvent('drop-receive', {
        bubbles: true,
        detail: { dataTransfer: e.dataTransfer, draggable },
      }));

      clearOver();
    }

    host.setAttribute('data-drop-target-active', '');
    host.addEventListener('dragenter', onDragEnter);
    host.addEventListener('dragover', onDragOver);
    host.addEventListener('dragleave', onDragLeave);
    host.addEventListener('drop', onDrop);

    return () => {
      host.removeEventListener('dragenter', onDragEnter);
      host.removeEventListener('dragover', onDragOver);
      host.removeEventListener('dragleave', onDragLeave);
      host.removeEventListener('drop', onDrop);
      host.removeAttribute('data-drop-target-active');
      host.removeAttribute('data-drop-target-over');
      host.removeAttribute('data-drop-target-rejected');
    };
  },
});
