/**
 * Modelica / Dymola-style post-solve variables for a selected component.
 * Built from diagram wiring + enriched `SimulationResult.state` edges.
 */

import type { Edge, Node } from "@xyflow/react";
import type { EntropykNodeData } from "./configBuilder";
import { COMPONENT_BY_TYPE, isSecondaryPort } from "./componentMeta";
import type { SimulationResult, SolvedVariable } from "./api";

export interface PortVariable {
  port: string;
  role: "in" | "out";
  stream: "primary" | "secondary" | "other";
  pressure_bar: number | null;
  temperature_c: number | null;
  tsat_c: number | null;
  enthalpy_kj_kg: number | null;
  mass_flow_kg_s: number | null;
  edgeIndex: number;
}

export interface ComponentInspector {
  title: string;
  typeLabel: string;
  type: string;
  status: string | null;
  ports: PortVariable[];
  /** Primary-side pressure drop [bar] (in − out). */
  delta_p_bar: number | null;
  /** Secondary-side pressure drop [bar]. */
  delta_p_sec_bar: number | null;
  /** m·Δh on primary [kW]. */
  q_primary_kw: number | null;
  /** m·Δh on secondary [kW]. */
  q_secondary_kw: number | null;
  /** |W| shaft estimate for compressors/fans/pumps [kW]. */
  work_kw: number | null;
  superheat_k: number | null;
  subcooling_k: number | null;
  summaryLines: string[];
}

type StateEntry = NonNullable<SimulationResult["state"]>[number];

function streamOf(port: string): PortVariable["stream"] {
  if (isSecondaryPort(port)) return "secondary";
  if (/inlet|outlet|suction|discharge/i.test(port)) return "primary";
  return "other";
}

function roleOf(port: string, isSource: boolean): PortVariable["role"] {
  if (/outlet|discharge/i.test(port)) return "out";
  if (/inlet|suction/i.test(port)) return "in";
  return isSource ? "out" : "in";
}

function findStateForEdge(
  edgeIndex: number,
  edge: Edge,
  nodes: Node<EntropykNodeData>[],
  state: StateEntry[] | undefined,
): StateEntry | undefined {
  if (!state?.length) return undefined;
  const byIndex = state[edgeIndex];
  if (byIndex) return byIndex;

  const src = nodes.find((n) => n.id === edge.source)?.data.name;
  const tgt = nodes.find((n) => n.id === edge.target)?.data.name;
  if (!src || !tgt) return undefined;
  return state.find((s) => s.source === src && s.target === tgt);
}

/** Build Modelica-style variables for the selected component after a solve. */
export function buildComponentInspector(
  node: Node<EntropykNodeData>,
  nodes: Node<EntropykNodeData>[],
  edges: Edge[],
  result: SimulationResult | null,
): ComponentInspector {
  const meta = COMPONENT_BY_TYPE[node.data.type];
  const title = node.data.name;
  const typeLabel =
    node.data.type === "ModuleInstance"
      ? `Sous-système Module (${String(node.data.params.module_name ?? "ekmod")})`
      : (meta?.label ?? node.data.type);
  const status = result ? String(result.status) : null;
  const state = result?.state;

  const ports: PortVariable[] = [];
  edges.forEach((edge, edgeIndex) => {
    const touches =
      edge.source === node.id || edge.target === node.id;
    if (!touches) return;
    const isSource = edge.source === node.id;
    const handle = isSource
      ? (edge.sourceHandle ?? "outlet")
      : (edge.targetHandle ?? "inlet");
    const entry = findStateForEdge(edgeIndex, edge, nodes, state);
    const stream = streamOf(handle);
    // Infer secondary from partner name when handle is plain outlet (BrineSource).
    const partnerName = isSource
      ? nodes.find((n) => n.id === edge.target)?.data.name ?? ""
      : nodes.find((n) => n.id === edge.source)?.data.name ?? "";
    const selfName = node.data.name;
    const looksWater =
      stream === "secondary" ||
      /water|brine|glycol/i.test(`${selfName} ${partnerName}`) ||
      /Brine|WaterPipe/i.test(node.data.type);
    const looksAir =
      /air|duct/i.test(`${selfName} ${partnerName}`) || /AirSource|AirSink|AirDuct/i.test(node.data.type);

    ports.push({
      port: handle,
      role: roleOf(handle, isSource),
      stream: looksAir ? "other" : looksWater ? "secondary" : stream,
      pressure_bar: entry?.pressure_bar ?? null,
      temperature_c: entry?.temperature_c ?? null,
      // Tsat only meaningful on refrigerant — hide for water/air loops.
      tsat_c:
        looksWater || looksAir ? null : (entry?.saturation_temperature_c ?? null),
      enthalpy_kj_kg: entry?.enthalpy_kj_kg ?? null,
      mass_flow_kg_s: entry?.mass_flow_kg_s ?? null,
      edgeIndex,
    });
  });

  // Prefer meta port order
  const order = meta?.ports ?? [];
  ports.sort((a, b) => {
    const ia = order.indexOf(a.port);
    const ib = order.indexOf(b.port);
    if (ia >= 0 && ib >= 0) return ia - ib;
    if (ia >= 0) return -1;
    if (ib >= 0) return 1;
    return a.port.localeCompare(b.port);
  });

  const primaryIn = ports.find((p) => p.stream === "primary" && p.role === "in");
  const primaryOut = ports.find((p) => p.stream === "primary" && p.role === "out");
  const secIn = ports.find((p) => p.stream === "secondary" && p.role === "in");
  const secOut = ports.find((p) => p.stream === "secondary" && p.role === "out");

  const delta_p_bar =
    primaryIn?.pressure_bar != null && primaryOut?.pressure_bar != null
      ? primaryIn.pressure_bar - primaryOut.pressure_bar
      : null;
  const delta_p_sec_bar =
    secIn?.pressure_bar != null && secOut?.pressure_bar != null
      ? secIn.pressure_bar - secOut.pressure_bar
      : null;

  const q_primary_kw = streamDutyKw(primaryIn, primaryOut);
  const q_secondary_kw = streamDutyKw(secIn, secOut);

  const isMachine = /Compressor|Pump|Fan|Centrifugal/i.test(node.data.type);
  const work_kw = isMachine && q_primary_kw != null ? Math.abs(q_primary_kw) : null;

  let superheat_k: number | null = null;
  let subcooling_k: number | null = null;
  if (
    primaryOut?.temperature_c != null &&
    primaryOut.tsat_c != null &&
    /Evaporator|Flooded|BphxEvap/i.test(node.data.type)
  ) {
    superheat_k = primaryOut.temperature_c - primaryOut.tsat_c;
  }
  if (
    primaryOut?.temperature_c != null &&
    primaryOut.tsat_c != null &&
    /Condenser|BphxCond|GasCooler/i.test(node.data.type)
  ) {
    subcooling_k = primaryOut.tsat_c - primaryOut.temperature_c;
  }
  // Suction superheat at compressor inlet
  if (
    primaryIn?.temperature_c != null &&
    primaryIn.tsat_c != null &&
    /Compressor/i.test(node.data.type)
  ) {
    superheat_k = primaryIn.temperature_c - primaryIn.tsat_c;
  }

  const summaryLines: string[] = [];
  if (delta_p_bar != null) {
    const dp_kpa = delta_p_bar * 100.0;
    summaryLines.push(
      Math.abs(dp_kpa) >= 0.05
        ? `ΔP = ${fmt(dp_kpa, 2)} kPa`
        : `ΔP = ${fmt(delta_p_bar * 1e5, 1)} Pa`,
    );
  }
  if (delta_p_sec_bar != null) {
    const dp_kpa = delta_p_sec_bar * 100.0;
    summaryLines.push(
      Math.abs(dp_kpa) >= 0.05
        ? `ΔP sec = ${fmt(dp_kpa, 2)} kPa`
        : `ΔP sec = ${fmt(delta_p_sec_bar * 1e5, 1)} Pa`,
    );
  }
  if (q_primary_kw != null) summaryLines.push(`Q̇ = ${fmt(Math.abs(q_primary_kw), 2)} kW`);
  if (q_secondary_kw != null) summaryLines.push(`Q̇ sec = ${fmt(Math.abs(q_secondary_kw), 2)} kW`);
  if (work_kw != null) summaryLines.push(`Ẇ = ${fmt(work_kw, 2)} kW`);
  if (superheat_k != null) summaryLines.push(`SH = ${fmt(superheat_k, 2)} K`);
  if (subcooling_k != null) summaryLines.push(`SC = ${fmt(subcooling_k, 2)} K`);
  const m = primaryIn?.mass_flow_kg_s ?? primaryOut?.mass_flow_kg_s;
  if (m != null) summaryLines.push(`ṁ = ${fmt(m, 4)} kg/s`);

  return {
    title,
    typeLabel,
    type: node.data.type,
    status,
    ports,
    delta_p_bar,
    delta_p_sec_bar,
    q_primary_kw,
    q_secondary_kw,
    work_kw,
    superheat_k,
    subcooling_k,
    summaryLines,
  };
}

function streamDutyKw(
  inn: PortVariable | undefined,
  out: PortVariable | undefined,
): number | null {
  if (!inn || !out) return null;
  const m = inn.mass_flow_kg_s ?? out.mass_flow_kg_s;
  const hIn = inn.enthalpy_kj_kg;
  const hOut = out.enthalpy_kj_kg;
  if (m == null || hIn == null || hOut == null) return null;
  // kW = kg/s · kJ/kg
  return m * (hOut - hIn);
}

function fmt(v: number, digits: number): string {
  return Number.isFinite(v) ? v.toFixed(digits) : "—";
}

/**
 * Returns the solver-computed unknowns (free actuators + calibration factors)
 * attached to a given component, filtered out of `result.solved_variables`.
 *
 * Returns an empty array when there is no result or the component owns no
 * solved variable. Globals (component === null) are excluded here — they are
 * not tied to a specific diagram node.
 */
export function getSolvedVariablesForComponent(
  result: SimulationResult | null,
  componentName: string,
): SolvedVariable[] {
  if (!result?.solved_variables?.length) return [];
  return result.solved_variables.filter(
    (sv) => sv.component != null && sv.component === componentName,
  );
}
