/**
 * Generate `docs/module-graph.md` from in-repo `peerDependencies`, the canonical
 * runtime edges. The deterministic output groups packages by directory and
 * renders both Mermaid and a dependency table; `--check` verifies freshness.
 */

import { resolve } from 'node:path'
import { readFileSync, writeFileSync } from 'node:fs'
import {
  collectPackageGraph,
  escapeMermaidLabel as escLabel,
  graphNodeId as nodeId,
  type PackageGraphNode,
} from './package-graph.ts'

const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/module-graph.md'
type Pkg = PackageGraphNode

const GROUP_ORDER = [
  'util',
  'llm',
  'core',
  'goal',
  'bash',
  'fs',
  'skill',
  'compact',
  'subagent',
  'web',
  'spill',
  'timeout',
  'todo',
  'plan',
  'cordis',
  'hooks',
  'session-persistence',
  'session-query',
  'session-title',
  'support',
  'acp',
  'ui',
]

function packageLink(pkg: Pkg): string {
  return `[\`${pkg.short}\`](../${pkg.rel})`
}

/** Render the full docs/module-graph.md content (pure, deterministic). */
function render(pkgs: Pkg[]): string {
  const edges: string[] = []
  for (const p of pkgs) {
    for (const d of p.deps) edges.push(`  ${nodeId('pkg', p.short)} --> ${nodeId('pkg', d)}`)
  }
  const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
  const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
    const ia = GROUP_ORDER.indexOf(a)
    const ib = GROUP_ORDER.indexOf(b)
    const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
    const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
    return na - nb || a.localeCompare(b)
  })
  const groupBlocks: string[] = []
  for (const group of groups) {
    groupBlocks.push(`  subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
    for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
      groupBlocks.push(`    ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
    }
    groupBlocks.push('  end')
  }
  const rows = pkgs.map((p) => {
    const deps = p.deps.length ? p.deps.map((d) => {
      const dep = byShort.get(d)
      return dep ? packageLink(dep) : `\`${d}\``
    }).join(', ') : '—'
    return `| ${packageLink(p)} | \`${p.group}\` | ${deps} |`
  })
  return [
    '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
    '     Run `pnpm run gen-module-graph` to regenerate. -->',
    '',
    '# Module dependency graph',
    '',
    'Inter-package dependencies among the `@deepseek-ai/dsh-*` harness packages, derived from each package\'s `peerDependencies` (the canonical runtime-dependency signal) and grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.',
    '',
    '```mermaid',
    'flowchart TD',
    ...groupBlocks,
    ...edges,
    '```',
    '',
    '| Package | Group | Depends on |',
    '| --- | --- | --- |',
    ...rows,
    '',
  ].join('\n')
}

const content = render(collectPackageGraph(root, GROUP_ORDER, 'gen-module-graph'))

if (process.argv.includes('--check')) {
  let committed: string | null = null
  try {
    committed = readFileSync(resolve(root, OUT), 'utf8')
  } catch {
    // A missing artifact is the expected read failure. Any read failure has the
    // same remedy here—regenerate—so it is reported as stale below.
    committed = null
  }
  if (committed === content) {
    console.log(`gen-module-graph: ${OUT} is up to date.`)
    process.exit(0)
  }
  console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`)
  process.exit(1)
}

writeFileSync(resolve(root, OUT), content)
console.log(`gen-module-graph: wrote ${OUT}.`)
