#!/usr/bin/env node
/**
 * scripts/validate-bridge-budget.ts — Pillar P7 (Efficiency-First),
 * P4 (Lean Coverage).
 *
 * Measures the per-adapter "bridge budget" — the share of an adapter's
 * context-window budget consumed by inlined bridge orchestration content
 * (the BRIDGE_ORCHESTRATION static plus the dynamic skill dispatch /
 * routing tables generated by `generateBridgeOrchestration`). Fails when
 * any adapter exceeds the configured ceiling (default 10%, override with
 * `--max-percent <N>`).
 *
 * Rationale: BRIDGE_ORCHESTRATION inlines into every adapter bridge file
 * (CLAUDE.md, GEMINI.md, .windsurfrules, .amp/AGENTS.md,
 * .github/copilot-instructions.md, .cursor/rules/hatch3r-bridge.mdc).
 * Adapters with smaller windows (e.g. zed 64K, copilot 64K, aider 64K)
 * are most at risk of bridge bloat crowding out project context; the 10%
 * ceiling cycle 9 (D6-SA6.1-F1) keeps headroom for canonical content,
 * inlined rules, and project files.
 *
 * Token estimate uses the standard 1 token ≈ 4 chars heuristic — matches
 * `src/adapters/contextBudget.ts:estimateTokens`. Provider-specific
 * tokenizers are out of scope; this gate is model-agnostic per D6.6.
 *
 * Usage:
 *   `npm run validate:efficiency`  (wired as the second leg)
 *   `tsx scripts/validate-bridge-budget.ts --max-percent 10`
 *   `tsx scripts/validate-bridge-budget.ts --json`
 *
 * Pillars: P7 (Efficiency-First), P4 (Lean Coverage), P5 (Governance Self-Quality).
 */
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
  BRIDGE_ORCHESTRATION,
  generateBridgeOrchestration,
} from "../src/cli/shared/agentsContent.js";
import {
  CONTEXT_BUDGET_TOKENS,
  estimateTokens,
} from "../src/adapters/contextBudget.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = resolve(__dirname, "..");
const AGENTS_DIR = resolve(ROOT, "agents");

const DEFAULT_MAX_PERCENT = 10;

// ── Types ─────────────────────────────────────────────────────────

export interface BridgeBudgetReport {
  /** Token count of the static BRIDGE_ORCHESTRATION constant. */
  staticTokens: number;
  /** Token count of the dynamically generated bridge (incl. skill table). */
  dynamicTokens: number;
  /** Token count used for the per-adapter % calculation (max of the two). */
  measuredTokens: number;
  /** Per-adapter utilization breakdown sorted by % descending. */
  perAdapter: AdapterUtilization[];
  /** Ceiling against which utilization is compared. */
  maxPercent: number;
  /** Adapters where utilization > maxPercent. */
  violations: AdapterUtilization[];
}

export interface AdapterUtilization {
  tool: string;
  budgetTokens: number;
  bridgeTokens: number;
  utilizationPercent: number;
  exceedsCeiling: boolean;
}

export interface RunOptions {
  /** Absolute path to the agents directory used for dynamic bridge generation. */
  agentsDir?: string;
  /** Override the default ceiling (10). Pass via `--max-percent`. */
  maxPercent?: number;
}

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

interface CliFlags {
  maxPercent: number;
  json: boolean;
}

function parseArgs(argv: readonly string[]): CliFlags {
  let maxPercent = DEFAULT_MAX_PERCENT;
  let json = false;
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === "--max-percent") {
      const next = argv[i + 1];
      const n = Number(next);
      if (!Number.isFinite(n) || n <= 0 || n > 100) {
        throw new Error(
          `validate-bridge-budget: --max-percent requires a number in (0, 100], got "${String(next)}"`,
        );
      }
      maxPercent = n;
      i++;
    } else if (a === "--json") {
      json = true;
    }
  }
  return { maxPercent, json };
}

// ── Core measurement ──────────────────────────────────────────────

/**
 * Measure the bridge orchestration token cost against every adapter's
 * budget. Returns a structured report; never throws on a single
 * adapter's overage (the orchestrator decides whether overages exit
 * non-zero, so unit tests can assert without process.exit).
 */
export async function measureBridgeBudget(
  opts: RunOptions = {},
): Promise<BridgeBudgetReport> {
  const maxPercent = opts.maxPercent ?? DEFAULT_MAX_PERCENT;
  const agentsDir = opts.agentsDir ?? AGENTS_DIR;

  const staticTokens = estimateTokens(BRIDGE_ORCHESTRATION.length);

  let dynamicContent: string;
  try {
    dynamicContent = await generateBridgeOrchestration(agentsDir);
    // Generation falls back to the static constant on errors (missing
    // dirs, unreadable skill files). The fallback is not a failure of
    // this validator — the static constant is itself a valid bridge
    // output — so we silently absorb and use whatever we got.
    // eslint-disable-next-line silent-failure/no-silent-catch
  } catch {
    dynamicContent = BRIDGE_ORCHESTRATION;
  }
  const dynamicTokens = estimateTokens(dynamicContent.length);

  // The worst-case bridge cost is the larger of the two: dynamic adds
  // skill table rows, but a minimal-preset install or empty skills/
  // dir falls back to the static constant. Measuring against max keeps
  // the gate honest across both shapes.
  const measuredTokens = Math.max(staticTokens, dynamicTokens);

  const perAdapter: AdapterUtilization[] = [];
  for (const [tool, budgetTokens] of Object.entries(CONTEXT_BUDGET_TOKENS)) {
    const utilizationPercent =
      Math.round((measuredTokens / budgetTokens) * 10_000) / 100; // 2dp
    perAdapter.push({
      tool,
      budgetTokens,
      bridgeTokens: measuredTokens,
      utilizationPercent,
      exceedsCeiling: utilizationPercent > maxPercent,
    });
  }
  perAdapter.sort((a, b) => b.utilizationPercent - a.utilizationPercent);
  const violations = perAdapter.filter((a) => a.exceedsCeiling);

  return {
    staticTokens,
    dynamicTokens,
    measuredTokens,
    perAdapter,
    maxPercent,
    violations,
  };
}

// ── Output formatting ─────────────────────────────────────────────

export function formatTextReport(report: BridgeBudgetReport): string {
  const lines: string[] = [];
  lines.push("validate-bridge-budget:");
  lines.push(
    `  static BRIDGE_ORCHESTRATION: ${report.staticTokens} tokens (~${BRIDGE_ORCHESTRATION.length} chars)`,
  );
  lines.push(`  dynamic (with skill table): ${report.dynamicTokens} tokens`);
  lines.push(
    `  measured (max of the two): ${report.measuredTokens} tokens; ceiling: ${report.maxPercent}% of per-adapter budget`,
  );
  lines.push("");
  lines.push("  Per-adapter utilization (% of context-window budget):");
  for (const a of report.perAdapter) {
    const marker = a.exceedsCeiling ? "FAIL" : "ok  ";
    lines.push(
      `    [${marker}] ${a.tool.padEnd(12)} ${String(a.utilizationPercent).padStart(6)}%   (${a.bridgeTokens} / ${a.budgetTokens} tokens)`,
    );
  }
  if (report.violations.length > 0) {
    lines.push("");
    lines.push(
      `  ${report.violations.length} adapter(s) exceed the ${report.maxPercent}% bridge-budget ceiling.`,
    );
    lines.push(
      "  Reduce src/cli/shared/agentsContent.ts BRIDGE_ORCHESTRATION or condense the dynamic skill table.",
    );
  } else {
    lines.push("");
    lines.push(
      `  All ${report.perAdapter.length} adapters stay within ${report.maxPercent}% bridge-budget ceiling.`,
    );
  }
  return lines.join("\n");
}

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

async function main(): Promise<void> {
  const flags = parseArgs(process.argv.slice(2));
  const report = await measureBridgeBudget({ maxPercent: flags.maxPercent });
  if (flags.json) {
    // eslint-disable-next-line no-console
    console.log(JSON.stringify(report, null, 2));
  } else {
    // eslint-disable-next-line no-console
    console.log(formatTextReport(report));
  }
  if (report.violations.length > 0) process.exit(1);
}

// Only auto-run when executed as a script, never when imported by tests.
const isMain = (() => {
  try {
    return resolve(process.argv[1] ?? "") === __filename;
  } catch {
    return false;
  }
})();

if (isMain) {
  main().catch((err) => {
    // eslint-disable-next-line no-console
    console.error("validate-bridge-budget failed:", err);
    process.exit(1);
  });
}
