/**
 * Parallel multi-run helpers: discover sweepable params from the diagram,
 * clone ScenarioConfig, and solve cases in parallel.
 */

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

export type SweepKind = "scalar" | "fluid";

export interface SweepSpec {
  /** `fluid` or `componentName.param` (CLI-flattened JSON). */
  path: string;
  label: string;
  kind: SweepKind;
  /** Comma / newline separated values. */
  valuesText: string;
}

export interface SweepTarget {
  path: string;
  label: string;
  kind: SweepKind;
  group: "boundaries" | "thermal" | "machine" | "global";
  /** Current value on the diagram (for suggested ranges). */
  current?: number | string | boolean;
  unit?: string;
  /** Component display name (empty for global). */
  componentName?: string;
  componentType?: string;
  paramKey?: string;
}

export interface MultiRunCase {
  id: string;
  label: string;
  config: unknown;
  overrides: Record<string, string | number>;
}

export interface MultiRunResult {
  case: MultiRunCase;
  ok: boolean;
  result?: SimulationResult;
  error?: string;
  durationMs: number;
}

/** Params engineers typically sweep on a cycle. */
const SWEEP_PARAM_PRIORITY: Record<string, { group: SweepTarget["group"]; rank: number }> = {
  t_set_c: { group: "boundaries", rank: 10 },
  t_dry_c: { group: "boundaries", rank: 11 },
  oat_k: { group: "boundaries", rank: 12 },
  m_flow_kg_s: { group: "boundaries", rank: 20 },
  air_face_velocity_m_s: { group: "boundaries", rank: 21 },
  ua: { group: "thermal", rank: 30 },
  opening: { group: "machine", rank: 40 },
  speed_ratio: { group: "machine", rank: 41 },
  frequency_hz: { group: "machine", rank: 42 },
  speed_rpm: { group: "machine", rank: 43 },
  slide_valve_position: { group: "machine", rank: 44 },
  volume_index: { group: "machine", rank: 45 },
  isentropic_efficiency: { group: "machine", rank: 46 },
};

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

/** Parse "1, 2, 3" or multiline into string tokens. */
export function parseValueList(text: string): string[] {
  return text
    .split(/[\n,;]+/)
    .map((s) => s.trim())
    .filter(Boolean);
}

function humanParamLabel(type: string, paramKey: string, unit?: string): string {
  const meta = COMPONENT_BY_TYPE[type]?.params.find((p) => p.key === paramKey);
  const base = meta?.label ?? paramKey;
  const u = unit ?? meta?.unit;
  return u ? `${base} (${u})` : base;
}

function friendlyComponentRole(type: string, name: string): string {
  if (type === "BrineSource") {
    if (/evap/i.test(name)) return "Eau évaporateur";
    if (/cond/i.test(name)) return "Eau condenseur";
    return `Source eau « ${name} »`;
  }
  if (type === "AirSource") {
    if (/cond|oat|outdoor/i.test(name)) return "Air extérieur";
    if (/evap|indoor/i.test(name)) return "Air intérieur";
    return `Source air « ${name} »`;
  }
  const label = COMPONENT_BY_TYPE[type]?.label ?? type;
  return `${label} « ${name} »`;
}

/**
 * Discover sweepable parameters from the live diagram.
 * Boundaries (water/air T) come first — that's what engineers sweep for ratings.
 */
export function discoverSweepTargets(
  nodes: Node<EntropykNodeData>[],
  fluid?: string,
): SweepTarget[] {
  const targets: SweepTarget[] = [
    {
      path: "fluid",
      label: "Fluide frigorigène",
      kind: "fluid",
      group: "global",
      current: fluid,
    },
  ];

  for (const node of nodes) {
    const type = node.data.type;
    const name = node.data.name;
    if (!type || !name || type === "SaturatedController") continue;
    const params = node.data.params ?? {};
    const meta = COMPONENT_BY_TYPE[type];

    for (const [key, val] of Object.entries(params)) {
      if (key.startsWith("__")) continue;
      const prio = SWEEP_PARAM_PRIORITY[key];
      if (!prio) continue;
      // Prefer live boundary setpoints over rating scalars on HX when both exist.
      if (
        (key === "secondary_inlet_temp_c" || key === "secondary_mass_flow_kg_s") &&
        !BOUNDARY_TYPES.has(type)
      ) {
        continue;
      }
      const paramMeta = meta?.params.find((p) => p.key === key);
      const role = friendlyComponentRole(type, name);
      const paramLabel = humanParamLabel(type, key, paramMeta?.unit);
      targets.push({
        path: `${name}.${key}`,
        label: `${role} — ${paramLabel}`,
        kind: typeof val === "string" && key === "fluid" ? "fluid" : "scalar",
        group: BOUNDARY_TYPES.has(type) ? "boundaries" : prio.group,
        current: val,
        unit: paramMeta?.unit,
        componentName: name,
        componentType: type,
        paramKey: key,
      });
    }
  }

  targets.sort((a, b) => {
    const order = { boundaries: 0, thermal: 1, machine: 2, global: 3 };
    const ga = order[a.group];
    const gb = order[b.group];
    if (ga !== gb) return ga - gb;
    const ra = a.paramKey ? (SWEEP_PARAM_PRIORITY[a.paramKey]?.rank ?? 99) : 0;
    const rb = b.paramKey ? (SWEEP_PARAM_PRIORITY[b.paramKey]?.rank ?? 99) : 0;
    if (ra !== rb) return ra - rb;
    return a.label.localeCompare(b.label);
  });

  return targets;
}

/** Suggest a small sweep around the current numeric value. */
export function suggestValues(current: number | string | boolean | undefined, paramKey?: string): string {
  if (typeof current !== "number" || !Number.isFinite(current)) {
    if (paramKey === "fluid" || current === undefined) return "R134a, R410A";
    return String(current ?? "");
  }
  if (paramKey === "t_set_c" || paramKey === "t_dry_c") {
    const step = 2;
    return [current - step, current, current + step].map((v) => String(v)).join(", ");
  }
  if (paramKey === "oat_k") {
    return [current - 5, current, current + 5].map((v) => String(v)).join(", ");
  }
  if (paramKey === "ua") {
    return [current * 0.8, current, current * 1.2]
      .map((v) => String(Math.round(v)))
      .join(", ");
  }
  if (paramKey === "m_flow_kg_s" || paramKey === "opening" || paramKey === "speed_ratio") {
    const a = Math.max(current * 0.8, 0);
    const b = current;
    const c = current * 1.2;
    return [a, b, c].map((v) => (Number.isInteger(v) ? String(v) : v.toFixed(3))).join(", ");
  }
  return String(current);
}

/** Default axes when opening Multi-run: both water temperatures if present. */
export function defaultSweepsFromDiagram(
  nodes: Node<EntropykNodeData>[],
  fluid?: string,
): SweepSpec[] {
  const targets = discoverSweepTargets(nodes, fluid);
  const waterTemps = targets.filter(
    (t) =>
      t.componentType === "BrineSource" &&
      t.paramKey === "t_set_c",
  );
  if (waterTemps.length >= 1) {
    return waterTemps.slice(0, 2).map((t) => ({
      path: t.path,
      label: t.label,
      kind: t.kind,
      valuesText: suggestValues(t.current, t.paramKey),
    }));
  }
  const airTemps = targets.filter(
    (t) => t.componentType === "AirSource" && t.paramKey === "t_dry_c",
  );
  if (airTemps.length >= 1) {
    return airTemps.slice(0, 2).map((t) => ({
      path: t.path,
      label: t.label,
      kind: t.kind,
      valuesText: suggestValues(t.current, t.paramKey),
    }));
  }
  const first = targets.find((t) => t.path !== "fluid") ?? targets[0];
  return [
    {
      path: first.path,
      label: first.label,
      kind: first.kind,
      valuesText: suggestValues(first.current, first.paramKey),
    },
  ];
}

/**
 * Set a parameter on a named component: `evap_water_in.t_set_c`.
 * Component names may contain underscores.
 */
export function applyOverride(
  config: unknown,
  path: string,
  raw: string,
  kind: SweepKind,
): unknown {
  const value: string | number =
    kind === "fluid" ? raw : Number.isFinite(Number(raw)) ? Number(raw) : raw;

  if (path === "fluid") {
    const c = structuredClone(config) as Record<string, unknown>;
    c.fluid = value;
    return c;
  }

  const dot = path.indexOf(".");
  if (dot > 0) {
    const name = path.slice(0, dot);
    const param = path.slice(dot + 1);
    if (name && param && !param.includes(".")) {
      const c = structuredClone(config) as {
        circuits?: Array<{
          components?: Array<Record<string, unknown>>;
        }>;
      };
      let hit = false;
      for (const circuit of c.circuits ?? []) {
        for (const comp of circuit.components ?? []) {
          if (comp.name === name) {
            comp[param] = value;
            hit = true;
          }
        }
      }
      if (!hit) {
        // Keep config but caller may surface a warning via empty hit.
      }
      return c;
    }
  }

  return structuredClone(config);
}

/** Cartesian product of sweep axes → run cases. */
export function buildSweepCases(
  baseConfig: unknown,
  sweeps: SweepSpec[],
): MultiRunCase[] {
  const axes = sweeps
    .map((s) => ({
      ...s,
      values: parseValueList(s.valuesText),
    }))
    .filter((s) => s.values.length > 0);

  if (axes.length === 0) {
    return [
      {
        id: "base",
        label: "base",
        config: structuredClone(baseConfig),
        overrides: {},
      },
    ];
  }

  let combos: Array<Record<string, string>> = [{}];
  for (const axis of axes) {
    const next: Array<Record<string, string>> = [];
    for (const prev of combos) {
      for (const v of axis.values) {
        next.push({ ...prev, [axis.path]: v });
      }
    }
    combos = next;
  }

  return combos.map((overrides, i) => {
    let cfg = structuredClone(baseConfig);
    const labels: string[] = [];
    for (const axis of axes) {
      const raw = overrides[axis.path];
      cfg = applyOverride(cfg, axis.path, raw, axis.kind);
      labels.push(`${axis.label || axis.path}=${raw}`);
    }
    return {
      id: `case-${i + 1}`,
      label: labels.join(" · "),
      config: cfg,
      overrides,
    };
  });
}

/** Run cases with a concurrency limit (default 4). */
export async function runParallel(
  cases: MultiRunCase[],
  options?: { concurrency?: number; onProgress?: (done: number, total: number) => void },
): Promise<MultiRunResult[]> {
  const concurrency = Math.max(1, options?.concurrency ?? 4);
  const total = cases.length;
  const results: MultiRunResult[] = new Array(total);
  let next = 0;
  let done = 0;

  async function worker() {
    while (next < total) {
      const idx = next++;
      const c = cases[idx];
      const t0 = performance.now();
      try {
        const resp = await simulate(c.config);
        results[idx] = {
          case: c,
          ok: !!resp.ok && !!resp.result,
          result: resp.result,
          error: resp.error,
          durationMs: performance.now() - t0,
        };
      } catch (e) {
        results[idx] = {
          case: c,
          ok: false,
          error: e instanceof Error ? e.message : String(e),
          durationMs: performance.now() - t0,
        };
      }
      done++;
      options?.onProgress?.(done, total);
    }
  }

  await Promise.all(Array.from({ length: Math.min(concurrency, total) }, () => worker()));
  return results;
}

/** Compact KPIs for comparison tables. */
export function extractKpis(result?: SimulationResult): {
  status: string;
  cop: number | null;
  qCoolKw: number | null;
  powerKw: number | null;
  iterations: number | null;
} {
  if (!result) {
    return { status: "error", cop: null, qCoolKw: null, powerKw: null, iterations: null };
  }
  const p = result.performance;
  const cop =
    p?.cop ??
    p?.cop_cooling ??
    (p?.cooling_capacity_w != null && p?.compressor_power_w
      ? p.cooling_capacity_w / p.compressor_power_w
      : null);
  const qCoolKw =
    p?.q_cooling_kw ??
    (p?.cooling_capacity_w != null ? p.cooling_capacity_w / 1000 : null);
  const powerKw =
    p?.compressor_power_kw ??
    (p?.compressor_power_w != null ? p.compressor_power_w / 1000 : null);
  return {
    status: result.status,
    cop: cop != null && Number.isFinite(cop) ? cop : null,
    qCoolKw: qCoolKw != null && Number.isFinite(qCoolKw) ? qCoolKw : null,
    powerKw: powerKw != null && Number.isFinite(powerKw) ? powerKw : null,
    iterations: result.iterations ?? result.convergence?.iterations ?? null,
  };
}
