#!/usr/bin/env node
'use strict';

/**
 * wikiGen.js — shared wiki auto-generation helper for every EVOLV node.
 *
 * Two subcommands:
 *
 *   node wikiGen.js contract  <commands-module>     [--write <wiki-path>]
 *   node wikiGen.js datamodel <specificClass-module> [--write <wiki-path>]
 *
 * `contract` walks the descriptor array exported by `src/commands/index.js`
 * and emits a markdown table mapping canonical topic → aliases → payload
 * schema → effect description.
 *
 * `datamodel` instantiates the domain with a minimal stub config, calls
 * `getOutput()` once, and emits a markdown table of (key, type, sample value).
 * If construction fails (because the domain needs a live runtime that isn't
 * trivially stubbable), the script falls back to a hand-curated partial at
 * `<repo>/wiki/_partial-datamodel.md.template` instead of crashing.
 *
 * When `--write <wiki-path>` is given, the output is spliced between the
 * matching `<!-- BEGIN AUTOGEN: <marker> -->` / `<!-- END AUTOGEN: ... -->`
 * markers in that file. Otherwise it prints to stdout.
 *
 * See `.claude/refactor/WIKI_TEMPLATE.md` (sections 5 and 8) and CONTRACTS.md
 * for the canonical topic naming and registry shape this script consumes.
 */

const fs = require('fs');
const path = require('path');

// ── CLI parsing ────────────────────────────────────────────────────────────

function parseArgs(argv) {
  const [, , subcmd, target, ...rest] = argv;
  const opts = { subcmd, target, write: null };
  for (let i = 0; i < rest.length; i++) {
    if (rest[i] === '--write' && rest[i + 1]) {
      opts.write = rest[i + 1];
      i++;
    }
  }
  return opts;
}

function usage() {
  process.stderr.write([
    'Usage:',
    '  node wikiGen.js contract  <path-to-commands/index.js>     [--write <wiki-path>]',
    '  node wikiGen.js datamodel <path-to-specificClass.js>      [--write <wiki-path>]',
    '',
  ].join('\n'));
}

// ── Shared helpers ─────────────────────────────────────────────────────────

function resolveAbs(p) {
  return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
}

function describeSchema(schema) {
  if (!schema) return '_unspecified_';
  const t = schema.type;
  if (!t) return '_unspecified_';
  if (t === 'any')    return '`any`';
  if (t === 'object') {
    const props = schema.properties || {};
    const keys = Object.keys(props);
    if (!keys.length) return '`object`';
    const parts = keys.map((k) => {
      const subType = props[k]?.type ?? 'any';
      return `${k}:${subType}`;
    });
    return '`{ ' + parts.join(', ') + ' }`';
  }
  return '`' + t + '`';
}

function topicEffectFallback(topic) {
  // Try to derive a short, plain-English effect from the canonical topic
  // when the descriptor doesn't carry a description field. Keep it terse —
  // a maintainer can override by adding `description` to the descriptor.
  const prefixes = {
    'set.':    'Replaces the named state value with the supplied payload.',
    'cmd.':    'Triggers an action / sequence — not idempotent.',
    'data.':   'Pushes a value into the node\'s measurement stream.',
    'query.':  'Read-only query; node replies on the same msg.',
    'child.':  'Parent/child plumbing — registers or unregisters a child node.',
  };
  for (const [pfx, line] of Object.entries(prefixes)) {
    if (topic.startsWith(pfx)) return line;
  }
  return '_(see handler)_';
}

function spliceAutogen(filePath, marker, body) {
  const begin = `<!-- BEGIN AUTOGEN: ${marker} -->`;
  const end   = `<!-- END AUTOGEN: ${marker} -->`;
  if (!fs.existsSync(filePath)) {
    throw new Error(`wikiGen: --write target '${filePath}' does not exist`);
  }
  const src = fs.readFileSync(filePath, 'utf8');
  const bIdx = src.indexOf(begin);
  const eIdx = src.indexOf(end);
  if (bIdx < 0 || eIdx < 0 || eIdx < bIdx) {
    throw new Error(`wikiGen: markers '${marker}' not found in ${filePath}`);
  }
  const before = src.slice(0, bIdx + begin.length);
  const after  = src.slice(eIdx);
  const out = before + '\n\n' + body.trimEnd() + '\n\n' + after;
  fs.writeFileSync(filePath, out, 'utf8');
}

// ── Subcommand: contract ───────────────────────────────────────────────────

function describeUnits(units) {
  // Descriptor.units is the validated `{ measure, default }` pair the
  // commandRegistry stores; render it as `<measure> (default <unit>)` so
  // a reader sees both the dimension and the canonical default that the
  // node coerces to. Em-dash for unit-less topics keeps the column tidy.
  if (!units || typeof units !== 'object') return '—';
  const { measure, default: def } = units;
  if (!measure || !def) return '—';
  return '`' + measure + '` (default `' + def + '`)';
}

function renderContract(commandsPath) {
  const abs = resolveAbs(commandsPath);
  // eslint-disable-next-line import/no-dynamic-require, global-require
  const registry = require(abs);
  if (!Array.isArray(registry)) {
    throw new Error(`wikiGen contract: ${abs} does not export an array of descriptors`);
  }

  const lines = [];
  lines.push('| Canonical topic | Aliases | Payload | Unit | Effect |');
  lines.push('|---|---|---|---|---|');
  for (const d of registry) {
    const topic = '`' + d.topic + '`';
    const aliases = (d.aliases && d.aliases.length)
      ? d.aliases.map((a) => '`' + a + '`').join(', ')
      : '_(none)_';
    const payload = describeSchema(d.payloadSchema);
    const unit = describeUnits(d.units);
    const effect = d.description ? String(d.description) : topicEffectFallback(d.topic);
    lines.push(`| ${topic} | ${aliases} | ${payload} | ${unit} | ${effect} |`);
  }
  return lines.join('\n');
}

// ── Subcommand: datamodel ──────────────────────────────────────────────────

function inferSampleType(v) {
  if (v === null) return 'null';
  if (Array.isArray(v)) return 'array';
  return typeof v;
}

function trySampleValue(v) {
  if (v === null || v === undefined) return '`null`';
  const t = typeof v;
  if (t === 'number' || t === 'boolean') return '`' + String(v) + '`';
  if (t === 'string') return '`"' + v + '"`';
  if (Array.isArray(v)) return '`[…]`';
  if (t === 'object') return '`{…}`';
  return '`' + String(v) + '`';
}

// Heuristic unit map for top-level snapshot keys that aren't structured as
// MeasurementContainer keys (e.g. `heightBasin`, `surfaceArea`). Best-effort
// — the canonical place for unit semantics is the node's config schema; the
// table below is just enough to keep the auto-generated data-model readable.
const FLAT_KEY_UNITS = {
  heightBasin:     'm',
  basinHeight:     'm',
  inflowLevel:     'm',
  outflowLevel:    'm',
  overflowLevel:   'm',
  startLevel:      'm',
  stopLevel:       'm',
  minLevel:        'm',
  maxLevel:        'm',
  surfaceArea:     'm2',
  volEmptyBasin:   'm3',
  maxVol:          'm3',
  maxVolAtOverflow:'m3',
  minVol:          'm3',
  minVolAtInflow:  'm3',
  minVolAtOutflow: 'm3',
  percControl:     '%',
  timeleft:        's',
};

function inferUnitFromKey(key) {
  // MeasurementContainer-shaped keys take precedence: `{type}.{variant}.{position}.{childId}`.
  const parts = key.split('.');
  if (parts.length >= 3) {
    const type = parts[0];
    const map = {
      flow:           'm3/s',
      pressure:       'Pa',
      power:          'W',
      temperature:    'K',
      level:          'm',
      volume:         'm3',
      volumePercent:  '%',
      netFlowRate:    'm3/s',
    };
    if (map[type]) return map[type];
  }
  return FLAT_KEY_UNITS[key] || '—';
}

function renderDatamodel(specificClassPath) {
  const abs = resolveAbs(specificClassPath);
  // eslint-disable-next-line import/no-dynamic-require, global-require
  const Domain = require(abs);

  // Minimum viable stub config — the BaseDomain pipeline pulls per-key
  // defaults from the JSON schema, so this only needs to supply the bits
  // that BaseDomain reads from `userConfig` directly.
  const stubConfig = {
    general: {
      name: `wikiGen-${Domain.name || 'domain'}`,
      id: `wikiGen-${Domain.name || 'domain'}-id`,
      logging: { enabled: false, logLevel: 'error' },
    },
  };

  // Look for a hand-curated fallback alongside the wiki. Path is
  // `<node>/wiki/_partial-datamodel.md.template` relative to the
  // *commands*-or-specificClass file's repo root.
  const repoRoot = findRepoRoot(abs);
  const fallback = repoRoot
    ? path.join(repoRoot, 'wiki', '_partial-datamodel.md.template')
    : null;

  let out;
  try {
    const instance = new Domain(stubConfig);
    out = instance.getOutput ? instance.getOutput() : null;
    if (!out || typeof out !== 'object') {
      throw new Error('getOutput() returned a non-object');
    }
  } catch (err) {
    process.stderr.write(`wikiGen datamodel: live instantiation failed: ${err.message}\n`);
    if (fallback && fs.existsSync(fallback)) {
      process.stderr.write(`wikiGen datamodel: using hand-curated fallback ${fallback}\n`);
      return fs.readFileSync(fallback, 'utf8').trimEnd();
    }
    process.stderr.write('wikiGen datamodel: no hand-curated fallback found — emitting placeholder\n');
    return [
      '| Key | Type | Unit | Sample |',
      '|---|---|---|---|',
      `| _live instantiation failed; provide ${fallback ? `\`wiki/_partial-datamodel.md.template\`` : 'a hand-curated template'}_ | — | — | — |`,
    ].join('\n');
  }

  const lines = [];
  lines.push('| Key | Type | Unit | Sample |');
  lines.push('|---|---|---|---|');
  for (const k of Object.keys(out).sort()) {
    const v = out[k];
    lines.push(`| \`${k}\` | ${inferSampleType(v)} | ${inferUnitFromKey(k)} | ${trySampleValue(v)} |`);
  }
  return lines.join('\n');
}

function findRepoRoot(startPath) {
  let dir = path.dirname(startPath);
  for (let i = 0; i < 8; i++) {
    if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
    const parent = path.dirname(dir);
    if (parent === dir) return null;
    dir = parent;
  }
  return null;
}

// ── Entry point ────────────────────────────────────────────────────────────

function main() {
  const opts = parseArgs(process.argv);
  if (!opts.subcmd || !opts.target) {
    usage();
    process.exit(1);
  }

  let body;
  let marker;
  if (opts.subcmd === 'contract') {
    body = renderContract(opts.target);
    marker = 'topic-contract';
  } else if (opts.subcmd === 'datamodel') {
    body = renderDatamodel(opts.target);
    marker = 'data-model';
  } else {
    usage();
    process.exit(1);
  }

  if (opts.write) {
    spliceAutogen(resolveAbs(opts.write), marker, body);
    process.stderr.write(`wikiGen: wrote ${marker} block into ${opts.write}\n`);
  } else {
    process.stdout.write(body + '\n');
  }
}

if (require.main === module) {
  main();
}

module.exports = { renderContract, renderDatamodel, spliceAutogen, describeSchema, describeUnits };
