/**
 * Supergit substrate (L3).
 *
 * A real git repository is the base instantiation. On top of it we append
 * LLM-workflow metadata:
 *
 *  - `.supergit/ledger.csv` — append-only decision ledger. Every decision,
 *    artifact, result, error and repair is a typed node with a step index,
 *    a tree parent, a backward causal link, and forward dependency pointers
 *    (dedicated `edge` rows, since the ledger is append-only).
 *  - git commits carry `Supergit-Node` / `Supergit-Step` / `Supergit-Type`
 *    trailers, so code position and ledger position are synchronized in both
 *    directions: node -> commit via `git log --grep`, commit -> node via the
 *    trailer. Error placement maps to the same coordinates through the file
 *    ownership index.
 *  - `.supergit/graph.json` — a derived, deterministically rebuildable
 *    materialization of the tree + edges that RHA traverses, so repair never
 *    has to reconstruct the graph from code.
 */
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import type { LedgerNodeType, LedgerRow } from './types';

const LEDGER_HEADER =
  'step,id,type,feature_id,parent_id,cause_id,edge_from,edge_to,artifact_path,bug_class,status,summary,ts';

function csvEscape(v: string | number | null): string {
  if (v === null || v === undefined) return '';
  const s = String(v);
  if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  return s;
}

export interface RecordNodeInput {
  type: LedgerNodeType;
  featureId?: string | null;
  parentId?: string | null;
  causeId?: string | null;
  artifactPath?: string | null;
  bugClass?: string | null;
  status?: LedgerRow['status'];
  summary: string;
}

export class Substrate {
  private step = 0;
  private rows: LedgerRow[] = [];
  commitCount = 0;

  private constructor(readonly dir: string) {}

  private get ledgerPath() {
    return path.join(this.dir, '.supergit', 'ledger.csv');
  }
  private get graphPath() {
    return path.join(this.dir, '.supergit', 'graph.json');
  }

  /** Initialize a fresh workspace: git repo + ledger genesis. */
  static init(dir: string): Substrate {
    fs.mkdirSync(path.join(dir, '.supergit'), { recursive: true });
    const s = new Substrate(dir);
    s.git(['init', '--initial-branch=main']);
    s.git(['config', 'user.name', 'rha-wrapper']);
    s.git(['config', 'user.email', 'rha-wrapper@local']);
    fs.writeFileSync(s.ledgerPath, LEDGER_HEADER + '\n');
    const genesis = s.recordNode({ type: 'genesis', summary: 'supergit substrate initialized' });
    s.commit(genesis, 'genesis: initialize supergit substrate');
    return s;
  }

  /** Reopen an existing workspace (rebuilds in-memory state from the ledger). */
  static open(dir: string): Substrate {
    const s = new Substrate(dir);
    const text = fs.readFileSync(s.ledgerPath, 'utf8').trimEnd();
    const lines = text.split('\n').slice(1);
    for (const line of lines) {
      const row = parseCsvRow(line);
      if (row) s.rows.push(row);
    }
    s.step = s.rows.length ? s.rows[s.rows.length - 1].step : 0;
    return s;
  }

  git(args: string[]): string {
    return execFileSync('git', args, { cwd: this.dir, encoding: 'utf8' });
  }

  /** Append a typed node to the ledger. Returns the node id. */
  recordNode(input: RecordNodeInput): string {
    this.step += 1;
    const id = `N${String(this.step).padStart(4, '0')}`;
    const row: LedgerRow = {
      step: this.step,
      id,
      type: input.type,
      featureId: input.featureId ?? null,
      parentId: input.parentId ?? null,
      causeId: input.causeId ?? null,
      edgeFrom: null,
      edgeTo: null,
      artifactPath: input.artifactPath ?? null,
      bugClass: input.bugClass ?? null,
      status: input.status ?? null,
      summary: input.summary,
      ts: new Date().toISOString(),
    };
    this.appendRow(row);
    return id;
  }

  /**
   * Forward dependency pointer: `from` is depended on by `to` — when `from`
   * changes, `to` is in the blast radius. Append-only, so pointers discovered
   * later get their own rows instead of mutating earlier ones.
   */
  recordEdge(from: string, to: string, summary = 'downstream-dependency'): string {
    this.step += 1;
    const id = `N${String(this.step).padStart(4, '0')}`;
    const row: LedgerRow = {
      step: this.step,
      id,
      type: 'edge',
      featureId: null,
      parentId: null,
      causeId: null,
      edgeFrom: from,
      edgeTo: to,
      artifactPath: null,
      bugClass: null,
      status: null,
      summary,
      ts: new Date().toISOString(),
    };
    this.appendRow(row);
    return id;
  }

  private appendRow(row: LedgerRow) {
    this.rows.push(row);
    const line = [
      row.step, row.id, row.type, row.featureId, row.parentId, row.causeId,
      row.edgeFrom, row.edgeTo, row.artifactPath, row.bugClass, row.status,
      row.summary, row.ts,
    ].map(csvEscape).join(',');
    fs.appendFileSync(this.ledgerPath, line + '\n');
    this.materializeGraph();
  }

  /**
   * Commit the working tree, tagging the commit with the ledger node it
   * realizes. This is the code-position <-> supergit-position synchronization.
   */
  commit(nodeId: string, message: string): string {
    const row = this.rows.find(r => r.id === nodeId);
    const full = `${message}\n\nSupergit-Node: ${nodeId}\nSupergit-Step: ${row?.step ?? ''}\nSupergit-Type: ${row?.type ?? ''}`;
    this.git(['add', '-A']);
    this.git(['commit', '--allow-empty', '-m', full]);
    this.commitCount += 1;
    return this.git(['rev-parse', 'HEAD']).trim();
  }

  /** Resolve the commit that realized a ledger node (node -> code position). */
  commitForNode(nodeId: string): string | null {
    try {
      const out = this.git(['log', '--format=%H', `--grep=Supergit-Node: ${nodeId}$`, '-n', '1']);
      return out.trim() || null;
    } catch {
      return null;
    }
  }

  allRows(): LedgerRow[] {
    return [...this.rows];
  }

  rowsForFeature(featureId: string): LedgerRow[] {
    return this.rows.filter(r => r.featureId === featureId);
  }

  /** Latest node of a type for a feature (e.g. current error node). */
  latest(type: LedgerNodeType, featureId?: string): LedgerRow | undefined {
    for (let i = this.rows.length - 1; i >= 0; i--) {
      const r = this.rows[i];
      if (r.type === type && (featureId === undefined || r.featureId === featureId)) return r;
    }
    return undefined;
  }

  /**
   * Causal lineage of a node, walking backward causal links then the tree
   * parent — the decision trail RHA feeds into bounded repair context.
   */
  lineage(nodeId: string, max = 8): string[] {
    const byId = new Map(this.rows.map(r => [r.id, r]));
    const out: string[] = [];
    let cur = byId.get(nodeId);
    while (cur && out.length < max) {
      out.push(`${cur.id} [${cur.type}${cur.featureId ? ' ' + cur.featureId : ''}${cur.bugClass ? ' ' + cur.bugClass : ''}] ${cur.summary}`);
      const next = cur.causeId ?? cur.parentId;
      cur = next ? byId.get(next) : undefined;
    }
    return out;
  }

  /** Forward pointers out of a node: everything in its blast radius. */
  downstream(nodeId: string): string[] {
    return this.rows.filter(r => r.type === 'edge' && r.edgeFrom === nodeId).map(r => r.edgeTo!)
      .filter((v): v is string => v !== null);
  }

  private materializeGraph() {
    const nodes = this.rows.filter(r => r.type !== 'edge').map(r => ({
      id: r.id, step: r.step, type: r.type, featureId: r.featureId,
      parentId: r.parentId, causeId: r.causeId, status: r.status,
      bugClass: r.bugClass, artifactPath: r.artifactPath, summary: r.summary,
    }));
    const edges = this.rows.filter(r => r.type === 'edge').map(r => ({
      from: r.edgeFrom, to: r.edgeTo,
    }));
    fs.writeFileSync(this.graphPath, JSON.stringify({ nodes, edges }, null, 2));
  }
}

function parseCsvRow(line: string): LedgerRow | null {
  const fields: string[] = [];
  let cur = '';
  let inQ = false;
  for (let i = 0; i < line.length; i++) {
    const c = line[i];
    if (inQ) {
      if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
      else if (c === '"') inQ = false;
      else cur += c;
    } else if (c === '"') inQ = true;
    else if (c === ',') { fields.push(cur); cur = ''; }
    else cur += c;
  }
  fields.push(cur);
  if (fields.length < 13) return null;
  const nn = (s: string) => (s === '' ? null : s);
  return {
    step: Number(fields[0]),
    id: fields[1],
    type: fields[2] as LedgerRow['type'],
    featureId: nn(fields[3]),
    parentId: nn(fields[4]),
    causeId: nn(fields[5]),
    edgeFrom: nn(fields[6]),
    edgeTo: nn(fields[7]),
    artifactPath: nn(fields[8]),
    bugClass: nn(fields[9]),
    status: nn(fields[10]) as LedgerRow['status'],
    summary: fields[11],
    ts: fields[12],
  };
}
