/**
 * Actionable DoF coaching — turns ledger diagnostics into ordered next steps
 * (guide, not just a red/green light).
 */

import type { Edge, Node } from "@xyflow/react";
import type { EntropykNodeData } from "./configBuilder";
import {
  computeDofLedger,
  type DofBalance,
  type DofLedger,
} from "./dofLedger";
import { COMPONENT_BY_TYPE } from "./componentMeta";
import { isBoundaryParamFixed } from "./boundaryFix";
import { hxFamily } from "./hxFamily";

export type CoachSeverity = "ok" | "tip" | "warn" | "block";

export interface CoachTip {
  id: string;
  severity: CoachSeverity;
  /** Short headline */
  title: string;
  /** What to do next */
  action: string;
  /** Optional component name to highlight */
  focus?: string;
}

export interface DofCoach {
  balance: DofBalance;
  headline: string;
  tips: CoachTip[];
  ledger: DofLedger;
}

export function buildDofCoach(
  nodes: Node<EntropykNodeData>[],
  edges: Edge[],
): DofCoach {
  const ledger = computeDofLedger(nodes, edges);
  const tips: CoachTip[] = [];

  if (ledger.balance === "empty") {
    return {
      balance: "empty",
      headline: "Glisse des composants depuis la bibliothèque pour commencer.",
      tips: [
        {
          id: "empty",
          severity: "tip",
          title: "Feuille vide",
          action:
            "Exemple : Compresseur → Condenseur → Détendeur → Évaporateur, puis boucles eau/air Source→HX→Sink.",
        },
      ],
      ledger,
    };
  }

  // Helper for safe node type retrieval
  const getNodeType = (n: Node<EntropykNodeData>): string =>
    String(n.data?.type || n.data?.kind || "");

  // Topology coaching
  const types = new Set(nodes.map((n) => getNodeType(n)));
  const hasComp = [...types].some((t) => t.includes("Compressor"));
  const hasExv = [...types].some((t) => t.includes("Valve") || t.includes("Expansion"));
  const hasCond = [...types].some((t) => t.includes("Condenser"));
  const hasEvap = [...types].some((t) => t.includes("Evaporator"));

  if (hasComp && hasCond && hasEvap && hasExv) {
    const openHx = nodes.filter((n) => {
      const type = getNodeType(n);
      const fam = hxFamily(type);
      if (!fam) return false;
      const meta = COMPONENT_BY_TYPE[type];
      if (!meta) return false;
      const secPorts = meta.ports.filter((p) => p.includes("secondary"));
      if (secPorts.length < 2) return false;
      const wired = edges.some(
        (e) =>
          (e.source === n.id || e.target === n.id) &&
          (String(e.sourceHandle).includes("secondary") ||
            String(e.targetHandle).includes("secondary")),
      );
      return !wired && !truthy(n.data.params?.secondary_mass_flow_kg_s);
    });
    for (const n of openHx.slice(0, 2)) {
      const type = getNodeType(n);
      const fam = hxFamily(type);
      tips.push({
        id: `sec-${n.id}`,
        severity: "tip",
        title: `${n.data.name} — secondaire non câblé`,
        action: fam
          ? `Relie BrineSource/AirSource → secondary_in → secondary_out → Sink (${fam.label}).`
          : "Relie une boucle Source → secondary → Sink.",
        focus: n.data.name,
      });
    }
  }

  // Boundary Fixed/Free coaching
  for (const n of nodes) {
    const type = getNodeType(n);
    if (!type.endsWith("Source")) continue;
    const p = n.data.params ?? {};
    const meta = COMPONENT_BY_TYPE[type];
    if (!meta) continue;
    const pMeta = meta.params.find((x) => x.key === "p_set_bar");
    const mMeta = meta.params.find((x) => x.key === "m_flow_kg_s");
    if (!pMeta || !mMeta) continue;
    const fixP = isBoundaryParamFixed(p, pMeta);
    const fixM =
      isBoundaryParamFixed(p, mMeta) &&
      p.m_flow_kg_s !== undefined &&
      p.m_flow_kg_s !== "" &&
      Number(p.m_flow_kg_s) > 0;
    if (fixP && fixM) {
      tips.push({
        id: `mflow-p-${n.id}`,
        severity: "warn",
        title: `${n.data.name} — Fixed P + Fixed ṁ`,
        action:
          "Modelica MassFlowSource_T : décoche Fixed sur P (garde ṁ + T). Le Sink ancre la pression.",
        focus: n.data.name,
      });
    }
  }

  // Air humidity tip
  for (const n of nodes) {
    const type = getNodeType(n);
    if (!type.includes("Condenser") && !type.includes("Evaporator")) continue;
    if (String(n.data.params?.secondary_fluid ?? "").toLowerCase() !== "air") continue;
    const w = n.data.params?.secondary_humidity_ratio;
    if (w === undefined || w === "" || Number(w) <= 0) {
      tips.push({
        id: `w-${n.id}`,
        severity: "warn",
        title: `${n.data.name} — W air manquant`,
        action:
          "Renseigne secondary_humidity_ratio (= W de l’AirSource, ex. 0.014 à 35 °C / 40 % RH) sinon le solveur peut diverger.",
        focus: n.data.name,
      });
    }
  }

  // Ledger diagnostics → tips
  for (const [i, d] of ledger.diagnostics.slice(0, 8).entries()) {
    const sev: CoachSeverity =
      ledger.balance === "balanced" && !d.startsWith("Over") && !d.startsWith("Under")
        ? "tip"
        : "warn";
    tips.push({
      id: `diag-${i}`,
      severity: sev,
      title: shortenDiagTitle(d),
      action: d,
    });
  }

  if (ledger.balance === "over-constrained") {
    tips.unshift({
      id: "over",
      severity: "block",
      title: `Trop d’équations (+${ledger.delta})`,
      action:
        "Décoche Fixed sur une frontière, active emergent_pressure, ou libère un actionneur (Z_UA, ouverture EXV).",
    });
  } else if (ledger.balance === "under-constrained") {
    tips.unshift({
      id: "under",
      severity: "block",
      title: `Pas assez d’équations (${ledger.delta})`,
      action:
        "Fixe ṁ ou T sur une Source, ancre P sur le Sink, ou coche emergent_pressure + sous-refroidissement / surchauffe.",
    });
  } else if (tips.length === 0) {
    tips.push({
      id: "ok",
      severity: "ok",
      title: "Système carré",
      action: "Tu peux lancer Solve. Les warnings soft (OutletClosure émergent) sont normaux.",
    });
  }

  // Deduplicate by title+action
  const seen = new Set<string>();
  const unique = tips.filter((t) => {
    const k = `${t.title}|${t.action}`;
    if (seen.has(k)) return false;
    seen.add(k);
    return true;
  });

  const headline =
    ledger.balance === "balanced"
      ? "Balance OK — prêt à simuler"
      : ledger.balance === "over-constrained"
        ? "Trop de Fixed — suis les étapes ci-dessous"
        : "Il manque des contraintes — suis le guide";

  return { balance: ledger.balance, headline, tips: unique.slice(0, 10), ledger };
}

function truthy(v: unknown): boolean {
  return v === true || v === "true" || v === 1 || v === "1";
}

function shortenDiagTitle(d: string): string {
  if (d.includes("MassFlowSource") || d.includes("Fixed P + Fixed")) return "Conflit P / ṁ";
  if (d.includes("secondary")) return "Boucle secondaire";
  if (d.includes("quality_control")) return "quality_control";
  if (d.includes("Over-constrained")) return "Surcontraint";
  if (d.includes("Under-constrained")) return "Sous-contraint";
  return d.length > 48 ? `${d.slice(0, 46)}…` : d;
}
