/**
 * @file Pipeline templates for lanes. A template is a plain JSON list of nodes
 * (`server/data/pipelines/*.json` plus any override dropped in
 * `DASHBOARD_PIPELINES_DIR`); this module resolves a lane's declared stage onto
 * a node through per-node `aliases`, and derives the five render states the
 * pipeline map draws. Pure functions — no DB, no I/O beyond the one-time
 * template load, so it stays trivially testable.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const fs = require("node:fs");
const path = require("node:path");

const DEFAULT_PIPELINE_ID = "default";
const BUILTIN_DIR = path.join(__dirname, "..", "data", "pipelines");

/** Load every template once. A malformed file is skipped, never fatal. */
function loadAll() {
  const dirs = [BUILTIN_DIR];
  if (process.env.DASHBOARD_PIPELINES_DIR) dirs.push(process.env.DASHBOARD_PIPELINES_DIR);
  const out = new Map();
  for (const dir of dirs) {
    let files = [];
    try {
      files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
    } catch {
      continue; // dir absent — fine
    }
    for (const f of files) {
      try {
        const doc = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
        if (!doc.id || !Array.isArray(doc.nodes) || !doc.nodes.length) continue;
        doc.nodes = doc.nodes.map((n) => ({
          id: n.id,
          label: n.label || n.id,
          icon: n.icon || "",
          gate: !!n.gate,
          aliases: Array.isArray(n.aliases) ? n.aliases : [],
          detect: Array.isArray(n.detect) ? n.detect : [],
        }));
        out.set(doc.id, doc); // later dir wins — user override beats builtin
      } catch {
        /* skip malformed template */
      }
    }
  }
  return out;
}

let cache = null;
function templates() {
  if (!cache) cache = loadAll();
  return cache;
}

/** Test/dev helper: forget the cached templates so a new file is picked up. */
function reload() {
  cache = null;
}

function listPipelines() {
  return [...templates().values()];
}

/** Never throws: an unknown id yields the default template. */
function getPipeline(id) {
  const t = templates();
  return t.get(id) || t.get(DEFAULT_PIPELINE_ID);
}

/** Index of the node matching `stage` by id or alias; -1 when unknown. */
function phaseIdx(pipeline, stage) {
  if (!stage) return -1;
  const s = String(stage).toLowerCase();
  return pipeline.nodes.findIndex(
    (n) => n.id.toLowerCase() === s || n.aliases.some((a) => a.toLowerCase() === s)
  );
}

/**
 * Index every recorded stage by the node it resolves to, so a stage declared
 * under an ALIAS keeps its record — and therefore its evidence. Keying the
 * records by node id alone (what `lane.stages` is keyed by, verbatim from the
 * declaration) silently drops `--evidence` the moment an agent says `e2e`
 * instead of `e2e-feature`, which is exactly what aliases exist to allow.
 * A record stored under the node's own id always wins over an alias record for
 * the same node; a key matching no node is skipped.
 */
function stageRecords(pipeline, stages) {
  const byIdx = new Map();
  for (const [key, rec] of Object.entries(stages || {})) {
    const i = phaseIdx(pipeline, key);
    if (i === -1) continue;
    const isCanonical = key.toLowerCase() === pipeline.nodes[i].id.toLowerCase();
    if (byIdx.has(i) && !isCanonical) continue;
    byIdx.set(i, rec);
  }
  return byIdx;
}

/**
 * Render state per node:
 *   failed              — the stage recorded result "fail"
 *   current             — the lane's current stage
 *   done                — recorded AND carries evidence (an artifact, not a claim)
 *   passed-no-evidence  — recorded without evidence, or implicitly skipped past
 *   pending             — not reached
 */
function nodeStates(pipeline, lane) {
  const records = stageRecords(pipeline, lane.stages);
  const cur = phaseIdx(pipeline, lane.stage);
  return pipeline.nodes.map((n, i) => {
    const rec = records.get(i);
    let state;
    if (rec && rec.result === "fail") state = "failed";
    else if (i === cur) state = "current";
    else if (rec) state = rec.evidence ? "done" : "passed-no-evidence";
    else if (cur > -1 && i < cur) state = "passed-no-evidence";
    else state = "pending";
    return { id: n.id, label: n.label, icon: n.icon, gate: n.gate, state };
  });
}

function progressPct(pipeline, lane) {
  const i = phaseIdx(pipeline, lane.stage);
  if (i < 0) return 0;
  return Math.round((i / (pipeline.nodes.length - 1)) * 100);
}

module.exports = {
  DEFAULT_PIPELINE_ID,
  listPipelines,
  getPipeline,
  phaseIdx,
  stageRecords,
  nodeStates,
  progressPct,
  reload,
};
