/**
 * Generic Modelica Fixed / Free (Unfixed) calibration assistants.
 *
 * Engine already emits embeddings for ANY Free `actuatorFactor` + Fixed Probe.
 * This module only explains DoF balance and offers one-click Free on compatible
 * adjacent actuators. Nothing is removed from the catalogue (all Z stay).
 */

import type { Edge, Node } from "@xyflow/react";
import {
  COMPONENT_BY_TYPE,
  fixedFlagKey,
  isParamFixed,
  type ParamMeta,
} from "./componentMeta";
import { FACTOR_COMPATIBILITY, PROBE_PARAM_KIND } from "./configBuilder";
import type { SolvedVariable } from "./api";

/** Minimal node data shape (avoids heavy coupling beyond pairing tables). */
export type CalibNodeData = {
  type: string;
  name: string;
  params: Record<string, number | string | boolean>;
  circuit?: number;
};

export type CalibHintLevel = "ok" | "tip" | "warn";

export interface CalibHint {
  level: CalibHintLevel;
  /** Short UI message (French). */
  message: string;
  component?: string;
}

export interface CalibParamPatch {
  nodeId: string;
  params: Record<string, number | string | boolean>;
}

type FixedProbeMeasure = {
  key: string;
  label: string;
  kind: string;
  factors: readonly string[];
};

function actuatorParams(type: string): ParamMeta[] {
  const meta = COMPONENT_BY_TYPE[type];
  if (!meta) return [];
  return meta.params.filter((p) => p.fixable && p.actuatorFactor);
}

function fixedProbeMeasures(probe: Node<CalibNodeData>): FixedProbeMeasure[] {
  const meta = COMPONENT_BY_TYPE.Probe;
  if (!meta || probe.data.type !== "Probe") return [];
  const params = probe.data.params ?? {};
  const out: FixedProbeMeasure[] = [];

  for (const p of meta.params) {
    if (!p.fixable || !p.measureOutput) continue;
    if (!isParamFixed(params, p)) continue;
    const raw = params[p.key];
    if (raw === undefined || raw === "") continue;
    const n = typeof raw === "number" ? raw : Number(raw);
    if (!Number.isFinite(n)) continue;
    const kind = PROBE_PARAM_KIND[p.key] ?? p.key;
    const factors = FACTOR_COMPATIBILITY[kind] ?? [];
    if (factors.length === 0) continue;
    out.push({ key: p.key, label: p.label, kind, factors });
  }

  // Legacy measure+target
  if (out.length === 0) {
    const fixedFlag = params.__fixed_target;
    if (fixedFlag === true || fixedFlag === "true") {
      const kind = String(params.measure ?? "");
      const factors = FACTOR_COMPATIBILITY[kind] ?? [];
      if (kind && params.target != null && factors.length > 0) {
        out.push({
          key: "target",
          label: kind,
          kind,
          factors,
        });
      }
    }
  }

  return out;
}

/** Neighbours connected by an edge (any component type). */
export function adjacentNodes(
  node: Node<CalibNodeData>,
  nodes: Node<CalibNodeData>[],
  edges: Edge[],
): Node<CalibNodeData>[] {
  const byId = new Map(nodes.map((n) => [n.id, n]));
  const out: Node<CalibNodeData>[] = [];
  const seen = new Set<string>();
  for (const e of edges) {
    const otherId = e.source === node.id ? e.target : e.target === node.id ? e.source : null;
    if (!otherId || seen.has(otherId)) continue;
    const other = byId.get(otherId);
    if (!other) continue;
    seen.add(otherId);
    out.push(other);
  }
  return out;
}

/** Probe names adjacent to a named component. */
export function adjacentProbeNames(
  componentName: string,
  nodes: Node<CalibNodeData>[],
  edges: Edge[],
): string[] {
  const byName = new Map(nodes.map((n) => [n.data.name, n]));
  const comp = byName.get(componentName);
  if (!comp) return [];
  return adjacentNodes(comp, nodes, edges)
    .filter((n) => n.data.type === "Probe")
    .map((n) => n.data.name);
}

/** True if the node participates in Fixed/Free calibration UX. */
export function isCalibRelevantNode(node: Node<CalibNodeData> | null | undefined): boolean {
  if (!node) return false;
  if (node.data.type === "Probe") return true;
  return actuatorParams(node.data.type).length > 0;
}

/**
 * Collect Modelica Fixed/Free tips for the selected node (or whole diagram).
 * Does not replace hard Free-without-equation errors from validateConfig.
 * Always returns at least a baseline tip for Probe / actuator components.
 */
export function collectCalibHints(
  nodes: Node<CalibNodeData>[],
  edges: Edge[],
  focus?: Node<CalibNodeData> | null,
): CalibHint[] {
  const hints: CalibHint[] = [];

  for (const probe of nodes) {
    if (probe.data.type !== "Probe") continue;
    const measures = fixedProbeMeasures(probe);
    if (measures.length === 0) continue;

    const neighbours = adjacentNodes(probe, nodes, edges).filter(
      (n) => n.data.type !== "Probe",
    );

    for (const measure of measures) {
      for (const neigh of neighbours) {
        if (
          focus &&
          focus.id !== probe.id &&
          focus.id !== neigh.id
        ) {
          continue;
        }
        for (const ap of actuatorParams(neigh.data.type)) {
          const factor = ap.actuatorFactor!;
          if (!measure.factors.includes(factor)) continue;
          const free = !isParamFixed(neigh.data.params ?? {}, ap);
          if (free) {
            hints.push({
              level: "ok",
              component: neigh.data.name,
              message:
                `OK Modelica : Probe « ${probe.data.name} ».${measure.label} Fixed ` +
                `(équation) + ${factor} Free sur « ${neigh.data.name} » (inconnue).`,
            });
          } else {
            hints.push({
              level: "tip",
              component: neigh.data.name,
              message:
                `Pour calibrer : décoche Fixed sur ${factor} de « ${neigh.data.name} » ` +
                `(Probe « ${probe.data.name} ».${measure.label} impose déjà la mesure).`,
            });
          }
        }
      }
    }
  }

  if (focus) {
    for (const ap of actuatorParams(focus.data.type)) {
      if (isParamFixed(focus.data.params ?? {}, ap)) continue;
      const factor = ap.actuatorFactor!;
      const probes = adjacentNodes(focus, nodes, edges).filter((n) => n.data.type === "Probe");
      const hasEquation = probes.some((pr) =>
        fixedProbeMeasures(pr).some((m) => m.factors.includes(factor)),
      );
      if (!hasEquation) {
        hints.push({
          level: "warn",
          component: focus.data.name,
          message:
            `${factor} est Free (inconnue) : ajoute une Probe Fixed compatible ` +
            `(${compatibleMeasureLabels(factor).join(", ") || "mesure"}) ` +
            `sur le circuit, ou remets ${factor} Fixed.`,
        });
      }
    }

    // Baseline always visible — otherwise users see nothing when all Fixed (default).
    if (hints.length === 0 && isCalibRelevantNode(focus)) {
      if (focus.data.type === "Probe") {
        const measures = fixedProbeMeasures(focus);
        if (measures.length === 0) {
          hints.push({
            level: "tip",
            component: focus.data.name,
            message:
              "Fixed/Free : coche Fixed sur une mesure (Tsat, P, T, Capacity…) " +
              "puis libère le facteur compatible sur le composant adjacent " +
              "(z_ua, z_dp, z_flow, f_w).",
          });
        }
      } else {
        const factors = actuatorParams(focus.data.type)
          .map((p) => p.actuatorFactor)
          .filter(Boolean)
          .join(", ");
        hints.push({
          level: "tip",
          component: focus.data.name,
          message:
            `Fixed/Free : facteurs ${factors || "Z"} — Fixed = connu, Free = inconnue. ` +
            `Pour calibrer : Probe Fixed adjacent + décoche Fixed sur le facteur.`,
        });
      }
    }
  }

  return hints;
}

function compatibleMeasureLabels(factor: string): string[] {
  const labels: string[] = [];
  for (const [kind, factors] of Object.entries(FACTOR_COMPATIBILITY)) {
    if (factors.includes(factor)) labels.push(kind);
  }
  return labels;
}

/**
 * One-click: Free every Fixed actuator on neighbours that is compatible with
 * this Probe's Fixed measures. Returns patches for `updateNodesParams`.
 */
export function calibrateAdjacentActuatorPatches(
  probeNode: Node<CalibNodeData>,
  nodes: Node<CalibNodeData>[],
  edges: Edge[],
): CalibParamPatch[] {
  if (probeNode.data.type !== "Probe") return [];
  const measures = fixedProbeMeasures(probeNode);
  if (measures.length === 0) return [];

  const wanted = new Set(measures.flatMap((m) => [...m.factors]));
  const byId = new Map<string, CalibParamPatch>();

  for (const neigh of adjacentNodes(probeNode, nodes, edges)) {
    if (neigh.data.type === "Probe") continue;
    let next = { ...(neigh.data.params ?? {}) };
    let changed = false;
    for (const ap of actuatorParams(neigh.data.type)) {
      const factor = ap.actuatorFactor!;
      if (!wanted.has(factor)) continue;
      if (!isParamFixed(next, ap)) continue;
      next = {
        ...next,
        [fixedFlagKey(ap.key)]: false,
        [ap.key]:
          typeof next[ap.key] === "number"
            ? next[ap.key]
            : (ap.default as number | undefined) ?? 1.0,
      };
      changed = true;
    }
    if (changed) {
      byId.set(neigh.id, { nodeId: neigh.id, params: next });
    }
  }

  return [...byId.values()];
}

/** @deprecated Prefer {@link calibrateAdjacentActuatorPatches}. */
export function calibrateAdjacentZUaPatch(
  probeNode: Node<CalibNodeData>,
  nodes: Node<CalibNodeData>[],
  edges: Edge[],
): CalibParamPatch | null {
  const patches = calibrateAdjacentActuatorPatches(probeNode, nodes, edges);
  const zUa = patches.find((p) => p.params[fixedFlagKey("z_ua")] === false);
  return zUa ?? patches[0] ?? null;
}

/**
 * Append derived UA_eff = UA × z_ua when both are known after solve.
 */
export function withDerivedUaEff(
  componentName: string,
  nodes: Node<CalibNodeData>[],
  solved: SolvedVariable[],
): SolvedVariable[] {
  const node = nodes.find((n) => n.data.name === componentName);
  if (!node) return solved;
  const z = solved.find((s) => s.variable === "z_ua");
  if (!z) return solved;
  const uaRaw = node.data.params?.ua;
  const ua = typeof uaRaw === "number" ? uaRaw : Number(uaRaw);
  if (!Number.isFinite(ua) || ua <= 0) return solved;
  if (solved.some((s) => s.variable === "UA_eff")) return solved;
  return [
    ...solved,
    {
      id: `${componentName}__UA_eff`,
      component: componentName,
      variable: "UA_eff",
      value: ua * z.value,
      min: 0,
      max: ua * z.max,
    },
  ];
}
