#!/usr/bin/env node
// Scenario runner for pumpingStation. Usage:
//
//   node simulations/run.js <scenario>            # run one
//   node simulations/run.js --all                 # run all scenarios
//
// Each scenario lives in simulations/scenarios/<name>.js and exports:
//   { name, description, durationSec, config, setup?, inputs, expectations? }
//
// The runner ticks the station once per simulated second, records every
// state into simulations/logs/<name>.jsonl, prints a summary table + event log,
// and checks expectations.

const path = require('path');
const fs = require('fs');
const PumpingStation = require('../src/specificClass');
const { formatTable } = require('./formatters/table');

function loadScenario(name) {
  return require(path.join(__dirname, 'scenarios', name));
}

function snapshot(t, ps) {
  const lvl = ps.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m');
  const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
  return {
    t,
    level: lvl,
    volume: vol,
    direction: ps.state?.direction ?? null,
    netFlow: ps.state?.netFlow ?? null,
    flowSource: ps.state?.flowSource ?? null,
    timeleft: ps.state?.seconds ?? null,
    percControl: ps.percControl,
    mode: ps.mode,
    safetyActive: !!ps.safetyControllerActive,
  };
}

function evalExpectation(ex, records) {
  const levels = records.map((r) => r.level).filter(Number.isFinite);
  const demands = records.map((r) => r.percControl).filter(Number.isFinite);
  const last = records[records.length - 1] || {};
  switch (ex.type) {
    case 'max_level_bounded': {
      const v = Math.max(...levels);
      return { ok: v <= ex.value, msg: `max level = ${v.toFixed(2)} m (bound: ≤ ${ex.value})` };
    }
    case 'min_level_bounded': {
      const v = Math.min(...levels);
      return { ok: v >= ex.value, msg: `min level = ${v.toFixed(2)} m (bound: ≥ ${ex.value})` };
    }
    case 'max_demand_bounded': {
      const v = Math.max(...demands);
      return { ok: v <= ex.value, msg: `max demand = ${v.toFixed(0)} % (bound: ≤ ${ex.value})` };
    }
    case 'max_demand_gt': {
      const v = Math.max(...demands);
      return { ok: v > ex.value, msg: `max demand = ${v.toFixed(0)} % (expected > ${ex.value})` };
    }
    case 'safety_trips_eq': {
      const n = records.filter((r) => r.safetyActive).length;
      return { ok: n === ex.value, msg: `${n} ticks with safetyActive (expected ${ex.value})` };
    }
    case 'safety_trips_gt': {
      const n = records.filter((r) => r.safetyActive).length;
      return { ok: n > ex.value, msg: `${n} ticks with safetyActive (expected > ${ex.value})` };
    }
    case 'end_state_eq': {
      return { ok: last[ex.field] === ex.value, msg: `end ${ex.field} = ${last[ex.field]} (expected ${ex.value})` };
    }
    case 'threshold_issues_eq': {
      const n = (records[0] && records[0].thresholdIssues) || 0;
      return { ok: n === ex.value, msg: `${n} threshold issues at startup (expected ${ex.value})` };
    }
    default:
      return { ok: false, msg: `unknown expectation type: ${ex.type}` };
  }
}

function events(records) {
  const out = [];
  let prev = null;
  for (const r of records) {
    if (!prev) { prev = r; continue; }
    if (r.direction !== prev.direction) out.push({ t: r.t, kind: 'direction', from: prev.direction, to: r.direction });
    if (r.safetyActive !== prev.safetyActive) out.push({ t: r.t, kind: 'safety', active: r.safetyActive });
    if (r.mode !== prev.mode) out.push({ t: r.t, kind: 'mode', from: prev.mode, to: r.mode });
    prev = r;
  }
  return out;
}

async function runScenario(name) {
  const scenario = loadScenario(name);

  // Use simulated time so the volume integrator sees 1 s per tick.
  // The class reads Date.now() internally; monkey-patching lets it
  // advance at scenario pace rather than wall-clock.
  const realNow = Date.now;
  let simTime = realNow();
  Date.now = () => simTime;

  try {
    const ps = new PumpingStation(scenario.config);
    if (scenario.setup) await scenario.setup(ps);

    const duration = scenario.durationSec ?? 600;
    const logDir = path.join(__dirname, 'logs');
    fs.mkdirSync(logDir, { recursive: true });
    const logPath = path.join(logDir, `${scenario.name}.jsonl`);
    const log = fs.createWriteStream(logPath);

    const records = [];
    for (let t = 0; t < duration; t += 1) {
      simTime += 1000;  // advance 1 simulated second
      if (scenario.inputs) scenario.inputs(t, ps);
      ps.tick();
      const snap = snapshot(t, ps);
      snap.thresholdIssues = ps.thresholdIssues?.length ?? 0;
      records.push(snap);
      log.write(JSON.stringify(snap) + '\n');
    }
    // Drain so the file is fully written before we return.
    await new Promise((resolve, reject) => { log.end(); log.on('finish', resolve); log.on('error', reject); });

    return { ps, records, scenario, duration, logPath };
  } finally {
    Date.now = realNow;
  }
}

async function runAndReport(name) {
  const { ps, records, scenario, duration, logPath } = await runScenario(name);

  // Output
  console.log(`\n═══ Scenario: ${scenario.name} ═══`);
  console.log(scenario.description);
  console.log(`Duration: ${duration}s, 1s ticks`);

  console.log('\n─── Samples (every 10%) ───');
  console.log(formatTable(records, Math.max(1, Math.floor(duration / 10))));

  const evts = events(records);
  console.log(`\n─── Events (${evts.length}) ───`);
  if (!evts.length) console.log('  (none)');
  for (const e of evts) {
    if (e.kind === 'direction') console.log(`  t=${String(e.t).padStart(4)}s  direction  ${e.from} → ${e.to}`);
    else if (e.kind === 'safety') console.log(`  t=${String(e.t).padStart(4)}s  safety     ${e.active ? 'ACTIVE ⚠' : 'cleared'}`);
    else if (e.kind === 'mode') console.log(`  t=${String(e.t).padStart(4)}s  mode       ${e.from} → ${e.to}`);
  }

  console.log('\n─── Metrics ───');
  const levels = records.map((r) => r.level).filter(Number.isFinite);
  const demands = records.map((r) => r.percControl).filter(Number.isFinite);
  const trips = records.filter((r) => r.safetyActive).length;
  if (levels.length) {
    console.log(`  level       min=${Math.min(...levels).toFixed(2)}  max=${Math.max(...levels).toFixed(2)}  end=${levels[levels.length-1].toFixed(2)} m`);
  }
  if (demands.length) {
    console.log(`  percControl min=${Math.min(...demands).toFixed(0)}%  max=${Math.max(...demands).toFixed(0)}%  end=${demands[demands.length-1].toFixed(0)}%`);
  }
  console.log(`  safety      trips=${trips} ticks`);
  console.log(`  threshold   issues=${ps.thresholdIssues?.length ?? 0} at startup`);

  let allOk = true;
  if (scenario.expectations?.length) {
    console.log('\n─── Expectations ───');
    for (const ex of scenario.expectations) {
      const { ok, msg } = evalExpectation(ex, records);
      allOk = allOk && ok;
      console.log(`  ${ok ? '✓' : '✗'} ${ex.name}: ${msg}`);
    }
  }

  console.log(`\nLog: ${path.relative(process.cwd(), logPath)} (${records.length} records)`);
  console.log(allOk ? '✅ PASS' : '❌ FAIL');
  return allOk;
}

async function main() {
  const arg = process.argv[2];
  if (!arg) {
    console.error('Usage: node simulations/run.js <scenario> | --all');
    console.error('Available:', fs.readdirSync(path.join(__dirname, 'scenarios')).map((f) => f.replace(/\.js$/, '')).join(', '));
    process.exit(1);
  }
  if (arg === '--all') {
    const names = fs.readdirSync(path.join(__dirname, 'scenarios')).filter((f) => f.endsWith('.js')).map((f) => f.replace(/\.js$/, ''));
    let allOk = true;
    for (const name of names) {
      try { allOk = (await runAndReport(name)) && allOk; }
      catch (err) { console.error(`ERROR in ${name}:`, err.message); allOk = false; }
    }
    process.exit(allOk ? 0 : 1);
  }
  try { process.exit((await runAndReport(arg)) ? 0 : 1); }
  catch (err) { console.error('ERROR:', err.message, '\n', err.stack); process.exit(1); }
}

main();
