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

/**
 * `droppable` — marks an element as a drop target for `draggable-list-item`.
 *
 * Authored for the Tasks UI playground (apps/tasks/spec/).
 * Per SPEC §6.2 + ARCHITECTURE-REVIEW H2 (category: 'input-interaction').
 *
 * Pairs with `draggable-list-item` (motion-positioning) which lifts a list
 * item and emits `dnd-lift` / `dnd-drop-target-change` / `dnd-drop` /
 * `dnd-drop-cancel` ON THE DOCUMENT. This trait observes the drag in
 * flight and emits target-side events ON THE HOST:
 *
 *   `dnd-drop-enter`    pointer entered this droppable
 *   `dnd-drop-leave`    pointer left this droppable
 *   `dnd-drop-receive`  the drop landed here (source-side `dnd-drop`
 *                       on the document is the lifter's "release"
 *                       signal; the target-side event MUST be named
 *                       differently or it bubbles back to the
 *                       document and re-triggers this same listener
 *                       in an infinite loop)
 *
 * Coordination: pure DOM events (light-DOM, bubbles: true). No shared
 * runtime state. The dragging item dispatches `dnd-drop-target-change`
 * with the candidate target id; the targeted droppable receives a
 * synthetic `dnd-drop-enter` from a tiny internal coordinator.
 *
 * For v1 the coordinator is a single shared module-level Map keyed by
 * `data-droppable-id`. Future versions may promote to a registry trait.
 */

const ATTR_ID = 'data-droppable-id';
const ATTR_OVER = 'data-droppable-over';
const ATTR_VALID = 'data-droppable-valid';

/** @type {Map<string, HTMLElement>} */
const REGISTRY = new Map();
/** @type {string | null} */
let CURRENT_TARGET_ID = null;

export const droppable = defineTrait({
  name: 'droppable',
  category: 'input-interaction',
  description: 'Marks an element as a drop target for draggable-list-item; emits dnd-drop-enter / leave / receive',
  attributes: [ATTR_ID, ATTR_OVER, ATTR_VALID],
  events: ['dnd-drop-enter', 'dnd-drop-leave', 'dnd-drop-receive'],
  config: [],
  setup({ host }) {
    // Assign a stable id for the registry. Authoring code may set it
    // explicitly via the attribute; otherwise generate.
    let id = host.getAttribute(ATTR_ID);
    if (!id) {
      id = `drop-${crypto.randomUUID().slice(0, 8)}`;
      host.setAttribute(ATTR_ID, id);
    }
    REGISTRY.set(id, host);

    // Listen for drag-in-flight signals on the document so this droppable
    // knows when it should self-mark as the target.
    //
    // Bug guard (2026-05-04): per-host attribute is the source of truth.
    // The previous implementation gated the leave branch on a shared
    // module variable CURRENT_TARGET_ID, which made the result dependent
    // on listener-execution order — when the target swap fired and the
    // NEW target's listener ran before the OLD target's, the old one
    // saw CURRENT_TARGET_ID already moved and skipped clearing its
    // ATTR_OVER, leaving multiple columns visually marked as the drop
    // target. Per-host hasAttribute() is independent of order.
    function onTargetChange(/** @type {CustomEvent} */ e) {
      const detail = e.detail;
      if (!detail) return;
      const isMe = detail.target_container_id === id;
      const wasOver = host.hasAttribute(ATTR_OVER);
      if (isMe && !wasOver) {
        host.setAttribute(ATTR_OVER, '');
        CURRENT_TARGET_ID = id;
        host.dispatchEvent(new CustomEvent('dnd-drop-enter', {
          bubbles: true,
          composed: false,
          detail: { source_id: detail.source_id, pointer_position: detail.pointer ?? { x: 0, y: 0 } },
        }));
      } else if (!isMe && wasOver) {
        host.removeAttribute(ATTR_OVER);
        if (CURRENT_TARGET_ID === id) CURRENT_TARGET_ID = null;
        host.dispatchEvent(new CustomEvent('dnd-drop-leave', {
          bubbles: true,
          composed: false,
          detail: { source_id: detail.source_id ?? null },
        }));
      }
    }

    function onDrop(/** @type {CustomEvent} */ e) {
      const detail = e.detail;
      if (!detail || detail.target_container_id !== id) return;
      host.removeAttribute(ATTR_OVER);
      CURRENT_TARGET_ID = null;
      host.dispatchEvent(new CustomEvent('dnd-drop-receive', {
        bubbles: true,
        composed: false,
        detail: {
          source_id: detail.source_id,
          source_index: detail.source_index,
          source_container_id: detail.source_container_id,
          target_container_id: id,
          target_index: detail.target_index,
        },
      }));
    }

    function onDragEnd() {
      // Whatever happened, reset the over state.
      host.removeAttribute(ATTR_OVER);
      if (CURRENT_TARGET_ID === id) CURRENT_TARGET_ID = null;
    }

    document.addEventListener('dnd-drop-target-change', onTargetChange);
    document.addEventListener('dnd-drop', onDrop);
    document.addEventListener('dnd-drop-cancel', onDragEnd);

    return () => {
      document.removeEventListener('dnd-drop-target-change', onTargetChange);
      document.removeEventListener('dnd-drop', onDrop);
      document.removeEventListener('dnd-drop-cancel', onDragEnd);
      REGISTRY.delete(id);
      host.removeAttribute(ATTR_ID);
      host.removeAttribute(ATTR_OVER);
      host.removeAttribute(ATTR_VALID);
    };
  },
});

/** Internal: lookup a droppable host by id. Used by `draggable-list-item`. */
export function getDroppable(id) {
  return REGISTRY.get(id) ?? null;
}
