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

/**
 * `keyboard-reorderable` — keyboard alternative to `draggable-list-item`.
 *
 * Authored for the Tasks UI playground (apps/tasks/spec/).
 * Per SPEC §6.2 + ARCHITECTURE-REVIEW H2 (category: 'keyboard-navigation').
 *
 * Implements the WCAG 2.5.7 single-pointer alternative + ARIA 1.1
 * draggable-attribute keyboard pattern:
 *
 *   Space / Enter (idle) → lift  (emit `dnd-lift`)
 *   ArrowUp / ArrowDown  → move target index within the source container
 *                          (emit `dnd-drop-target-change`)
 *   ArrowLeft / Right    → switch container (move to prev/next sibling
 *                          `[data-droppable-id]` of the parent collection)
 *   Space / Enter (lift) → drop  (emit `dnd-drop`)
 *   Escape               → cancel (emit `dnd-drop-cancel`)
 *
 * Source-of-truth model is the same as `draggable-list-item`: emit DOM
 * CustomEvents that `[droppable]` and `[droppable-collection]` already
 * listen for; the source element does NOT physically move during the
 * keyboard "lift" — only `data-keyboard-reorderable-lifting` reflects.
 *
 * Pairs with `draggable-list-item` on the same host. Both consume the
 * existing `announcer-stage.js` aria-live regions (same singleton the
 * `announcer` trait uses) so a screen reader does not hear two parallel
 * narrations.
 */

import { announce } from '../announcer/announcer-stage.js';

// Same debounce gate as draggable-list-item — module-private throttle
// for drop-target-change announcements; immediate calls supersede.
const DEBOUNCE_MS = 200;
let debounceHandle = null;
function announceDebounced(message) {
  if (debounceHandle != null) clearTimeout(debounceHandle);
  debounceHandle = setTimeout(() => {
    debounceHandle = null;
    announce(message, 'polite');
  }, DEBOUNCE_MS);
}
function announceImmediate(message) {
  if (debounceHandle != null) {
    clearTimeout(debounceHandle);
    debounceHandle = null;
  }
  announce(message, 'polite');
}

const ATTR_LIFTING = 'data-keyboard-reorderable-lifting';
const ATTR_SOURCE_ID = 'data-keyboard-reorderable-id';

function readSiblingsContext(host) {
  const container = host.parentElement;
  if (!container) return null;
  const siblings = Array.from(container.children).filter((c) => c.matches?.('[data-keyboard-reorderable-id], [data-draggable-list-item-id]'));
  const index = siblings.indexOf(host);
  return { container, index, siblings };
}

function readContainerId(el) {
  /** @type {Element | null} */
  let cur = el;
  while (cur && cur !== document.body) {
    if (cur.hasAttribute?.('data-droppable-id')) return cur.getAttribute('data-droppable-id');
    cur = cur.parentElement;
  }
  return null;
}

function readCollectionContainers(host) {
  // Walk up to the nearest droppable-collection, then enumerate its
  // descendant droppables. If there's no collection, return just the
  // single source container.
  /** @type {Element | null} */
  let cur = host;
  while (cur && cur !== document.body) {
    if (cur.hasAttribute?.('data-droppable-collection-active')) {
      return Array.from(cur.querySelectorAll('[data-droppable-id]'));
    }
    cur = cur.parentElement;
  }
  const sourceContainer = host.closest?.('[data-droppable-id]');
  return sourceContainer ? [sourceContainer] : [];
}

export const keyboardReorderable = defineTrait({
  name: 'keyboard-reorderable',
  category: 'keyboard-navigation',
  description: 'Keyboard alternative to draggable-list-item: arrow keys + Space to lift / drop / Esc to cancel',
  attributes: [ATTR_LIFTING, ATTR_SOURCE_ID],
  events: ['dnd-lift', 'dnd-drop-target-change', 'dnd-drop', 'dnd-drop-cancel'],
  config: [],
  setup({ host }) {
    let id = host.getAttribute(ATTR_SOURCE_ID);
    if (!id) {
      id = `kbreorder-${crypto.randomUUID().slice(0, 8)}`;
      host.setAttribute(ATTR_SOURCE_ID, id);
    }
    if (!host.hasAttribute('tabindex')) host.setAttribute('tabindex', '0');

    let lifted = false;
    /** @type {string | null} */
    let targetContainerId = null;
    let targetIndex = -1;
    /** @type {{ index: number; container: Element } | null} */
    let sourceCtx = null;
    let sourceContainerId = null;

    function isDisabled() {
      return host.hasAttribute('disabled') || host.getAttribute('aria-disabled') === 'true';
    }

    function lift() {
      const ctx = readSiblingsContext(host);
      if (!ctx) return;
      sourceCtx = ctx;
      sourceContainerId = readContainerId(ctx.container);
      lifted = true;
      targetContainerId = sourceContainerId;
      targetIndex = ctx.index;
      host.setAttribute(ATTR_LIFTING, '');
      host.dispatchEvent(new CustomEvent('dnd-lift', {
        bubbles: true,
        composed: false,
        detail: {
          source_id: id,
          source_index: ctx.index,
          source_container_id: sourceContainerId,
        },
      }));
      announceImmediate('Picked up item. Use arrow keys to move. Press Space to drop. Press Escape to cancel.');
      emitTargetChange();
    }

    function emitTargetChange() {
      document.dispatchEvent(new CustomEvent('dnd-drop-target-change', {
        bubbles: true,
        composed: false,
        detail: {
          source_id: id,
          target_container_id: targetContainerId,
          target_index: targetIndex,
          pointer: { x: 0, y: 0 },
        },
      }));
      if (targetContainerId) {
        announceDebounced(`Drop position ${targetIndex + 1} in ${targetContainerId}.`);
      }
    }

    function moveWithinContainer(delta) {
      const containers = readCollectionContainers(host);
      const containerEl = containers.find((c) => c.getAttribute('data-droppable-id') === targetContainerId);
      const total = containerEl
        ? containerEl.querySelectorAll('[data-keyboard-reorderable-id], [data-draggable-list-item-id]').length
        : (sourceCtx?.siblings.length ?? 0);
      // Allow target index 0..total inclusive (insert-after-last counts).
      const next = Math.max(0, Math.min(total, targetIndex + delta));
      if (next === targetIndex) return;
      targetIndex = next;
      emitTargetChange();
    }

    function switchContainer(delta) {
      const containers = readCollectionContainers(host);
      if (containers.length <= 1) return;
      const ids = containers.map((c) => c.getAttribute('data-droppable-id'));
      const curIdx = ids.indexOf(targetContainerId);
      const nextIdx = Math.max(0, Math.min(ids.length - 1, curIdx + delta));
      if (nextIdx === curIdx) return;
      targetContainerId = ids[nextIdx];
      targetIndex = 0;
      emitTargetChange();
    }

    function drop() {
      if (!lifted || !sourceCtx) return;
      document.dispatchEvent(new CustomEvent('dnd-drop', {
        bubbles: true,
        composed: false,
        detail: {
          source_id: id,
          source_index: sourceCtx.index,
          source_container_id: sourceContainerId,
          target_container_id: targetContainerId,
          target_index: targetIndex,
        },
      }));
      announceImmediate(`Dropped item in ${targetContainerId} at position ${targetIndex + 1}.`);
      reset();
    }

    function cancel() {
      if (!lifted) return;
      document.dispatchEvent(new CustomEvent('dnd-drop-cancel', {
        bubbles: true,
        composed: false,
        detail: { source_id: id, reason: 'esc' },
      }));
      announceImmediate('Drag cancelled. Item returned to its original position.');
      reset();
    }

    function reset() {
      lifted = false;
      sourceCtx = null;
      sourceContainerId = null;
      targetContainerId = null;
      targetIndex = -1;
      host.removeAttribute(ATTR_LIFTING);
    }

    function onKeyDown(e) {
      if (isDisabled()) return;
      if (e.key === ' ' || e.key === 'Enter') {
        e.preventDefault();
        if (!lifted) lift();
        else drop();
        return;
      }
      if (e.key === 'Escape' && lifted) {
        e.preventDefault();
        cancel();
        return;
      }
      if (!lifted) return;
      switch (e.key) {
        case 'ArrowUp':    e.preventDefault(); moveWithinContainer(-1); break;
        case 'ArrowDown':  e.preventDefault(); moveWithinContainer(1); break;
        case 'ArrowLeft':  e.preventDefault(); switchContainer(-1); break;
        case 'ArrowRight': e.preventDefault(); switchContainer(1); break;
      }
    }

    function onBlur() {
      // If focus leaves the host while lifted, cancel — keyboard mode
      // is bound to the focused source. Pointer drag has its own flow.
      if (lifted) cancel();
    }

    host.addEventListener('keydown', onKeyDown);
    host.addEventListener('blur', onBlur);

    return () => {
      host.removeEventListener('keydown', onKeyDown);
      host.removeEventListener('blur', onBlur);
      reset();
      host.removeAttribute(ATTR_SOURCE_ID);
    };
  },
});
