/**
 * Draw.io diagram model that generates XML without requiring
 * the browser extension or Draw.io application.
 *
 * Compression uses `node:zlib` (Deno Node-compat) for raw deflate
 * because the Web Compression API does not expose a synchronous API.
 * Encoding uses `@std/encoding/base64` and standard `TextEncoder` /
 * `TextDecoder` — no Node.js `Buffer` dependency.
 *
 * Known optimization opportunities (deferred):
 * - INCREMENTAL BOUNDS/LAYER STATS: Full bounds and layer distribution are
 *   still derived from current cells. A fully incremental approach would need
 *   additional bookkeeping, especially for deletes and moves.
 * - ASYNC COMPRESSION: `deflateRawSync` blocks the event loop. Using the async
 *   `deflateRaw` would require making `toXml` async, rippling through handlers.
 */

import { deflateRawSync, inflateRawSync } from "node:zlib";
import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
import { XMLParser } from "fast-xml-parser";
import denoConfig from "../deno.json" with { type: "json" };
import { PLACEHOLDER_MARKER, stripImageFromStyle } from "./placeholder.ts";

/** Shared XML parser instance for importing Draw.io XML */
const xmlParser = new XMLParser({
  ignoreAttributes: false,
  attributeNamePrefix: "",
  isArray: (name: string) =>
    name === "diagram" || name === "mxCell" || name === "UserObject",
  processEntities: true,
});

/** Cell ID for the server watermark. Stripped on import and re-generated on every export. */
export const WATERMARK_CELL_ID = "drawio-mcp-watermark";

/** Static XML template for the watermark. Only version, date, and position are injected at render time. */
const WATERMARK_TEMPLATE =
  `<mxCell id="${WATERMARK_CELL_ID}" value="Generated by APEX - The Agentic Platform Engineering eXperience for Azure.&lt;br&gt;https://github.com/jonathan-vella/azure-agentic-infraops&lt;br&gt;Creation date: {{DATE}}" ` +
  `style="text;html=1;fontSize=11;fontColor=#000000;align=left;verticalAlign=top;whiteSpace=wrap;overflow=hidden;strokeColor=none;fillColor=none;" vertex="1" parent="1">` +
  `<mxGeometry x="{{X}}" y="{{Y}}" width="{{W}}" height="{{H}}" as="geometry"/>` +
  `</mxCell>`;

export interface StructuredError {
  code: string;
  message: string;
  cell_id?: string;
  index?: number;
  suggestion?: string;
}

export interface GroupContainmentWarning {
  code: "OUTSIDE_GROUP_BOUNDS";
  cell_id: string;
  group_id: string;
  message: string;
  suggestion: string;
}

export interface Cell {
  id: string;
  type: "vertex" | "edge";
  value: string;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
  style?: string;
  sourceId?: string;
  targetId?: string;
  parent?: string;
  /** When true, this vertex acts as a container/group for other cells */
  isGroup?: boolean;
  /** IDs of child cells contained in this group */
  children?: string[];
}

export interface Layer {
  id: string;
  name: string;
}

interface Point {
  x: number;
  y: number;
}

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

/** Normalised attributes extracted from an mxCell or UserObject XML element */
interface ParsedCellAttrs {
  id: string;
  value: string;
  style: string;
  parent: string;
  source?: string;
  target?: string;
  isVertex: boolean;
  isEdge: boolean;
  geometry?: Record<string, unknown>;
}

interface DiagramStats {
  total_cells: number;
  vertices: number;
  edges: number;
  groups: number;
  layers: number;
  bounds: { minX: number; minY: number; maxX: number; maxY: number } | null;
  cells_with_text: number;
  cells_without_text: number;
  cells_by_layer: Record<string, number>;
}

export class DiagramModel {
  private cells: Map<string, Cell> = new Map();
  private vertexEdgeIndex: Map<string, Set<string>> = new Map();
  private cachedStats: DiagramStats | null = null;
  private layers: Layer[] = [{ id: "1", name: "Default Layer" }];
  private activeLayerId: string = "1";
  private nextId: number = 2;
  /** Monotonic counter for layer IDs — prevents collisions after deletions */
  private nextLayerId: number = 2;

  private generateId(): string {
    return `cell-${this.nextId++}`;
  }

  private invalidateStatsCache(): void {
    this.cachedStats = null;
  }

  private cloneStats(stats: DiagramStats): DiagramStats {
    return {
      ...stats,
      bounds: stats.bounds ? { ...stats.bounds } : null,
      cells_by_layer: { ...stats.cells_by_layer },
    };
  }

  private addEdgeReference(vertexId: string | undefined, edgeId: string): void {
    if (!vertexId) return;
    const edgeIds = this.vertexEdgeIndex.get(vertexId) ?? new Set<string>();
    edgeIds.add(edgeId);
    this.vertexEdgeIndex.set(vertexId, edgeIds);
  }

  private removeEdgeReference(
    vertexId: string | undefined,
    edgeId: string,
  ): void {
    if (!vertexId) return;
    const edgeIds = this.vertexEdgeIndex.get(vertexId);
    if (!edgeIds) return;
    edgeIds.delete(edgeId);
    if (edgeIds.size === 0) {
      this.vertexEdgeIndex.delete(vertexId);
    }
  }

  private addEdgeToIndex(edge: Cell): void {
    if (edge.type !== "edge") return;
    this.addEdgeReference(edge.sourceId, edge.id);
    this.addEdgeReference(edge.targetId, edge.id);
  }

  private removeEdgeFromIndex(edge: Cell): void {
    if (edge.type !== "edge") return;
    this.removeEdgeReference(edge.sourceId, edge.id);
    this.removeEdgeReference(edge.targetId, edge.id);
  }

  private normalizeEdgeLabel(text: string): string {
    return text.trim().replace(/\s+/g, " ").toLowerCase();
  }

  /**
   * Build a dedupe key for edge labels based on flattened geometry.
   *
   * For orthogonal edges with no explicit route points, Draw.io visually flattens
   * identical endpoint pairs to the same line. To avoid overlapping duplicate
   * transport labels (e.g. "https" twice on the same visual line), we treat
   * source/target as an unordered pair and include a normalized label.
   */
  private getFlattenedEdgeLabelKey(edge: Cell): string | null {
    if (edge.type !== "edge" || !edge.sourceId || !edge.targetId) {
      return null;
    }

    const normalizedLabel = this.normalizeEdgeLabel(edge.value);
    if (!normalizedLabel) {
      return null;
    }

    const [first, second] =
      edge.sourceId < edge.targetId
        ? [edge.sourceId, edge.targetId]
        : [edge.targetId, edge.sourceId];
    const parent = edge.parent ?? "";

    return `${parent}|${first}|${second}|${normalizedLabel}`;
  }

  private getFlattenedEdgeRouteKey(edge: Cell): string | null {
    if (edge.type !== "edge" || !edge.sourceId || !edge.targetId) {
      return null;
    }

    const [first, second] =
      edge.sourceId < edge.targetId
        ? [edge.sourceId, edge.targetId]
        : [edge.targetId, edge.sourceId];
    const parent = edge.parent ?? "";

    return `${parent}|${first}|${second}`;
  }

  private getAlternatingLabelOffset(labelIndexOnRoute: number): number {
    if (labelIndexOnRoute <= 0) {
      return 0;
    }

    const step = Math.ceil(labelIndexOnRoute / 2);
    const direction = labelIndexOnRoute % 2 === 1 ? -1 : 1;
    return direction * step * 14;
  }

  private renderEdgeGeometryXml(
    waypoints: Point[],
    labelOffsetY: number,
  ): string {
    const yAttr = labelOffsetY !== 0 ? ` y="${labelOffsetY}"` : "";

    if (waypoints.length > 0) {
      const pointsXml = waypoints
        .map((point) => `<mxPoint x="${point.x}" y="${point.y}"/>`)
        .join("");
      return `<mxGeometry relative="1"${yAttr} as="geometry"><Array as="points">${pointsXml}</Array></mxGeometry>`;
    }

    return `<mxGeometry relative="1"${yAttr} as="geometry"/>`;
  }

  private getAbsoluteBounds(cellId: string): Rect | null {
    const cell = this.cells.get(cellId);
    if (!cell || cell.type !== "vertex") {
      return null;
    }

    let x = cell.x ?? 0;
    let y = cell.y ?? 0;
    const width = cell.width ?? 0;
    const height = cell.height ?? 0;

    let parentId = cell.parent;
    while (parentId) {
      const parentCell = this.cells.get(parentId);
      if (!parentCell || parentCell.type !== "vertex" || !parentCell.isGroup) {
        break;
      }
      x += parentCell.x ?? 0;
      y += parentCell.y ?? 0;
      parentId = parentCell.parent;
    }

    return { x, y, width, height };
  }

  private getAbsoluteBoundsFromMap(
    cellId: string,
    cellMap: Map<string, Cell>,
    cache: Map<string, Rect | null>,
  ): Rect | null {
    if (cache.has(cellId)) {
      return cache.get(cellId)!;
    }

    const cell = cellMap.get(cellId);
    if (!cell || cell.type !== "vertex") {
      cache.set(cellId, null);
      return null;
    }

    let x = cell.x ?? 0;
    let y = cell.y ?? 0;
    const width = cell.width ?? 0;
    const height = cell.height ?? 0;

    const parentId = cell.parent;
    if (parentId) {
      const parentCell = cellMap.get(parentId);
      if (parentCell?.type === "vertex" && parentCell.isGroup) {
        const parentBounds = this.getAbsoluteBoundsFromMap(
          parentCell.id,
          cellMap,
          cache,
        );
        if (parentBounds) {
          x += parentBounds.x;
          y += parentBounds.y;
        }
      }
    }

    const bounds: Rect = { x, y, width, height };
    cache.set(cellId, bounds);
    return bounds;
  }

  private getCellCenter(cellId: string): Point | null {
    const bounds = this.getAbsoluteBounds(cellId);
    if (!bounds) {
      return null;
    }

    return {
      x: bounds.x + bounds.width / 2,
      y: bounds.y + bounds.height / 2,
    };
  }

  private getAbsoluteParentOffset(parentId?: string): Point {
    if (!parentId) {
      return { x: 0, y: 0 };
    }

    const parentCell = this.cells.get(parentId);
    if (!parentCell || parentCell.type !== "vertex" || !parentCell.isGroup) {
      return { x: 0, y: 0 };
    }

    const bounds = this.getAbsoluteBounds(parentId);
    // deno-coverage-ignore
    if (!bounds) {
      // deno-coverage-ignore
      return { x: 0, y: 0 };
      // deno-coverage-ignore
    }

    return { x: bounds.x, y: bounds.y };
  }

  private getGroupContainmentWarning(
    cellId: string,
    groupId: string,
  ): GroupContainmentWarning | null {
    const cellBounds = this.getAbsoluteBounds(cellId);
    const groupBounds = this.getAbsoluteBounds(groupId);

    if (!cellBounds || !groupBounds) {
      return null;
    }

    const cellRight = cellBounds.x + cellBounds.width;
    const cellBottom = cellBounds.y + cellBounds.height;
    const groupRight = groupBounds.x + groupBounds.width;
    const groupBottom = groupBounds.y + groupBounds.height;

    const outside =
      cellBounds.x < groupBounds.x ||
      cellBounds.y < groupBounds.y ||
      cellRight > groupRight ||
      cellBottom > groupBottom;

    if (!outside) {
      return null;
    }

    return {
      code: "OUTSIDE_GROUP_BOUNDS",
      cell_id: cellId,
      group_id: groupId,
      message:
        `Cell '${cellId}' is partially or fully outside group '${groupId}'. ` +
        `Child bounds=(x:${Math.round(cellBounds.x)}, y:${Math.round(cellBounds.y)}, w:${Math.round(cellBounds.width)}, h:${Math.round(cellBounds.height)}), ` +
        `group bounds=(x:${Math.round(groupBounds.x)}, y:${Math.round(groupBounds.y)}, w:${Math.round(groupBounds.width)}, h:${Math.round(groupBounds.height)}).`,
      suggestion:
        "Increase group width/height or move the child so it fits within the group's visible boundary with padding.",
    };
  }

  private isCellInsideGroup(cellId: string, groupId: string): boolean {
    if (cellId === groupId) {
      return true;
    }

    let current = this.cells.get(cellId);
    while (current?.parent) {
      if (current.parent === groupId) {
        return true;
      }
      current = this.cells.get(current.parent);
      if (!current || !current.isGroup) {
        break;
      }
    }

    return false;
  }

  private pointInsideRect(point: Point, rect: Rect): boolean {
    return (
      point.x >= rect.x &&
      point.x <= rect.x + rect.width &&
      point.y >= rect.y &&
      point.y <= rect.y + rect.height
    );
  }

  private lineSegmentsIntersect(
    p1: Point,
    p2: Point,
    q1: Point,
    q2: Point,
  ): boolean {
    const orientation = (a: Point, b: Point, c: Point): number => {
      return (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
    };

    const onSegment = (a: Point, b: Point, c: Point): boolean => {
      return (
        b.x <= Math.max(a.x, c.x) &&
        b.x >= Math.min(a.x, c.x) &&
        b.y <= Math.max(a.y, c.y) &&
        b.y >= Math.min(a.y, c.y)
      );
    };

    const o1 = orientation(p1, p2, q1);
    const o2 = orientation(p1, p2, q2);
    const o3 = orientation(q1, q2, p1);
    const o4 = orientation(q1, q2, p2);

    if (
      ((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) &&
      ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))
    ) {
      return true;
    }

    if (o1 === 0 && onSegment(p1, q1, p2)) return true;
    if (o2 === 0 && onSegment(p1, q2, p2)) return true;
    if (o3 === 0 && onSegment(q1, p1, q2)) return true;
    if (o4 === 0 && onSegment(q1, p2, q2)) return true;

    return false;
  }

  private segmentIntersectsRect(start: Point, end: Point, rect: Rect): boolean {
    if (this.pointInsideRect(start, rect) || this.pointInsideRect(end, rect)) {
      return true;
    }

    const topLeft = { x: rect.x, y: rect.y };
    const topRight = { x: rect.x + rect.width, y: rect.y };
    const bottomLeft = { x: rect.x, y: rect.y + rect.height };
    const bottomRight = { x: rect.x + rect.width, y: rect.y + rect.height };

    return (
      this.lineSegmentsIntersect(start, end, topLeft, topRight) ||
      this.lineSegmentsIntersect(start, end, topRight, bottomRight) ||
      this.lineSegmentsIntersect(start, end, bottomRight, bottomLeft) ||
      this.lineSegmentsIntersect(start, end, bottomLeft, topLeft)
    );
  }

  private getGroupAvoidanceWaypoints(
    edge: Cell,
    precomputedCenters?: Map<string, Point>,
    precomputedGroupBounds?: Array<{ id: string; bounds: Rect }>,
  ): Point[] {
    if (edge.type !== "edge" || !edge.sourceId || !edge.targetId) {
      return [];
    }

    const sourceCenter =
      precomputedCenters?.get(edge.sourceId) ??
      this.getCellCenter(edge.sourceId);
    const targetCenter =
      precomputedCenters?.get(edge.targetId) ??
      this.getCellCenter(edge.targetId);
    if (!sourceCenter || !targetCenter) {
      return [];
    }

    const groups =
      precomputedGroupBounds ??
      Array.from(this.cells.values())
        .filter((cell) => cell.type === "vertex" && cell.isGroup)
        .map((group) => ({
          id: group.id,
          bounds: this.getAbsoluteBounds(group.id),
        }))
        .filter(
          (entry): entry is { id: string; bounds: Rect } =>
            entry.bounds !== null,
        );

    const intersectingGroup = groups.find((group) => {
      if (
        this.isCellInsideGroup(edge.sourceId!, group.id) ||
        this.isCellInsideGroup(edge.targetId!, group.id)
      ) {
        return false;
      }

      return (
        this.segmentIntersectsRect(sourceCenter, targetCenter, group.bounds) ||
        this.pointInsideRect(
          { x: sourceCenter.x, y: targetCenter.y },
          group.bounds,
        ) ||
        this.pointInsideRect(
          { x: targetCenter.x, y: sourceCenter.y },
          group.bounds,
        )
      );
    });

    if (!intersectingGroup) {
      return [];
    }

    const margin = 30;
    const { bounds } = intersectingGroup;

    const sourceBounds = this.getAbsoluteBounds(edge.sourceId);
    const targetBounds = this.getAbsoluteBounds(edge.targetId);

    // Determine spatial relation of each node to the group
    const sourceLeft = sourceBounds
      ? sourceBounds.x + sourceBounds.width < bounds.x
      : sourceCenter.x < bounds.x;
    const sourceRight = sourceBounds
      ? sourceBounds.x > bounds.x + bounds.width
      : sourceCenter.x > bounds.x + bounds.width;
    const sourceAbove = sourceBounds
      ? sourceBounds.y + sourceBounds.height < bounds.y
      : sourceCenter.y < bounds.y;
    const sourceBelow = sourceBounds
      ? sourceBounds.y > bounds.y + bounds.height
      : sourceCenter.y > bounds.y + bounds.height;

    const targetLeft = targetBounds
      ? targetBounds.x + targetBounds.width < bounds.x
      : targetCenter.x < bounds.x;
    const targetRight = targetBounds
      ? targetBounds.x > bounds.x + bounds.width
      : targetCenter.x > bounds.x + bounds.width;
    const targetAbove = targetBounds
      ? targetBounds.y + targetBounds.height < bounds.y
      : targetCenter.y < bounds.y;
    const targetBelow = targetBounds
      ? targetBounds.y > bounds.y + bounds.height
      : targetCenter.y > bounds.y + bounds.height;

    const sourceHSide = sourceLeft || sourceRight;
    const sourceVSide = sourceAbove || sourceBelow;
    const targetHSide = targetLeft || targetRight;
    const targetVSide = targetAbove || targetBelow;

    // Gap channel: one node is on a horizontal side, the other on a vertical side (L-shaped path)
    if ((sourceHSide && targetVSide) || (sourceVSide && targetHSide)) {
      // Source is left/right of group → vertical channel (channelX) in the gap
      if (sourceLeft || sourceRight) {
        if (sourceLeft) {
          const edgeRight = sourceBounds
            ? sourceBounds.x + sourceBounds.width
            : sourceCenter.x;
          const channelX = edgeRight + (bounds.x - edgeRight) / 2;
          return [
            { x: channelX, y: sourceCenter.y },
            { x: channelX, y: targetCenter.y },
          ];
        } else {
          const edgeLeft = sourceBounds ? sourceBounds.x : sourceCenter.x;
          const groupRight = bounds.x + bounds.width;
          const channelX = groupRight + (edgeLeft - groupRight) / 2;
          return [
            { x: channelX, y: sourceCenter.y },
            { x: channelX, y: targetCenter.y },
          ];
        }
      }
      // Source is above/below group → horizontal channel (channelY) in the gap
      if (sourceAbove) {
        const edgeBottom = sourceBounds
          ? sourceBounds.y + sourceBounds.height
          : sourceCenter.y;
        const channelY = edgeBottom + (bounds.y - edgeBottom) / 2;
        return [
          { x: sourceCenter.x, y: channelY },
          { x: targetCenter.x, y: channelY },
        ];
      } else {
        const edgeTop = sourceBounds ? sourceBounds.y : sourceCenter.y;
        const groupBottom = bounds.y + bounds.height;
        const channelY = groupBottom + (edgeTop - groupBottom) / 2;
        return [
          { x: sourceCenter.x, y: channelY },
          { x: targetCenter.x, y: channelY },
        ];
      }
    }

    // Default: horizontal route above or below the group
    const routeAboveY = bounds.y - margin;
    const routeBelowY = bounds.y + bounds.height + margin;
    const aboveCost =
      Math.abs(sourceCenter.y - routeAboveY) +
      Math.abs(targetCenter.y - routeAboveY);
    const belowCost =
      Math.abs(sourceCenter.y - routeBelowY) +
      Math.abs(targetCenter.y - routeBelowY);
    const routeY = aboveCost <= belowCost ? routeAboveY : routeBelowY;

    return [
      { x: sourceCenter.x, y: routeY },
      { x: targetCenter.x, y: routeY },
    ];
  }

  private hasStyleKey(style: string, key: string): boolean {
    return new RegExp(`(?:^|;)${key}=`).test(style);
  }

  private clampAnchor(value: number): number {
    return Math.round(Math.max(0.05, Math.min(0.95, value)) * 100) / 100;
  }

  private withSymmetricEdgeAnchors(
    edge: Cell,
    precomputedCenters?: Map<string, Point>,
  ): string {
    const baseStyle = edge.style ?? "";
    const hasExplicitAnchors =
      this.hasStyleKey(baseStyle, "exitX") ||
      this.hasStyleKey(baseStyle, "exitY") ||
      this.hasStyleKey(baseStyle, "entryX") ||
      this.hasStyleKey(baseStyle, "entryY");
    if (hasExplicitAnchors || !edge.sourceId || !edge.targetId) {
      return baseStyle;
    }

    const sourceCenter =
      precomputedCenters?.get(edge.sourceId) ??
      this.getCellCenter(edge.sourceId);
    const targetCenter =
      precomputedCenters?.get(edge.targetId) ??
      this.getCellCenter(edge.targetId);
    if (!sourceCenter || !targetCenter) {
      return baseStyle;
    }

    const sourceCell = this.cells.get(edge.sourceId);
    const targetCell = this.cells.get(edge.targetId);
    const sourceIsGroup = sourceCell?.isGroup === true;
    const targetIsGroup = targetCell?.isGroup === true;

    const dx = targetCenter.x - sourceCenter.x;
    const dy = targetCenter.y - sourceCenter.y;

    const styleWithTerminator =
      baseStyle.endsWith(";") || baseStyle.length === 0
        ? baseStyle
        : `${baseStyle};`;

    if (Math.abs(dx) >= Math.abs(dy)) {
      let exitY = 0.5;
      let entryY = 0.5;

      if (sourceIsGroup || targetIsGroup) {
        const sourceBounds = this.getAbsoluteBounds(edge.sourceId);
        const targetBounds = this.getAbsoluteBounds(edge.targetId);

        if (sourceBounds && targetBounds) {
          if (sourceIsGroup && targetIsGroup) {
            const midY = (sourceCenter.y + targetCenter.y) / 2;
            if (sourceBounds.height > 0)
              exitY = this.clampAnchor(
                (midY - sourceBounds.y) / sourceBounds.height,
              );
            if (targetBounds.height > 0)
              entryY = this.clampAnchor(
                (midY - targetBounds.y) / targetBounds.height,
              );
          } else if (targetIsGroup) {
            if (targetBounds.height > 0) {
              entryY = this.clampAnchor(
                (sourceCenter.y - targetBounds.y) / targetBounds.height,
              );
            }
          } else {
            if (sourceBounds.height > 0) {
              exitY = this.clampAnchor(
                (targetCenter.y - sourceBounds.y) / sourceBounds.height,
              );
            }
          }
        }
      }

      return dx >= 0
        ? `${styleWithTerminator}exitX=1;exitY=${exitY};entryX=0;entryY=${entryY};`
        : `${styleWithTerminator}exitX=0;exitY=${exitY};entryX=1;entryY=${entryY};`;
    }

    let exitX = 0.5;
    let entryX = 0.5;

    if (sourceIsGroup || targetIsGroup) {
      const sourceBounds = this.getAbsoluteBounds(edge.sourceId);
      const targetBounds = this.getAbsoluteBounds(edge.targetId);

      if (sourceBounds && targetBounds) {
        if (sourceIsGroup && targetIsGroup) {
          const midX = (sourceCenter.x + targetCenter.x) / 2;
          if (sourceBounds.width > 0)
            exitX = this.clampAnchor(
              (midX - sourceBounds.x) / sourceBounds.width,
            );
          if (targetBounds.width > 0)
            entryX = this.clampAnchor(
              (midX - targetBounds.x) / targetBounds.width,
            );
        } else if (targetIsGroup) {
          if (targetBounds.width > 0)
            entryX = this.clampAnchor(
              (sourceCenter.x - targetBounds.x) / targetBounds.width,
            );
        } else {
          if (sourceBounds.width > 0)
            exitX = this.clampAnchor(
              (targetCenter.x - sourceBounds.x) / sourceBounds.width,
            );
        }
      }
    }

    return dy >= 0
      ? `${styleWithTerminator}exitX=${exitX};exitY=1;entryX=${entryX};entryY=0;`
      : `${styleWithTerminator}exitX=${exitX};exitY=0;entryX=${entryX};entryY=1;`;
  }

  private rebuildEdgeIndex(): void {
    this.vertexEdgeIndex = new Map();
    for (const cell of this.cells.values()) {
      this.addEdgeToIndex(cell);
    }
  }

  addRectangle(params: {
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    text?: string;
    style?: string;
  }): Cell {
    const id = this.generateId();
    const cell: Cell = {
      id,
      type: "vertex",
      value: params.text ?? "New Cell",
      x: params.x ?? 100,
      y: params.y ?? 100,
      width: Math.max(1, params.width ?? 200),
      height: Math.max(1, params.height ?? 100),
      style:
        params.style ??
        "whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;",
      parent: this.activeLayerId,
    };
    this.cells.set(id, cell);
    this.invalidateStatsCache();
    return cell;
  }

  addEdge(params: {
    sourceId: string;
    targetId: string;
    text?: string;
    style?: string;
  }): Cell | { error: StructuredError } {
    // Validate source and target exist
    if (!this.cells.has(params.sourceId)) {
      return {
        error: {
          code: "SOURCE_NOT_FOUND",
          message: `Source cell '${params.sourceId}' not found`,
          cell_id: params.sourceId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    if (!this.cells.has(params.targetId)) {
      return {
        error: {
          code: "TARGET_NOT_FOUND",
          message: `Target cell '${params.targetId}' not found`,
          cell_id: params.targetId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }

    const id = this.generateId();
    const cell: Cell = {
      id,
      type: "edge",
      value: params.text ?? "",
      style:
        params.style ??
        "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;verticalAlign=bottom;labelBackgroundColor=#ffffff;",
      sourceId: params.sourceId,
      targetId: params.targetId,
      parent: this.activeLayerId,
    };
    this.cells.set(id, cell);
    this.addEdgeToIndex(cell);
    this.invalidateStatsCache();
    return cell;
  }

  deleteCell(cellId: string): { deleted: boolean; cascadedEdgeIds: string[] } {
    const cell = this.cells.get(cellId);
    if (!cell) return { deleted: false, cascadedEdgeIds: [] };

    const cascadedEdgeIds: string[] = [];
    if (cell.type === "vertex") {
      const relatedEdgeIds = this.vertexEdgeIndex.get(cellId);
      if (relatedEdgeIds) {
        cascadedEdgeIds.push(...relatedEdgeIds);
      }
      for (const id of cascadedEdgeIds) {
        const edge = this.cells.get(id);
        if (edge?.type === "edge") {
          this.removeEdgeFromIndex(edge);
          this.cells.delete(id);
        }
      }
      this.vertexEdgeIndex.delete(cellId);
    } else {
      this.removeEdgeFromIndex(cell);
    }

    this.cells.delete(cellId);
    this.invalidateStatsCache();
    return { deleted: true, cascadedEdgeIds };
  }

  editCell(
    cellId: string,
    params: {
      text?: string;
      x?: number;
      y?: number;
      width?: number;
      height?: number;
      style?: string;
    },
  ): Cell | { error: StructuredError } {
    const cell = this.cells.get(cellId);
    if (!cell) {
      return {
        error: {
          code: "CELL_NOT_FOUND",
          message: `Cell '${cellId}' not found`,
          cell_id: cellId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    if (cell.type !== "vertex") {
      return {
        error: {
          code: "WRONG_CELL_TYPE",
          message: `Cell '${cellId}' is not a vertex`,
          cell_id: cellId,
          suggestion: `This cell is a ${cell.type}. Use edit-edge for edge cells.`,
        },
      };
    }

    if (params.text !== undefined) cell.value = params.text;
    if (params.x !== undefined) cell.x = params.x;
    if (params.y !== undefined) cell.y = params.y;
    if (params.width !== undefined) cell.width = params.width;
    if (params.height !== undefined) cell.height = params.height;
    if (params.style !== undefined) cell.style = params.style;

    this.invalidateStatsCache();
    return cell;
  }

  editEdge(
    cellId: string,
    params: {
      text?: string;
      sourceId?: string;
      targetId?: string;
      style?: string;
    },
  ): Cell | { error: StructuredError } {
    const cell = this.cells.get(cellId);
    if (!cell) {
      return {
        error: {
          code: "CELL_NOT_FOUND",
          message: `Edge '${cellId}' not found`,
          cell_id: cellId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    if (cell.type !== "edge") {
      return {
        error: {
          code: "WRONG_CELL_TYPE",
          message: `Cell '${cellId}' is not an edge`,
          cell_id: cellId,
          suggestion: `This cell is a ${cell.type}. Use edit-cell for vertex cells.`,
        },
      };
    }

    if (params.text !== undefined) cell.value = params.text;
    if (params.sourceId !== undefined) {
      if (!this.cells.has(params.sourceId)) {
        return {
          error: {
            code: "SOURCE_NOT_FOUND",
            message: `Source cell '${params.sourceId}' not found`,
            cell_id: params.sourceId,
            suggestion: "Use list-paged-model to see available cells",
          },
        };
      }
      this.removeEdgeReference(cell.sourceId, cell.id);
      cell.sourceId = params.sourceId;
      this.addEdgeReference(cell.sourceId, cell.id);
      this.invalidateStatsCache();
    }
    if (params.targetId !== undefined) {
      if (!this.cells.has(params.targetId)) {
        return {
          error: {
            code: "TARGET_NOT_FOUND",
            message: `Target cell '${params.targetId}' not found`,
            cell_id: params.targetId,
            suggestion: "Use list-paged-model to see available cells",
          },
        };
      }
      this.removeEdgeReference(cell.targetId, cell.id);
      cell.targetId = params.targetId;
      this.addEdgeReference(cell.targetId, cell.id);
      this.invalidateStatsCache();
    }
    if (params.style !== undefined) cell.style = params.style;
    if (params.text !== undefined || params.style !== undefined) {
      this.invalidateStatsCache();
    }

    return cell;
  }

  getCell(cellId: string): Cell | undefined {
    return this.cells.get(cellId);
  }

  listCells(filter?: { cellType?: "vertex" | "edge" }): Cell[] {
    let cells = Array.from(this.cells.values());
    if (filter?.cellType) {
      cells = cells.filter((c) => c.type === filter.cellType);
    }
    return cells;
  }

  createLayer(name: string): Layer {
    const id = `layer-${this.nextLayerId++}`;
    const layer: Layer = { id, name };
    this.layers.push(layer);
    this.invalidateStatsCache();
    return layer;
  }

  listLayers(): Layer[] {
    return [...this.layers];
  }

  setActiveLayer(layerId: string): Layer | { error: StructuredError } {
    const layer = this.layers.find((l) => l.id === layerId);
    if (!layer) {
      return {
        error: {
          code: "LAYER_NOT_FOUND",
          message: `Layer '${layerId}' not found`,
          suggestion: "Use list-layers to see available layers",
        },
      };
    }
    this.activeLayerId = layerId;
    return layer;
  }

  getActiveLayer(): Layer {
    return this.layers.find((l) => l.id === this.activeLayerId)!;
  }

  moveCellToLayer(
    cellId: string,
    targetLayerId: string,
  ): Cell | { error: StructuredError } {
    const cell = this.cells.get(cellId);
    if (!cell) {
      return {
        error: {
          code: "CELL_NOT_FOUND",
          message: `Cell '${cellId}' not found`,
          cell_id: cellId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    const layer = this.layers.find((l) => l.id === targetLayerId);
    if (!layer) {
      return {
        error: {
          code: "LAYER_NOT_FOUND",
          message: `Layer '${targetLayerId}' not found`,
          suggestion: "Use list-layers to see available layers",
        },
      };
    }
    cell.parent = targetLayerId;
    this.invalidateStatsCache();
    return cell;
  }

  // ─── Group / Container Support ───────────────────────────────────

  /**
   * Create a group (container) cell that can hold child cells.
   */
  createGroup(params: {
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    text?: string;
    style?: string;
  }): Cell {
    const id = this.generateId();
    const cell: Cell = {
      id,
      type: "vertex",
      value: params.text ?? "",
      x: params.x ?? 0,
      y: params.y ?? 0,
      width: params.width ?? 400,
      height: params.height ?? 300,
      style:
        params.style ??
        "rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;dashed=1;container=1;collapsible=0;verticalAlign=bottom;align=center;verticalLabelPosition=top;labelPosition=center;",
      parent: this.activeLayerId,
      isGroup: true,
      children: [],
    };
    this.cells.set(id, cell);
    this.invalidateStatsCache();
    return cell;
  }

  /**
   * Batch create multiple groups in a single operation.
   */
  batchCreateGroups(
    groups: Array<{
      x?: number;
      y?: number;
      width?: number;
      height?: number;
      text?: string;
      style?: string;
      tempId?: string;
    }>,
  ): Array<{ success: boolean; cell: Cell; tempId?: string }> {
    return groups.map((g) => {
      const cell = this.createGroup(g);
      return { success: true, cell, tempId: g.tempId };
    });
  }

  /**
   * Add a cell to a group. The child cell's parent is set to the group.
   */
  addCellToGroup(
    cellId: string,
    groupId: string,
  ): Cell | { error: StructuredError } {
    const cell = this.cells.get(cellId);
    if (!cell) {
      return {
        error: {
          code: "CELL_NOT_FOUND",
          message: `Cell '${cellId}' not found`,
          cell_id: cellId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    const group = this.cells.get(groupId);
    if (!group) {
      return {
        error: {
          code: "GROUP_NOT_FOUND",
          message: `Group '${groupId}' not found`,
          cell_id: groupId,
          suggestion: "Use list-paged-model to see available groups",
        },
      };
    }
    if (!group.isGroup) {
      return {
        error: {
          code: "NOT_A_GROUP",
          message: `Cell '${groupId}' is not a group/container`,
          cell_id: groupId,
          suggestion: "Use create-group to create a group first",
        },
      };
    }
    if (cellId === groupId) {
      return {
        error: {
          code: "SELF_REFERENCE",
          message: "Cannot add a group to itself",
          cell_id: cellId,
          suggestion: "Provide a different cell_id and group_id",
        },
      };
    }

    const previousParentId = cell.parent;
    // deno-coverage-ignore
    const previousParentGroup = previousParentId
      ? this.cells.get(previousParentId)
      : undefined;

    // Preserve visual position while changing parent coordinate space.
    // Draw.io child coordinates are relative to the parent (group), so we convert
    // from old-parent-relative -> absolute -> new-parent-relative for vertex cells.
    if (cell.type === "vertex") {
      const cellBounds = this.getAbsoluteBounds(cellId);
      const targetParentOffset = this.getAbsoluteParentOffset(groupId);
      if (cellBounds) {
        cell.x = cellBounds.x - targetParentOffset.x;
        cell.y = cellBounds.y - targetParentOffset.y;
      }
    }

    if (previousParentGroup?.isGroup && previousParentGroup.children) {
      previousParentGroup.children = previousParentGroup.children.filter(
        (id) => id !== cellId,
      );
    }

    cell.parent = groupId;
    if (!group.children) {
      group.children = [];
    }
    if (!group.children.includes(cellId)) {
      group.children.push(cellId);
    }
    this.invalidateStatsCache();
    return cell;
  }

  /**
   * Batch add multiple cells to groups in a single operation.
   */
  batchAddCellsToGroup(
    assignments: Array<{ cellId: string; groupId: string }>,
  ): Array<{
    success: boolean;
    cell?: Cell;
    error?: StructuredError;
    cellId: string;
    groupId: string;
    warnings?: GroupContainmentWarning[];
  }> {
    const results = assignments.map((a) => {
      const result = this.addCellToGroup(a.cellId, a.groupId);
      if ("error" in result) {
        return {
          success: false,
          error: result.error,
          cellId: a.cellId,
          groupId: a.groupId,
        } as const;
      }

      return {
        success: true,
        cell: result,
        cellId: a.cellId,
        groupId: a.groupId,
      } as const;
    });

    // Center children within each affected group and resize if needed
    const affectedGroupIds = new Set(
      results.filter((r) => r.success).map((r) => r.groupId),
    );
    for (const groupId of affectedGroupIds) {
      this.centerChildrenInGroup(groupId);
    }

    // Run containment checks after centering
    return results.map((r) => {
      if (!r.success) return r;
      const warning = this.getGroupContainmentWarning(r.cellId, r.groupId);
      return {
        ...r,
        // deno-coverage-ignore
        ...(warning && { warnings: [warning] }),
      };
    });
  }

  /**
   * Center all vertex children within a group, stacking them vertically.
   * Resizes the group if it is too small to contain all children with padding.
   */
  private centerChildrenInGroup(groupId: string): void {
    const group = this.cells.get(groupId);
    if (!group?.isGroup || !group.children?.length) return;

    const PADDING = 40;
    const SPACING = 40;

    // Collect vertex children with dimensions
    const children = group.children
      .map((id) => this.cells.get(id))
      .filter((c): c is Cell => c != null && c.type === "vertex");

    if (children.length === 0) return;

    // Calculate total height needed for vertical stack
    const totalChildHeight = children.reduce(
      (sum, c) => sum + (c.height ?? 48),
      0,
    );
    const totalSpacing = (children.length - 1) * SPACING;
    const requiredHeight = totalChildHeight + totalSpacing + PADDING * 2;
    const maxChildWidth = Math.max(...children.map((c) => c.width ?? 48));
    const requiredWidth = maxChildWidth + PADDING * 2;

    // Expand group if needed
    const groupWidth = group.width ?? 0;
    const groupHeight = group.height ?? 0;
    if (requiredWidth > groupWidth) group.width = requiredWidth;
    if (requiredHeight > groupHeight) group.height = requiredHeight;

    const finalWidth = group.width!;
    const finalHeight = group.height!;

    // Stack children vertically, centered horizontally within the group
    const startY = (finalHeight - totalChildHeight - totalSpacing) / 2;
    let currentY = startY;

    for (const child of children) {
      const cw = child.width ?? 48;
      const ch = child.height ?? 48;
      child.x = (finalWidth - cw) / 2;
      child.y = currentY;
      currentY += ch + SPACING;
    }
  }

  /**
   * Remove a cell from its group, returning it to the active layer.
   */
  removeCellFromGroup(cellId: string): Cell | { error: StructuredError } {
    const cell = this.cells.get(cellId);
    if (!cell) {
      return {
        error: {
          code: "CELL_NOT_FOUND",
          message: `Cell '${cellId}' not found`,
          cell_id: cellId,
          suggestion: "Use list-paged-model to see available cells",
        },
      };
    }
    // Check if the cell is currently in a group (parent is a cell, not a layer)
    const parentCell = this.cells.get(cell.parent!);
    if (!parentCell || !parentCell.isGroup) {
      return {
        error: {
          code: "NOT_IN_GROUP",
          message: `Cell '${cellId}' is not inside a group`,
          cell_id: cellId,
          suggestion: "Cell is already at the layer level",
        },
      };
    }
    // Remove from parent's children list
    if (parentCell.children) {
      parentCell.children = parentCell.children.filter((id) => id !== cellId);
    }

    // Preserve visual position while moving from group-relative to layer absolute coordinates.
    if (cell.type === "vertex") {
      const cellBounds = this.getAbsoluteBounds(cellId);
      if (cellBounds) {
        cell.x = cellBounds.x;
        cell.y = cellBounds.y;
      }
    }

    // Return to the active layer
    cell.parent = this.activeLayerId;
    this.invalidateStatsCache();
    return cell;
  }

  /**
   * Validate whether all children are visually inside the given group bounds.
   */
  validateGroupContainment(groupId: string):
    | {
        group: Cell;
        totalChildren: number;
        inBoundsChildren: number;
        outOfBoundsChildren: number;
        warnings: GroupContainmentWarning[];
      }
    | { error: StructuredError } {
    const group = this.cells.get(groupId);
    if (!group) {
      return {
        error: {
          code: "GROUP_NOT_FOUND",
          message: `Group '${groupId}' not found`,
          cell_id: groupId,
          suggestion: "Use list-paged-model to see available groups",
        },
      };
    }
    if (!group.isGroup) {
      return {
        error: {
          code: "NOT_A_GROUP",
          message: `Cell '${groupId}' is not a group/container`,
          cell_id: groupId,
          suggestion: "Use create-group to create a group first",
        },
      };
    }

    const childIds = group.children ?? [];
    const warnings: GroupContainmentWarning[] = [];

    for (const childId of childIds) {
      const warning = this.getGroupContainmentWarning(childId, groupId);
      if (warning) {
        warnings.push(warning);
      }
    }

    return {
      group,
      totalChildren: childIds.length,
      inBoundsChildren: childIds.length - warnings.length,
      outOfBoundsChildren: warnings.length,
      warnings,
    };
  }

  /**
   * List the children of a group.
   */
  listGroupChildren(groupId: string): Cell[] | { error: StructuredError } {
    const group = this.cells.get(groupId);
    if (!group) {
      return {
        error: {
          code: "GROUP_NOT_FOUND",
          message: `Group '${groupId}' not found`,
          cell_id: groupId,
          suggestion: "Use list-paged-model to see available groups",
        },
      };
    }
    if (!group.isGroup) {
      return {
        error: {
          code: "NOT_A_GROUP",
          message: `Cell '${groupId}' is not a group/container`,
          cell_id: groupId,
          suggestion: "Use create-group to create a group first",
        },
      };
    }
    return group
      .children!.map((id) => this.cells.get(id))
      .filter((c): c is Cell => c !== undefined);
  }

  // ─── Import / Load XML ───────────────────────────────────────────

  /**
   * Import a Draw.io XML string, replacing the current diagram state.
   * Supports single-page and multi-page documents. When multiple pages are
   * present, all cells and layers are merged into a single flat model.
   * Returns the number of source pages, merged cells, and merged layers.
   */
  importXml(
    xml: string,
  ):
    | { pages: number; cells: number; layers: number }
    | { error: StructuredError } {
    // Basic validation
    if (!xml || !xml.trim()) {
      return {
        error: {
          code: "EMPTY_XML",
          message: "XML string is empty",
          suggestion: "Provide a valid Draw.io XML string",
        },
      };
    }

    // Strip CDATA wrapper if present (e.g. <![CDATA[<mxfile...>]]>)
    const cdataMatch = xml.match(/^<!\[CDATA\[([\s\S]*)\]\]>$/);
    if (cdataMatch) {
      xml = cdataMatch[1];
    }

    if (!xml.includes("<mxfile") && !xml.includes("<mxGraphModel")) {
      return {
        error: {
          code: "INVALID_XML",
          message: "XML does not appear to be a Draw.io file",
          suggestion:
            "Provide XML that contains <mxfile> or <mxGraphModel> elements",
        },
      };
    }

    const parsed = xmlParser.parse(xml) as Record<string, unknown>;

    // Extract diagram elements
    let diagramElements: Array<Record<string, unknown>> = [];

    const mxfile = parsed.mxfile as Record<string, unknown> | undefined;
    const requestedActiveLayerId =
      typeof mxfile?.activeLayerId === "string"
        ? mxfile.activeLayerId
        : undefined;
    if (mxfile?.diagram) {
      diagramElements = mxfile.diagram as Array<Record<string, unknown>>;
    } else if (parsed.mxGraphModel) {
      // Bare mxGraphModel without mxfile wrapper — wrap it as a single diagram
      diagramElements = [{ mxGraphModel: parsed.mxGraphModel }];
    } else {
      // mxfile without diagram children (e.g., <mxfile></mxfile>)
      diagramElements = [parsed];
    }

    // Reset state entirely
    this.cells = new Map();
    this.vertexEdgeIndex = new Map();
    this.cachedStats = null;
    this.layers = [{ id: "1", name: "Default Layer" }];
    this.activeLayerId = "1";
    this.nextId = 2;
    this.nextLayerId = 2;

    for (let di = 0; di < diagramElements.length; di++) {
      let diag = diagramElements[di];

      // Detect compressed diagram content: when the diagram element has a text
      // node instead of an mxGraphModel child, it contains deflate+base64 data.
      if (!diag.mxGraphModel && typeof diag["#text"] === "string") {
        try {
          const decompressedXml = DiagramModel.decompressXml(diag["#text"]);
          const innerParsed = xmlParser.parse(decompressedXml) as Record<
            string,
            unknown
          >;
          diag = { ...diag, mxGraphModel: innerParsed.mxGraphModel };
        } catch {
          return {
            error: {
              code: "DECOMPRESS_FAILED",
              message:
                "Failed to decompress diagram content — the data is not valid base64+deflate",
              suggestion:
                "Provide uncompressed diagram XML, or ensure the compressed content is valid Draw.io deflate+base64 format",
            },
          };
        }
      }

      const { cells, layers, nextId } = this.parseMxGraphContent(diag);

      // Merge cells into the model
      for (const [id, cell] of cells) {
        this.cells.set(id, cell);
      }

      // Merge layers (skip default layer already present)
      for (const layer of layers) {
        if (layer.id !== "1" && !this.layers.some((l) => l.id === layer.id)) {
          this.layers.push(layer);
        }
      }

      // Track the highest nextId across all pages
      if (nextId > this.nextId) {
        this.nextId = nextId;
      }
    }

    if (
      requestedActiveLayerId &&
      this.layers.some((l) => l.id === requestedActiveLayerId)
    ) {
      this.activeLayerId = requestedActiveLayerId;
    }

    this.rebuildEdgeIndex();
    this.invalidateStatsCache();

    return {
      pages: diagramElements.length,
      cells: this.cells.size,
      layers: this.layers.length,
    };
  }

  /**
   * Extract attributes common to mxCell / UserObject elements.
   */
  private extractCellAttrs(
    obj: Record<string, unknown>,
    geometry: Record<string, unknown> | undefined,
  ): ParsedCellAttrs {
    return {
      id: String(obj.id ?? ""),
      value: String(obj.value ?? obj.label ?? ""),
      style: String(obj.style ?? ""),
      parent: String(obj.parent ?? ""),
      source: obj.source !== undefined ? String(obj.source) : undefined,
      target: obj.target !== undefined ? String(obj.target) : undefined,
      isVertex: String(obj.vertex) === "1",
      isEdge: String(obj.edge) === "1",
      geometry,
    };
  }

  /**
   * Parse mxGraphModel content from a parsed XML object and extract cells and layers.
   */
  private parseMxGraphContent(diagramObj: Record<string, unknown>): {
    cells: Map<string, Cell>;
    layers: Layer[];
    nextId: number;
  } {
    const cells = new Map<string, Cell>();
    const layers: Layer[] = [{ id: "1", name: "Default Layer" }];
    let maxId = 1;

    // Navigate to root element: diagramObj.mxGraphModel.root
    const mxGraphModel = diagramObj.mxGraphModel as
      | Record<string, unknown>
      | undefined;
    const root = mxGraphModel?.root as Record<string, unknown> | undefined;
    if (!root) {
      return { cells, layers, nextId: maxId + 1 };
    }

    // Collect normalised cell entries from mxCell and UserObject arrays
    const rawElements: ParsedCellAttrs[] = [];

    // Process mxCell elements
    const mxCells = (root.mxCell ?? []) as Array<Record<string, unknown>>;
    for (const cell of mxCells) {
      rawElements.push(
        this.extractCellAttrs(
          cell,
          cell.mxGeometry as Record<string, unknown> | undefined,
        ),
      );
    }

    // Process UserObject elements
    const userObjects = (root.UserObject ?? []) as Array<
      Record<string, unknown>
    >;
    for (const uo of userObjects) {
      // Geometry may live on the nested mxCell child
      let geometry: Record<string, unknown> | undefined;
      const innerCells = uo.mxCell as
        | Array<Record<string, unknown>>
        | undefined;
      if (innerCells && innerCells.length > 0) {
        const innerCell = innerCells[0];
        geometry = innerCell.mxGeometry as Record<string, unknown> | undefined;
        // Merge inner mxCell attributes that weren't set on the UserObject itself
        for (const key of [
          "style",
          "vertex",
          "edge",
          "parent",
          "source",
          "target",
        ]) {
          if (innerCell[key] !== undefined && uo[key] === undefined) {
            uo[key] = innerCell[key];
          }
        }
      }
      rawElements.push(this.extractCellAttrs(uo, geometry));
    }

    // Process all collected elements
    for (const elem of rawElements) {
      // Skip root cells (id=0 or id=1 with parent=0)
      if (elem.id === "0") continue;
      if (elem.id === "1" && elem.parent === "0") continue;

      // Skip watermark cell (re-generated on every export)
      if (elem.id === WATERMARK_CELL_ID) continue;

      // Track numeric IDs for nextId
      const numMatch = elem.id.match(/\d+/);
      if (numMatch) {
        maxId = Math.max(maxId, parseInt(numMatch[0], 10));
      }

      // Layer detection: parent="0", not a vertex or edge
      if (elem.parent === "0" && !elem.isVertex && !elem.isEdge) {
        if (elem.id !== "1") {
          layers.push({ id: elem.id, name: elem.value || elem.id });
        }
        continue;
      }

      // Parse geometry
      let x: number | undefined;
      let y: number | undefined;
      let width: number | undefined;
      let height: number | undefined;

      if (elem.geometry) {
        const geo = elem.geometry;
        if (geo.x !== undefined) x = parseFloat(String(geo.x));
        if (geo.y !== undefined) y = parseFloat(String(geo.y));
        if (geo.width !== undefined) width = parseFloat(String(geo.width));
        if (geo.height !== undefined) height = parseFloat(String(geo.height));
      }

      // Determine if it's a group/container
      const isGroup =
        elem.style.includes("container=1") || /swimlane/i.test(elem.style);

      if (elem.isEdge) {
        const cell: Cell = {
          id: elem.id,
          type: "edge",
          value: elem.value,
          style: elem.style,
          sourceId: elem.source,
          targetId: elem.target,
          parent: elem.parent || "1",
        };
        cells.set(elem.id, cell);
      } else {
        const cell: Cell = {
          id: elem.id,
          type: "vertex",
          value: elem.value,
          x: x ?? 0,
          y: y ?? 0,
          width: width ?? 200,
          height: height ?? 100,
          style: elem.style,
          parent: elem.parent || "1",
          ...(isGroup && { isGroup: true, children: [] }),
        };
        cells.set(elem.id, cell);
      }
    }

    // Post-process: populate children arrays for groups
    for (const cell of cells.values()) {
      if (cell.parent && cells.has(cell.parent)) {
        const parentCell = cells.get(cell.parent)!;
        if (parentCell.isGroup) {
          parentCell.children!.push(cell.id);
        }
      }
    }

    return { cells, layers, nextId: maxId + 1 };
  }

  /**
   * Export the diagram as Draw.io XML format (single page).
   *
   * @param options.compress - If `true`, deflate-compress and base64-encode the
   *   `<mxGraphModel>` content inside the `<diagram>` element. This matches the
   *   format used by the Draw.io desktop application and typically achieves 60-80%
   *   size reduction. Defaults to `false` (plain XML).
   * @param options.transactional - If `true`, strip SVG image data from shape cells
   *   and mark them as placeholders. Used for lightweight intermediate responses
   *   during batch diagram creation. Real SVG is restored only at finish-diagram time.
   *   Defaults to `false` (full production XML with all image data).
   */
  toXml(options?: {
    compress?: boolean;
    transactional?: boolean;
    watermark?: boolean;
    background?: string;
  }): string {
    const compress = options?.compress ?? false;
    const transactional = options?.transactional ?? false;
    const watermark = options?.watermark ?? false;
    const background = options?.background ?? "#FFFFFF";

    const backgroundAttr =
      background === "none"
        ? ""
        : ` background="${this.escapeXml(background)}"`;
    const pageXml = this.renderPageXml(this.cells, this.layers, {
      transactional,
      watermark,
    });
    const graphModelXml = `<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"${backgroundAttr}><root><mxCell id="0"/><mxCell id="1" parent="0"/>${pageXml}</root></mxGraphModel>`;
    const diagramContent = compress
      ? DiagramModel.compressXml(graphModelXml)
      : graphModelXml;

    return `<mxfile host="drawio-mcp-server" activeLayerId="${this.escapeXml(this.activeLayerId)}"><diagram id="page-1" name="Page-1">${diagramContent}</diagram></mxfile>`;
  }

  /**
   * Compress an XML string using the Draw.io format:
   * encodeURIComponent(xml) → deflateRaw → base64.
   *
   * This matches Draw.io's `Graph.compress` which URL-encodes the XML first,
   * then deflates the URL-encoded bytes, then base64-encodes the result.
   */
  static compressXml(xml: string): string {
    const encoder = new TextEncoder();
    const uriEncoded = encodeURIComponent(xml);
    const deflated = deflateRawSync(encoder.encode(uriEncoded));
    return encodeBase64(deflated);
  }

  /**
   * Decompress a Draw.io compressed diagram string:
   * base64 → inflateRaw → decodeURIComponent.
   *
   * This matches Draw.io's `Graph.decompress` which base64-decodes the input,
   * inflates the raw deflate stream, then URL-decodes the result to recover
   * the original XML.
   */
  static decompressXml(compressed: string): string {
    const deflated = decodeBase64(compressed);
    const inflated = inflateRawSync(deflated);
    const decoder = new TextDecoder();
    return decodeURIComponent(decoder.decode(inflated));
  }

  /**
   * Render cells and layers for a single page.
   * @param options.transactional - If true, strip SVG image data from styles (for placeholders)
   */
  private renderPageXml(
    cells: Map<string, Cell>,
    layers: Layer[],
    options?: { transactional?: boolean; watermark?: boolean },
  ): string {
    // Emit custom layer cells (skip the default layer id="1" which is always present)
    const layerCellsXml = layers
      .filter((l) => l.id !== "1")
      .map(
        (l) =>
          `<mxCell id="${this.escapeXml(l.id)}" value="${this.escapeXml(l.name)}" style="" parent="0"/>`,
      )
      .join("");

    const absoluteBoundsCache = new Map<string, Rect | null>();
    const precomputedCenters = new Map<string, Point>();
    const precomputedGroupBounds: Array<{ id: string; bounds: Rect }> = [];

    for (const cell of cells.values()) {
      if (cell.type !== "vertex") {
        continue;
      }

      const bounds = this.getAbsoluteBoundsFromMap(
        cell.id,
        cells,
        absoluteBoundsCache,
      );
      // deno-coverage-ignore
      if (!bounds) {
        // deno-coverage-ignore
        continue;
        // deno-coverage-ignore
      }

      precomputedCenters.set(cell.id, {
        x: bounds.x + bounds.width / 2,
        y: bounds.y + bounds.height / 2,
      });

      if (cell.isGroup) {
        precomputedGroupBounds.push({ id: cell.id, bounds });
      }
    }

    const emittedFlattenedEdgeLabelKeys = new Set<string>();
    const emittedLabelCountByRoute = new Map<string, number>();

    const cellsXml = Array.from(cells.values())
      .map((cell) => {
        if (cell.type === "vertex") {
          const groupAttrs = cell.isGroup ? ' connectable="0"' : "";
          let containerStyle =
            cell.isGroup && cell.style && !cell.style.includes("container=1")
              ? cell.style + "container=1;"
              : cell.style;

          // In transactional mode, strip SVG image data and mark as placeholder
          if (
            options?.transactional &&
            containerStyle &&
            containerStyle.includes("image=")
          ) {
            containerStyle = stripImageFromStyle(containerStyle);
            if (!containerStyle.includes(PLACEHOLDER_MARKER)) {
              // deno-coverage-ignore
              containerStyle = `${containerStyle}${containerStyle.endsWith(";") ? "" : ";"}${PLACEHOLDER_MARKER}`;
            }
          }

          return `<mxCell id="${this.escapeXml(cell.id)}" value="${this.escapeXml(cell.value)}" style="${this.escapeXml(containerStyle!)}" vertex="1"${groupAttrs} parent="${cell.parent!}"><mxGeometry x="${cell.x!}" y="${cell.y!}" width="${cell.width!}" height="${cell.height!}" as="geometry"/></mxCell>`;
        } else {
          const flattenedLabelKey = this.getFlattenedEdgeLabelKey(cell);
          const shouldHideDuplicateLabel =
            !!flattenedLabelKey &&
            emittedFlattenedEdgeLabelKeys.has(flattenedLabelKey);
          if (flattenedLabelKey && !shouldHideDuplicateLabel) {
            emittedFlattenedEdgeLabelKeys.add(flattenedLabelKey);
          }

          const edgeValue = shouldHideDuplicateLabel ? "" : cell.value;
          const flattenedRouteKey = this.getFlattenedEdgeRouteKey(cell);
          let labelOffsetY = 0;
          if (flattenedRouteKey && edgeValue.trim().length > 0) {
            const labelIndexOnRoute =
              emittedLabelCountByRoute.get(flattenedRouteKey) ?? 0;
            labelOffsetY = this.getAlternatingLabelOffset(labelIndexOnRoute);
            emittedLabelCountByRoute.set(
              flattenedRouteKey,
              labelIndexOnRoute + 1,
            );
          }

          const edgeStyle = this.withSymmetricEdgeAnchors(
            cell,
            precomputedCenters,
          );
          const waypoints = this.getGroupAvoidanceWaypoints(
            cell,
            precomputedCenters,
            precomputedGroupBounds,
          );
          const geometryXml = this.renderEdgeGeometryXml(
            waypoints,
            labelOffsetY,
          );
          const sourceAttr = cell.sourceId ? ` source="${cell.sourceId}"` : "";
          const targetAttr = cell.targetId ? ` target="${cell.targetId}"` : "";
          return `<mxCell id="${this.escapeXml(cell.id)}" value="${this.escapeXml(edgeValue)}" style="${this.escapeXml(edgeStyle)}" edge="1" parent="${cell.parent!}"${sourceAttr}${targetAttr}>${geometryXml}</mxCell>`;
        }
      })
      .join("");

    const watermarkXml = options?.watermark ? this.renderWatermarkXml() : "";
    return `${layerCellsXml}${cellsXml}${watermarkXml}`;
  }

  /**
   * Render the server watermark at a fixed position near the bottom of the diagram page.
   * Uses a static XML template — only version, date, and position are injected.
   * Position is based on the page dimensions (850×1100) with a fixed bottom offset.
   */
  private renderWatermarkXml(): string {
    const w = 320;
    const h = 50;
    const x = 40;
    const y = 1100 - h - 5; // 5 from the bottom of the page

    const now = new Date();
    const date = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}`;

    return WATERMARK_TEMPLATE.replace("{{VERSION}}", denoConfig.version)
      .replace("{{DATE}}", date)
      .replace("{{X}}", String(x))
      .replace("{{Y}}", String(y))
      .replace("{{W}}", String(w))
      .replace("{{H}}", String(h));
  }

  /** Lookup map for single-pass XML escaping */
  private static readonly XML_ESCAPE_MAP: Record<string, string> = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&apos;",
  };

  private escapeXml(str: string): string {
    return str.replace(/[&<>"']/g, (ch) => DiagramModel.XML_ESCAPE_MAP[ch]);
  }

  /**
   * Clear all cells and reset the diagram to its initial empty state.
   */
  clear(): { vertices: number; edges: number } {
    let vertices = 0;
    let edges = 0;
    for (const c of this.cells.values()) {
      if (c.type === "vertex") vertices++;
      else edges++;
    }

    this.cells = new Map();
    this.vertexEdgeIndex = new Map();
    this.cachedStats = null;
    this.layers = [{ id: "1", name: "Default Layer" }];
    this.activeLayerId = "1";
    this.nextId = 2;
    this.nextLayerId = 2;
    return { vertices, edges };
  }

  /**
   * Get statistics about the current diagram (active page)
   */
  getStats(): {
    total_cells: number;
    vertices: number;
    edges: number;
    groups: number;
    layers: number;
    bounds: { minX: number; minY: number; maxX: number; maxY: number } | null;
    cells_with_text: number;
    cells_without_text: number;
    cells_by_layer: Record<string, number>;
  } {
    if (this.cachedStats) {
      return this.cloneStats(this.cachedStats);
    }

    // Single-pass collection of all statistics
    let vertexCount = 0;
    let edgeCount = 0;
    let groupCount = 0;
    let cellsWithText = 0;
    let minX = Infinity;
    let minY = Infinity;
    let maxX = -Infinity;
    let maxY = -Infinity;
    let hasPositions = false;
    const cellsByLayer: Record<string, number> = {};

    for (const cell of this.cells.values()) {
      // Type counts
      if (cell.type === "vertex") {
        vertexCount++;
        if (cell.isGroup) groupCount++;
        // Bounding box (vertices only)
        if (cell.x !== undefined && cell.y !== undefined) {
          hasPositions = true;
          if (cell.x < minX) minX = cell.x;
          if (cell.y < minY) minY = cell.y;
          const right = cell.x + cell.width!;
          const bottom = cell.y + cell.height!;
          if (right > maxX) maxX = right;
          if (bottom > maxY) maxY = bottom;
        }
      } else {
        edgeCount++;
      }

      // Text counts
      if (cell.value && cell.value.trim().length > 0) cellsWithText++;

      // Layer counts
      const layer = cell.parent!;
      cellsByLayer[layer] = (cellsByLayer[layer] ?? 0) + 1;
    }

    const totalCells = vertexCount + edgeCount;

    const stats: DiagramStats = {
      total_cells: totalCells,
      vertices: vertexCount,
      edges: edgeCount,
      groups: groupCount,
      layers: this.layers.length,
      bounds: hasPositions ? { minX, minY, maxX, maxY } : null,
      cells_with_text: cellsWithText,
      cells_without_text: totalCells - cellsWithText,
      cells_by_layer: cellsByLayer,
    };

    this.cachedStats = stats;
    return this.cloneStats(stats);
  }

  /**
   * Batch add multiple cells (vertices and edges) in a single operation.
   * Validates entire batch before executing to fail fast.
   * Returns an array of results with created cells or errors.
   */
  batchAddCells(
    items: Array<{
      type: "vertex" | "edge";
      x?: number;
      y?: number;
      width?: number;
      height?: number;
      text?: string;
      style?: string;
      sourceId?: string;
      targetId?: string;
      tempId?: string;
    }>,
    options?: { dryRun?: boolean },
  ): Array<{
    success: boolean;
    cell?: Cell;
    error?: StructuredError;
    tempId?: string;
  }> {
    // Pre-validate entire batch
    const validationErrors = this.validateBatchCells(items);
    if (validationErrors.length > 0) {
      return validationErrors;
    }

    // If dry-run, return success without persisting
    if (options?.dryRun) {
      return items.map((item, index) => ({
        success: true,
        tempId: item.tempId,
        cell: {
          id: `temp-cell-${index}`,
          type: item.type,
          value: item.text ?? "",
          x: item.x,
          y: item.y,
          width: item.width,
          height: item.height,
          style: item.style,
          sourceId: item.sourceId,
          targetId: item.targetId,
        } as Cell,
      }));
    }

    // Execute batch operations
    const results: Array<{
      success: boolean;
      cell?: Cell;
      error?: StructuredError;
      tempId?: string;
    }> = [];
    const tempIdMap = new Map<string, string>();

    for (const item of items) {
      if (item.type === "vertex") {
        let cell: Cell;

        // If tempId is a placeholder ID (starts with "placeholder-"), use it directly as the cell ID
        if (item.tempId && item.tempId.startsWith("placeholder-")) {
          // Create cell with explicit placeholder ID
          cell = {
            id: item.tempId,
            type: "vertex",
            value: item.text ?? "New Cell",
            x: item.x ?? 100,
            y: item.y ?? 100,
            width: Math.max(1, item.width ?? 200),
            height: Math.max(1, item.height ?? 100),
            style:
              item.style ??
              "whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;",
            parent: this.activeLayerId,
          };
          this.cells.set(cell.id, cell);
          this.invalidateStatsCache();
        } else {
          // Use standard addRectangle for non-placeholder cells
          cell = this.addRectangle({
            x: item.x,
            y: item.y,
            width: item.width,
            height: item.height,
            text: item.text,
            style: item.style,
          });
        }

        if (item.tempId) {
          tempIdMap.set(item.tempId, cell.id);
        }
        results.push({ success: true, cell, tempId: item.tempId });
      } else if (item.type === "edge") {
        // Validation guarantees sourceId/targetId are truthy for edges
        const sourceId = tempIdMap.get(item.sourceId!) ?? item.sourceId!;
        const targetId = tempIdMap.get(item.targetId!) ?? item.targetId!;

        const result = this.addEdge({
          sourceId,
          targetId,
          text: item.text,
          style: item.style,
        });

        // Validation guarantees addEdge succeeds — cast safely
        const edge = result as Cell;
        if (item.tempId) {
          tempIdMap.set(item.tempId, edge.id);
        }
        results.push({ success: true, cell: edge, tempId: item.tempId });
      }
    }

    return results;
  }

  private validateBatchCells(
    items: Array<{
      type: "vertex" | "edge";
      sourceId?: string;
      targetId?: string;
      tempId?: string;
    }>,
  ): Array<{ success: boolean; error: StructuredError; tempId?: string }> {
    const errors: Array<{
      success: boolean;
      error: StructuredError;
      tempId?: string;
    }> = [];
    const createdTempIds = new Set<string>();

    for (let i = 0; i < items.length; i++) {
      const item = items[i];

      if (item.type === "edge") {
        // Validate source exists (check temp IDs from earlier items and existing cells)
        const sourceExists = !!(
          item.sourceId &&
          (createdTempIds.has(item.sourceId) || this.cells.has(item.sourceId))
        );
        if (!sourceExists) {
          errors.push({
            success: false,
            tempId: item.tempId,
            error: {
              code: "INVALID_SOURCE",
              message: `Edge at index ${i}: source cell '${item.sourceId}' not found`,
              index: i,
              suggestion:
                "Ensure source_id references an existing cell or a temp_id defined earlier in the batch",
            },
          });
        }

        // Validate target exists
        const targetExists = !!(
          item.targetId &&
          (createdTempIds.has(item.targetId) || this.cells.has(item.targetId))
        );
        if (!targetExists) {
          errors.push({
            success: false,
            tempId: item.tempId,
            error: {
              code: "INVALID_TARGET",
              message: `Edge at index ${i}: target cell '${item.targetId}' not found`,
              index: i,
              suggestion:
                "Ensure target_id references an existing cell or a temp_id defined earlier in the batch",
            },
          });
        }
      }

      // Track temp IDs for validation
      if (item.tempId) {
        createdTempIds.add(item.tempId);
      }
    }

    return errors;
  }

  /**
   * Batch edit multiple edges in a single operation.
   */
  batchEditEdges(
    updates: Array<{
      cell_id: string;
      text?: string;
      source_id?: string;
      target_id?: string;
      style?: string;
    }>,
  ): Array<{
    success: boolean;
    cell?: Cell;
    error?: StructuredError;
    cell_id: string;
  }> {
    return updates.map((update) => {
      const result = this.editEdge(update.cell_id, {
        text: update.text,
        sourceId: update.source_id,
        targetId: update.target_id,
        style: update.style,
      });

      if ("error" in result) {
        return {
          success: false,
          cell_id: update.cell_id,
          error: result.error,
        };
      }

      return {
        success: true,
        cell_id: update.cell_id,
        cell: result,
      };
    });
  }

  /**
   * Validate edge placement conventions and return warnings for:
   * 1. Backwards flow (leftward or upward edges)
   * 2. External-to-child targeting (edge from outside a group targets a child instead of the group cell)
   *
   * Returns an empty array when there are no violations.
   */
  validateEdgeConventions(edgeId: string): string[] {
    const edge = this.cells.get(edgeId);
    if (!edge || edge.type !== "edge" || !edge.sourceId || !edge.targetId) {
      return [];
    }

    const warnings: string[] = [];

    // 1. Backwards flow detection
    const sourceCenter = this.getCellCenter(edge.sourceId);
    const targetCenter = this.getCellCenter(edge.targetId);
    if (sourceCenter && targetCenter) {
      const dx = targetCenter.x - sourceCenter.x;
      const dy = targetCenter.y - sourceCenter.y;

      if (dx < -10) {
        warnings.push(
          `Edge flows leftward (source x=${Math.round(sourceCenter.x)}, target x=${Math.round(targetCenter.x)}). ` +
            `Prefer left-to-right flow — reposition the target to the right of the source.`,
        );
      } else if (Math.abs(dx) <= 10 && dy < -10) {
        warnings.push(
          `Edge flows upward (source y=${Math.round(sourceCenter.y)}, target y=${Math.round(targetCenter.y)}). ` +
            `Prefer top-to-bottom secondary flow — reposition the target below the source.`,
        );
      }
    }

    // 2. Group targeting: external source → child of group should target the group cell
    const targetCell = this.cells.get(edge.targetId);
    if (targetCell && targetCell.parent) {
      const parentCell = this.cells.get(targetCell.parent);
      if (
        parentCell &&
        parentCell.isGroup &&
        !this.isCellInsideGroup(edge.sourceId, parentCell.id)
      ) {
        warnings.push(
          `Edge from outside group '${parentCell.value || parentCell.id}' targets child ` +
            `'${targetCell.value || edge.targetId}' directly. Target the group cell '${parentCell.id}' instead — ` +
            `each external source should have exactly one edge entering the group.`,
        );
      }
    }

    return warnings;
  }

  /**
   * Batch edit multiple cells in a single operation.
   */
  batchEditCells(
    updates: Array<{
      cell_id: string;
      text?: string;
      x?: number;
      y?: number;
      width?: number;
      height?: number;
      style?: string;
    }>,
  ): Array<{
    success: boolean;
    cell?: Cell;
    error?: StructuredError;
    cell_id: string;
  }> {
    return updates.map((update) => {
      const result = this.editCell(update.cell_id, {
        text: update.text,
        x: update.x,
        y: update.y,
        width: update.width,
        height: update.height,
        style: update.style,
      });

      if ("error" in result) {
        return {
          success: false,
          cell_id: update.cell_id,
          error: result.error,
        };
      }

      return {
        success: true,
        cell_id: update.cell_id,
        cell: result,
      };
    });
  }
}
