/**
 * Modelica.Fluid.Sources Fixed/Free — single source of truth.
 *
 * PROOF (official MSL + Modelica community):
 * 1) Package index:
 *    https://doc.modelica.org/om/Modelica.Fluid.Sources.html
 *    — Boundary_pT | Boundary_ph | MassFlowSource_T | MassFlowSource_h
 * 2) Boundary_pT (prescribes P + T, NOT ṁ):
 *    https://doc.modelica.org/om/Modelica.Fluid.Sources.Boundary_pT.html
 * 3) MassFlowSource_T (prescribes ṁ + T, NOT P):
 *    https://doc.modelica.org/Modelica%204.0.0/Resources/helpWSM/Modelica/Modelica.Fluid.Sources.MassFlowSource_T.html
 * 4) MSL example SimplePipeline: Boundary_pT → StaticPipe(ΔP) → Boundary_pT
 *    (Context7 /modelica/modelicastandardlibrary) — double Fixed P ONLY with hydraulic ΔP.
 * 5) Legal pipe-end combinations (Rene Just Nielsen):
 *    https://stackoverflow.com/questions/79349553/how-pressure-and-flow-ports-are-different-in-modelica
 *    — Boundary_pT+Boundary_pT | Boundary_pT+MassFlowSource | MassFlowSource+Boundary_pT
 *    — ILLEGAL: MassFlowSource + MassFlowSource
 *    — "no boundary component specifying both mass flow rate and pressure"
 *
 * Entropyk mapping (secondary water/air branch):
 *   MassFlowSource_T  → Source: Fixed T, Fixed ṁ, Free P ; Sink: Fixed P (anchor)
 *   Boundary_pT       → Source: Fixed P, Fixed T, Free ṁ ; Sink: Fixed P if ΔP else one P free
 *   Illegal           → Fixed ṁ + Fixed P on same Source
 *                   → Fixed ṁ + Fixed T_out on partner Sink
 *                   → Fixed P both ends + Fixed ṁ (MassFlowSource + double P)
 */

import type { Edge, Node } from "@xyflow/react";
import {
  COMPONENT_BY_TYPE,
  FIXED_FLAG_PREFIX,
  fixedFlagKey,
  type ParamMeta,
} from "./componentMeta";

export interface BoundaryNodeData {
  type: string;
  name: string;
  params: Record<string, number | string | boolean>;
  [key: string]: unknown;
}

export const CLI_FIX_FLAG: Record<string, string> = {
  p_set_bar: "fix_pressure",
  p_back_bar: "fix_pressure",
  t_set_c: "fix_temperature",
  t_dry_c: "fix_temperature",
  t_back_c: "fix_temperature",
  m_flow_kg_s: "fix_mass_flow",
};

export function cliFixFlagForParam(paramKey: string): string | undefined {
  return CLI_FIX_FLAG[paramKey];
}

export function isBoundaryParamFixed(
  params: Record<string, number | string | boolean | undefined>,
  meta: ParamMeta,
): boolean {
  if (!meta.fixable) return true;

  const uiFlag = params[fixedFlagKey(meta.key)];
  if (uiFlag === true || uiFlag === "true") return true;
  if (uiFlag === false || uiFlag === "false") return false;

  const cliKey = cliFixFlagForParam(meta.key);
  if (cliKey && params[cliKey] !== undefined) {
    const v = params[cliKey];
    if (v === true || v === "true") return true;
    if (v === false || v === "false") return false;
  }

  return meta.defaultFixed !== false;
}

export function hydrateBoundaryFixFlags(
  type: string,
  params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
  const meta = COMPONENT_BY_TYPE[type];
  if (!meta) return params;
  const out: Record<string, number | string | boolean> = { ...params };

  for (const p of meta.params) {
    if (!p.fixable) continue;
    const cliKey = cliFixFlagForParam(p.key);
    if (!cliKey || out[cliKey] === undefined) continue;
    const v = out[cliKey];
    out[fixedFlagKey(p.key)] = v === true || v === "true";
    delete out[cliKey];
  }

  // Legacy sink: t_set present without flag ⇒ Fixed T_out.
  if (type === "BrineSink" || type === "AirSink") {
    const tKey = type === "AirSink" ? "t_back_c" : "t_set_c";
    const flagKey = fixedFlagKey(tKey);
    if (out[flagKey] === undefined && out[tKey] !== undefined && out[tKey] !== "") {
      out[flagKey] = true;
    }
  }

  // Modelica MassFlowSource_T: cannot Fixed P and Fixed ṁ together.
  // If imported JSON has ṁ without fix_pressure=false, Free P on source.
  if (type === "BrineSource" || type === "AirSource") {
    const mMeta = meta.params.find((p) => p.key === "m_flow_kg_s");
    const pMeta = meta.params.find((p) => p.key === "p_set_bar");
    if (mMeta && pMeta && isBoundaryParamFixed(out, mMeta)) {
      const pFlag = fixedFlagKey("p_set_bar");
      if (out[pFlag] === undefined && out.fix_pressure === undefined) {
        // Default MassFlowSource: Free P unless explicitly Fixed without ṁ.
        out[pFlag] = false;
      }
      if (isBoundaryParamFixed(out, mMeta) && isBoundaryParamFixed(out, pMeta)) {
        out[pFlag] = false;
      }
    }
  }

  return out;
}

const SECONDARY_TRAVERSABLE = (type: string): boolean =>
  type === "BrineSource" ||
  type === "BrineSink" ||
  type === "AirSource" ||
  type === "AirSink" ||
  type.includes("Pipe") ||
  type.includes("Pump") ||
  type.includes("Fan") ||
  type.includes("Condenser") ||
  type.includes("Evaporator") ||
  type.includes("HeatExchanger") ||
  type.includes("Bphx") ||
  type === "FloodedEvaporator" ||
  type === "ThermalLoad";

/** HX types whose secondary side is typically isobaric (no water ΔP residual). */
const ISOBARIC_SECONDARY = (type: string): boolean =>
  type === "FloodedEvaporator" ||
  type === "FloodedCondenser" ||
  (type.includes("Evaporator") && !type.includes("Bphx")) ||
  (type.includes("Condenser") && type !== "BphxCondenser" && !type.includes("Mchx"));

export function findConnectedSecondaryBoundary(
  startId: string,
  nodes: Node<BoundaryNodeData>[],
  edges: Edge[],
  targetType: "BrineSource" | "BrineSink" | "AirSource" | "AirSink",
): Node<BoundaryNodeData> | null {
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
  const adj = new Map<string, string[]>();
  for (const e of edges) {
    if (!adj.has(e.source)) adj.set(e.source, []);
    if (!adj.has(e.target)) adj.set(e.target, []);
    adj.get(e.source)!.push(e.target);
    adj.get(e.target)!.push(e.source);
  }
  const seen = new Set<string>([startId]);
  const queue = [startId];
  while (queue.length > 0) {
    const id = queue.shift()!;
    for (const next of adj.get(id) ?? []) {
      if (seen.has(next)) continue;
      seen.add(next);
      const n = nodeById.get(next);
      if (!n) continue;
      if (n.data.type === targetType) return n;
      if (SECONDARY_TRAVERSABLE(n.data.type)) queue.push(next);
    }
  }
  return null;
}

/** True if path Source→Sink crosses an isobaric secondary HX (no water ΔP). */
function pathHasIsobaricSecondary(
  sourceId: string,
  sinkId: string,
  nodes: Node<BoundaryNodeData>[],
  edges: Edge[],
): boolean {
  const nodeById = new Map(nodes.map((n) => [n.id, n]));
  const adj = new Map<string, string[]>();
  for (const e of edges) {
    if (!adj.has(e.source)) adj.set(e.source, []);
    if (!adj.has(e.target)) adj.set(e.target, []);
    adj.get(e.source)!.push(e.target);
    adj.get(e.target)!.push(e.source);
  }
  const seen = new Set<string>([sourceId]);
  const queue = [sourceId];
  let isobaric = false;
  while (queue.length > 0) {
    const id = queue.shift()!;
    if (id === sinkId) return isobaric;
    for (const next of adj.get(id) ?? []) {
      if (seen.has(next)) continue;
      seen.add(next);
      const n = nodeById.get(next);
      if (!n) continue;
      if (ISOBARIC_SECONDARY(n.data.type)) isobaric = true;
      if (SECONDARY_TRAVERSABLE(n.data.type) || n.id === sinkId) queue.push(next);
    }
  }
  return false;
}

export interface SecondaryLoopDofConflict {
  sourceName: string;
  sinkName: string;
  message: string;
}

export function findSecondaryLoopDofConflicts(
  nodes: Node<BoundaryNodeData>[],
  edges: Edge[],
): SecondaryLoopDofConflict[] {
  const conflicts: SecondaryLoopDofConflict[] = [];

  for (const source of nodes) {
    if (source.data.type !== "BrineSource" && source.data.type !== "AirSource") continue;
    const pMeta = COMPONENT_BY_TYPE[source.data.type]?.params.find((x) => x.key === "p_set_bar");
    const mMeta = COMPONENT_BY_TYPE[source.data.type]?.params.find((x) => x.key === "m_flow_kg_s");
    if (!pMeta || !mMeta) continue;

    // MassFlowSource cannot Fixed P and Fixed ṁ (Modelica local balance).
    if (isBoundaryParamFixed(source.data.params, pMeta) && isBoundaryParamFixed(source.data.params, mMeta)) {
      conflicts.push({
        sourceName: source.data.name,
        sinkName: "",
        message:
          `${source.data.name}: Fixed P + Fixed ṁ illegal (Modelica MassFlowSource_T ` +
          `prescribes ṁ+T only — Free P). See Modelica.Fluid.Sources.MassFlowSource_T.`,
      });
    }

    const sinkType = source.data.type === "AirSource" ? "AirSink" : "BrineSink";
    const sink = findConnectedSecondaryBoundary(source.id, nodes, edges, sinkType);
    if (!sink) continue;

    const sinkP = COMPONENT_BY_TYPE[sinkType]?.params.find((x) => x.key === "p_back_bar");
    const tKey = sinkType === "AirSink" ? "t_back_c" : "t_set_c";
    const sinkT = COMPONENT_BY_TYPE[sinkType]?.params.find((x) => x.key === tKey);

    // Fixed ṁ + Fixed T_out
    if (
      sinkT &&
      isBoundaryParamFixed(source.data.params, mMeta) &&
      isBoundaryParamFixed(sink.data.params, sinkT) &&
      sink.data.params[tKey] !== undefined &&
      sink.data.params[tKey] !== ""
    ) {
      conflicts.push({
        sourceName: source.data.name,
        sinkName: sink.data.name,
        message:
          `${sink.data.name}: Fixed T_out + ${source.data.name} Fixed ṁ — ` +
          `use Boundary_pT (Free ṁ on source).`,
      });
    }

    // Fixed P both ends + Fixed ṁ, or Fixed P both ends on isobaric secondary
    if (
      sinkP &&
      isBoundaryParamFixed(source.data.params, pMeta) &&
      isBoundaryParamFixed(sink.data.params, sinkP)
    ) {
      const mFixed = isBoundaryParamFixed(source.data.params, mMeta);
      const isobaric = pathHasIsobaricSecondary(source.id, sink.id, nodes, edges);
      if (mFixed) {
        conflicts.push({
          sourceName: source.data.name,
          sinkName: sink.data.name,
          message:
            `${source.data.name}/${sink.data.name}: Fixed P both ends + Fixed ṁ — ` +
            `Modelica: MassFlowSource_T + Boundary_p (Free P on source, Fixed P on sink only).`,
        });
      } else if (isobaric) {
        conflicts.push({
          sourceName: source.data.name,
          sinkName: sink.data.name,
          message:
            `${source.data.name}/${sink.data.name}: Fixed P both ends on isobaric secondary — ` +
            `Modelica allows Boundary_pT×2 only with hydraulic ΔP (pipe/friction). ` +
            `Free P on source OR add WaterPipe ΔP.`,
        });
      }
    }
  }

  return conflicts;
}

/**
 * Pairing patches (atomic store apply).
 * Modelica rules when toggling Fixed.
 */
export function boundaryFixPairingPatches(
  nodeId: string,
  paramKey: string,
  fixed: boolean,
  nodes: Node<BoundaryNodeData>[],
  edges: Edge[],
): Map<string, Record<string, number | string | boolean>> {
  const patches = new Map<string, Record<string, number | string | boolean>>();
  const node = nodes.find((n) => n.id === nodeId);
  if (!node) return patches;

  const self: Record<string, number | string | boolean> = {
    [fixedFlagKey(paramKey)]: fixed,
  };
  patches.set(nodeId, self);

  const isSource = node.data.type === "BrineSource" || node.data.type === "AirSource";
  const isSink = node.data.type === "BrineSink" || node.data.type === "AirSink";

  // MassFlowSource_T: Fixed ṁ ⇒ Free P on same source (cannot Fixed both).
  if (isSource && paramKey === "m_flow_kg_s" && fixed) {
    self[fixedFlagKey("p_set_bar")] = false;
    const sinkType = node.data.type === "AirSource" ? "AirSink" : "BrineSink";
    const partner = findConnectedSecondaryBoundary(nodeId, nodes, edges, sinkType);
    if (partner) {
      const tKey = sinkType === "AirSink" ? "t_back_c" : "t_set_c";
      patches.set(partner.id, { [fixedFlagKey(tKey)]: false });
    }
  }

  // Boundary_pT on source: Fixed P ⇒ Free ṁ (cannot Fixed both).
  if (isSource && paramKey === "p_set_bar" && fixed) {
    self[fixedFlagKey("m_flow_kg_s")] = false;
  }

  // Fixed T_out on Sink ⇒ Free ṁ on Source (energy closes ṁ).
  if (isSink && (paramKey === "t_set_c" || paramKey === "t_back_c") && fixed) {
    const srcType = node.data.type === "AirSink" ? "AirSource" : "BrineSource";
    const partner = findConnectedSecondaryBoundary(nodeId, nodes, edges, srcType);
    if (partner) {
      patches.set(partner.id, {
        [fixedFlagKey("m_flow_kg_s")]: false,
        // Boundary_pT mode: keep / restore Fixed P on source when freeing ṁ
        [fixedFlagKey("p_set_bar")]: true,
      });
    }
  }

  // Fixed P on Sink while Source has Fixed ṁ ⇒ ensure Source Free P (MassFlowSource+Boundary_p).
  if (isSink && paramKey === "p_back_bar" && fixed) {
    const srcType = node.data.type === "AirSink" ? "AirSource" : "BrineSource";
    const partner = findConnectedSecondaryBoundary(nodeId, nodes, edges, srcType);
    if (partner) {
      const mMeta = COMPONENT_BY_TYPE[srcType]?.params.find((p) => p.key === "m_flow_kg_s");
      if (mMeta && isBoundaryParamFixed(partner.data.params, mMeta)) {
        const prev = patches.get(partner.id) ?? {};
        patches.set(partner.id, { ...prev, [fixedFlagKey("p_set_bar")]: false });
      }
    }
  }

  return patches;
}

/**
 * Emit-time compiler: force Modelica-legal fix_* on the JSON sent to CLI.
 *
 * MassFlowSource_T:  Fixed ṁ + Fixed T + Free P ; Sink Fixed P
 * Boundary_pT:       Fixed P + Fixed T + Free ṁ ; one P-anchor if isobaric
 */
export function enforceModelicaBoundaryEmit(
  components: Record<string, unknown>[],
  nodes: Node<BoundaryNodeData>[],
  edges: Edge[],
): void {
  const byName = new Map(components.map((c) => [String(c.name), c]));

  for (const sourceNode of nodes) {
    if (sourceNode.data.type !== "BrineSource" && sourceNode.data.type !== "AirSource") {
      continue;
    }
    const sourceComp = byName.get(sourceNode.data.name);
    if (!sourceComp) continue;

    const sinkType = sourceNode.data.type === "AirSource" ? "AirSink" : "BrineSink";
    const sinkNode = findConnectedSecondaryBoundary(sourceNode.id, nodes, edges, sinkType);
    const sinkComp = sinkNode ? byName.get(sinkNode.data.name) : undefined;
    const isobaric =
      !!sinkNode && pathHasIsobaricSecondary(sourceNode.id, sinkNode.id, nodes, edges);

    // Fixed T_out on sink ⇒ Free ṁ (energy closes ṁ)
    if (sinkComp?.fix_temperature === true) {
      sourceComp.fix_mass_flow = false;
    }

    // Never Fixed P and Fixed ṁ on the same source (Modelica local balance)
    if (sourceComp.fix_mass_flow === true) {
      sourceComp.fix_pressure = false;
    }

    // Sink is the pressure anchor when present
    if (sinkComp) {
      if (sinkComp.fix_pressure === undefined) sinkComp.fix_pressure = true;

      // Isobaric secondary: only one P Dirichlet (sink). Source Free P.
      // (Boundary_pT×2 requires hydraulic ΔP — MSL SimplePipeline + StaticPipe.)
      if (isobaric && sinkComp.fix_pressure !== false) {
        sourceComp.fix_pressure = false;
      }
    }
  }
}

export function stripCliFixFlags(
  params: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
  const out: Record<string, number | string | boolean> = {};
  for (const [k, v] of Object.entries(params)) {
    if (k === "fix_pressure" || k === "fix_temperature" || k === "fix_mass_flow") continue;
    if (k.startsWith(FIXED_FLAG_PREFIX)) {
      out[k] = v;
      continue;
    }
    out[k] = v;
  }
  return out;
}
