/**
 * Drop-target hit testing (CONTRACT §7.3): given a pointer position inside the canvas,
 * decide where a dragged thing lands — before/after a widget, before/after a container or
 * inside it (between two children, or as the first child of an empty one), or on the page
 * itself — and where the indicator line is drawn. Pure: it reads the canvas DOM and the
 * document, never the store.
 */

import type { Document, Element } from '@nexwordx/renderer';
import { findElement, isWithin } from '../store/tree';
import type { Box } from '../canvas/rects';
import { boxOf } from '../canvas/rects';

export interface DropTarget {
  parentId: string | null;
  index: number;
  indicator: { kind: 'line' | 'inside'; box: Box };
  /** The container the drop goes into (null = the page). */
  containerId: string | null;
}

interface Axis {
  horizontal: boolean;
  reversed: boolean;
  /** A Grid container (§4.4): items flow in rows, so the nearest cell decides. */
  grid?: boolean;
}

const EDGE_ZONE = 24;

/** The flex box that lays a container's children out (the inner div when boxed). */
function flexBox(containerNode: HTMLElement): HTMLElement {
  return containerNode.querySelector<HTMLElement>(':scope > .nxp-c-inner') || containerNode;
}

function axisOf(win: Window, containerNode: HTMLElement | null): Axis {
  if (!containerNode) return { horizontal: false, reversed: false };
  const style = win.getComputedStyle(flexBox(containerNode));
  if (style.display === 'grid' || style.display === 'inline-grid') return { horizontal: true, reversed: false, grid: true };
  const direction = style.flexDirection || 'row';
  return { horizontal: direction.startsWith('row'), reversed: direction.endsWith('reverse') };
}

function lineBox(target: Box, axis: Axis, leading: boolean): Box {
  const thickness = 3;
  if (axis.horizontal) {
    return {
      left: leading ? target.left - thickness / 2 : target.left + target.width - thickness / 2,
      top: target.top,
      width: thickness,
      height: target.height,
    };
  }
  return {
    left: target.left,
    top: leading ? target.top - thickness / 2 : target.top + target.height - thickness / 2,
    width: target.width,
    height: thickness,
  };
}

/** Element nodes with a data-nxe-id (nodeType test: iframe nodes fail a parent-realm instanceof). */
function elementChildren(parent: globalThis.Element): HTMLElement[] {
  return Array.from(parent.children).filter(
    (node): node is HTMLElement => node.nodeType === 1 && node.hasAttribute('data-nxe-id'),
  );
}

/** Child element nodes of a container, in DOM (= document) order. */
function childNodes(containerNode: HTMLElement): HTMLElement[] {
  return elementChildren(flexBox(containerNode));
}

/** Grid items: the item nearest to the pointer, before or after it by the pointer's side. */
function indexInGrid(children: HTMLElement[], x: number, y: number): { index: number; box: Box | null; leading: boolean } {
  let best = -1;
  let bestDistance = Infinity;
  let bestBox: Box | null = null;
  for (let i = 0; i < children.length; i += 1) {
    const box = boxOf(children[i]);
    const dx = x < box.left ? box.left - x : x > box.left + box.width ? x - box.left - box.width : 0;
    const dy = y < box.top ? box.top - y : y > box.top + box.height ? y - box.top - box.height : 0;
    const distance = dx * dx + dy * dy;
    if (distance < bestDistance) {
      best = i;
      bestDistance = distance;
      bestBox = box;
    }
  }
  if (best === -1 || !bestBox) return { index: children.length, box: null, leading: false };
  const leading = x < bestBox.left + bestBox.width / 2;
  return { index: leading ? best : best + 1, box: bestBox, leading };
}

/** Index among `children` the pointer falls before, along the parent's axis. */
function indexAlongAxis(children: HTMLElement[], axis: Axis, x: number, y: number): { index: number; box: Box | null; leading: boolean } {
  if (axis.grid) return indexInGrid(children, x, y);
  for (let i = 0; i < children.length; i += 1) {
    const box = boxOf(children[i]);
    const mid = axis.horizontal ? box.left + box.width / 2 : box.top + box.height / 2;
    const pointer = axis.horizontal ? x : y;
    const beforeMid = pointer < mid;
    if (beforeMid) {
      // Visually before this child; in a reversed row/column that is logically after it.
      return { index: axis.reversed ? i + 1 : i, box, leading: true };
    }
  }
  const last = children.length ? boxOf(children[children.length - 1]) : null;
  return { index: axis.reversed ? 0 : children.length, box: last, leading: false };
}

export interface HitContext {
  doc: globalThis.Document;
  win: Window;
  document: Document;
  /** The element being moved (never a target for itself or its subtree). */
  draggedId?: string | null;
  /** Widgets only: the page root is still a valid target (a container gets created). */
  payloadIsWidget: boolean;
}

/** The deepest `[data-nxe-id]` node under the pointer that is not part of the dragged subtree. */
function elementUnderPointer(ctx: HitContext, x: number, y: number): HTMLElement | null {
  const stack = ctx.doc.elementsFromPoint(x, y);
  for (const node of stack) {
    const target = node.closest<HTMLElement>('[data-nxe-id]');
    if (!target) continue;
    const id = target.getAttribute('data-nxe-id') || '';
    if (ctx.draggedId && isWithin(ctx.document, ctx.draggedId, id)) {
      // Inside the dragged subtree: climb out of it and use the ancestor instead.
      let ancestor = target.parentElement?.closest<HTMLElement>('[data-nxe-id]') || null;
      while (ancestor && isWithin(ctx.document, ctx.draggedId, ancestor.getAttribute('data-nxe-id') || '')) {
        ancestor = ancestor.parentElement?.closest<HTMLElement>('[data-nxe-id]') || null;
      }
      return ancestor;
    }
    return target;
  }
  return null;
}

export function computeDropTarget(ctx: HitContext, x: number, y: number): DropTarget | null {
  const root = ctx.doc.getElementById('nxe-root');
  if (!root) return null;
  // The site header / footer around the page belong to other templates (§7.5): no drops.
  const top = ctx.doc.elementFromPoint(x, y);
  if (top && top.closest('[data-nxe-layout]')) return null;
  // The add zone under the page (AddZone.tsx): the end of the page, the box lit as the target.
  const zone = top ? top.closest<HTMLElement>('[data-nxe-addzone]') : null;
  if (zone) {
    const pageNode = root.querySelector<HTMLElement>(':scope > .nxp-page') || root;
    const box = zone.querySelector<HTMLElement>('[data-nxe-addzone-box]') || zone;
    return { parentId: null, index: elementChildren(pageNode).length, containerId: null, indicator: { kind: 'inside', box: boxOf(box) } };
  }
  const node = elementUnderPointer(ctx, x, y);

  // Nothing under the pointer: the page itself, between top-level containers. They are the
  // children of <NexWordXPage>'s `.nxp-page` div inside the root, not of the root itself.
  if (!node) {
    const pageNode = root.querySelector<HTMLElement>(':scope > .nxp-page') || root;
    const tops = elementChildren(pageNode);
    const rootBox = boxOf(root);
    const pageAxis: Axis = { horizontal: false, reversed: false };
    const hit = indexAlongAxis(tops, pageAxis, x, y);
    const box = hit.box ? lineBox(hit.box, pageAxis, hit.leading) : { left: rootBox.left + 8, top: rootBox.top + 8, width: Math.max(0, rootBox.width - 16), height: 3 };
    return { parentId: null, index: hit.index, containerId: null, indicator: { kind: tops.length ? 'line' : 'inside', box: tops.length ? box : { left: rootBox.left + 8, top: rootBox.top + 8, width: Math.max(0, rootBox.width - 16), height: Math.max(80, rootBox.height - 16) } } };
  }

  const id = node.getAttribute('data-nxe-id') || '';
  const info = findElement(ctx.document, id);
  if (!info) return null;
  const parentNode = info.parentId ? node.parentElement?.closest<HTMLElement>('[data-nxe-id]') || null : null;
  const parentAxis = axisOf(ctx.win, parentNode);
  const box = boxOf(node);

  if (info.element.type === 'widget') {
    const mid = parentAxis.horizontal ? box.left + box.width / 2 : box.top + box.height / 2;
    const leading = (parentAxis.horizontal ? x : y) < mid;
    const index = leading !== parentAxis.reversed ? info.index : info.index + 1;
    return { parentId: info.parentId, index, containerId: info.parentId, indicator: { kind: 'line', box: lineBox(box, parentAxis, leading) } };
  }

  // A container: near its leading/trailing edge (along the parent's axis) → sibling drop.
  const size = parentAxis.horizontal ? box.width : box.height;
  const edge = Math.min(EDGE_ZONE, size * 0.2);
  const pointer = parentAxis.horizontal ? x : y;
  const start = parentAxis.horizontal ? box.left : box.top;
  const children = childNodes(node);
  const nearLeading = pointer - start < edge;
  const nearTrailing = start + size - pointer < edge;
  if ((nearLeading || nearTrailing) && (children.length > 0 || info.parentId !== null)) {
    const leading = nearLeading;
    const index = leading !== parentAxis.reversed ? info.index : info.index + 1;
    return { parentId: info.parentId, index, containerId: info.parentId, indicator: { kind: 'line', box: lineBox(box, parentAxis, leading) } };
  }

  // Inside the container.
  const axis = axisOf(ctx.win, node);
  if (!children.length) {
    const inner = boxOf(flexBox(node));
    return { parentId: id, index: 0, containerId: id, indicator: { kind: 'inside', box: inner } };
  }
  const hit = indexAlongAxis(children, axis, x, y);
  const indicator = hit.box ? lineBox(hit.box, axis, hit.leading) : lineBox(box, axis, false);
  return { parentId: id, index: hit.index, containerId: id, indicator: { kind: 'line', box: indicator } };
}

/** True when `element` may be dropped at `target` (no element inside itself, no locked moves). */
export function targetAccepts(document: Document, target: DropTarget, element: Element | null): boolean {
  if (!element) return true;
  if (element.locked) return false;
  if (target.parentId && isWithin(document, element.id, target.parentId)) return false;
  return true;
}
