import { useMemo } from "react";
import type { ExtractionNode } from "../regex/model/match";
import { handleTreeNavigation } from "./tree-navigation";

const MAXIMUM_RENDERED_EXTRACTION_NODES = 2_000;
const MAXIMUM_VALUE_PREVIEW_UTF16 = 100;

export interface ExtractionTreeProps {
  readonly nodes: readonly ExtractionNode[];
  readonly selectedId?: string;
  readonly onSelect: (node: ExtractionNode) => void;
  readonly repeatedCaptureNumbers: ReadonlySet<number>;
}

function printable(node: ExtractionNode): string {
  const value = node.value;
  if (value === undefined) return "—";
  if (value === "") return "∅ empty";
  const visible = value
    .slice(0, MAXIMUM_VALUE_PREVIEW_UTF16)
    .replaceAll("\n", "↵")
    .replaceAll("\r", "␍")
    .replaceAll("\t", "⇥");
  const exactLength = node.range
    ? node.range.endUtf16 - node.range.startUtf16
    : undefined;
  const uiTruncated = value.length > MAXIMUM_VALUE_PREVIEW_UTF16;
  if (node.status === "truncated") {
    return `${visible}${uiTruncated ? "…" : ""} (engine value prefix: ${value.length.toLocaleString()} of ${exactLength?.toLocaleString() ?? "unknown"} UTF-16 units${uiTruncated ? "; UI shows the first 100" : ""})`;
  }
  return uiTruncated
    ? `${visible}… (${(exactLength ?? value.length).toLocaleString()} UTF-16 units total; UI preview)`
    : visible;
}

function countNodes(nodes: readonly ExtractionNode[]): number {
  let count = 0;
  const pending = [...nodes];
  while (pending.length > 0) {
    const node = pending.pop();
    if (!node) continue;
    count += 1;
    pending.push(...node.children);
  }
  return count;
}

function boundedNodes(
  nodes: readonly ExtractionNode[],
  maximum: number,
): readonly ExtractionNode[] {
  let remaining = maximum;
  const takeNode = (node: ExtractionNode): ExtractionNode | undefined => {
    if (remaining <= 0) return undefined;
    remaining -= 1;
    const children: ExtractionNode[] = [];
    for (const child of node.children) {
      const visible = takeNode(child);
      if (!visible) break;
      children.push(visible);
    }
    return { ...node, children };
  };
  const visible: ExtractionNode[] = [];
  for (const node of nodes) {
    const bounded = takeNode(node);
    if (!bounded) break;
    visible.push(bounded);
  }
  return visible;
}

function containsNode(nodes: readonly ExtractionNode[], id: string): boolean {
  const pending = [...nodes];
  while (pending.length > 0) {
    const node = pending.pop();
    if (!node) continue;
    if (node.id === id) return true;
    pending.push(...node.children);
  }
  return false;
}

function ExtractionTreeNode({
  node,
  selectedId,
  onSelect,
  repeatedCaptureNumbers,
  depth,
  focusableId,
}: {
  readonly node: ExtractionNode;
  readonly selectedId?: string;
  readonly onSelect: (node: ExtractionNode) => void;
  readonly repeatedCaptureNumbers: ReadonlySet<number>;
  readonly depth: number;
  readonly focusableId: string;
}) {
  const repeatedFinal =
    node.groupNumber !== undefined &&
    repeatedCaptureNumbers.has(node.groupNumber);
  return (
    <li role="none">
      <button
        type="button"
        role="treeitem"
        aria-level={depth}
        aria-selected={node.id === selectedId}
        tabIndex={node.id === focusableId ? 0 : -1}
        className={`tree-row extraction-row ${
          node.id === selectedId ? "is-selected" : ""
        }`}
        onClick={() => onSelect(node)}
        onKeyDown={handleTreeNavigation}
      >
        <span className="tree-token">{node.label}</span>
        <span className="tree-explanation">{printable(node)}</span>
        <span className={`status status-${node.status}`}>
          {node.status.replaceAll("-", " ")}
        </span>
        <span className="tree-details extraction-details">
          <span>
            UTF-16 range:{" "}
            {node.range
              ? `${node.range.startUtf16}…${node.range.endUtf16}`
              : "unavailable"}
          </span>
          <span>
            native range:{" "}
            {node.nativeRange
              ? `${node.nativeRange.start}…${node.nativeRange.end} ${node.nativeRange.unit}`
              : "unavailable"}
          </span>
          <span>provenance: {node.provenance}</span>
        </span>
      </button>
      {repeatedFinal ? (
        <p className="tree-note">
          This engine exposes only the final retained capture for this repeated
          group.
        </p>
      ) : null}
      {node.children.length > 0 ? (
        <ul role="group">
          {node.children.map((child) => (
            <ExtractionTreeNode
              key={child.id}
              node={child}
              selectedId={selectedId}
              onSelect={onSelect}
              repeatedCaptureNumbers={repeatedCaptureNumbers}
              depth={depth + 1}
              focusableId={focusableId}
            />
          ))}
        </ul>
      ) : null}
    </li>
  );
}

export function ExtractionTree({
  nodes,
  selectedId,
  onSelect,
  repeatedCaptureNumbers,
}: ExtractionTreeProps) {
  const totalNodes = useMemo(() => countNodes(nodes), [nodes]);
  const visibleNodes = useMemo(
    () => boundedNodes(nodes, MAXIMUM_RENDERED_EXTRACTION_NODES),
    [nodes],
  );
  const renderedNodes = Math.min(totalNodes, MAXIMUM_RENDERED_EXTRACTION_NODES);
  const selectedNodeVisible =
    selectedId === undefined || containsNode(visibleNodes, selectedId);
  const focusableId =
    selectedId && selectedNodeVisible
      ? selectedId
      : (visibleNodes[0]?.id ?? "");
  return (
    <section className="panel tree-panel" aria-labelledby="extraction-heading">
      <header className="panel-heading">
        <div>
          <p className="eyebrow">Engine results</p>
          <h2 id="extraction-heading">Extraction tree</h2>
        </div>
        <span className="provenance-badge is-engine">Actual engine result</span>
      </header>
      {totalNodes > renderedNodes ? (
        <p
          className="render-limit-banner"
          role="status"
          data-testid="extraction-render-limit"
        >
          <strong>Tree preview limited.</strong> Showing the first{" "}
          {renderedNodes.toLocaleString()} of {totalNodes.toLocaleString()}{" "}
          returned result nodes. All returned data remains available to the
          other result views and exports.
          {!selectedNodeVisible
            ? " The selected result is outside this bounded tree preview."
            : ""}
        </p>
      ) : null}
      {nodes.length > 0 ? (
        <ul className="tree" role="tree" aria-label="Match extraction">
          {visibleNodes.map((node) => (
            <ExtractionTreeNode
              key={node.id}
              node={node}
              selectedId={selectedId}
              onSelect={onSelect}
              repeatedCaptureNumbers={repeatedCaptureNumbers}
              depth={1}
              focusableId={focusableId}
            />
          ))}
        </ul>
      ) : (
        <p className="empty-state">No matches to extract.</p>
      )}
    </section>
  );
}
