/**
 * Pure, framework-free layout logic for the spreadsheet-like terminal grid
 * (L4). No React, no Tauri here — these functions are the TS mirror of the
 * domain's pure `LayoutTree` operations (ARCHITECTURE §1.3, §7), so they are
 * trivially unit-testable (Vitest) and reusable by the mock gateway.
 *
 * The size calculation (`normalizeWeights`) is the load-bearing pure function
 * the spec calls out: it turns a list of relative weights into rendering
 * percentages, independent of any DOM.
 */

import type {
  Direction,
  LayoutNode,
  LayoutOperation,
  LayoutTree,
  LeafCell,
  SplitContainer,
  WeightedChild,
} from "@/domain";

/** A simple UUID v4 generator (mock/UI side; the backend mints its own ids). */
export function newId(): string {
  // crypto.randomUUID is available in modern browsers and jsdom.
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
    return crypto.randomUUID();
  }
  // Fallback (deterministic enough for non-crypto UI ids).
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    const v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

/** A single empty leaf tree — the default layout (one cell, no session). */
export function singleLeafTree(id: string = newId()): LayoutTree {
  return { root: { type: "leaf", node: { id, session: null } } };
}

/**
 * Normalises relative weights into rendering percentages that sum to ~100.
 *
 * Pure and total: an empty list yields `[]`; non-positive/NaN weights are
 * floored to a tiny epsilon so a malformed tree still renders rather than
 * collapsing a track to zero. This is the function the grid component uses to
 * size each track (`grid-template-*` / flex-basis).
 */
export function normalizeWeights(weights: number[]): number[] {
  if (weights.length === 0) return [];
  const EPS = 1e-6;
  const safe = weights.map((w) => (Number.isFinite(w) && w > 0 ? w : EPS));
  const total = safe.reduce((a, b) => a + b, 0);
  if (total <= 0) {
    const even = 100 / safe.length;
    return safe.map(() => even);
  }
  return safe.map((w) => (w / total) * 100);
}

/**
 * Computes new weights for a split after dragging the separator between
 * children `index` and `index + 1` by `deltaFraction` (a signed fraction of the
 * container's full extent, e.g. `+0.05` = grow the left/top child by 5%).
 *
 * Pure: only the two adjacent tracks are affected; both stay strictly positive
 * (clamped to a small minimum). Returns a fresh array.
 */
export function resizeAdjacent(
  weights: number[],
  index: number,
  deltaFraction: number,
): number[] {
  const next = [...weights];
  if (index < 0 || index + 1 >= next.length) return next;
  const total = next.reduce((a, b) => a + b, 0);
  const delta = deltaFraction * total;
  const MIN = total * 0.05;
  const a = next[index] + delta;
  const b = next[index + 1] - delta;
  if (a < MIN || b < MIN) return next; // refuse to crush a track
  next[index] = a;
  next[index + 1] = b;
  return next;
}

/** Recursively rebuilds a node, applying `f` (post-order, like the domain). */
function mapNode(node: LayoutNode, f: (n: LayoutNode) => LayoutNode): LayoutNode {
  let rebuilt: LayoutNode;
  switch (node.type) {
    case "leaf":
      rebuilt = node;
      break;
    case "customPluginLayout":
      // No children to recurse into — same leaf-like treatment as "leaf".
      rebuilt = node;
      break;
    case "split":
      rebuilt = {
        type: "split",
        node: {
          ...node.node,
          children: node.node.children.map((c) => ({
            node: mapNode(c.node, f),
            weight: c.weight,
          })),
        },
      };
      break;
    case "grid":
      rebuilt = {
        type: "grid",
        node: {
          ...node.node,
          cells: node.node.cells.map((c) => ({
            ...c,
            node: mapNode(c.node, f),
          })),
        },
      };
      break;
  }
  return f(rebuilt);
}

/** Finds the session hosted by leaf `id`, or `undefined` if no such leaf. */
function sessionInLeaf(node: LayoutNode, id: string): string | null | undefined {
  let found: string | null | undefined;
  mapNode(node, (n) => {
    if (n.type === "leaf" && n.node.id === id) found = n.node.session ?? null;
    return n;
  });
  return found;
}

/**
 * Applies a {@link LayoutOperation} to a tree, returning a **new** tree (pure,
 * immutable). Mirrors the domain operations so the mock gateway behaves like the
 * real backend offline. Throws on an invalid operation (unknown node, etc.).
 */
export function applyOperation(
  tree: LayoutTree,
  op: LayoutOperation,
): LayoutTree {
  switch (op.type) {
    case "split": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "leaf" && n.node.id === op.target) {
          found = true;
          const newLeaf: LeafCell = { id: op.newLeaf, session: null };
          const container: SplitContainer = {
            id: op.container,
            direction: op.direction,
            children: [
              { node: n, weight: 1 },
              { node: { type: "leaf", node: newLeaf }, weight: 1 },
            ],
          };
          return { type: "split", node: container };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
    case "merge": {
      let touched = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "split" && n.node.id === op.container) {
          const keep = n.node.children[op.keepIndex];
          if (!keep) throw invalid(`merge keepIndex ${op.keepIndex} out of range`);
          touched = true;
          return keep.node;
        }
        return n;
      });
      if (!touched) throw notFound(op.container);
      return { root };
    }
    case "resize": {
      let touched = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "split" && n.node.id === op.container) {
          if (n.node.children.length !== op.weights.length) {
            throw invalid("resize weight count mismatch");
          }
          touched = true;
          const children: WeightedChild[] = n.node.children.map((c, i) => ({
            node: c.node,
            weight: op.weights[i],
          }));
          return { type: "split", node: { ...n.node, children } };
        }
        return n;
      });
      if (!touched) throw notFound(op.container);
      return { root };
    }
    case "move": {
      const session = sessionInLeaf(tree.root, op.from);
      if (session === undefined) throw notFound(op.from);
      if (session === null) throw invalid("source cell has no session");
      const target = sessionInLeaf(tree.root, op.to);
      if (target === undefined) throw notFound(op.to);
      if (target !== null) throw invalid("target cell is occupied");
      const root = mapNode(tree.root, (n) => {
        // Preserve agent / conversationId / agentWasRunning on both ends — only
        // the ephemeral `session` moves (mirror of the Rust `move_session`).
        if (n.type === "leaf" && n.node.id === op.from) {
          return { type: "leaf", node: { ...n.node, session: null } };
        }
        if (n.type === "leaf" && n.node.id === op.to) {
          return { type: "leaf", node: { ...n.node, session } };
        }
        return n;
      });
      return { root };
    }
    case "setSession": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "leaf" && n.node.id === op.target) {
          found = true;
          return {
            type: "leaf",
            node: { ...n.node, session: op.session ?? null },
          };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
    case "setCellAgent": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "leaf" && n.node.id === op.target) {
          found = true;
          const { agent: _a, ...rest } = n.node;
          const updated = op.agent !== null
            ? { ...rest, agent: op.agent }
            : rest;
          return { type: "leaf", node: updated };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
    case "setCellConversation": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "leaf" && n.node.id === op.target) {
          found = true;
          const { conversationId: _c, ...rest } = n.node;
          // Omit when null (mirrors the backend `skip_serializing_if`).
          const updated = op.conversationId !== null
            ? { ...rest, conversationId: op.conversationId }
            : rest;
          return { type: "leaf", node: updated };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
    case "setAgentRunning": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "leaf" && n.node.id === op.target) {
          found = true;
          const { agentWasRunning: _r, ...rest } = n.node;
          // Omit when false (mirrors the backend `skip_serializing_if`).
          const updated = op.running
            ? { ...rest, agentWasRunning: true }
            : rest;
          return { type: "leaf", node: updated };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
    case "setPluginLayoutState": {
      let found = false;
      const root = mapNode(tree.root, (n) => {
        if (n.type === "customPluginLayout" && n.node.id === op.target) {
          found = true;
          return { type: "customPluginLayout", node: { ...n.node, state: op.state } };
        }
        return n;
      });
      if (!found) throw notFound(op.target);
      return { root };
    }
  }
}

/** Collects all leaf cells of a tree in document order. */
export function leaves(tree: LayoutTree): LeafCell[] {
  const out: LeafCell[] = [];
  mapNode(tree.root, (n) => {
    if (n.type === "leaf") out.push(n.node);
    return n;
  });
  return out;
}

/**
 * Sessions hosted by the children a `merge` would **drop** — every child of
 * `container` except the kept one, walked recursively (a dropped child may be a
 * nested split with several terminals). Used by the close button to tear down
 * the discarded PTYs instead of orphaning them, mirroring `setCellAgent`. Returns
 * `[]` if the container isn't found or holds no sessions.
 */
export function droppedSessions(
  tree: LayoutTree,
  container: string,
  keepIndex: number,
): string[] {
  const out: string[] = [];
  mapNode(tree.root, (n) => {
    if (n.type === "split" && n.node.id === container) {
      n.node.children.forEach((c, i) => {
        if (i === keepIndex) return;
        mapNode(c.node, (m) => {
          if (m.type === "leaf" && m.node.session) out.push(m.node.session);
          return m;
        });
      });
    }
    return n;
  });
  return out;
}

/**
 * Pure helper for patching a `customPluginLayout` node's opaque `state`.
 * The runtime path persists the same change through `mutate_layout` with
 * `setPluginLayoutState`; this helper remains useful for local tree transforms
 * and focused parsing tests.
 */
export function setCustomPluginLayoutState(
  tree: LayoutTree,
  nodeId: string,
  state: unknown,
): LayoutTree {
  return {
    root: mapNode(tree.root, (n) => {
      if (n.type === "customPluginLayout" && n.node.id === nodeId) {
        return { type: "customPluginLayout", node: { ...n.node, state } };
      }
      return n;
    }),
  };
}

/**
 * Locally replaces a `customPluginLayout` node with a blank terminal leaf of
 * the same id (#43, F4 "Choisir un autre layout" fallback action). Same
 * client-side-only contract as {@link setCustomPluginLayoutState} — no
 * backend operation exists to persist the node-kind change yet.
 */
export function replaceCustomPluginLayoutWithTerminal(
  tree: LayoutTree,
  nodeId: string,
): LayoutTree {
  return {
    root: mapNode(tree.root, (n) => {
      if (n.type === "customPluginLayout" && n.node.id === nodeId) {
        return { type: "leaf", node: { id: nodeId } };
      }
      return n;
    }),
  };
}

/** Convenience: builds a `split` operation splitting `target` in `direction`. */
export function splitOp(target: string, direction: Direction): LayoutOperation {
  return {
    type: "split",
    target,
    direction,
    newLeaf: newId(),
    container: newId(),
  };
}

function notFound(id: string): GatewayLikeError {
  return { code: "NOT_FOUND", message: `layout node ${id} not found` };
}
function invalid(msg: string): GatewayLikeError {
  return { code: "INVALID", message: msg };
}

interface GatewayLikeError {
  code: string;
  message: string;
}
