"use client";

import { create } from "zustand";
import type { Edge, Node, OnNodesChange, OnEdgesChange, OnConnect } from "@xyflow/react";
import { applyNodeChanges, applyEdgeChanges, addEdge } from "@xyflow/react";
import { hydrateBoundaryFixFlags } from "@/lib/boundaryFix";
import { defaultParams } from "@/lib/componentMeta";
import {
  CONTROL_NODE_TYPE,
  canonicalizeParams,
  isAutoCalibrationControlId,
} from "@/lib/configBuilder";
import type { ControlConfig } from "@/lib/configBuilder";
import type { SimulationResult } from "@/lib/api";
import {
  circuitFromEdge,
  planEdgeInsert,
  validatePipeOnEdge,
} from "@/lib/edgeInsert";
import { nodeSize } from "@/lib/componentMeta";
import { normaliseRotation as normaliseRotationDeg } from "@/lib/orientation";

export interface EntropykNodeData {
  type: string;
  name: string;
  circuit: number;
  /** Icon rotation in degrees (0/90/180/270), Dymola-style. */
  rotation: number;
  /** Mirror about vertical axis (Modelica flip horizontal). */
  flipH: boolean;
  /** Mirror about horizontal axis (Modelica flip vertical). */
  flipV: boolean;
  params: Record<string, number | string | boolean>;
  [key: string]: unknown;
}

type EntropykNode = Node<EntropykNodeData>;
type PrimitiveParam = number | string | boolean;
type ImportedComponent = Record<string, unknown> & { type: string; name: string };
type ImportedCircuit = {
  id: number;
  components: ImportedComponent[];
  edges?: Array<{ from: string; to: string }>;
};
type ImportedSubsystem = {
  params?: Record<string, PrimitiveParam>;
  components: ImportedComponent[];
  edges?: Array<{ from: string; to: string }>;
  ports?: Record<string, string>;
};
type ImportedInstance = {
  of: string;
  name: string;
  circuit?: number;
  params?: Record<string, PrimitiveParam>;
};
type ImportedScenario = {
  fluid?: string;
  fluid_backend?: string;
  controls?: ControlConfig[];
  solver?: {
    strategy?: string;
    max_iterations?: number;
    tolerance?: number;
  };
  circuits?: ImportedCircuit[];
  subsystems?: Record<string, ImportedSubsystem>;
  instances?: ImportedInstance[];
  connections?: Array<{ from: string; to: string }>;
};

/** Diagram grid pitch in px (Dymola snaps icons to a fixed grid). */
export const GRID_SIZE = 12;

/** Snap a single coordinate to the diagram grid. */
export function snapToGridValue(value: number, grid: number = GRID_SIZE): number {
  return Math.round(value / grid) * grid;
}

/** Snap an {x, y} position to the diagram grid. */
export function snapPosition(
  position: { x: number; y: number },
  grid: number = GRID_SIZE,
): { x: number; y: number } {
  return { x: snapToGridValue(position.x, grid), y: snapToGridValue(position.y, grid) };
}

/** Normalise any angle to one of 0/90/180/270. */
export function normaliseRotation(deg: number): number {
  return normaliseRotationDeg(deg);
}

interface DiagramState {
  nodes: EntropykNode[];
  edges: Edge[];
  selectedNodeId: string | null;
  fluid: string;
  fluidBackend: string;
  solverStrategy: string;
  maxIterations: number;
  tolerance: number;
  controls: ControlConfig[];
  snapToGrid: boolean;
  /** Pinned sweep axes (format "componentName.paramKey") pushed from the properties panel. */
  pinnedSweepPaths: string[];
  result: SimulationResult | null;
  lastConfig: unknown | null;
  simulating: boolean;
  simError: string | null;

  // Undo / redo history
  history: Array<{ nodes: EntropykNode[]; edges: Edge[]; selectedNodeId: string | null }>;
  historyIndex: number;
  canUndo: boolean;
  canRedo: boolean;

  onNodesChange: OnNodesChange;
  onEdgesChange: OnEdgesChange;
  onConnect: OnConnect;
  addComponent: (type: string, position: { x: number; y: number }, extraParams?: Record<string, string | number | boolean>) => void;
  /** Insert a 2-port component into an existing edge (A→B → A→comp→B). */
  insertOnEdge: (
    type: string,
    position: { x: number; y: number },
    edgeId: string,
  ) => { ok: boolean; reason?: string };
  updateNodeParams: (id: string, params: Record<string, number | string | boolean>) => void;
  /** Apply param patches to several nodes in one atomic set() (Modelica Fixed pairing). */
  updateNodesParams: (
    patches: Map<string, Record<string, number | string | boolean>>,
  ) => void;
  updateNodeName: (id: string, name: string) => void;
  updateNodeCircuit: (id: string, circuit: number) => void;
  rotateNode: (id: string, dir?: 1 | -1) => void;
  /** Modelica flip about vertical axis. */
  flipNodeH: (id: string) => void;
  /** Modelica flip about horizontal axis. */
  flipNodeV: (id: string) => void;
  setSelected: (id: string | null) => void;
  removeNode: (id: string) => void;
  setFluid: (fluid: string) => void;
  setFluidBackend: (backend: string) => void;
  setSolverStrategy: (strategy: string) => void;
  setMaxIterations: (maxIterations: number) => void;
  setTolerance: (tolerance: number) => void;
  setControls: (controls: ControlConfig[]) => void;
  toggleSnapToGrid: () => void;
  togglePinnedSweepPath: (path: string) => void;
  clearPinnedSweepPaths: () => void;
  setLastConfig: (config: unknown | null) => void;
  setResult: (result: SimulationResult | null, error?: string | null) => void;
  setSimulating: (v: boolean) => void;
  loadFromConfig: (config: unknown) => void;
  /** Remove leftover canvas nodes from Fixed/Free auto-calib (not real controllers). */
  purgeAutoCalibrationControlNodes: () => void;
  clear: () => void;
  undo: () => void;
  redo: () => void;
  recordHistory: () => void;
}

let nameCounter: Record<string, number> = {};

function uniqueName(type: string): string {
  nameCounter[type] = (nameCounter[type] ?? 0) + 1;
  const base = type.toLowerCase().replace(/[^a-z0-9]/g, "_").slice(0, 8);
  return `${base}_${nameCounter[type]}`;
}

function substituteTemplateValue(value: unknown, params: Record<string, PrimitiveParam>): unknown {
  if (typeof value === "string" && value.startsWith("$")) {
    return params[value.slice(1)] ?? value;
  }
  return value;
}

function substituteComponent(
  component: ImportedComponent,
  params: Record<string, PrimitiveParam>,
  prefix?: string,
): ImportedComponent {
  const next: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(component)) {
    next[key] = substituteTemplateValue(value, params);
  }
  next.name = prefix ? `${prefix}.${component.name}` : component.name;
  return next as ImportedComponent;
}

function prefixEdgeRef(ref: string, prefix: string): string {
  const [component, port] = ref.split(":");
  return port ? `${prefix}.${component}:${port}` : `${prefix}.${component}`;
}

function resolveInstancePortRef(
  ref: string,
  instances: Map<string, ImportedInstance>,
  subsystems: Record<string, ImportedSubsystem>,
): { circuit: number; ref: string } | null {
  const [instanceName, portName] = ref.split(".");
  if (!instanceName || !portName) return null;

  const instance = instances.get(instanceName);
  if (!instance) return null;

  const subsystem = subsystems[instance.of];
  const mapped = subsystem?.ports?.[portName];
  if (!mapped) return null;

  return {
    circuit: instance.circuit ?? 0,
    ref: prefixEdgeRef(mapped, instance.name),
  };
}

function flattenImportedScenario(cfg: ImportedScenario): ImportedScenario {
  if (!cfg.subsystems || !cfg.instances?.length) return cfg;

  const circuits = new Map<number, ImportedCircuit>(
    (cfg.circuits ?? []).map((circuit) => [
      circuit.id,
      {
        ...circuit,
        components: [...circuit.components],
        edges: [...(circuit.edges ?? [])],
      },
    ]),
  );
  const instancesByName = new Map(cfg.instances.map((instance) => [instance.name, instance]));

  for (const instance of cfg.instances) {
    const subsystem = cfg.subsystems[instance.of];
    if (!subsystem) continue;

    const circuitId = instance.circuit ?? 0;
    const circuit = circuits.get(circuitId) ?? { id: circuitId, components: [], edges: [] };
    const params = { ...(subsystem.params ?? {}), ...(instance.params ?? {}) };

    circuit.components.push(
      ...subsystem.components.map((component) => substituteComponent(component, params, instance.name)),
    );
    circuit.edges?.push(
      ...(subsystem.edges ?? []).map((edge) => ({
        from: prefixEdgeRef(edge.from, instance.name),
        to: prefixEdgeRef(edge.to, instance.name),
      })),
    );

    circuits.set(circuitId, circuit);
  }

  for (const connection of cfg.connections ?? []) {
    const from = resolveInstancePortRef(connection.from, instancesByName, cfg.subsystems);
    const to = resolveInstancePortRef(connection.to, instancesByName, cfg.subsystems);
    if (!from || !to || from.circuit !== to.circuit) continue;

    const circuit = circuits.get(from.circuit) ?? { id: from.circuit, components: [], edges: [] };
    circuit.edges?.push({ from: from.ref, to: to.ref });
    circuits.set(from.circuit, circuit);
  }

  return {
    ...cfg,
    circuits: Array.from(circuits.values()).sort((a, b) => a.id - b.id),
  };
}

function controlParams(control: ControlConfig): Record<string, PrimitiveParam> {
  return {
    measure_component: control.measure.component,
    measure_output: control.measure.output,
    actuator_component: control.actuator.component,
    actuator_factor: control.actuator.factor,
    initial: control.actuator.initial ?? 0.15,
    min: control.actuator.min,
    max: control.actuator.max,
    target: control.target,
    gain: control.gain ?? -0.5,
    band: control.band ?? 5.0,
    ...(control.smooth_eps !== undefined ? { smooth_eps: control.smooth_eps } : {}),
    ...(control.alpha !== undefined ? { alpha: control.alpha } : {}),
    objectives_json: JSON.stringify(control.objectives ?? []),
  };
}

function isCompressorNode(node: EntropykNode): boolean {
  return node.data.type.includes("Compressor");
}

function isCondenserNode(node: EntropykNode): boolean {
  const text = `${node.data.type} ${node.data.name}`.toLowerCase();
  return text.includes("condenser") || /\bcond\b/.test(text);
}

function isEvaporatorNode(node: EntropykNode): boolean {
  const text = `${node.data.type} ${node.data.name}`.toLowerCase();
  return text.includes("evaporator") || /\bevap\b/.test(text);
}

function isExpansionNode(node: EntropykNode): boolean {
  const text = `${node.data.type} ${node.data.name}`.toLowerCase();
  return node.data.type.includes("Valve") || text.includes("exv") || text.includes("expansion");
}

function isGenericHeatExchangerNode(node: EntropykNode): boolean {
  return node.data.type === "HeatExchanger";
}

function isControllerNode(node: EntropykNode): boolean {
  return node.data.type === CONTROL_NODE_TYPE;
}

function isSecondaryBoundaryNode(node: EntropykNode): boolean {
  return node.data.type.startsWith("Brine") || node.data.type.startsWith("Air");
}

function connectedPortOn(nodeId: string, edge: Edge): string {
  if (edge.source === nodeId) return String(edge.sourceHandle ?? "");
  if (edge.target === nodeId) return String(edge.targetHandle ?? "");
  return "";
}

function oppositeNodeId(nodeId: string, edge: Edge): string | null {
  if (edge.source === nodeId) return edge.target;
  if (edge.target === nodeId) return edge.source;
  return null;
}

function sortedByName(nodes: EntropykNode[]): EntropykNode[] {
  return [...nodes].sort((a, b) => a.data.name.localeCompare(b.data.name));
}

function firstByFamily(nodes: EntropykNode[], predicate: (node: EntropykNode) => boolean): EntropykNode | undefined {
  return sortedByName(nodes.filter(predicate))[0];
}

function layoutImportedNodes(nodes: EntropykNode[], edges: Edge[]): EntropykNode[] {
  if (nodes.length === 0) return nodes;

  const nodeById = new Map(nodes.map((node) => [node.id, node]));
  const positions = new Map<string, { x: number; y: number }>();
  const byCircuit = new Map<number, EntropykNode[]>();
  for (const node of nodes) {
    const circuit = node.data.circuit ?? 0;
    byCircuit.set(circuit, [...(byCircuit.get(circuit) ?? []), node]);
  }

  const secondaryCounts = new Map<string, number>();
  const fallbackCounts = new Map<number, number>();

  for (const [layoutIndex, [circuitId, circuitNodes]] of [...byCircuit.entries()].sort(([a], [b]) => a - b).entries()) {
    const xOffset = layoutIndex * 1100;
    const yOffset = layoutIndex * 40;
    const evaporator = firstByFamily(circuitNodes, isEvaporatorNode);
    const compressor = firstByFamily(circuitNodes, isCompressorNode);
    const condenser = firstByFamily(circuitNodes, isCondenserNode);
    const expansion = firstByFamily(circuitNodes, isExpansionNode);
    const genericHeatExchangers = sortedByName(circuitNodes.filter(isGenericHeatExchangerNode));

    if (evaporator) positions.set(evaporator.id, { x: 420 + xOffset, y: 300 + yOffset });
    if (compressor) positions.set(compressor.id, { x: 720 + xOffset, y: 270 + yOffset });
    if (condenser) positions.set(condenser.id, { x: 1040 + xOffset, y: 300 + yOffset });
    if (expansion) positions.set(expansion.id, { x: 1260 + xOffset, y: 450 + yOffset });
    genericHeatExchangers.forEach((node, index) => {
      if (!positions.has(node.id)) {
        positions.set(node.id, { x: 420 + xOffset + index * 260, y: 300 + yOffset });
      }
    });

    for (const controller of sortedByName(circuitNodes.filter(isControllerNode))) {
      const measuredName = String(controller.data.params.measure_component ?? controller.data.params.actuator_component ?? "");
      const anchor =
        circuitNodes.find((node) => node.data.name === measuredName) ??
        compressor ??
        evaporator ??
        condenser;
      const anchorPosition = anchor ? positions.get(anchor.id) ?? anchor.position : { x: 600 + xOffset, y: 220 + yOffset };
      positions.set(controller.id, { x: anchorPosition.x - 120, y: anchorPosition.y - 150 });
    }

    for (const boundary of sortedByName(circuitNodes.filter(isSecondaryBoundaryNode))) {
      const link = edges
        .filter((edge) => edge.source === boundary.id || edge.target === boundary.id)
        .map((edge) => {
          const otherId = oppositeNodeId(boundary.id, edge);
          const other = otherId ? nodeById.get(otherId) : undefined;
          return other ? { edge, other } : null;
        })
        .find((entry): entry is { edge: Edge; other: EntropykNode } => !!entry);
      if (!link) continue;

      const anchorPosition = positions.get(link.other.id);
      if (!anchorPosition) continue;

      const exchangerPort = connectedPortOn(link.other.id, link.edge);
      const key = `${link.other.id}:${exchangerPort || "secondary"}`;
      const count = secondaryCounts.get(key) ?? 0;
      secondaryCounts.set(key, count + 1);

      const isOutlet = exchangerPort.includes("outlet");
      const vertical = isOutlet ? -150 : 150;
      const horizontal =
        link.other === condenser
          ? isOutlet ? -40 : -70
          : isOutlet ? 120 : -40;
      positions.set(boundary.id, {
        x: anchorPosition.x + horizontal + count * 84,
        y: anchorPosition.y + vertical,
      });
    }

    for (const node of sortedByName(circuitNodes)) {
      if (positions.has(node.id)) continue;
      const count = fallbackCounts.get(circuitId) ?? 0;
      fallbackCounts.set(circuitId, count + 1);
      positions.set(node.id, {
        x: 120 + count * 170 + xOffset,
        y: 120 + yOffset + (count % 2) * 140,
      });
    }
  }

  return nodes.map((node) => ({
    ...node,
    position: snapPosition(positions.get(node.id) ?? node.position),
  }));
}

export const useDiagramStore = create<DiagramState>((set, get) => ({
  nodes: [],
  edges: [],
  selectedNodeId: null,
  fluid: "R410A",
  fluidBackend: "CoolProp",
  solverStrategy: "newton",
  maxIterations: 300,
  tolerance: 1e-6,
  controls: [],
  snapToGrid: true,
  pinnedSweepPaths: [],
  result: null,
  lastConfig: null,
  simulating: false,
  simError: null,
  history: [{ nodes: [], edges: [], selectedNodeId: null }],
  historyIndex: 0,
  canUndo: false,
  canRedo: false,

  recordHistory: () => {
    const { nodes, edges, selectedNodeId, history, historyIndex } = get();
    const snapshot = {
      nodes: structuredClone(nodes),
      edges: structuredClone(edges),
      selectedNodeId,
    };
    const nextHistory = history.slice(0, historyIndex + 1);
    nextHistory.push(snapshot);
    // Keep history bounded to avoid memory bloat.
    if (nextHistory.length > 200) nextHistory.shift();
    const nextIndex = nextHistory.length - 1;
    set({
      history: nextHistory,
      historyIndex: nextIndex,
      canUndo: nextIndex > 0,
      canRedo: false,
    });
  },

  undo: () => {
    const { historyIndex, history } = get();
    if (historyIndex <= 0) return;
    const nextIndex = historyIndex - 1;
    const snapshot = history[nextIndex];
    set({
      nodes: structuredClone(snapshot.nodes),
      edges: structuredClone(snapshot.edges),
      selectedNodeId: snapshot.selectedNodeId,
      historyIndex: nextIndex,
      canUndo: nextIndex > 0,
      canRedo: true,
    });
  },

  redo: () => {
    const { historyIndex, history } = get();
    if (historyIndex >= history.length - 1) return;
    const nextIndex = historyIndex + 1;
    const snapshot = history[nextIndex];
    set({
      nodes: structuredClone(snapshot.nodes),
      edges: structuredClone(snapshot.edges),
      selectedNodeId: snapshot.selectedNodeId,
      historyIndex: nextIndex,
      canUndo: true,
      canRedo: nextIndex < history.length - 1,
    });
  },

  onNodesChange: (changes) => {
    const removed = changes.filter((c) => c.type === "remove").map((c) => c.id);
    const sel = get().selectedNodeId;
    set({
      nodes: applyNodeChanges(changes, get().nodes) as EntropykNode[],
      selectedNodeId: sel && removed.includes(sel) ? null : sel,
    });
  },

  onEdgesChange: (changes) =>
    set({ edges: applyEdgeChanges(changes, get().edges) }),

  onConnect: (connection) => {
    get().recordHistory();
    set({
      edges: addEdge(
        { ...connection, animated: false, type: "smoothstep" },
        get().edges,
      ),
    });
  },

  addComponent: (type, position, extraParams) => {
    get().recordHistory();
    const id = crypto.randomUUID();
    const pos = get().snapToGrid ? snapPosition(position) : position;
    const node: EntropykNode = {
      id,
      type: "entropykNode",
      position: pos,
      data: {
        type,
        name: uniqueName(type === "ModuleInstance" && extraParams?.module_name ? String(extraParams.module_name) : type),
        circuit: 0,
        rotation: 0,
        flipH: false,
        flipV: false,
        params: { ...defaultParams(type), ...(extraParams ?? {}) },
      },
    };
    set({ nodes: [...get().nodes, node], selectedNodeId: id });
  },

  insertOnEdge: (type, position, edgeId) => {
    const { nodes, edges, snapToGrid } = get();
    const edge = edges.find((e) => e.id === edgeId);
    if (!edge) return { ok: false, reason: "Ligne introuvable." };

    const params = defaultParams(type);
    const check = validatePipeOnEdge(type, params, edge, nodes as EntropykNode[]);
    if (!check.ok) return check;

    const src = nodes.find((n) => n.id === edge.source);
    const tgt = nodes.find((n) => n.id === edge.target);
    if (!src || !tgt) return { ok: false, reason: "Extrémités de ligne manquantes." };

    const srcSize = nodeSize(src.data.type);
    const tgtSize = nodeSize(tgt.data.type);
    const sourceCenter = {
      x: src.position.x + srcSize.w / 2,
      y: src.position.y + srcSize.h / 2,
    };
    const targetCenter = {
      x: tgt.position.x + tgtSize.w / 2,
      y: tgt.position.y + tgtSize.h / 2,
    };

    const id = crypto.randomUUID();
    const name = uniqueName(type);
    const pos = snapToGrid ? snapPosition(position) : position;
    const plan = planEdgeInsert({
      type,
      id,
      name,
      position: pos,
      params,
      edge,
      circuit: circuitFromEdge(edge, nodes as EntropykNode[]),
      sourceCenter,
      targetCenter,
    });

    if (snapToGrid) {
      plan.node.position = snapPosition(plan.node.position);
    }

    get().recordHistory();
    set({
      nodes: [...nodes, plan.node as EntropykNode],
      edges: [
        ...edges.filter((e) => e.id !== plan.edgeIdToRemove),
        ...plan.edgesToAdd,
      ],
      selectedNodeId: id,
    });
    return { ok: true };
  },

  updateNodeParams: (id, params) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) =>
        n.id === id ? { ...n, data: { ...n.data, params: { ...n.data.params, ...params } } } : n,
      ),
    });
  },

  updateNodesParams: (patches) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) => {
        const patch = patches.get(n.id);
        if (!patch) return n;
        return { ...n, data: { ...n.data, params: { ...n.data.params, ...patch } } };
      }),
    });
  },

  updateNodeName: (id, name) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) => (n.id === id ? { ...n, data: { ...n.data, name } } : n)),
    });
  },

  updateNodeCircuit: (id, circuit) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) => (n.id === id ? { ...n, data: { ...n.data, circuit } } : n)),
    });
  },

  rotateNode: (id, dir = 1) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) =>
        n.id === id
          ? {
              ...n,
              data: {
                ...n.data,
                rotation: normaliseRotation((n.data.rotation ?? 0) + dir * 90),
              },
            }
          : n,
      ),
    });
  },

  flipNodeH: (id) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) =>
        n.id === id ? { ...n, data: { ...n.data, flipH: !n.data.flipH } } : n,
      ),
    });
  },

  flipNodeV: (id) => {
    get().recordHistory();
    set({
      nodes: get().nodes.map((n) =>
        n.id === id ? { ...n, data: { ...n.data, flipV: !n.data.flipV } } : n,
      ),
    });
  },

  setSelected: (id) => {
    if (get().selectedNodeId === id) return;
    set({ selectedNodeId: id });
  },

  removeNode: (id) => {
    get().recordHistory();
    set({
      nodes: get().nodes.filter((n) => n.id !== id),
      edges: get().edges.filter((e) => e.source !== id && e.target !== id),
      selectedNodeId: get().selectedNodeId === id ? null : get().selectedNodeId,
    });
  },

  setFluid: (fluid) => set({ fluid }),
  setFluidBackend: (backend) => set({ fluidBackend: backend }),
  setSolverStrategy: (strategy) => set({ solverStrategy: strategy }),
  setMaxIterations: (maxIterations) => set({ maxIterations }),
  setTolerance: (tolerance) => set({ tolerance }),
  setControls: (controls) => set({ controls }),
  toggleSnapToGrid: () => set({ snapToGrid: !get().snapToGrid }),
  togglePinnedSweepPath: (path) =>
    set((s) => ({
      pinnedSweepPaths: s.pinnedSweepPaths.includes(path)
        ? s.pinnedSweepPaths.filter((p) => p !== path)
        : [...s.pinnedSweepPaths, path],
    })),
  clearPinnedSweepPaths: () => set({ pinnedSweepPaths: [] }),

  setLastConfig: (config) => set({ lastConfig: config }),
  setResult: (result, error = null) => set({ result, simError: error }),
  setSimulating: (v) => set({ simulating: v }),

  loadFromConfig: (config) => {
    const cfg = flattenImportedScenario(config as ImportedScenario);
    if (!cfg?.circuits) return;

    const nodes: EntropykNode[] = [];
    let x = 100;
    const nameToId = new Map<string, string>();

    for (const circuit of cfg.circuits) {
      let y = 100;
      for (const comp of circuit.components) {
        const id = crypto.randomUUID();
        nameToId.set(`${circuit.id}:${comp.name}`, id);
        const { type, name, ...rest } = comp;
        // Keep only primitive params.
        let params: Record<string, PrimitiveParam> = {};
        for (const [k, v] of Object.entries(rest)) {
          if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") {
            params[k] = v;
          }
        }
        params = canonicalizeParams(type as string, params);
        params = hydrateBoundaryFixFlags(type as string, params);
        // Merge meta defaults without clobbering imported Fixed flags / setpoints.
        const defaults = defaultParams(type as string);
        params = { ...defaults, ...params };
        nodes.push({
          id,
          type: "entropykNode",
          position: { x, y },
          data: {
            type: type as string,
            name: name as string,
            circuit: circuit.id,
            rotation: 0,
            flipH: false,
            flipV: false,
            params,
          },
        });
        y += 120;
      }
      x += 280;
    }

    // Explicit Advanced controllers only. Fixed/Free calib (`calib_*`) is a
    // solver DoF swap regenerated from Probe/component Fixed flags — never a
    // canvas SaturatedController block.
    let explicitControlIndex = 0;
    for (const control of cfg.controls ?? []) {
      if (isAutoCalibrationControlId(control.id)) continue;
      const measuredNode = nodes.find((node) => node.data.name === control.measure.component);
      const id = crypto.randomUUID();
      nodes.push({
        id,
        type: "entropykNode",
        position: measuredNode
          ? { x: measuredNode.position.x + 140, y: Math.max(40, measuredNode.position.y - 72) }
          : { x: 100 + explicitControlIndex * 150, y: 40 },
        data: {
          type: CONTROL_NODE_TYPE,
          name: control.id,
          circuit: measuredNode?.data.circuit ?? 0,
          rotation: 0,
          flipH: false,
          flipV: false,
          params: controlParams(control),
        },
      });
      explicitControlIndex += 1;
    }

    const edges: Edge[] = [];
    for (const circuit of cfg.circuits) {
      circuit.edges?.forEach((e, i) => {
        const [fromName, fromPort] = e.from.split(":");
        const [toName, toPort] = e.to.split(":");
        const sId = nameToId.get(`${circuit.id}:${fromName}`);
        const tId = nameToId.get(`${circuit.id}:${toName}`);
        if (sId && tId) {
          edges.push({
            id: `e${circuit.id}-${i}`,
            source: sId,
            target: tId,
            sourceHandle: fromPort,
            targetHandle: toPort,
            animated: true,
          });
        }
      });
    }

    set({
      nodes: layoutImportedNodes(nodes, edges),
      edges,
      fluid: cfg.fluid || "R410A",
      fluidBackend: cfg.fluid_backend || "CoolProp",
      solverStrategy: cfg.solver?.strategy || "newton",
      maxIterations: cfg.solver?.max_iterations ?? 300,
      tolerance: cfg.solver?.tolerance ?? 1e-6,
      controls: (cfg.controls ?? []).filter((c) => !isAutoCalibrationControlId(c.id)),
      result: null,
      lastConfig: cfg,
      simError: null,
      selectedNodeId: null,
    });
    get().recordHistory();
  },

  purgeAutoCalibrationControlNodes: () => {
    const { nodes, edges, selectedNodeId } = get();
    const removeIds = new Set(
      nodes
        .filter(
          (n) =>
            n.data.type === CONTROL_NODE_TYPE &&
            isAutoCalibrationControlId(n.data.name),
        )
        .map((n) => n.id),
    );
    if (removeIds.size === 0) return;
    set({
      nodes: nodes.filter((n) => !removeIds.has(n.id)),
      edges: edges.filter((e) => !removeIds.has(e.source) && !removeIds.has(e.target)),
      selectedNodeId:
        selectedNodeId && removeIds.has(selectedNodeId) ? null : selectedNodeId,
      controls: get().controls.filter((c) => !isAutoCalibrationControlId(c.id)),
    });
    get().recordHistory();
  },

  clear: () => {
    const empty = { nodes: [], edges: [], selectedNodeId: null };
    set({
      nodes: [],
      edges: [],
      controls: [],
      result: null,
      lastConfig: null,
      simError: null,
      selectedNodeId: null,
      history: [empty],
      historyIndex: 0,
      canUndo: false,
      canRedo: false,
    });
  },
}));
