#!/usr/bin/env node
// Strong build gate for canonical maxy MCP tool names (Task 502).
//
// Two checks, both must pass:
//   1. Regenerate-and-diff: the committed generated artifact
//      (canonical-tool-names.generated.ts) must byte-match a fresh generation
//      from platform/plugins/*/PLUGIN.md. A stale artifact fails the build —
//      this is what stops a PLUGIN.md tool add/remove from drifting away from
//      the names instructions are bound to.
//   2. Instruction-name validation: every `mcp__...` tool name in an
//      instruction file (specialist templates, agent prompts, plugin
//      references/skills, PLUGIN.md prose) that targets a MAXY plugin must be
//      the canonical long prefix AND must exist in the live set. A short-prefix
//      maxy name (mcp__<plugin>__<tool>) fails — it must be long. An unknown
//      long-prefix maxy name fails — typo or stale. Non-maxy bridge namespaces
//      (playwright, etc.) are ignored: their tool names are upstream-owned.

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import { computeCanonicalToolNames, MAXY_CODE_ROOT } from './lib/canonical-tool-names.mjs'
import { renderArtifact, renderPluginsList } from './generate-canonical-tool-names.mjs'

const GENERATED = join(
  MAXY_CODE_ROOT, 'platform', 'services', 'claude-session-manager', 'src',
  'canonical-tool-names.generated.ts',
)
const PLUGINS_TXT = join(
  MAXY_CODE_ROOT, 'platform', 'plugins', 'admin', 'hooks', 'lib',
  'maxy-mcp-plugins.txt',
)

const SCAN_DIRS = [
  join(MAXY_CODE_ROOT, 'platform', 'templates', 'specialists', 'agents'),
  join(MAXY_CODE_ROOT, 'platform', 'plugins'),
]
const SCAN_EXT = new Set(['.md'])
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'payload', '__tests__'])
// mcp__<seg>__<tool>  — seg may itself contain `_` (long prefix plugin_a_b).
const TOOL_RE = /mcp__([A-Za-z0-9-]+(?:_[A-Za-z0-9-]+)*)__([A-Za-z0-9_-]+)/g

function* walk(dir) {
  let entries
  try { entries = readdirSync(dir) } catch { return }
  for (const e of entries.sort()) {
    if (e.startsWith('.') || SKIP_DIRS.has(e)) continue
    const p = join(dir, e)
    let st
    try { st = statSync(p) } catch { continue }
    if (st.isDirectory()) { yield* walk(p); continue }
    const ext = e.includes('.') ? '.' + e.split('.').pop() : ''
    if (SCAN_EXT.has(ext)) yield p
  }
}

function main() {
  const { plugins, names } = computeCanonicalToolNames()
  const maxyPlugins = new Set(plugins)
  const canonical = new Set(names)

  // Check 1 — regenerate-and-diff both generated outputs.
  let driftError = null
  const staleOutputs = []
  if ((existsSync(GENERATED) ? readFileSync(GENERATED, 'utf8') : '') !== renderArtifact()) staleOutputs.push(GENERATED)
  if ((existsSync(PLUGINS_TXT) ? readFileSync(PLUGINS_TXT, 'utf8') : '') !== renderPluginsList()) staleOutputs.push(PLUGINS_TXT)
  if (staleOutputs.length > 0) {
    driftError =
      `[gate] canonical-tool-names: GENERATED ARTIFACT STALE.\n` +
      staleOutputs.map((p) => `  ${p}`).join('\n') + '\n' +
      `  Regenerate: cd platform && npm run gen:canonical-tools`
  }

  // Check 2 — instruction-name validation.
  const violations = []
  for (const root of SCAN_DIRS) {
    for (const file of walk(root)) {
      const text = readFileSync(file, 'utf8')
      for (const m of text.matchAll(TOOL_RE)) {
        const seg = m[1]
        const full = m[0]
        // Skip glob patterns in prose (`mcp__plugin_x_x__*`): the `*` is not a
        // tool char so the match captured a partial name, not a real reference.
        if (text[m.index + full.length] === '*') continue
        if (seg.startsWith('plugin_')) {
          // Long prefix mcp__plugin_<plugin>_<server>__<tool>.
          const inner = seg.slice('plugin_'.length)
          // plugin === server for maxy; the long prefix is `<plugin>_<plugin>`.
          const plugin = [...maxyPlugins].find((p) => inner === `${p}_${p}`)
          if (!plugin) continue // non-maxy bridge (playwright) — upstream-owned.
          if (!canonical.has(full)) {
            violations.push({ file, name: full, why: 'unknown maxy tool (typo or stale)' })
          }
        } else if (maxyPlugins.has(seg)) {
          // Short prefix mcp__<maxyplugin>__<tool> — must be long now.
          violations.push({ file, name: full, why: `short prefix for maxy plugin '${seg}' — use canonical long name` })
        }
      }
    }
  }

  const failed = driftError || violations.length > 0
  if (driftError) console.error(driftError)
  if (violations.length > 0) {
    console.error('[gate] canonical-tool-names: instruction files name tools outside the live set:')
    for (const v of violations) console.error(`  ${v.file.replace(MAXY_CODE_ROOT + '/', '')}: ${v.name} — ${v.why}`)
  }
  if (failed) process.exit(1)
  console.log(`[gate] check-canonical-tool-names: PASS (${names.length} canonical names, scanned instruction files)`)
}

main()
