/**
 * Entropyk component catalogue.
 *
 * Mirrors the backend `/api/components` endpoint. Each entry defines:
 *  - type:        CLI JSON "type" string (matches crates/cli/src/run.rs create_component)
 *  - label:       human-readable name shown in the palette and node
 *  - category:    palette grouping
 *  - ports:       ordered port names (inlet/outlet/suction/discharge/...)
 *                  — used to place React Flow handles.
 *  - color:       hex colour for the node header
 *  - params:      editable parameters with type, unit, default, required flag
 */

export type ParamKind = "number" | "string" | "boolean";

export interface ParamMeta {
  key: string;
  label: string;
  kind: ParamKind;
  unit?: string;
  default?: number | string | boolean;
  required?: boolean;
  section?: string;
  description?: string;
  min?: number;
  max?: number;
  step?: number;
  options?: Array<{ value: string | number; label: string }>;
  advanced?: boolean;
  /**
   * Modelica `fixed` attribute (Dymola-style checkbox).
   * Fixed ON  = `parameter` / binding equation (known).
   * Fixed OFF = unknown — needs another equation to keep the model balanced.
   */
  fixable?: boolean;
  /** Default for the Fixed checkbox when `fixable` (true = Fixed). */
  defaultFixed?: boolean;
  /**
   * When Fixed ON on a Probe: adds equation `output = value`
   * (e.g. Tsat = 5 °C). Internal wire name for the solver output.
   */
  measureOutput?: string;
  /**
   * When Fixed OFF: this factor becomes a Newton unknown (Modelica
   * `parameter ...(fixed=false)`). Pair with a Fixed Probe equation.
   */
  actuatorFactor?: string;
  /** Bounds / start guess when the factor is an unknown. */
  freeMin?: number;
  freeMax?: number;
  /**
   * Whether this param can be picked as a Multi-run sweep axis.
   * Source of truth for sweepability — replaces the old hardcoded whitelist.
   */
  sweepable?: boolean;
  /** Multi-run optgroup for the param (only meaningful when `sweepable`). */
  sweepGroup?: "boundaries" | "thermal" | "machine" | "global";
}

/** Internal params key prefix for Fixed flags (not sent as component fields). */
export const FIXED_FLAG_PREFIX = "__fixed_";

export function fixedFlagKey(paramKey: string): string {
  return `${FIXED_FLAG_PREFIX}${paramKey}`;
}

/** Whether a param is Fixed (default = defaultFixed ?? true). */
export function isParamFixed(
  params: Record<string, number | string | boolean | undefined>,
  meta: ParamMeta,
): boolean {
  if (!meta.fixable) return true;
  const flag = params[fixedFlagKey(meta.key)];
  if (flag === true || flag === "true") return true;
  if (flag === false || flag === "false") return false;
  return meta.defaultFixed !== false;
}

export interface ComponentMeta {
  type: string;
  label: string;
  category: string;
  /** Short purpose shown under the component title (plain language). */
  description: string;
  /**
   * Contextual help (French) shown in the Help panel.
   * Explain when to use it, main ports, and typical Fixed/Free usage.
   */
  help?: string;
  /** Doc file under docs/components/ (without path), e.g. "flooded-evaporator". */
  docSlug?: string;
  ports: string[];
  color: string;
  params: ParamMeta[];
}

export function genericMeta(type?: string): ComponentMeta {
  const cleanType = String(type || "GenericComponent");
  return {
    type: cleanType,
    label: cleanType,
    category: "Generic",
    description: `Composant thermodynamique (${cleanType})`,
    ports: ["inlet", "outlet"],
    color: "#64748b",
    params: [],
  };
}

/**
 * Port side on the node: "inlet"-like ports go on the left, "outlet"-like
 * ports on the right. Economizer / liquid_outlet sit on the top/bottom.
 */
export function portSide(port: string): "left" | "right" | "top" | "bottom" {
  const p = String(port || "");
  if (p === "secondary_inlet") return "bottom";
  if (p === "secondary_outlet") return "top";
  if (p === "hot_inlet" || p === "cold_inlet") return "left";
  if (p === "hot_outlet" || p === "cold_outlet") return "right";
  if (p.startsWith("outlet")) return "right";
  if (p === "discharge") return "right";
  if (p === "liquid_outlet" || p === "vapor_outlet") return "bottom";
  if (p.startsWith("inlet")) return "left";
  if (p === "suction") return "left";
  if (p === "economizer") return "top";
  return "left";
}

/** True for the secondary (heat-transfer fluid / caloporteur) ports of an exchanger. */
export function isSecondaryPort(port: string): boolean {
  const p = String(port || "");
  return p === "secondary_inlet" || p === "secondary_outlet";
}

/**
 * Whether a port emits flow (source) or receives it (target).
 * Flow in a P&ID is directional: a connection is dragged from a source
 * (outlet/discharge) to a target (inlet/suction).
 */
export function portRole(port: string): "source" | "target" {
  const p = String(port || "");
  if (
    p.startsWith("outlet") ||
    p === "discharge" ||
    p === "liquid_outlet" ||
    p === "vapor_outlet" ||
    p.endsWith("_outlet")
  ) {
    return "source";
  }
  return "target";
}

/**
 * Per-family node footprint (px), Dymola-style: a heat exchanger is a wide
 * vessel, a separator is a tall column, a pipe is a long thin run, a valve is
 * compact. Size encodes what the part is, not just decoration.
 */
export function nodeSize(type: string): { w: number; h: number } {
  const t = String(type || "");
  if (t === "Probe") return { w: 36, h: 36 };
  if (t === "SaturatedController") return { w: 184, h: 96 };
  if (t.includes("Compressor")) return { w: 76, h: 76 };
  if (t === "HeatExchanger") return { w: 92, h: 92 };
  // Flooded is a horizontal drum; plate a tall pack; coils wide batteries.
  if (t.includes("Flooded")) return { w: 128, h: 92 };
  if (t.includes("Bphx")) return { w: 80, h: 110 };
  if (t.includes("FinCoil") || t.includes("AirCooled") || t.includes("Mchx"))
    return { w: 126, h: 88 };
  if (t.includes("Condenser") || t.includes("Evaporator")) return { w: 126, h: 88 };
  if (t.includes("Valve")) return { w: 56, h: 56 };
  if (t === "Pump" || t === "Fan") return { w: 72, h: 72 };
  if (t === "Pipe" || t.includes("Pipe") || t === "AirDuct") return { w: 118, h: 42 };
  if (t === "Drum") return { w: 58, h: 108 };
  if (t === "FlowSplitter" || t === "FlowMerger") return { w: 72, h: 64 };
  if (t.endsWith("Source") || t.endsWith("Sink")) return { w: 62, h: 56 };
  return { w: 88, h: 58 };
}

/** Short label shown next to a handle on the node. */
export function portLabel(port: string): string {
  const map: Record<string, string> = {
    inlet: "in",
    outlet: "out",
    suction: "suc",
    discharge: "dis",
    economizer: "eco",
    liquid_outlet: "liq",
    vapor_outlet: "vap",
    outlet_a: "A",
    outlet_b: "B",
    inlet_a: "A",
    inlet_b: "B",
    hot_inlet: "h·in",
    hot_outlet: "h·out",
    cold_inlet: "c·in",
    cold_outlet: "c·out",
    secondary_inlet: "htf·in",
    secondary_outlet: "htf·out",
  };
  return map[port] ?? port;
}

export const COMPONENT_CATEGORIES = [
  "Compressors",
  "Heat Exchangers",
  "Expansion",
  "Piping",
  "Hydraulics",
  "Boundaries",
  "Instrumentation",
  "Advanced",
  "Generic",
] as const;

export const COMPONENTS: ComponentMeta[] = [
  // ── Instrumentation: measurement taps for calibration (Probe = all measurements) ──
  {
    type: "Probe",
    label: "Probe (sonde)",
    category: "Instrumentation",
    description:
      "Sonde sur une ligne : fige T, Tsat, P, X ou Tsh (= T−Tsat, SC négatif) pour calibrer.",
    help:
      "Sonde de calibration (Probe)\n" +
      "1. Glisse le Probe sur un fil.\n" +
      "2. Coche Fixed sur les grandeurs physiques mesurées (T, Tsat, P, X, Tsh).\n" +
      "3. Libère le facteur correspondant sur le composant :\n" +
      "   • Tsat / Tsh → z_ua (HX) ou opening (EXV)\n" +
      "   • T (DGT refoulement) → f_w (rétention énergie carter)\n" +
      "   • Capacity → z_flow (capacité machine)\n" +
      "   • P → z_dp\n" +
      "Tsh = T − Tsat (surchauffe > 0, sous-refroidissement < 0).",
    ports: ["inlet", "outlet"],
    color: "#0ea5e9",
    params: [
      {
        key: "fluid",
        label: "Fluide",
        kind: "string",
        default: "R134a",
        required: true,
        section: "Mesure",
        description: "Fluide pour Tsat / Tsh / X (CoolProp).",
      },
      {
        key: "t_c",
        label: "T (température)",
        kind: "number",
        unit: "°C",
        default: 50,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "temperature",
        description: "Température de ligne. Sur refoulement = DGT → pairer avec f_w.",
      },
      {
        key: "tsat_c",
        label: "Tsat",
        kind: "number",
        unit: "°C",
        default: 5,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "saturationTemperature",
        description: "Tsat(P) à la sonde. Aspiration = SST, liquide = SDT → z_ua.",
      },
      {
        key: "p_bar",
        label: "P",
        kind: "number",
        unit: "bar",
        default: 5,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "pressure",
        description: "Pression absolue → pairer avec z_dp.",
      },
      {
        key: "x",
        label: "X (titre)",
        kind: "number",
        default: 1,
        min: 0,
        max: 1,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "quality",
        description: "Titre vapeur [0–1]. Usage avancé.",
      },
      {
        key: "tsh_k",
        label: "Tsh (= T − Tsat)",
        kind: "number",
        unit: "K",
        default: 5,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "superheat",
        description:
          "Écart à la saturation : >0 surchauffe, <0 sous-refroidissement. Remplace SH/SC sur l’échangeur.",
      },
      {
        key: "capacity_w",
        label: "Capacité",
        kind: "number",
        unit: "W",
        default: 10000,
        section: "Paramètres physiques",
        fixable: true,
        defaultFixed: false,
        measureOutput: "capacity",
        description: "Capacité (bilan secondaire). Pairer avec z_flow du compresseur.",
      },
    ],
  },

  // ── Advanced regulation (optional — most users use Fixed on Calibration tab) ──
  {
    type: "SaturatedController",
    label: "Regulation loop (advanced)",
    category: "Advanced",
    description:
      "Optional. Links a measured quantity (e.g. superheat) to a free actuator (e.g. valve opening). For simple calibration (SST + Z_UA), prefer the Calibration tab Fixed checkboxes — you do not need this block.",
    help:
      "À quoi ça sert ?\n" +
      "• Boucle de régulation avancée : tu imposes une grandeur mesurée et tu libères un actionneur.\n" +
      "• Exemple : superheat mesuré → ouverture EXV libre.\n\n" +
      "Quand NE PAS l’utiliser ?\n" +
      "• Pour calibrer Z_UA sur une SST : utilise l’onglet Calibration du composant (cases Fixed).\n" +
      "• Ce bloc est pour les cas multi-objectifs / priorités (override chain).\n\n" +
      "Ce n’est pas un composant thermodynamique : il n’a pas de ports fluide.",
    docSlug: undefined,
    ports: [],
    color: "#7c3aed",
    params: [
      {
        key: "measure_component",
        label: "Composant mesuré",
        kind: "string",
        default: "evap",
        required: true,
        section: "Mesure (imposé)",
        description: "Nom du composant dont on lit la grandeur (ex. evap).",
      },
      {
        key: "measure_output",
        label: "Grandeur mesurée",
        kind: "string",
        default: "saturationTemperature",
        required: true,
        section: "Mesure (imposé)",
        description: "Ex. saturationTemperature (SST/SDT), superheat, capacity…",
      },
      {
        key: "target",
        label: "Consigne",
        kind: "number",
        default: 278.15,
        required: true,
        section: "Mesure (imposé)",
        description: "Valeur cible en SI : K pour températures, Pa, W, kg/s…",
      },
      {
        key: "gain",
        label: "Gain",
        kind: "number",
        default: -0.5,
        section: "Mesure (imposé)",
        description: "Signe et échelle de l’erreur (consigne − mesure).",
      },
      {
        key: "actuator_component",
        label: "Composant actionné",
        kind: "string",
        default: "evap",
        required: true,
        section: "Actionneur (libre)",
        description: "Composant dont on libère un facteur (souvent le même que mesuré).",
      },
      {
        key: "actuator_factor",
        label: "Facteur libre",
        kind: "string",
        default: "z_ua",
        required: true,
        section: "Actionneur (libre)",
        description: "z_ua, opening, z_flow, z_power… Valeur initiale ci-dessous = point de départ.",
      },
      {
        key: "initial",
        label: "Valeur initiale",
        kind: "number",
        default: 1.0,
        required: true,
        section: "Actionneur (libre)",
        description: "Départ du facteur libre (ex. Z_UA = 1).",
      },
      {
        key: "min",
        label: "Minimum",
        kind: "number",
        default: 0.2,
        required: true,
        section: "Actionneur (libre)",
      },
      {
        key: "max",
        label: "Maximum",
        kind: "number",
        default: 3.0,
        required: true,
        section: "Actionneur (libre)",
      },
      { key: "band", label: "Bande de saturation", kind: "number", default: 5.0, section: "Réglages fins", min: 0.000001, advanced: true },
      { key: "smooth_eps", label: "Lissage saturation", kind: "number", default: 0.001, section: "Réglages fins", min: 0.000000001, advanced: true },
      { key: "alpha", label: "Lissage overrides", kind: "number", default: 0.001, section: "Réglages fins", min: 0.000000001, advanced: true },
    ],
  },

  // ── Compressors ────────────────────────────────────────────────────────
  {
    type: "IsentropicCompressor",
    label: "Isentropic Compressor",
    category: "Compressors",
    description: "Compresseur physique : η_is + (option) ṁ = ρ·V·N·η_vol.",
    help:
      "Modèle : compression isentropique corrigée η_is.\n" +
      "h_dis = h_suc + (h_is − h_suc) / η_is\n" +
      "Mode émergent : ṁ = ρ_suc · V_d · N · η_vol · z_flow\n" +
      "T_cond / T_evap = guesses d’init si pressions émergentes côté HX.\n" +
      "Doc : docs/components/isentropic-compressor.md · Inventaire : correlations-and-maps.md",
    docSlug: "isentropic-compressor",
    ports: ["inlet", "outlet"],
    color: "#6366f1",
    params: [
      {
        key: "isentropic_efficiency",
        label: "η isentropique",
        kind: "number",
        default: 0.75,
        required: true,
        section: "Model",
        min: 0.05,
        max: 1.0,
        description: "η_is : h_dis = h_suc + (h_is−h_suc)/η_is",
        sweepable: true,
        sweepGroup: "machine",
      },
      {
        key: "emergent_pressure",
        label: "Pression émergente (ṁ par cylindrée)",
        kind: "boolean",
        default: true,
        section: "Model",
        description: "ON : ṁ = ρ·V·N·η_vol (recommandé machine réelle).",
      },
      {
        key: "displacement_m3",
        label: "Cylindrée V_d",
        kind: "number",
        unit: "m³/tr",
        default: 5.0e-5,
        section: "Displacement map",
        min: 0,
        description: "Volume balayé par tour — requis si pression émergente.",
      },
      {
        key: "speed_hz",
        label: "Vitesse N",
        kind: "number",
        unit: "tr/s",
        default: 50.0,
        section: "Displacement map",
        min: 0,
      },
      {
        key: "volumetric_efficiency",
        label: "η volumétrique",
        kind: "number",
        default: 0.92,
        section: "Displacement map",
        min: 0.05,
        max: 1.0,
      },
      {
        key: "t_cond_k",
        label: "T_cond guess",
        kind: "number",
        unit: "K",
        default: 323.15,
        section: "Init / design",
        description: "Guess d’init (ou pin si non émergent).",
      },
      {
        key: "t_evap_k",
        label: "T_evap guess",
        kind: "number",
        unit: "K",
        default: 275.15,
        section: "Init / design",
      },
      {
        key: "superheat_k",
        label: "Superheat aspiration guess",
        kind: "number",
        unit: "K",
        default: 5.0,
        section: "Init / design",
      },
      {
        key: "z_flow",
        label: "z_flow (capacité)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_flow",
        freeMin: 0.5,
        freeMax: 2.0,
        description:
          "Multiplicateur de débit / capacité machine. Fixed OFF + Probe Capacity Fixed.",
      },
      {
        key: "f_w",
        label: "f_w (rétention énergie)",
        kind: "number",
        default: 1.0,
        min: 0,
        max: 1,
        section: "Calibration",
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "f_w",
        freeMin: 0.0,
        freeMax: 1.0,
        description:
          "Fraction du travail conservée dans le fluide : 1 = adiabatique, 0.98 ≈ 2 % perdu, 0 = tout perdu. Fixed OFF + Probe T (DGT) sur refoulement.",
      },
    ],
  },
  {
    type: "Compressor",
    label: "Map Compressor (AHRI / SST-SDT)",
    category: "Compressors",
    description: "Deux modèles : AHRI 540 (M1–M10) ou polynôme bilinéaire SST/SDT.",
    help:
      "Choisis model_type :\n" +
      "• Ahri540 : ṁ = M1·(1−(Ps/Pd)^(1/M2))·ρ·V·N/60 ; Ẇ = M3…M10\n" +
      "• SstSdt : ṁ,Ẇ = a00 + a10·SST + a01·SDT + a11·SST·SDT (SST/SDT en K)\n" +
      "Doc : compressor-ahri540.md · correlations-and-maps.md",
    docSlug: "compressor-ahri540",
    ports: ["inlet", "outlet"],
    color: "#6366f1",
    params: [
      {
        key: "model_type",
        label: "Modèle de carte",
        kind: "string",
        default: "Ahri540",
        section: "Model",
        required: true,
        options: [
          { value: "Ahri540", label: "AHRI 540 (M1–M10)" },
          { value: "SstSdt", label: "Polynôme SST / SDT (bilinéaire)" },
        ],
        description: "Ahri540 = coefficients M1–M10. SstSdt = carte constructeur ṁ(SST,SDT), Ẇ(SST,SDT).",
      },
      {
        key: "fluid",
        label: "Frigorigène",
        kind: "string",
        default: "R134a",
        section: "Machine",
        required: true,
      },
      {
        key: "speed_rpm",
        label: "Vitesse",
        kind: "number",
        unit: "tr/min",
        default: 3000,
        required: true,
        section: "Machine",
        min: 1,
        sweepable: true,
        sweepGroup: "machine",
      },
      {
        key: "displacement_m3",
        label: "Cylindrée",
        kind: "number",
        unit: "m³",
        default: 0.0005,
        required: true,
        section: "Machine",
        min: 0,
      },
      {
        key: "efficiency",
        label: "Efficacité",
        kind: "number",
        default: 0.85,
        section: "Machine",
        min: 0.05,
        max: 1,
      },
      // AHRI 540
      { key: "m1", label: "M1 (flow scale)", kind: "number", default: 0.85, section: "AHRI 540 (si Ahri540)", description: "ṁ ∝ M1·(1−(Ps/Pd)^(1/M2))" },
      { key: "m2", label: "M2 (PR exponent)", kind: "number", default: 2.5, section: "AHRI 540 (si Ahri540)", min: 0.01 },
      { key: "m3", label: "M3 (Ẇ cool const)", kind: "number", default: 500, section: "AHRI 540 (si Ahri540)", unit: "W" },
      { key: "m4", label: "M4 (Ẇ cool · PR)", kind: "number", default: 1500, section: "AHRI 540 (si Ahri540)" },
      { key: "m5", label: "M5 (Ẇ cool · Ts)", kind: "number", default: -2.5, section: "AHRI 540 (si Ahri540)" },
      { key: "m6", label: "M6 (Ẇ cool · Td)", kind: "number", default: 1.8, section: "AHRI 540 (si Ahri540)" },
      { key: "m7", label: "M7 (Ẇ heat const)", kind: "number", default: 600, section: "AHRI 540 (si Ahri540)", unit: "W", advanced: true },
      { key: "m8", label: "M8 (Ẇ heat · PR)", kind: "number", default: 1600, section: "AHRI 540 (si Ahri540)", advanced: true },
      { key: "m9", label: "M9 (Ẇ heat · Ts)", kind: "number", default: -3.0, section: "AHRI 540 (si Ahri540)", advanced: true },
      { key: "m10", label: "M10 (Ẇ heat · Td)", kind: "number", default: 2.0, section: "AHRI 540 (si Ahri540)", advanced: true },
      // SST/SDT bilinear
      { key: "mf_a00", label: "ṁ a00", kind: "number", default: 0.05, section: "Map SST/SDT (si SstSdt)", description: "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT (K)" },
      { key: "mf_a10", label: "ṁ a10 (×SST)", kind: "number", default: 0.001, section: "Map SST/SDT (si SstSdt)" },
      { key: "mf_a01", label: "ṁ a01 (×SDT)", kind: "number", default: 0.0005, section: "Map SST/SDT (si SstSdt)" },
      { key: "mf_a11", label: "ṁ a11 (×SST·SDT)", kind: "number", default: 0.00001, section: "Map SST/SDT (si SstSdt)" },
      { key: "pw_b00", label: "Ẇ b00", kind: "number", default: 1000, unit: "W", section: "Map SST/SDT (si SstSdt)", description: "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT (K)" },
      { key: "pw_b10", label: "Ẇ b10 (×SST)", kind: "number", default: 50, section: "Map SST/SDT (si SstSdt)" },
      { key: "pw_b01", label: "Ẇ b01 (×SDT)", kind: "number", default: 30, section: "Map SST/SDT (si SstSdt)" },
      { key: "pw_b11", label: "Ẇ b11 (×SST·SDT)", kind: "number", default: 0.5, section: "Map SST/SDT (si SstSdt)" },
    ],
  },
  {
    type: "CentrifugalCompressor",
    label: "Centrifugal Compressor",
    category: "Compressors",
    description: "Centrifugal compressor with φ–Mach polytropic map (CLI-wired).",
    help:
      "Carte φ / Mach → tête polytropique et η_p. VFD via speed_rpm.\n" +
      "Modèle encore simplifié côté solveur (Jacobien partiel).",
    ports: ["inlet", "outlet"],
    color: "#4338ca",
    params: [
      { key: "diameter_m", label: "Impeller diameter", kind: "number", unit: "m", default: 0.25, required: true },
      { key: "speed_rpm", label: "Speed", kind: "number", unit: "rpm", default: 9000.0, required: true, sweepable: true, sweepGroup: "machine" },
      { key: "gas_constant", label: "Gas constant R", kind: "number", unit: "J/kg·K", default: 188.9, advanced: true },
      { key: "gamma", label: "Heat capacity ratio γ", kind: "number", default: 1.12, advanced: true },
    ],
  },
  {
    type: "ScrewEconomizerCompressor",
    label: "Screw Compressor (Economizer)",
    category: "Compressors",
    description: "Vis 3 ports — polynôme bilinéaire SST/SDT + presets.",
    help:
      "Carte bilinéaire :\n" +
      "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT\n" +
      "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT\n" +
      "Presets : bitzer_generic_200kw, grasso_generic_200kw.\n" +
      "Ports : suction, discharge, economizer.\n" +
      "Doc : screw-economizer-compressor.md · correlations-and-maps.md",
    docSlug: "screw-economizer-compressor",
    ports: ["suction", "discharge", "economizer"],
    color: "#4f46e5",
    params: [
      {
        key: "preset",
        label: "Preset carte",
        kind: "string",
        default: "bitzer_generic_200kw",
        section: "Model",
        options: [
          { value: "bitzer_generic_200kw", label: "Bitzer generic ~200 kW" },
          { value: "grasso_generic_200kw", label: "Grasso generic ~200 kW" },
          { value: "", label: "Custom (defaults)" },
        ],
        description: "Remplit les coeffs mf_* / pw_* (surchargeables).",
      },
      {
        key: "frequency_hz",
        label: "Fréquence",
        kind: "number",
        unit: "Hz",
        default: 50.0,
        section: "Machine",
        sweepable: true,
        sweepGroup: "machine",
      },
      {
        key: "nominal_frequency_hz",
        label: "Fréquence nominale",
        kind: "number",
        unit: "Hz",
        default: 50.0,
        section: "Machine",
      },
      {
        key: "mechanical_efficiency",
        label: "η mécanique",
        kind: "number",
        default: 0.92,
        section: "Machine",
        min: 0.05,
        max: 1,
      },
      {
        key: "economizer_fraction",
        label: "Fraction éco ṁ",
        kind: "number",
        default: 0.13,
        section: "Machine",
        min: 0,
        max: 1,
      },
      {
        key: "volume_index",
        label: "Volume index Vi",
        kind: "number",
        default: 2.8,
        section: "Machine",
        min: 1.1,
        max: 6,
        description: "Built-in Vi; slide valve reduces effective Vi.",
        sweepable: true,
        sweepGroup: "machine",
      },
      {
        key: "slide_valve_position",
        label: "Slide valve (0–1)",
        kind: "number",
        default: 1.0,
        section: "Machine",
        min: 0.05,
        max: 1,
        sweepable: true,
        sweepGroup: "machine",
      },
      // Mass-flow bilinear (Bitzer defaults as numbers)
      { key: "mf_a00", label: "ṁ a00", kind: "number", default: 1.35, section: "Map ṁ (SST,SDT)", description: "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT" },
      { key: "mf_a10", label: "ṁ a10 (×SST)", kind: "number", default: 0.004, section: "Map ṁ (SST,SDT)" },
      { key: "mf_a01", label: "ṁ a01 (×SDT)", kind: "number", default: -0.0025, section: "Map ṁ (SST,SDT)" },
      { key: "mf_a11", label: "ṁ a11 (×SST·SDT)", kind: "number", default: 0.000012, section: "Map ṁ (SST,SDT)" },
      { key: "pw_b00", label: "Ẇ b00", kind: "number", default: 58000, unit: "W", section: "Map Ẇ (SST,SDT)", description: "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT" },
      { key: "pw_b10", label: "Ẇ b10 (×SST)", kind: "number", default: 180, section: "Map Ẇ (SST,SDT)" },
      { key: "pw_b01", label: "Ẇ b01 (×SDT)", kind: "number", default: -280, section: "Map Ẇ (SST,SDT)" },
      { key: "pw_b11", label: "Ẇ b11 (×SST·SDT)", kind: "number", default: 0.4, section: "Map Ẇ (SST,SDT)" },
    ],
  },

  // ── Heat exchangers ────────────────────────────────────────────────────
  {
    type: "Condenser",
    label: "Condenser",
    category: "Heat Exchangers",
    description: "Condenseur frigo avec secondaire eau/air (ε-NTU).",
    help:
      "Rôle : rejette la chaleur du frigorigène vers l’eau ou l’air.\n\n" +
      "Ports : inlet/outlet frigo · secondary_in/out caloporteur.\n\n" +
      "ΔP frigo : dp_model=msh (Müller–Steinhagen–Heck + accélération) avec géométrie tube, " +
      "ou quadratic (dp_nominal), ou isobaric.\n\n" +
      "Machine réelle : cocher « Solve condensing pressure » et câbler Source/Sink secondaires.\n" +
      "Calibration : Fixed sur SDT + Z_UA libre (Z_UA défaut = 1).",
    docSlug: "condenser",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#ef4444",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" },
      { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 5000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" },
      { key: "t_sat_k", label: "Fixed saturation temperature", kind: "number", unit: "K", default: 323.15, section: "Thermal model", description: "Used only in fixed-pressure mode. Do not treat it as a solved-cycle input when emergent pressure is enabled." },
      {
        key: "secondary_inlet_temp_c",
        label: "Secondary inlet T (rating only)",
        kind: "number",
        unit: "°C",
        default: 30.0,
        section: "Secondary side (rating)",
        description: "Rating scalar. Prefer live secondary ports + BrineSource for system mode.",
      },
      {
        key: "secondary_mass_flow_kg_s",
        label: "Secondary mass flow (rating only)",
        kind: "number",
        unit: "kg/s",
        default: 2.0,
        section: "Secondary side (rating)",
        min: 0.0,
      },
      {
        key: "secondary_cp_j_per_kgk",
        label: "Secondary specific heat (rating only)",
        kind: "number",
        unit: "J/kg·K",
        default: 4186.0,
        section: "Secondary side (rating)",
        min: 0.0,
        advanced: true,
      },
      { key: "secondary_humidity_ratio", label: "Secondary humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Secondary side (rating)", min: 0.0, advanced: true },
      {
        key: "dp_model",
        label: "Refrigerant ΔP model",
        kind: "string",
        default: "msh",
        section: "Hydraulics",
        options: [
          { value: "msh", label: "MSH 1986 (tube, + accel)" },
          { value: "friedel", label: "Friedel 1979 (tube, + accel)" },
          { value: "quadratic", label: "Quadratic (dp_nominal)" },
          { value: "isobaric", label: "Isobaric (ΔP = 0)" },
        ],
        description:
          "MSH = NIST EVAP-COND default two-phase friction at mean quality + acceleration. Quadratic = Modelica Buildings k·ṁ². Flooded shell-side → isobaric.",
      },
      { key: "tube_length_m", label: "Tube length", kind: "number", unit: "m", default: 6.0, section: "Hydraulics", min: 0.0, description: "Path length for MSH/Friedel." },
      { key: "tube_diameter_m", label: "Tube diameter", kind: "number", unit: "m", default: 0.0095, section: "Hydraulics", min: 0.0, advanced: true },
      { key: "n_parallel_tubes", label: "Parallel tubes", kind: "number", default: 2, section: "Hydraulics", min: 1, advanced: true },
      {
        key: "rated_pressure_drop_pa",
        label: "Design ΔP (quadratic)",
        kind: "number",
        unit: "Pa",
        default: 15000,
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
        description: "Only when dp_model=quadratic. Calibrates k = ΔP/ṁ².",
      },
      {
        key: "rated_mass_flow_kg_s",
        label: "Design ṁ (quadratic)",
        kind: "number",
        unit: "kg/s",
        default: 0.05,
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
      },
      {
        key: "pressure_drop_coeff_pa_s2_kg2",
        label: "Pressure-drop coefficient k",
        kind: "number",
        unit: "Pa·s²/kg²",
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
        description: "Direct k for quadratic mode.",
      },
      {
        key: "secondary_rated_pressure_drop_pa",
        label: "Water/air design ΔP",
        kind: "number",
        unit: "Pa",
        default: 30000,
        section: "Secondary hydraulics",
        min: 0.0,
        description:
          "Secondary-side ΔP at rated ṁ (Modelica dp_nominal). NOT the refrigerant tube MSH model. 0 = isobaric water path.",
      },
      {
        key: "secondary_rated_m_flow_kg_s",
        label: "Water/air design ṁ",
        kind: "number",
        unit: "kg/s",
        default: 0.45,
        section: "Secondary hydraulics",
        min: 0.0,
        description: "Pairs with Water/air design ΔP → k = ΔP/ṁ².",
      },
      { key: "emergent_pressure", label: "Solve condensing pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON (recommandé machine): P_cond émerge du bilan — pas un Dirichlet fixe (esprit Modelica cycle fermé)." },
      { key: "subcooling_k", label: "Outlet subcooling closure", kind: "number", unit: "K", default: 5.0, section: "Solved-cycle mode", min: 0.0, description: "FIX residual when emergent_pressure is ON (+1 eq, closes P_cond with energy balance)." },
      { key: "fan_head_pressure_target_c", label: "Fan head-pressure target", kind: "number", unit: "°C", section: "Controls", advanced: true },
      { key: "flooded_head_pressure_target_c", label: "Flooded head-pressure target", kind: "number", unit: "°C", section: "Controls", advanced: true },
      { key: "skip_pressure_eq", label: "Skip pressure equation", kind: "boolean", default: false, section: "Solver structure", advanced: true },
      {
        key: "z_ua",
        label: "Z_UA (UA factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_ua",
        freeMin: 0.05,
        freeMax: 3.0,
        description: "Fixed = valeur figée. Décocher Fixed = libre (calibration).",
      },
      {
        key: "z_dp",
        label: "Z_dP (pressure-drop factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        advanced: true,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_dp",
        freeMin: 0.05,
        freeMax: 3.0,
      },
    ],
  },
  {
    type: "Evaporator",
    label: "Evaporator",
    category: "Heat Exchangers",
    description: "Évaporateur DX (détente directe) avec secondaire eau/brine.",
    help:
      "Rôle : évaporateur à détente directe (sortie surchauffée).\n\n" +
      "Ports : inlet/outlet frigo · secondary_in/out.\n\n" +
      "Différence flooded : ici la sortie n’est pas noyée — superheat en sortie.\n" +
      "Calibration : Fixed sur SST + Z_UA libre (défaut Z_UA = 1).",
    docSlug: "evaporator",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#3b82f6",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" },
      { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 6000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" },
      { key: "t_sat_k", label: "Fixed saturation temperature", kind: "number", unit: "K", default: 275.15, section: "Thermal model", description: "Used only in fixed-pressure mode." },
      { key: "superheat_k", label: "Outlet superheat closure", kind: "number", unit: "K", default: 5.0, section: "Thermal model", min: 0.0 },
      {
        key: "z_ua",
        label: "Z_UA (UA factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_ua",
        freeMin: 0.05,
        freeMax: 3.0,
        description: "Fixed = figé. Décocher Fixed = libre pour le solveur.",
      },
      {
        key: "z_dp",
        label: "Z_dP (pressure-drop factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        advanced: true,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_dp",
        freeMin: 0.05,
        freeMax: 3.0,
      },
      {
        key: "secondary_inlet_temp_c",
        label: "Secondary inlet T (rating only)",
        kind: "number",
        unit: "°C",
        default: 12.0,
        section: "Secondary side (rating)",
        description: "Rating scalar. Prefer live secondary ports + BrineSource for system mode.",
      },
      {
        key: "secondary_mass_flow_kg_s",
        label: "Secondary mass flow (rating only)",
        kind: "number",
        unit: "kg/s",
        default: 1.5,
        section: "Secondary side (rating)",
        min: 0.0,
      },
      {
        key: "secondary_cp_j_per_kgk",
        label: "Secondary specific heat (rating only)",
        kind: "number",
        unit: "J/kg·K",
        default: 4186.0,
        section: "Secondary side (rating)",
        min: 0.0,
        advanced: true,
      },
      { key: "secondary_humidity_ratio", label: "Secondary humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Secondary side (rating)", min: 0.0, advanced: true },
      {
        key: "dp_model",
        label: "Refrigerant ΔP model",
        kind: "string",
        default: "msh",
        section: "Hydraulics",
        options: [
          { value: "msh", label: "MSH 1986 (tube, + accel)" },
          { value: "friedel", label: "Friedel 1979 (tube, + accel)" },
          { value: "quadratic", label: "Quadratic (dp_nominal)" },
          { value: "isobaric", label: "Isobaric (ΔP = 0)" },
        ],
        description: "DX tube ΔP. For flooded shell-side use FloodedEvaporator (ΔP ≈ 0).",
      },
      { key: "tube_length_m", label: "Tube length", kind: "number", unit: "m", default: 6.0, section: "Hydraulics", min: 0.0 },
      { key: "tube_diameter_m", label: "Tube diameter", kind: "number", unit: "m", default: 0.0095, section: "Hydraulics", min: 0.0, advanced: true },
      { key: "n_parallel_tubes", label: "Parallel tubes", kind: "number", default: 2, section: "Hydraulics", min: 1, advanced: true },
      {
        key: "rated_pressure_drop_pa",
        label: "Design ΔP (quadratic)",
        kind: "number",
        unit: "Pa",
        default: 15000,
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
      },
      {
        key: "rated_mass_flow_kg_s",
        label: "Design ṁ (quadratic)",
        kind: "number",
        unit: "kg/s",
        default: 0.05,
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
      },
      {
        key: "pressure_drop_coeff_pa_s2_kg2",
        label: "Pressure-drop coefficient k",
        kind: "number",
        unit: "Pa·s²/kg²",
        section: "Hydraulics",
        min: 0.0,
        advanced: true,
      },
      {
        key: "secondary_rated_pressure_drop_pa",
        label: "Water/air design ΔP",
        kind: "number",
        unit: "Pa",
        default: 30000,
        section: "Secondary hydraulics",
        min: 0.0,
        description: "Secondary-side ΔP at rated ṁ. Distinct from refrigerant dp_model. 0 = isobaric.",
      },
      {
        key: "secondary_rated_m_flow_kg_s",
        label: "Water/air design ṁ",
        kind: "number",
        unit: "kg/s",
        default: 0.5,
        section: "Secondary hydraulics",
        min: 0.0,
      },
      { key: "emergent_pressure", label: "Solve evaporating pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON (recommandé machine): P_evap émerge du bilan — pas un Dirichlet fixe." },
      {
        key: "superheat_regulated",
        label: "Regulate superheat with EXV controller",
        kind: "boolean",
        default: false,
        section: "Solved-cycle mode",
        description: "Drops SH residual (−1 eq). Pair with SaturatedController → EXV opening (FREE).",
      },
      { key: "skip_pressure_eq", label: "Skip pressure equation", kind: "boolean", default: false, section: "Solver structure", advanced: true },
    ],
  },
  {
    type: "MchxCondenserCoil",
    label: "MCHX Condenser Coil",
    category: "Heat Exchangers",
    description: "Multi-channel heat exchanger air-cooled condenser coil.",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#f97316",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification", description: "Supplier or coil manufacturer. Stored as project traceability data." },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification", description: "Manufacturer model, selection reference, or internal equipment code." },
      { key: "ua_nominal_kw_k", label: "Nominal UA", kind: "number", unit: "kW/K", default: 15.0, required: true, section: "Thermal model", min: 0.001, description: "Overall conductance at nominal air flow." },
      { key: "n_air_exponent", label: "Air-flow exponent", kind: "number", default: 0.5, section: "Thermal model", min: 0.0, max: 2.0, description: "Exponent used to scale UA with air-flow ratio." },
      { key: "coil_index", label: "Coil index", kind: "number", default: 0, section: "Identification", min: 0, step: 1 },
      { key: "air_inlet_temp_c", label: "Air inlet temperature", kind: "number", unit: "°C", default: 35.0, section: "Air side" },
      { key: "fan_speed", label: "Fan speed ratio", kind: "number", default: 1.0, section: "Air side", min: 0.0, max: 1.0, step: 0.01, sweepable: true, sweepGroup: "machine" },
    ],
  },
  {
    type: "AirCooledCondenser",
    label: "Air-Cooled Condenser",
    category: "Heat Exchangers",
    description: "Condenser with air-side fan model. The caloporteur ports represent the ambient air stream.",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#f59e0b",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "ua", label: "UA at full fan", kind: "number", unit: "W/K", default: 4000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" },
      { key: "oat_k", label: "Outdoor air temperature", kind: "number", unit: "K", default: 308.15, section: "Air side", description: "Ambient dry-bulb temperature used by the condenser model.", sweepable: true, sweepGroup: "boundaries" },
      { key: "approach_k", label: "Condensing approach", kind: "number", unit: "K", default: 12.0, section: "Thermal model", min: 0.0 },
    ],
  },
  {
    type: "FinCoilCondenser",
    label: "Finned-Tube Condenser Coil",
    category: "Heat Exchangers",
    description:
      "Round-tube plate-fin air-cooled condenser. UA and condensing temperature are derived from the actual coil face, tube rows, fin pitch and air velocity.",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#d97706",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification", description: "Coil supplier or OEM." },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification", description: "Supplier selection reference or equipment code." },
      { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "face_width_m", label: "Coil face width", kind: "number", unit: "m", default: 1.5, required: true, section: "Coil geometry", min: 0.01 },
      { key: "face_height_m", label: "Coil face height", kind: "number", unit: "m", default: 1.0, required: true, section: "Coil geometry", min: 0.01 },
      { key: "n_rows", label: "Tube rows", kind: "number", default: 3, required: true, section: "Coil geometry", min: 1, step: 1 },
      {
        key: "fin_type",
        label: "Fin type",
        kind: "string",
        default: "louvered",
        section: "Fin geometry",
        options: [
          { value: "louvered", label: "Louvered — Chang & Wang" },
          { value: "slit", label: "Slit fin" },
          { value: "wavy", label: "Wavy / corrugated" },
          { value: "plain", label: "Plain fin" },
        ],
      },
      { key: "fin_pitch_fpi", label: "Fin pitch", kind: "number", unit: "FPI", default: 14.0, required: true, section: "Fin geometry", min: 1.0, description: "Number of fins per inch." },
      { key: "fin_thickness_mm", label: "Fin thickness", kind: "number", unit: "mm", default: 0.1, section: "Fin geometry", min: 0.01 },
      { key: "louver_pitch_mm", label: "Louver pitch", kind: "number", unit: "mm", default: 1.4, section: "Fin geometry", min: 0.01 },
      { key: "tube_od_mm", label: "Tube outside diameter", kind: "number", unit: "mm", default: 9.525, required: true, section: "Tube geometry", min: 0.1 },
      { key: "tube_pitch_mm", label: "Transverse tube pitch", kind: "number", unit: "mm", default: 25.4, required: true, section: "Tube geometry", min: 0.1, description: "Longitudinal pitch is derived using the staggered-tube ratio." },
      { key: "oat_k", label: "Outdoor air temperature", kind: "number", unit: "K", default: 308.15, required: true, section: "Air side", sweepable: true, sweepGroup: "boundaries" },
      { key: "air_face_velocity_m_s", label: "Air face velocity", kind: "number", unit: "m/s", default: 2.5, required: true, section: "Air side", min: 0.01, sweepable: true, sweepGroup: "boundaries" },
      { key: "design_capacity_kw", label: "Design heat rejection", kind: "number", unit: "kW", default: 100.0, required: true, section: "Design point", min: 0.01 },
      {
        key: "wet_surface",
        label: "Wet air-side (Wang)",
        kind: "boolean",
        default: false,
        section: "Air side",
        description: "Apply Wang wet j-factor correction when condensate forms.",
      },
    ],
  },
  {
    type: "BphxEvaporator",
    label: "Brazed Plate Evaporator",
    category: "Heat Exchangers",
    description: "Évaporateur à plaques brasées : géométrie + corrélation Longo/Shah → UA.",
    help:
      "Modèle : corrélation biphasique (Longo 2004 / Shah 1979 / Shah 2021) estime h et UA à partir des plaques, puis le solveur utilise un HX ε-NTU.\n\n" +
      "DX uniquement (sortie surchauffée). Pour un noyé calandre/tubes → FloodedEvaporator.\n\n" +
      "Corrélations et formules : docs/components/bphx.md",
    docSlug: "bphx",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#0891b2",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids", description: "Water, brine, or glycol solution connected to the secondary ports." },
      { key: "n_plates", label: "Number of plates", kind: "number", default: 20, required: true, section: "Plate pack", min: 2, step: 1 },
      { key: "plate_length_m", label: "Effective plate length", kind: "number", unit: "m", default: 0.25, section: "Plate pack", min: 0.001 },
      { key: "plate_width_m", label: "Effective plate width", kind: "number", unit: "m", default: 0.05, section: "Plate pack", min: 0.001 },
      { key: "plate_thickness_mm", label: "Plate thickness", kind: "number", unit: "mm", default: 0.6, section: "Plate pack", min: 0.01 },
      { key: "channel_spacing_mm", label: "Channel spacing", kind: "number", unit: "mm", default: 1.5, section: "Corrugation", min: 0.01 },
      { key: "chevron_angle_deg", label: "Chevron angle", kind: "number", unit: "°", default: 60.0, section: "Corrugation", min: 10.0, max: 80.0 },
      { key: "corrugation_pitch_mm", label: "Corrugation pitch", kind: "number", unit: "mm", default: 3.0, section: "Corrugation", min: 0.01 },
      { key: "dh_m", label: "Hydraulic diameter override", kind: "number", unit: "m", section: "Direct manufacturer data", min: 0.000001, advanced: true, description: "Optional direct override. Use together with heat-transfer area when the supplier provides both values." },
      { key: "area_m2", label: "Heat-transfer area override", kind: "number", unit: "m²", section: "Direct manufacturer data", min: 0.000001, advanced: true },
      { key: "target_superheat_k", label: "Target superheat", kind: "number", unit: "K", default: 5.0, section: "Operating model", min: 0.0, description: "Brazed plate evaporators are Direct Expansion (DX) only in this project — the refrigerant outlet is always superheated. A flooded design is a system-level property (Drum + recirculation loop); use the Flooded Evaporator (shell-and-tube) component for that." },
      {
        key: "emergent_pressure",
        label: "Solve evaporating pressure",
        kind: "boolean",
        default: true,
        section: "Solved-cycle mode",
        description:
          "ON: +1 outlet-closure residual pins h_out to target superheat so P_evap emerges from the secondary balance.",
      },
      {
        key: "correlation",
        label: "Two-phase correlation",
        kind: "string",
        default: "Longo2004",
        section: "Heat-transfer model",
        options: [
          { value: "Longo2004", label: "Longo 2004" },
          { value: "Shah1979", label: "Shah 1979" },
          { value: "Shah2021", label: "Shah 2021" },
        ],
      },
      {
        key: "dp_correlation",
        label: "Pressure-drop correlation",
        kind: "string",
        default: "SimplifiedChannel",
        section: "Pressure drop",
        description:
          "Channel friction on both hot and cold sides (independent of HTC). Default SimplifiedChannel; Martin1996 uses chevron angle.",
        options: [
          { value: "SimplifiedChannel", label: "Simplified channel (default)" },
          { value: "Martin1996", label: "Martin 1996 (chevron)" },
        ],
      },
      { key: "ua", label: "UA override", kind: "number", unit: "W/K", section: "Calibration", min: 0.0, advanced: true },
      {
        key: "z_ua",
        label: "Z_UA (UA factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_ua",
        freeMin: 0.05,
        freeMax: 3.0,
      },
      {
        key: "z_dp",
        label: "Z_dP (pressure-drop factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_dp",
        freeMin: 0.05,
        freeMax: 3.0,
      },
    ],
  },
  {
    type: "BphxCondenser",
    label: "Brazed Plate Condenser",
    category: "Heat Exchangers",
    description: "Condenseur à plaques brasées : géométrie + corrélation Longo/Shah → UA.",
    help:
      "Même famille que BphxEvaporator. Corrélation de condensation (Longo/Shah) → h → UA, solveur ε-NTU.\n\n" +
      "Détail des Nu / Re_eq / ΔP : docs/components/bphx.md",
    docSlug: "bphx",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#dc2626",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" },
      { key: "n_plates", label: "Number of plates", kind: "number", default: 20, required: true, section: "Plate pack", min: 2, step: 1 },
      { key: "plate_length_m", label: "Effective plate length", kind: "number", unit: "m", default: 0.25, section: "Plate pack", min: 0.001 },
      { key: "plate_width_m", label: "Effective plate width", kind: "number", unit: "m", default: 0.05, section: "Plate pack", min: 0.001 },
      { key: "plate_thickness_mm", label: "Plate thickness", kind: "number", unit: "mm", default: 0.6, section: "Plate pack", min: 0.01 },
      { key: "channel_spacing_mm", label: "Channel spacing", kind: "number", unit: "mm", default: 1.5, section: "Corrugation", min: 0.01 },
      { key: "chevron_angle_deg", label: "Chevron angle", kind: "number", unit: "°", default: 60.0, section: "Corrugation", min: 10.0, max: 80.0 },
      { key: "corrugation_pitch_mm", label: "Corrugation pitch", kind: "number", unit: "mm", default: 3.0, section: "Corrugation", min: 0.01 },
      { key: "dh_m", label: "Hydraulic diameter override", kind: "number", unit: "m", section: "Direct manufacturer data", min: 0.000001, advanced: true },
      { key: "area_m2", label: "Heat-transfer area override", kind: "number", unit: "m²", section: "Direct manufacturer data", min: 0.000001, advanced: true },
      { key: "target_subcooling_k", label: "Target subcooling", kind: "number", unit: "K", default: 3.0, section: "Operating model", min: 0.0 },
      {
        key: "emergent_pressure",
        label: "Solve condensing pressure",
        kind: "boolean",
        default: true,
        section: "Solved-cycle mode",
        description:
          "ON: +1 outlet-closure residual pins h_out to target subcooling so P_cond emerges from the secondary balance.",
      },
      {
        key: "correlation",
        label: "Two-phase correlation",
        kind: "string",
        default: "Longo2004",
        section: "Heat-transfer model",
        options: [
          { value: "Longo2004", label: "Longo 2004" },
          { value: "Shah1979", label: "Shah 1979" },
          { value: "Shah2021", label: "Shah 2021" },
        ],
      },
      {
        key: "dp_correlation",
        label: "Pressure-drop correlation",
        kind: "string",
        default: "SimplifiedChannel",
        section: "Pressure drop",
        description:
          "Channel friction on both hot and cold sides (independent of HTC). Default SimplifiedChannel; Martin1996 uses chevron angle.",
        options: [
          { value: "SimplifiedChannel", label: "Simplified channel (default)" },
          { value: "Martin1996", label: "Martin 1996 (chevron)" },
        ],
      },
      { key: "ua", label: "UA override", kind: "number", unit: "W/K", section: "Calibration", min: 0.0, advanced: true },
      {
        key: "z_ua",
        label: "Z_UA (UA factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_ua",
        freeMin: 0.05,
        freeMax: 3.0,
      },
      {
        key: "z_dp",
        label: "Z_dP (pressure-drop factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_dp",
        freeMin: 0.05,
        freeMax: 3.0,
      },
    ],
  },
  {
    type: "FloodedEvaporator",
    label: "Flooded Evaporator",
    category: "Heat Exchangers",
    description: "Évaporateur noyé (shell & tube). Sortie vapeur saturée par défaut.",
    help:
      "Rôle : évaporateur noyé — le frigorigène bout dans la calandre, l’eau/glycol dans les tubes.\n\n" +
      "Ports : inlet/outlet frigo · secondary_in/out eau.\n\n" +
      "Mode machine : relier BrineSource → secondary_in → secondary_out → BrineSink.\n" +
      "Mode rating : renseigner T et débit secondaires sans câbler l’eau.\n\n" +
      "Calibration : onglet Calibration — Fixed sur SST + Fixed décoché sur Z_UA (défaut Z_UA = 1).\n\n" +
      "quality_control : laisser OFF sauf modèle de recirculation spécialisé.",
    docSlug: "flooded-evaporator",
    ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"],
    color: "#06b6d4",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" },
      { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" },
      { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 8000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" },
      {
        key: "secondary_rated_pressure_drop_pa",
        label: "Tube-side water design ΔP",
        kind: "number",
        unit: "Pa",
        default: 40000,
        section: "Secondary hydraulics",
        min: 0.0,
        description:
          "Water/brine ΔP through the tubes at rated ṁ (quadratic). 0 = isobaric (legacy).",
      },
      {
        key: "secondary_rated_m_flow_kg_s",
        label: "Tube-side water design ṁ",
        kind: "number",
        unit: "kg/s",
        default: 0.55,
        section: "Secondary hydraulics",
        min: 0.0,
      },
      {
        key: "z_ua",
        label: "Z_UA (UA factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_ua",
        freeMin: 0.05,
        freeMax: 3.0,
        description: "Fixed = figé. Décocher Fixed = le solveur ajuste Z_UA.",
      },
      {
        key: "z_dp",
        label: "Z_dP (pressure-drop factor)",
        kind: "number",
        default: 1.0,
        section: "Calibration",
        min: 0.000001,
        advanced: true,
        fixable: true,
        defaultFixed: true,
        actuatorFactor: "z_dp",
        freeMin: 0.05,
        freeMax: 3.0,
      },
      {
        key: "quality_control",
        label: "Quality control residual",
        kind: "boolean",
        default: false,
        section: "Solved-cycle mode",
        description:
          "FIX: adds q_out − target residual (+1 equation). Over-constrains a closed cycle unless you FREE an actuator (EXV/level). Prefer OFF for compressor suction.",
      },
      {
        key: "target_quality",
        label: "Outlet vapour quality target",
        kind: "number",
        default: 0.7,
        section: "Solved-cycle mode",
        min: 0.0,
        max: 1.0,
        description: "Only used when quality_control is ON. Legacy flooded recirculation target — not physical for dry suction.",
      },
      {
        key: "secondary_inlet_temp_c",
        label: "Secondary inlet T (rating only)",
        kind: "number",
        unit: "°C",
        default: 12.0,
        section: "Secondary side (rating)",
        description: "Rating-mode scalar. For system solves, impose T via BrineSource on secondary_inlet instead.",
      },
      {
        key: "secondary_mass_flow_kg_s",
        label: "Secondary mass flow (rating only)",
        kind: "number",
        unit: "kg/s",
        default: 1.5,
        section: "Secondary side (rating)",
        min: 0.0,
        description: "Rating-mode scalar. For system solves, set ṁ on BrineSource.",
      },
      {
        key: "secondary_cp_j_per_kgk",
        label: "Secondary cp (rating only)",
        kind: "number",
        unit: "J/kg·K",
        default: 4186.0,
        section: "Secondary side (rating)",
        min: 0.0,
        advanced: true,
      },
      {
        key: "ua_mode",
        label: "UA mode",
        kind: "string",
        default: "fixed",
        section: "Thermal model",
        options: [
          { value: "fixed", label: "Fixed UA" },
          { value: "cooper", label: "Cooper pool boiling" },
        ],
        description: "cooper = outer-loop UA from Cooper + Palen (Picard).",
      },
      {
        key: "area_ref_m2",
        label: "Refrigerant HTA",
        kind: "number",
        unit: "m²",
        default: 20.0,
        section: "Pool boiling",
        advanced: true,
      },
      {
        key: "area_sec_m2",
        label: "Secondary HTA",
        kind: "number",
        unit: "m²",
        default: 18.0,
        section: "Pool boiling",
        advanced: true,
      },
    ],
  },
  {
    type: "HeatExchanger",
    label: "Generic Heat Exchanger",
    category: "Heat Exchangers",
    description: "Strict four-port two-fluid heat exchanger. Every side is a real solver edge.",
    ports: ["hot_inlet", "hot_outlet", "cold_inlet", "cold_outlet"],
    color: "#8b5cf6",
    params: [
      { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" },
      { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" },
      { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 3000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" },
      { key: "hot_fluid_id", label: "Hot-side fluid", kind: "string", default: "Water", section: "Fluids" },
      { key: "cold_fluid_id", label: "Cold-side fluid", kind: "string", default: "Air", section: "Fluids" },
      { key: "hot_humidity_ratio", label: "Hot-side humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Psychrometrics", min: 0.0, advanced: true },
      { key: "cold_humidity_ratio", label: "Cold-side humidity ratio", kind: "number", unit: "kg/kg", default: 0.01, section: "Psychrometrics", min: 0.0, advanced: true },
    ],
  },

  // ── Expansion ──────────────────────────────────────────────────────────
  {
    type: "IsenthalpicExpansionValve",
    label: "Expansion Valve (EXV)",
    category: "Expansion",
    description: "Détendeur isenthalpique (EXV).",
    help:
      "Rôle : fait chuter la pression frigo (h constant).\n\n" +
      "Ports : inlet (liquide HP) · outlet (biphasique BP).\n\n" +
      "Sans orifice_kv : isenthalpique seul — opening n’a aucun effet physique.\n" +
      "Avec orifice_kv : ṁ = Kv·opening·√(2·ρ·ΔP). Opening Fixed (défaut) = paramètre ; " +
      "Free = inconnue (régulation / SaturatedController).\n" +
      "Orifice Fixed → le compresseur lâche la loi de ṁ (débit métré par l’EXV).\n" +
      "t_evap_k : guess / mode pression fixée selon le cycle.",
    docSlug: "isenthalpic-expansion-valve",
    ports: ["inlet", "outlet"],
    color: "#10b981",
    params: [
      { key: "t_evap_k", label: "Evaporating temp (K)", kind: "number", unit: "K", default: 275.15 },
      {
        key: "orifice_kv",
        label: "Orifice Kv",
        kind: "number",
        unit: "m²",
        section: "Orifice",
        description:
          "Capacité d’orifice. Obligatoire pour que opening change le débit. Typique ~1e-7…1e-5 m².",
      },
      {
        key: "opening",
        label: "Valve opening (0–1)",
        kind: "number",
        default: 1.0,
        min: 0,
        max: 1,
        section: "Orifice",
        fixable: true,
        defaultFixed: true,
        description: "Nécessite orifice_kv. Fixed = impose opening ; Free = inconnue solver.",
        sweepable: true,
        sweepGroup: "machine",
      },
    ],
  },
  {
    type: "ExpansionValve",
    label: "Expansion Valve (basic)",
    category: "Expansion",
    description: "Physical EXV/TXV with selectable flow model (orifice, Cd·A, Eames).",
    ports: ["inlet", "outlet"],
    color: "#10b981",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "R134a", required: true },
      { key: "opening", label: "Opening (0–1)", kind: "number", default: 1.0, sweepable: true, sweepGroup: "machine" },
      {
        key: "flow_model",
        label: "Flow model",
        kind: "string",
        default: "orifice",
        options: [
          { value: "orifice", label: "Isenthalpic orifice" },
          { value: "exv", label: "EXV Cd·A" },
          { value: "txv", label: "TXV Eames" },
        ],
      },
      { key: "beta_m2", label: "β / orifice area", kind: "number", unit: "m²", default: 1e-6, advanced: true },
      { key: "cd", label: "Discharge Cd", kind: "number", default: 0.7, advanced: true },
      { key: "area_max_m2", label: "A_max", kind: "number", unit: "m²", default: 2e-6, advanced: true },
      { key: "bulb_pressure_pa", label: "TXV bulb P", kind: "number", unit: "Pa", default: 400000, advanced: true },
    ],
  },
  {
    type: "CapillaryTube",
    label: "Capillary Tube",
    category: "Expansion",
    description: "Adiabatic capillary (segmented ΔP, isenthalpic).",
    help: "Diamètre / longueur / segments. Corrélation DP MSH par défaut.",
    ports: ["inlet", "outlet"],
    color: "#059669",
    params: [
      { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.001, required: true },
      { key: "length_m", label: "Length", kind: "number", unit: "m", default: 2.0, required: true },
      { key: "n_segments", label: "Segments", kind: "number", default: 20, advanced: true },
    ],
  },
  {
    type: "ReversingValve",
    label: "Reversing Valve (4-way)",
    category: "Expansion",
    description: "Four-way reversing valve for heat-pump mode switch.",
    ports: ["inlet", "outlet"],
    color: "#0d9488",
    params: [
      {
        key: "mode",
        label: "Mode",
        kind: "string",
        default: "cooling",
        options: [
          { value: "cooling", label: "Cooling" },
          { value: "heating", label: "Heating" },
        ],
      },
      { key: "pressure_drop_pa", label: "Pressure drop", kind: "number", unit: "Pa", default: 5000 },
    ],
  },
  {
    type: "BypassValve",
    label: "Bypass Valve",
    category: "Expansion",
    description: "Proportional bypass valve.",
    ports: ["inlet", "outlet"],
    color: "#14b8a6",
    params: [
      { key: "opening", label: "Opening (0–1)", kind: "number", default: 0.5, sweepable: true, sweepGroup: "machine" },
    ],
  },

  // ── Piping (Modelica/TIL media: green ref · blue water · yellow air) ────
  {
    type: "RefrigerantPipe",
    label: "Pipe — refrigerant",
    category: "Piping",
    description: "Ligne frigorifique (Darcy–Weisbach). Glisser sur une ligne verte pour l’insérer.",
    help:
      "Conduit frigorigène. Dépose-le sur une connexion verte pour couper la ligne et l’insérer automatiquement.\n" +
      "CLI type: RefrigerantPipe (alias Pipe).\n" +
      "Design ΔP = 0 (défaut) → perte Darcy–Weisbach depuis L/D + ṁ.\n" +
      "Design ΔP > 0 → impose cette chute constante (ignore L/D pour ΔP).",
    ports: ["inlet", "outlet"],
    color: "#2e7d32",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "R134a", section: "Medium" },
      { key: "length_m", label: "Length", kind: "number", unit: "m", default: 3.0, required: true, section: "Geometry" },
      { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.012, required: true, section: "Geometry" },
      {
        key: "pressure_drop_pa",
        label: "Design ΔP (0 = Darcy)",
        kind: "number",
        unit: "Pa",
        default: 0,
        section: "Hydraulics",
        description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.",
      },
      { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.0000015, section: "Geometry", advanced: true },
      { key: "density_kg_m3", label: "Density (design)", kind: "number", unit: "kg/m³", default: 1140, section: "Properties", advanced: true },
      { key: "viscosity_pa_s", label: "Viscosity (design)", kind: "number", unit: "Pa·s", default: 0.0002, section: "Properties", advanced: true },
    ],
  },
  {
    type: "WaterPipe",
    label: "Pipe — water / brine",
    category: "Piping",
    description: "Tuyauterie eau/glycol. Glisser sur une ligne bleue pour l’insérer.",
    help:
      "Conduit caloporteur. Dépose-le sur une connexion bleue (BrineSource ↔ HX) pour l’insérer dans le circuit.\n" +
      "Design ΔP = 0 → Darcy L/D ; >0 → ΔP imposé.",
    ports: ["inlet", "outlet"],
    color: "#1565c0",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "Water", section: "Medium" },
      { key: "length_m", label: "Length", kind: "number", unit: "m", default: 5.0, required: true, section: "Geometry" },
      { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.025, required: true, section: "Geometry" },
      {
        key: "pressure_drop_pa",
        label: "Design ΔP (0 = Darcy)",
        kind: "number",
        unit: "Pa",
        default: 0,
        section: "Hydraulics",
        description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.",
      },
      { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.000045, section: "Geometry", advanced: true },
      { key: "density_kg_m3", label: "Density", kind: "number", unit: "kg/m³", default: 998, section: "Properties", advanced: true },
      { key: "viscosity_pa_s", label: "Viscosity", kind: "number", unit: "Pa·s", default: 0.001, section: "Properties", advanced: true },
    ],
  },
  {
    type: "AirDuct",
    label: "Duct — air",
    category: "Piping",
    description: "Gaine d’air. Glisser sur une ligne jaune pour l’insérer.",
    help:
      "Conduit air. Dépose-le sur une connexion jaune (AirSource / Fan / coil air) pour l’insérer.\n" +
      "Design ΔP = 0 → Darcy L/D ; >0 → ΔP imposé.",
    ports: ["inlet", "outlet"],
    color: "#f9a825",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "Air", section: "Medium" },
      { key: "length_m", label: "Length", kind: "number", unit: "m", default: 5.0, required: true, section: "Geometry" },
      { key: "diameter_m", label: "Hydraulic diameter", kind: "number", unit: "m", default: 0.2, required: true, section: "Geometry" },
      {
        key: "pressure_drop_pa",
        label: "Design ΔP (0 = Darcy)",
        kind: "number",
        unit: "Pa",
        default: 0,
        section: "Hydraulics",
        description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.",
      },
      { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.00015, section: "Geometry", advanced: true },
      { key: "density_kg_m3", label: "Density", kind: "number", unit: "kg/m³", default: 1.2, section: "Properties", advanced: true },
      { key: "viscosity_pa_s", label: "Viscosity", kind: "number", unit: "Pa·s", default: 0.000018, section: "Properties", advanced: true },
    ],
  },
  {
    type: "Pipe",
    label: "Pipe (generic)",
    category: "Piping",
    description: "Legacy generic pipe — prefer medium-specific pipes above.",
    ports: ["inlet", "outlet"],
    color: "#64748b",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "R134a" },
      { key: "length_m", label: "Length (m)", kind: "number", unit: "m", default: 10.0, required: true },
      { key: "diameter_m", label: "Diameter (m)", kind: "number", unit: "m", default: 0.022, required: true },
      { key: "roughness_m", label: "Roughness (m)", kind: "number", unit: "m", default: 0.0000015 },
    ],
  },
  {
    type: "Drum",
    label: "Liquid-Vapor Separator",
    category: "Piping",
    description: "Phase separator / accumulator.",
    ports: ["inlet", "liquid_outlet", "vapor_outlet"],
    color: "#94a3b8",
    params: [],
  },
  {
    type: "FlowSplitter",
    label: "Flow Splitter",
    category: "Piping",
    description: "Splits a flow into two branches.",
    ports: ["inlet", "outlet_a", "outlet_b"],
    color: "#94a3b8",
    params: [
      { key: "split_ratio", label: "Split ratio (outlet_a)", kind: "number", default: 0.5 },
    ],
  },
  {
    type: "FlowMerger",
    label: "Flow Merger",
    category: "Piping",
    description: "Merges two flows into one.",
    ports: ["inlet_a", "inlet_b", "outlet"],
    color: "#94a3b8",
    params: [],
  },

  // ── Hydraulics ─────────────────────────────────────────────────────────
  {
    type: "Pump",
    label: "Pump",
    category: "Hydraulics",
    description: "Pompe — impose ΔP/courbe (esprit Modelica.Fluid.Machines).",
    help:
      "Modelica: une pompe prescrit ṁ ou ΔP via sa courbe — ne pas aussi Fixed ṁ " +
      "sur un MassFlowSource de la même branche (surcontraint).",
    ports: ["inlet", "outlet"],
    color: "#a855f7",
    params: [
      { key: "head_coeffs", label: "Head curve (a0,a1,a2)", kind: "string", unit: "m", default: "30,-10,-50", required: true },
      { key: "eff_coeffs", label: "Efficiency curve (a0,a1,a2)", kind: "string", default: "0.5,0.3,-0.5", required: true },
      { key: "density_kg_m3", label: "Fluid density (kg/m³)", kind: "number", unit: "kg/m³", default: 1000.0 },
    ],
  },
  {
    type: "Fan",
    label: "Fan",
    category: "Hydraulics",
    description: "Edge-coupled fan for air-side loops; pressure rise and shaft heat are solved on live edges.",
    ports: ["inlet", "outlet"],
    color: "#d946ef",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "Air" },
      { key: "speed_ratio", label: "Speed ratio", kind: "number", default: 1.0, sweepable: true, sweepGroup: "machine" },
      { key: "air_density_kg_m3", label: "Air density", kind: "number", unit: "kg/m³", default: 1.204 },
      { key: "design_flow_m3_s", label: "Design flow", kind: "number", unit: "m³/s", default: 0.83, required: true },
      { key: "curve_p0", label: "Curve p0", kind: "number", unit: "Pa", default: 250.0 },
      { key: "curve_p1", label: "Curve p1", kind: "number", default: 0.0 },
      { key: "curve_p2", label: "Curve p2", kind: "number", default: -20.0 },
      { key: "eff_e0", label: "Efficiency e0", kind: "number", default: 0.65 },
      { key: "eff_e1", label: "Efficiency e1", kind: "number", default: 0.0 },
      { key: "eff_e2", label: "Efficiency e2", kind: "number", default: 0.0 },
      {
        key: "drive_chain",
        label: "Wire-to-air drive chain",
        kind: "boolean",
        default: false,
        description: "Apply η_VFD × η_motor on top of fan efficiency (Bernier–Bourret).",
      },
    ],
  },
  {
    type: "FreeCoolingExchanger",
    label: "Free Cooling Exchanger",
    category: "Heat Exchangers",
    description: "Water–water free-cooling HX (effectiveness / UA).",
    ports: ["cold_inlet", "cold_outlet", "hot_inlet", "hot_outlet"],
    color: "#7c3aed",
    params: [
      { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 10000.0, required: true, sweepable: true, sweepGroup: "thermal" },
      { key: "effectiveness", label: "Effectiveness", kind: "number", default: 0.85 },
    ],
  },

  // ── Boundaries ─────────────────────────────────────────────────────────
  {
    type: "RefrigerantSource",
    label: "Refrigerant Source",
    category: "Boundaries",
    description: "Fixed-state refrigerant boundary.",
    ports: ["outlet"],
    color: "#f1f5f9",
    params: [
      { key: "pressure_bar", label: "Pressure (bar)", kind: "number", unit: "bar", default: 10.0, required: true },
      { key: "temperature_c", label: "Temperature (°C)", kind: "number", unit: "°C", default: 40.0 },
    ],
  },
  {
    type: "RefrigerantSink",
    label: "Refrigerant Sink",
    category: "Boundaries",
    description: "Fixed-pressure refrigerant boundary.",
    ports: ["inlet"],
    color: "#f1f5f9",
    params: [
      { key: "pressure_bar", label: "Pressure (bar)", kind: "number", unit: "bar", default: 4.0, required: true },
    ],
  },
  {
    type: "BrineSource",
    label: "Brine / Glycol Source",
    category: "Boundaries",
    description: "Modelica MassFlowSource_T / Boundary_pT.",
    help:
      "Modelica.Fluid.Sources (doc.modelica.org):\n" +
      "• MassFlowSource_T — Fixed T + Fixed ṁ, Free P (défaut)\n" +
      "• Boundary_pT — Fixed P + Fixed T, Free ṁ\n" +
      "Jamais Fixed P et Fixed ṁ ensemble.\n" +
      "Ancre P = Sink. Double Fixed P seulement avec ΔP hydraulique (pipe).\n" +
      "T_out imposé : Free ṁ ici + Fixed T_out sur le Sink.",
    docSlug: "boundaries",
    ports: ["outlet"],
    color: "#e0f2fe",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "Water" },
      {
        key: "p_set_bar",
        label: "Pressure setpoint",
        kind: "number",
        unit: "bar",
        default: 2.0,
        required: true,
        fixable: true,
        defaultFixed: false,
        description: "Free (défaut MassFlowSource_T). Fixed ⇒ mode Boundary_pT (Free ṁ).",
      },
      {
        key: "t_set_c",
        label: "Temperature setpoint",
        kind: "number",
        unit: "°C",
        default: 12.0,
        required: true,
        fixable: true,
        defaultFixed: true,
        sweepable: true,
        sweepGroup: "boundaries",
      },
      {
        key: "m_flow_kg_s",
        label: "Mass flow",
        kind: "number",
        unit: "kg/s",
        default: 0.5,
        required: true,
        fixable: true,
        defaultFixed: true,
        description: "Fixed ⇒ Free P auto. Free ⇒ ṁ issu du bilan (avec Fixed T_out au Sink).",
        sweepable: true,
        sweepGroup: "boundaries",
      },
    ],
  },
  {
    type: "BrineSink",
    label: "Brine / Glycol Sink",
    category: "Boundaries",
    description: "Modelica Boundary_p — ancre de pression.",
    help:
      "Ancre P du circuit (Boundary_p). Fixed P par défaut.\n" +
      "Fixed T_out ⇒ Free ṁ sur la Source (pas de Fixed ṁ en même temps).",
    docSlug: "boundaries",
    ports: ["inlet"],
    color: "#e0f2fe",
    params: [
      { key: "fluid", label: "Fluid", kind: "string", default: "Water" },
      {
        key: "p_back_bar",
        label: "Back pressure",
        kind: "number",
        unit: "bar",
        default: 2.0,
        required: true,
        fixable: true,
        defaultFixed: true,
      },
      {
        key: "t_set_c",
        label: "Outlet temperature",
        kind: "number",
        unit: "°C",
        default: 7.0,
        fixable: true,
        defaultFixed: false,
        description: "Fixed ON ⇒ impose h_out. Libère ṁ sur la Source.",
        sweepable: true,
        sweepGroup: "boundaries",
      },
    ],
  },
  {
    type: "AirSource",
    label: "Air Source",
    category: "Boundaries",
    description: "Modelica MassFlowSource_T (air).",
    help:
      "MassFlowSource_T : Fixed T + Fixed ṁ, Free P (défaut).\n" +
      "Boundary_pT : Fixed P + Free ṁ. Jamais Fixed P+ṁ ensemble.",
    ports: ["outlet"],
    color: "#f0fdf4",
    params: [
      {
        key: "p_set_bar",
        label: "Pressure setpoint",
        kind: "number",
        unit: "bar",
        default: 1.01325,
        required: true,
        fixable: true,
        defaultFixed: false,
      },
      {
        key: "t_dry_c",
        label: "Dry-bulb temperature",
        kind: "number",
        unit: "°C",
        default: 20.0,
        required: true,
        fixable: true,
        defaultFixed: true,
        sweepable: true,
        sweepGroup: "boundaries",
      },
      { key: "rh", label: "Relative humidity", kind: "number", unit: "%", default: 50.0 },
      {
        key: "m_flow_kg_s",
        label: "Mass flow",
        kind: "number",
        unit: "kg/s",
        default: 1.0,
        required: true,
        fixable: true,
        defaultFixed: true,
        sweepable: true,
        sweepGroup: "boundaries",
      },
    ],
  },
  {
    type: "AirSink",
    label: "Air Sink",
    category: "Boundaries",
    description: "Modelica Boundary_p (air) — ancre P.",
    ports: ["inlet"],
    color: "#f0fdf4",
    params: [
      {
        key: "p_back_bar",
        label: "Back pressure",
        kind: "number",
        unit: "bar",
        default: 1.01325,
        required: true,
        fixable: true,
        defaultFixed: true,
      },
      {
        key: "t_back_c",
        label: "Return temperature",
        kind: "number",
        unit: "°C",
        default: 25.0,
        fixable: true,
        defaultFixed: false,
      },
    ],
  },

  // ── Generic ────────────────────────────────────────────────────────────
  {
    type: "Placeholder",
    label: "Placeholder",
    category: "Generic",
    description: "Generic placeholder with configurable equation count.",
    ports: ["inlet", "outlet"],
    color: "#e2e8f0",
    params: [
      { key: "n_equations", label: "Number of equations", kind: "number", default: 2, required: true },
    ],
  },
];

export const COMPONENT_BY_TYPE: Record<string, ComponentMeta> = Object.fromEntries(
  COMPONENTS.map((c) => [c.type, c]),
);

/** Build the default parameter values for a component type. */
export function defaultParams(type: string): Record<string, number | string | boolean> {
  const meta = COMPONENT_BY_TYPE[type];
  if (!meta) return {};
  const out: Record<string, number | string | boolean> = {};
  for (const p of meta.params) {
    if (p.default !== undefined) out[p.key] = p.default;
    // Seed Fixed flags so Free/Fixed state is explicit after drop
    if (p.fixable) {
      out[fixedFlagKey(p.key)] = p.defaultFixed !== false;
    }
  }
  return out;
}

/**
 * Value shown / used for a param: stored value, else meta.default.
 * Ensures Z_UA=1 etc. always appear even if an old node lacks the key.
 */
export function effectiveParamValue(
  params: Record<string, number | string | boolean | undefined>,
  meta: ParamMeta,
): number | string | boolean | undefined {
  const v = params[meta.key];
  if (v !== undefined && v !== "") return v;
  return meta.default;
}

/** Merge missing defaults into a params object (non-destructive for existing keys). */
export function mergeDefaultParams(
  type: string,
  params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
  const defaults = defaultParams(type);
  return { ...defaults, ...params };
}
