"use client";

/**
 * Parameter dialog — Dymola / OMEdit style.
 *
 * Layout (simulation tools convention):
 *   ┌ Identity (name, type) ─────────────┐
 *   │ [General] [Model] [Secondary] …    │  tabs
 *   │ Name          Value      Unit Fixed│  table header
 *   │ UA            [8000   ]  W/K   —   │
 *   │ SST           [5      ]  °C   [✓]  │
 *   │ Z_UA          [1.0    ]  —    [ ]  │
 *   └────────────────────────────────────┘
 *
 * Fixed checkbox (EES / Dymola): ON = imposed, OFF = free for solver.
 * Descriptions are tooltips only — no walls of text.
 */

import { useEffect, useMemo, useState } from "react";
import { useDiagramStore } from "@/store/diagramStore";
import {
  boundaryFixPairingPatches,
  isBoundaryParamFixed,
} from "@/lib/boundaryFix";
import {
  COMPONENT_BY_TYPE,
  effectiveParamValue,
  fixedFlagKey,
  isParamFixed,
  isSecondaryPort,
  mergeDefaultParams,
  type ParamMeta,
} from "@/lib/componentMeta";
import {
  parseControlObjectives,
  CONTROL_NODE_TYPE,
  type ControlObjectiveConfig,
  type EntropykNodeData,
} from "@/lib/configBuilder";
import { buildComponentInspector, getSolvedVariablesForComponent } from "@/lib/componentInspector";
import type { SolvedVariable } from "@/lib/api";
import {
  calibrateAdjacentActuatorPatches,
  collectCalibHints,
  isCalibRelevantNode,
  withDerivedUaEff,
} from "@/lib/calibAssist";
import { ComponentIcon } from "@/components/canvas/ComponentIcon";
import ComponentDocPanel from "@/components/panels/ComponentDocPanel";
import { modelBannerForType } from "@/lib/componentDocMap";
import { ArrowDown, ArrowUp, HelpCircle, Pin, Plus, Trash2 } from "lucide-react";
import type { Edge, Node } from "@xyflow/react";

type StoreSnapshot = ReturnType<typeof useDiagramStore.getState>;
type DiagramNode = StoreSnapshot["nodes"][number];

const BOUNDARY_TYPES = new Set(["BrineSource", "BrineSink", "AirSource", "AirSink"]);

/** Map raw sections → a small set of clean tabs (like Dymola dialog groups). */
const TAB_ORDER = ["General", "Model", "Secondary", "Calibration", "Control", "Advanced"] as const;
type TabId = (typeof TAB_ORDER)[number];

function tabForParam(p: ParamMeta, componentType: string): TabId {
  const s = (p.section ?? "").toLowerCase();
  // Calibration Fixed/Free only on HX factors & measure targets
  if (p.fixable && (p.measureOutput || p.actuatorFactor || s.includes("calibration"))) {
    return "Calibration";
  }
  if (s.includes("calibration")) return "Calibration";
  if (s.includes("secondary") || s.includes("rating") || s.includes("caloporteur")) return "Secondary";
  // "Control" tab only for the optional regulation-loop component
  if (componentType === "SaturatedController") {
    if (s.includes("mesure") || s.includes("measure") || s.includes("primary")) return "General";
    if (s.includes("actionneur") || s.includes("actuator") || s.includes("bounded")) return "Model";
    return "Advanced";
  }
  if (s.includes("control") && !s.includes("quality")) return "Advanced";
  if (
    p.advanced ||
    s.includes("solver") ||
    s.includes("hydraulic") ||
    s.includes("advanced") ||
    s.includes("manufacturer data") ||
    s.includes("réglages fins")
  ) {
    return "Advanced";
  }
  if (
    s.includes("identification") ||
    s.includes("fluid") ||
    s === "" ||
    s.includes("component parameter")
  ) {
    return "General";
  }
  return "Model";
}

function hasLiveSecondaryPorts(node: DiagramNode, edges: Edge[]): boolean {
  const connected = new Set<string>();
  for (const e of edges) {
    if (e.source === node.id && e.sourceHandle) connected.add(e.sourceHandle);
    if (e.target === node.id && e.targetHandle) connected.add(e.targetHandle);
  }
  return connected.has("secondary_inlet") && connected.has("secondary_outlet");
}

export default function PropertiesPanel() {
  const {
    nodes,
    edges,
    result,
    selectedNodeId,
    updateNodeParams,
    updateNodesParams,
    updateNodeName,
    updateNodeCircuit,
    removeNode,
  } = useDiagramStore();
  const pinnedSweepPaths = useDiagramStore((s) => s.pinnedSweepPaths);
  const togglePinnedSweepPath = useDiagramStore((s) => s.togglePinnedSweepPath);

  const node = nodes.find((n) => n.id === selectedNodeId) as DiagramNode | undefined;
  const [tab, setTab] = useState<TabId>("General");
  /** Modelica: Parameters vs Results (Variable Browser). */
  const [panelMode, setPanelMode] = useState<"parameters" | "results">("parameters");
  // Doc technique repliée par défaut — les paramètres passent toujours en premier
  const [showHelp, setShowHelp] = useState(false);

  const liveSecondary = useMemo(
    () => (node ? hasLiveSecondaryPorts(node, edges) : false),
    [node, edges],
  );

  const inspector = useMemo(() => {
    if (!node) return null;
    return buildComponentInspector(
      node as Node<EntropykNodeData>,
      nodes as Node<EntropykNodeData>[],
      edges,
      result,
    );
  }, [node, nodes, edges, result]);

  // Solver-computed unknowns owned by this component (free actuators +
  // calibration factors). Empty until a solve produces them.
  const solvedVars = useMemo<SolvedVariable[]>(() => {
    if (!node) return [];
    const base = getSolvedVariablesForComponent(result, node.data.name);
    return withDerivedUaEff(node.data.name, nodes, base);
  }, [node, nodes, result]);

  const calibHints = useMemo(
    () => (node ? collectCalibHints(nodes, edges, node) : []),
    [node, nodes, edges],
  );

  /** One-click Free on adjacent actuators compatible with this Probe's Fixed measures. */
  const calibActuatorPatches = useMemo(() => {
    if (!node || node.data.type !== "Probe") return [];
    return calibrateAdjacentActuatorPatches(node, nodes, edges);
  }, [node, nodes, edges]);

  // After a successful solve, jump to Results when selecting a part (Dymola-like).
  useEffect(() => {
    if (!node) return;
    if (result && String(result.status).toLowerCase() === "converged") {
      setPanelMode((mode) => (mode === "results" ? mode : "results"));
    }
  }, [node?.id, result?.status, result?.iterations]); // eslint-disable-line react-hooks/exhaustive-deps

  // Always fill missing defaults (Z_UA=1, etc.) when a node is selected — once per id.
  useEffect(() => {
    if (!node) return;
    const merged = mergeDefaultParams(node.data.type, node.data.params ?? {});
    const missing = Object.keys(merged).some(
      (k) => node.data.params[k] === undefined && merged[k] !== undefined,
    );
    if (missing) {
      updateNodeParams(node.id, merged);
    }
    // Intentionally only when selection / type changes — not on every params update.
  }, [node?.id, node?.data.type]); // eslint-disable-line react-hooks/exhaustive-deps

  if (!node) {
    return (
      <div className="flex flex-col border-b border-[var(--line)]">
        <div className="flex h-9 items-center px-3">
          <span className="text-[11px] font-semibold text-[var(--ink-dim)]">Parameters</span>
        </div>
        <div className="space-y-2 px-3 pb-4 text-[12px] leading-relaxed text-[var(--ink-dim)]">
          <p>Sélectionne un composant sur le schéma pour voir ses paramètres.</p>
          <p className="text-[11px] text-[var(--ink-faint)]">
            Après Simulate, clique un composant pour ouvrir ses variables (P, T, ṁ, ΔP…) comme dans
            Modelica / Dymola.
          </p>
          <div className="rounded border border-[var(--line)] bg-[var(--chrome-2)] p-2 text-[10px]">
            <p className="mb-1 font-semibold text-[var(--ink)]">DoF Modelica</p>
            <ul className="list-inside list-disc space-y-0.5 text-[var(--ink-dim)]">
              <li>
                <strong>Fixed ☑</strong> = <code>parameter</code> (connu)
              </li>
              <li>
                <strong>Fixed ☐</strong> = inconnue → il faut une équation de plus
              </li>
              <li>
                Calib : Probe mesure <strong>Fixed</strong> (= équation) + facteur{" "}
                <strong>Free</strong> (= inconnue) — z_ua, z_dp, z_flow, f_w…
              </li>
              <li>n_eq = n_unk. Pas de boucle de régulation.</li>
            </ul>
          </div>
        </div>
      </div>
    );
  }

  const meta = COMPONENT_BY_TYPE[node.data.type];
  if (!meta) {
    return (
      <div className="px-3 py-4 text-[12px] text-[var(--hot)]">
        Unknown component type: {node.data.type}
      </div>
    );
  }

  const nodeType = String(node.data.type || node.data.kind || "");
  const params = mergeDefaultParams(nodeType, node.data.params ?? {});

  // Group params into tabs
  const byTab = new Map<TabId, ParamMeta[]>();
  for (const p of meta.params) {
    const t = tabForParam(p, nodeType);
    const list = byTab.get(t) ?? [];
    list.push(p);
    byTab.set(t, list);
  }
  // Never show empty "Control" tab for normal components
  const availableTabs = TAB_ORDER.filter((t) => (byTab.get(t)?.length ?? 0) > 0);
  const activeTab = availableTabs.includes(tab) ? tab : availableTabs[0] ?? "General";
  const rows = byTab.get(activeTab) ?? [];
  const showFixedCol = rows.some((p) => p.fixable);
  const showPinCol = rows.some((p) => p.kind === "number");
  const isRegLoop = nodeType === "SaturatedController";

  return (
    <div className="flex flex-col border-b border-[var(--line)]">
      {/* ── Title bar ── */}
      <div className="flex h-9 items-center justify-between gap-2 border-b border-[var(--line)] bg-[var(--chrome-2)] px-2.5">
        <div className="flex min-w-0 items-center gap-2">
          <span className="grid h-6 w-6 shrink-0 place-items-center rounded border border-[var(--line)] bg-white">
            <span style={{ width: 14, height: 14 }}>
              <ComponentIcon
                type={nodeType}
                color={meta.color}
                secondaryFluid={
                  node.data.params?.secondary_fluid != null
                    ? String(node.data.params.secondary_fluid)
                    : null
                }
              />
            </span>
          </span>
          <div className="min-w-0">
            <div className="truncate text-[12px] font-semibold text-[var(--ink)]">
              {node.data.name}
              <span className="ml-1 font-normal text-[var(--ink-faint)]">· {meta.label}</span>
            </div>
            <div className="truncate text-[9px] text-[var(--ink-faint)]">
              {panelMode === "results" && inspector?.summaryLines[0]
                ? inspector.summaryLines.slice(0, 2).join(" · ")
                : meta.description}
            </div>
          </div>
        </div>
        <div className="flex items-center gap-0.5">
          <button
            type="button"
            onClick={() => setShowHelp((v) => !v)}
            className={`rounded p-1 ${showHelp ? "bg-white text-[var(--accent)]" : "text-[var(--ink-faint)] hover:bg-white"}`}
            title="Documentation technique (bas du panneau)"
          >
            <HelpCircle size={14} />
          </button>
          <button
            type="button"
            onClick={() => removeNode(node.id)}
            className="rounded p-1 text-[var(--ink-faint)] hover:bg-red-50 hover:text-[var(--hot)]"
            title="Supprimer"
          >
            <Trash2 size={14} />
          </button>
        </div>
      </div>

      {/* Modelica: Parameters ↔ Results (Variable Browser) */}
      {!isRegLoop && (
        <div className="grid grid-cols-2 border-b border-[var(--line)] bg-white text-[11px]">
          <button
            type="button"
            onClick={() => setPanelMode("parameters")}
            className={`py-1.5 font-medium ${
              panelMode === "parameters"
                ? "border-b-2 border-[var(--accent)] text-[var(--ink)]"
                : "text-[var(--ink-faint)] hover:text-[var(--ink-dim)]"
            }`}
          >
            Parameters
          </button>
          <button
            type="button"
            onClick={() => setPanelMode("results")}
            className={`py-1.5 font-medium ${
              panelMode === "results"
                ? "border-b-2 border-[var(--accent)] text-[var(--ink)]"
                : "text-[var(--ink-faint)] hover:text-[var(--ink-dim)]"
            }`}
          >
            Results
            {result ? (
              <span className="ml-1 mono text-[9px] text-[var(--ok)]">●</span>
            ) : null}
          </button>
        </div>
      )}

      {/* Always visible on Probe / Z-factor components (also in Results after solve). */}
      {!isRegLoop &&
        isCalibRelevantNode(node) &&
        (calibHints.length > 0 || calibActuatorPatches.length > 0) && (
          <div className="space-y-1 border-b border-sky-200 bg-sky-50 px-2.5 py-2 text-[10px] leading-snug">
            <p className="font-semibold text-sky-950">Fixed / Free (Modelica)</p>
            {calibHints.map((h, i) => (
              <p
                key={`${h.level}-${i}`}
                className={
                  h.level === "warn"
                    ? "text-amber-900"
                    : h.level === "ok"
                      ? "text-emerald-800"
                      : "text-sky-900"
                }
              >
                {h.level === "ok" ? "✓ " : h.level === "warn" ? "⚠ " : "💡 "}
                {h.message}
              </p>
            ))}
            {calibActuatorPatches.length > 0 && (
              <button
                type="button"
                className="mt-0.5 rounded border border-sky-600 bg-white px-2 py-1 text-[10px] font-medium text-sky-800 hover:bg-sky-100"
                onClick={() => {
                  const map = new Map(
                    calibActuatorPatches.map((p) => [p.nodeId, p.params]),
                  );
                  updateNodesParams(map);
                }}
              >
                Libérer facteurs compatibles (Free)
              </button>
            )}
          </div>
        )}

      {!isRegLoop && panelMode === "results" && (
        <ModelicaResultsView
          inspector={inspector}
          hasResult={!!result}
          solvedVariables={solvedVars}
        />
      )}

      {(isRegLoop || panelMode === "parameters") && (
        <>
          {isRegLoop && (
            <div className="border-b border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[10px] text-amber-950">
              Optionnel. Calibration générique → onglet <strong>Calibration</strong> (Fixed/Free
              sur mesures Probe + facteurs Z), pas ce nœud.
            </div>
          )}

          {/* Model / correlation banner (compressors, HX, …) */}
          {modelBannerForType(node.data.type) && (
            <div className="border-b border-[var(--line)] bg-[#eff6ff] px-2.5 py-1.5 text-[10px] leading-snug text-[#1e3a5f]">
              <span className="font-semibold">Modèle : </span>
              <span className="mono">{modelBannerForType(node.data.type)}</span>
            </div>
          )}

          {/* ── Identity row (always first after header) ── */}
          <div className="grid grid-cols-[1fr_72px] gap-2 border-b border-[var(--line)] px-2.5 py-2">
            <label className="min-w-0">
              <span className="mb-0.5 block text-[9px] text-[var(--ink-faint)]">Name</span>
              <input
                type="text"
                value={node.data.name}
                onChange={(e) => updateNodeName(node.id, e.target.value)}
                className="param-input mono w-full"
              />
            </label>
            <label>
              <span className="mb-0.5 block text-[9px] text-[var(--ink-faint)]">Circuit</span>
              <input
                type="number"
                min={0}
                value={node.data.circuit}
                onChange={(e) => updateNodeCircuit(node.id, parseInt(e.target.value || "0", 10))}
                className="param-input mono w-full text-right"
              />
            </label>
          </div>

          {/* Secondary status — one line only */}
          {meta.ports?.some(isSecondaryPort) && (
            <div
              className={`border-b px-2.5 py-1.5 text-[10px] ${
                liveSecondary
                  ? "border-emerald-100 bg-emerald-50/80 text-emerald-900"
                  : "border-amber-100 bg-amber-50/80 text-amber-950"
              }`}
            >
              {liveSecondary
                ? "Secondary loop connected"
                : "Secondary ports open — connect Source / Sink, or use rating values"}
            </div>
          )}

          {/* ── Tabs ── */}
          {availableTabs.length > 0 && (
            <div className="flex flex-wrap gap-0 border-b border-[var(--line)] bg-[var(--chrome-2)] px-1">
              {availableTabs.map((t) => (
                <button
                  key={t}
                  type="button"
                  onClick={() => setTab(t)}
                  className={`px-2.5 py-1.5 text-[11px] font-medium transition-colors ${
                    activeTab === t
                      ? "border-b-2 border-[var(--accent)] text-[var(--ink)]"
                      : "text-[var(--ink-faint)] hover:text-[var(--ink-dim)]"
                  }`}
                >
                  {t}
                </button>
              ))}
            </div>
          )}

          {/* ── Parameter table ── */}
          <div className="max-h-[min(48vh,380px)] overflow-y-auto">
            {rows.length === 0 ? (
              <p className="px-3 py-4 text-[11px] text-[var(--ink-faint)]">No parameters in this tab.</p>
            ) : (
              <table className="w-full border-collapse text-[11px]">
                <thead className="sticky top-0 z-[1] bg-[var(--chrome-2)]">
                  <tr className="border-b border-[var(--line)] text-left text-[9px] font-semibold uppercase tracking-wide text-[var(--ink-faint)]">
                    <th className="px-2 py-1.5 font-semibold">Parameter</th>
                    <th className="w-[108px] px-1 py-1.5 font-semibold">Value</th>
                    <th className="w-12 px-1 py-1.5 font-semibold">Unit</th>
                    {showFixedCol && (
                      <th
                        className="w-12 px-1 py-1.5 text-center font-semibold"
                        title="Fixed = imposed · Free = solved"
                      >
                        Fixed
                      </th>
                    )}
                    {showPinCol && (
                      <th
                        className="w-8 px-1 py-1.5 text-center font-semibold"
                        title="Épingler pour le multi-run"
                      >
                        <Pin size={11} className="inline text-[var(--ink-faint)]" />
                      </th>
                    )}
                  </tr>
                </thead>
                <tbody>
                  {rows.map((p) => {
                    const fixed = BOUNDARY_TYPES.has(node.data.type)
                      ? isBoundaryParamFixed(params, p)
                      : isParamFixed(params, p);
                    const free = Boolean(p.fixable && !fixed);
                    const displayVal = effectiveParamValue(params, p);
                    // Probe: label the target row with the chosen measurement kind
                    // (SST / SDT / SH / SC / …) instead of the generic "Cible".
                    const label =
                      node.data.type === "Probe" && p.key === "target"
                        ? (COMPONENT_BY_TYPE["Probe"]?.params
                            .find((mp) => mp.key === "measure")
                            ?.options?.find((o) => o.value === params.measure)?.label ?? p.label)
                        : p.label;
                    return (
                      <tr
                        key={p.key}
                        className="border-b border-[var(--line)]/70 hover:bg-[var(--chrome-2)]/80"
                        title={p.description ?? label}
                      >
                        <td
                          className="max-w-[140px] truncate px-2 py-1 text-[var(--ink-dim)]"
                          title={p.description ?? label}
                        >
                          {label}
                          {p.required ? <span className="text-[var(--hot)]"> *</span> : null}
                        </td>
                        <td className="px-1 py-0.5">
                          <ParamCell
                            node={node}
                            param={p}
                            nodes={nodes}
                            free={free}
                            displayValue={displayVal}
                            updateNodeParams={updateNodeParams}
                          />
                        </td>
                        <td className="mono px-1 py-1 text-[10px] text-[var(--ink-faint)]">
                          {p.unit ?? ""}
                        </td>
                        {showFixedCol && (
                          <td className="px-1 py-1 text-center">
                            {p.fixable ? (
                              <input
                                type="checkbox"
                                checked={fixed}
                                onChange={(e) => {
                                  const checked = e.target.checked;
                                  const seed =
                                    node.data.params[p.key] === undefined &&
                                    p.default !== undefined
                                      ? { [p.key]: p.default }
                                      : {};

                                  if (BOUNDARY_TYPES.has(node.data.type)) {
                                    const patches = boundaryFixPairingPatches(
                                      node.id,
                                      p.key,
                                      checked,
                                      nodes as Node<EntropykNodeData>[],
                                      edges,
                                    );
                                    const self = patches.get(node.id) ?? {};
                                    patches.set(node.id, { ...seed, ...self });
                                    updateNodesParams(patches);
                                  } else {
                                    updateNodeParams(node.id, {
                                      [fixedFlagKey(p.key)]: checked,
                                      ...seed,
                                    });
                                  }
                                }}
                                className="h-3.5 w-3.5 accent-[var(--accent)]"
                                title={
                                  fixed
                                    ? "Fixed : valeur imposée"
                                    : "Free : le solveur calcule (valeur = départ)"
                                }
                              />
                            ) : (
                              <span className="text-[var(--ink-faint)]">·</span>
                            )}
                          </td>
                        )}
                        {showPinCol && (
                          <td className="px-0.5 py-1 text-center align-middle">
                            {p.kind === "number" ? (
                              <button
                                type="button"
                                className="toolish inline-flex items-center justify-center p-0.5"
                                title="Épingler pour le multi-run"
                                aria-label="Épingler pour le multi-run"
                                aria-pressed={pinnedSweepPaths.includes(
                                  `${node.data.name}.${p.key}`,
                                )}
                                onClick={() =>
                                  togglePinnedSweepPath(`${node.data.name}.${p.key}`)
                                }
                              >
                                <Pin
                                  size={13}
                                  className={
                                    pinnedSweepPaths.includes(
                                      `${node.data.name}.${p.key}`,
                                    )
                                      ? "text-[var(--accent)]"
                                      : "text-[var(--ink-faint)]"
                                  }
                                  fill={
                                    pinnedSweepPaths.includes(
                                      `${node.data.name}.${p.key}`,
                                    )
                                      ? "currentColor"
                                      : "none"
                                  }
                                />
                              </button>
                            ) : null}
                          </td>
                        )}
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            )}

            {activeTab === "Calibration" && (
              <div className="space-y-1.5 border-t border-[var(--line)] bg-[var(--chrome-2)] px-2.5 py-1.5 text-[10px] leading-snug text-[var(--ink-dim)]">
                <p>
                  <strong>Fixed</strong> = parameter connu (équation si Probe).{" "}
                  <strong>Free</strong> = inconnue Newton. Pairing : Tsat/Tsh↔z_ua,
                  P↔z_dp, Capacity↔z_flow, T↔f_w. Tous les Z restent disponibles.
                </p>
                <p>
                  Ex. UA nominale × Z_UA → <code>UA_eff</code>. Start = valeur affichée.
                </p>
                {solvedVars.length > 0 && (
                  <SolvedVariablesBlock items={solvedVars} title="Inconnues résolues" />
                )}
              </div>
            )}

            {isRegLoop && (
              <div className="border-t border-[var(--line)] p-2">
                <OverrideEditor node={node} nodes={nodes} updateNodeParams={updateNodeParams} />
              </div>
            )}
          </div>

          {/* ── Doc technique (collapsed by default — never above params) ── */}
          <div className="border-t border-[var(--line)]">
            <button
              type="button"
              onClick={() => setShowHelp((v) => !v)}
              className="flex w-full items-center justify-between px-2.5 py-1.5 text-[10px] font-medium text-[var(--ink-dim)] hover:bg-[var(--chrome-2)]"
            >
              <span className="flex items-center gap-1.5">
                <HelpCircle size={12} />
                Documentation technique
              </span>
              <span className="text-[var(--ink-faint)]">{showHelp ? "−" : "+"}</span>
            </button>
            {showHelp && (
              <ComponentDocPanel componentType={node.data.type} fallbackHelp={meta.help} />
            )}
          </div>
        </>
      )}

      <style jsx>{`
        :global(.param-input) {
          width: 100%;
          border-radius: 3px;
          border: 1px solid var(--line);
          background: #fff;
          padding: 3px 6px;
          font-size: 11px;
          color: var(--ink);
        }
        :global(.param-input:focus) {
          border-color: var(--accent);
          outline: none;
        }
        :global(.param-input.free) {
          border-color: #86efac;
          background: #f0fdf4;
        }
        .toolish {
          border-radius: 4px;
          color: var(--ink-faint);
        }
        .toolish:hover {
          background: var(--chrome-2);
        }
      `}</style>
    </div>
  );
}

function ParamCell({
  node,
  param,
  nodes,
  free,
  displayValue,
  updateNodeParams,
}: {
  node: DiagramNode;
  param: ParamMeta;
  nodes: DiagramNode[];
  free: boolean;
  displayValue: number | string | boolean | undefined;
  updateNodeParams: (id: string, params: Record<string, number | string | boolean>) => void;
}) {
  const update = (next: number | string | boolean) => updateNodeParams(node.id, { [param.key]: next });
  const selectOptions = controllerSelectOptions(node, param.key, nodes);
  const options = selectOptions ?? param.options;
  const cls = free ? "param-input free mono" : "param-input mono";
  const value = displayValue;

  if (options) {
    return (
      <select
        value={String(value ?? param.default ?? options[0]?.value ?? "")}
        onChange={(e) => update(e.target.value)}
        className="param-input w-full"
      >
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
    );
  }

  if (param.kind === "boolean") {
    return (
      <div className="flex justify-end pr-1">
        <input
          type="checkbox"
          checked={Boolean(value)}
          onChange={(e) => update(e.target.checked)}
          className="h-3.5 w-3.5 accent-[var(--accent)]"
        />
      </div>
    );
  }

  const numOrText =
    value === undefined || value === null
      ? ""
      : typeof value === "number" && !Number.isFinite(value)
        ? ""
        : String(value);

  return (
    <input
      type={param.kind === "number" ? "number" : "text"}
      min={param.kind === "number" ? param.min : undefined}
      max={param.kind === "number" ? param.max : undefined}
      step={param.kind === "number" ? (param.step ?? "any") : undefined}
      value={numOrText}
      onChange={(e) => {
        if (param.kind === "number") {
          const n = parseFloat(e.target.value);
          update(Number.isFinite(n) ? n : (param.default as number) ?? 0);
        } else {
          update(e.target.value);
        }
      }}
      className={`${cls} w-full text-right`}
      title={
        free
          ? "Point de départ (paramètre libre)"
          : param.default !== undefined
            ? `Défaut : ${param.default}`
            : undefined
      }
      placeholder={param.default !== undefined ? String(param.default) : undefined}
    />
  );
}

/* ── Saturated controller overrides (kept compact) ───────────────────── */

const CONTROL_OUTPUT_OPTIONS = [
  { value: "temperature", label: "Temperature" },
  { value: "pressure", label: "Pressure" },
  { value: "massFlowRate", label: "Mass flow rate" },
  { value: "capacity", label: "Capacity" },
  { value: "heatTransferRate", label: "Heat transfer rate" },
  { value: "superheat", label: "Superheat" },
  { value: "subcooling", label: "Subcooling" },
  { value: "saturationTemperature", label: "Saturation temperature" },
] as const;

function OverrideEditor({
  node,
  nodes,
  updateNodeParams,
}: {
  node: DiagramNode;
  nodes: DiagramNode[];
  updateNodeParams: (id: string, params: Record<string, number | string | boolean>) => void;
}) {
  const objectives = parseControlObjectives(node.data.params.objectives_json);
  const componentNodes = nodes.filter((c) => c.data.type !== "SaturatedController");
  const write = (next: ControlObjectiveConfig[]) =>
    updateNodeParams(node.id, { objectives_json: JSON.stringify(next) });
  const updateObjective = (index: number, patch: Partial<ControlObjectiveConfig>) =>
    write(objectives.map((o, i) => (i === index ? { ...o, ...patch } : o)));
  const move = (index: number, direction: -1 | 1) => {
    const target = index + direction;
    if (target < 0 || target >= objectives.length) return;
    const next = [...objectives];
    [next[index], next[target]] = [next[target], next[index]];
    write(next);
  };

  return (
    <div className="rounded border border-[var(--line)] bg-white">
      <div className="flex items-center justify-between border-b border-[var(--line)] px-2 py-1.5">
        <span className="text-[10px] font-semibold text-[var(--ink-dim)]">Override chain</span>
        <button
          type="button"
          onClick={() =>
            write([
              ...objectives,
              {
                component: componentNodes[0]?.data.name ?? "component",
                output: "temperature",
                setpoint: 0,
                gain: 1,
                combine: "min",
              },
            ])
          }
          className="flex items-center gap-0.5 text-[10px] font-medium text-[var(--accent)]"
        >
          <Plus size={12} /> Add
        </button>
      </div>
      {objectives.length === 0 ? (
        <p className="px-2 py-2 text-[10px] text-[var(--ink-faint)]">Primary objective only.</p>
      ) : (
        <div className="space-y-1 p-1.5">
          {objectives.map((objective, index) => (
            <div key={index} className="rounded border border-[var(--line)] p-1.5">
              <div className="mb-1 flex justify-end gap-0.5">
                <button type="button" onClick={() => move(index, -1)} className="p-0.5 text-[var(--ink-faint)]">
                  <ArrowUp size={11} />
                </button>
                <button type="button" onClick={() => move(index, 1)} className="p-0.5 text-[var(--ink-faint)]">
                  <ArrowDown size={11} />
                </button>
                <button
                  type="button"
                  onClick={() => write(objectives.filter((_, i) => i !== index))}
                  className="p-0.5 text-[var(--ink-faint)] hover:text-[var(--hot)]"
                >
                  <Trash2 size={11} />
                </button>
              </div>
              <div className="grid grid-cols-2 gap-1">
                <select
                  value={objective.component}
                  onChange={(e) => updateObjective(index, { component: e.target.value })}
                  className="param-input mono"
                >
                  {componentNodes.map((c) => (
                    <option key={c.id} value={c.data.name}>
                      {c.data.name}
                    </option>
                  ))}
                </select>
                <select
                  value={objective.output}
                  onChange={(e) => updateObjective(index, { output: e.target.value })}
                  className="param-input"
                >
                  {CONTROL_OUTPUT_OPTIONS.map((o) => (
                    <option key={o.value} value={o.value}>
                      {o.label}
                    </option>
                  ))}
                </select>
                <input
                  type="number"
                  step="any"
                  value={objective.setpoint}
                  onChange={(e) => updateObjective(index, { setpoint: Number(e.target.value) })}
                  className="param-input mono text-right"
                  title="Setpoint"
                />
                <select
                  value={objective.combine}
                  onChange={(e) =>
                    updateObjective(index, { combine: e.target.value as "min" | "max" })
                  }
                  className="param-input mono"
                >
                  <option value="min">MIN</option>
                  <option value="max">MAX</option>
                </select>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function controllerSelectOptions(
  node: DiagramNode,
  key: string,
  nodes: DiagramNode[],
): Array<{ value: string; label: string }> | null {
  if (node.data.type !== "SaturatedController") return null;
  const componentNodes = nodes.filter((c) => c.data.type !== "SaturatedController");

  if (key === "measure_component" || key === "actuator_component") {
    return componentNodes.map((c) => ({
      value: c.data.name,
      label: `${c.data.name} (${c.data.type})`,
    }));
  }
  if (key === "measure_output") return [...CONTROL_OUTPUT_OPTIONS];
  if (key === "actuator_factor") {
    return [
      { value: "injection", label: "injection" },
      { value: "opening", label: "opening" },
      { value: "z_flow", label: "z_flow" },
      { value: "z_dp", label: "z_dp" },
      { value: "z_ua", label: "z_ua" },
      { value: "z_power", label: "z_power" },
      { value: "z_etav", label: "z_etav" },
    ];
  }
  return null;
}

/* ── Modelica / Dymola Variable Browser for one component ─────────────── */

function ModelicaResultsView({
  inspector,
  hasResult,
  solvedVariables,
}: {
  inspector: ReturnType<typeof buildComponentInspector> | null;
  hasResult: boolean;
  solvedVariables: SolvedVariable[];
}) {
  if (!hasResult || !inspector) {
    return (
      <div className="space-y-2 px-3 py-4 text-[11px] text-[var(--ink-dim)]">
        <p className="font-semibold text-[var(--ink)]">Pas encore de résultats</p>
        <p>
          Lance <strong>Simulate</strong>, puis reclique ce composant — tu verras P, T, Tsat, ṁ, ΔP,
          Q̇ comme dans le Variable Browser Modelica.
        </p>
      </div>
    );
  }

  const { title, typeLabel, summaryLines, ports } = inspector;

  return (
    <div className="max-h-[min(56vh,480px)] space-y-2 overflow-y-auto px-2.5 py-2.5">
      <div className="rounded-md border border-[var(--line)] bg-[var(--chrome-2)] px-2.5 py-2">
        <div className="mono text-[12px] font-semibold text-[var(--ink)]">{title}</div>
        <div className="text-[10px] text-[var(--ink-faint)]">{typeLabel}</div>
        {summaryLines.length > 0 && (
          <div className="mt-1.5 flex flex-wrap gap-1">
            {summaryLines.map((line) => (
              <span
                key={line}
                className="mono rounded bg-white px-1.5 py-0.5 text-[10px] text-[var(--ink-dim)] ring-1 ring-[var(--line)]"
              >
                {line}
              </span>
            ))}
          </div>
        )}
      </div>

      <div className="grid grid-cols-2 gap-1.5">
        {inspector.delta_p_bar != null && (
          <MiniMetric
            label="ΔP"
            value={
              Math.abs(inspector.delta_p_bar) * 100 >= 0.05
                ? `${(inspector.delta_p_bar * 100).toFixed(2)} kPa`
                : `${(inspector.delta_p_bar * 1e5).toFixed(1)} Pa`
            }
          />
        )}
        {inspector.delta_p_sec_bar != null && (
          <MiniMetric
            label="ΔP sec"
            value={
              Math.abs(inspector.delta_p_sec_bar) * 100 >= 0.05
                ? `${(inspector.delta_p_sec_bar * 100).toFixed(2)} kPa`
                : `${(inspector.delta_p_sec_bar * 1e5).toFixed(1)} Pa`
            }
          />
        )}
        {inspector.q_primary_kw != null && (
          <MiniMetric label="Q̇" value={`${Math.abs(inspector.q_primary_kw).toFixed(2)} kW`} />
        )}
        {inspector.q_secondary_kw != null && (
          <MiniMetric
            label="Q̇ sec"
            value={`${Math.abs(inspector.q_secondary_kw).toFixed(2)} kW`}
          />
        )}
        {inspector.work_kw != null && (
          <MiniMetric label="Ẇ" value={`${inspector.work_kw.toFixed(2)} kW`} />
        )}
        {inspector.superheat_k != null && (
          <MiniMetric label="Superheat" value={`${inspector.superheat_k.toFixed(2)} K`} />
        )}
        {inspector.subcooling_k != null && (
          <MiniMetric label="Subcooling" value={`${inspector.subcooling_k.toFixed(2)} K`} />
        )}
      </div>

      {solvedVariables.length > 0 && (
        <SolvedVariablesBlock items={solvedVariables} title="Variables résolues" />
      )}

      <div className="overflow-hidden rounded-md border border-[var(--line)]">
        <div className="border-b border-[var(--line)] bg-[var(--chrome-2)] px-2 py-1 text-[9px] font-semibold uppercase tracking-wide text-[var(--ink-faint)]">
          Ports / variables
        </div>
        {ports.length === 0 ? (
          <p className="px-2 py-3 text-[10px] text-[var(--ink-faint)]">Aucun port câblé.</p>
        ) : (
          <table className="w-full text-[10px]">
            <thead>
              <tr className="text-left text-[9px] text-[var(--ink-faint)]">
                <th className="px-1.5 py-1 font-medium">Port</th>
                <th className="px-1 py-1 font-medium">P</th>
                <th className="px-1 py-1 font-medium">T</th>
                <th className="px-1 py-1 font-medium">Tsat</th>
                <th className="px-1 py-1 font-medium">h</th>
                <th className="px-1 py-1 font-medium">ṁ</th>
              </tr>
            </thead>
            <tbody>
              {ports.map((p) => (
                <tr key={`${p.port}-${p.edgeIndex}`} className="border-t border-[var(--line)] mono">
                  <td className="px-1.5 py-1 text-[var(--ink)]">
                    {p.port}
                    <span className="ml-1 text-[8px] text-[var(--ink-faint)]">
                      {p.stream === "secondary" ? "sec" : p.role}
                    </span>
                  </td>
                  <td className="px-1 py-1">{fmtOpt(p.pressure_bar, 3)}</td>
                  <td className="px-1 py-1">{fmtOpt(p.temperature_c, 1)}</td>
                  <td className="px-1 py-1">{fmtOpt(p.tsat_c, 1)}</td>
                  <td className="px-1 py-1">{fmtOpt(p.enthalpy_kj_kg, 1)}</td>
                  <td className="px-1 py-1">{fmtOpt(p.mass_flow_kg_s, 4)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
        <div className="border-t border-[var(--line)] px-2 py-1 text-[8px] text-[var(--ink-faint)]">
          Unités : P [bar] · T/Tsat [°C] · h [kJ/kg] · ṁ [kg/s]
        </div>
      </div>
    </div>
  );
}

function MiniMetric({ label, value }: { label: string; value: string }) {
  return (
    <div className="rounded border border-[var(--line)] bg-[var(--chrome-2)] px-1.5 py-1">
      <div className="text-[8px] uppercase tracking-wide text-[var(--ink-faint)]">{label}</div>
      <div className="mono text-[11px] text-[var(--ink)]">{value}</div>
    </div>
  );
}

function fmtOpt(v: number | null | undefined, digits: number): string {
  return typeof v === "number" && Number.isFinite(v) ? v.toFixed(digits) : "—";
}

/**
 * Formats a solved-unknown value compactly: 4 significant digits with trailing
 * zeros trimmed (e.g. 1.024, 0.62, 0.02, 2). Non-finite → "—".
 */
function fmtSolved(v: number): string {
  if (!Number.isFinite(v)) return "—";
  return Number(v.toPrecision(4)).toString();
}

/**
 * Compact block listing solver-computed unknowns for the selected component.
 * Renders one row per variable: `variable = value (min..max)`. Only shown when
 * the component owns at least one solved variable.
 */
function SolvedVariablesBlock({
  items,
  title,
}: {
  items: SolvedVariable[];
  title: string;
}) {
  if (items.length === 0) return null;
  return (
    <div className="rounded-md border border-[var(--line)] bg-[var(--chrome-2)] px-2 py-1.5">
      <div className="mb-1 text-[9px] font-semibold uppercase tracking-wide text-[var(--ink-faint)]">
        {title}
      </div>
      <div className="space-y-0.5">
        {items.map((sv) => (
          <div
            key={sv.id}
            className="mono flex items-baseline gap-1.5 text-[10px]"
            title={`${sv.id} ∈ [${sv.min}, ${sv.max}]`}
          >
            <span className="text-[var(--ink)]">{sv.variable}</span>
            <span className="text-[var(--ink-faint)]">=</span>
            <span className="tabular-nums text-[var(--accent)]">{fmtSolved(sv.value)}</span>
            <span className="tabular-nums text-[var(--ink-faint)]">
              ({fmtSolved(sv.min)}..{fmtSolved(sv.max)})
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}
