// trust-gate — kb#147 (A2A-15): vault access = trusted-only, enforced here.
//
// agap-mcp has no per-caller identity today (every MCP client hits the same
// unauthenticated /mcp endpoint on :3100) — that's the gap DESIGN-a2a-agents.md
// v2.1 §5 flags as the crux of "vault access = trusted only" (DECIDED, alvis).
// This module is the enforcement point: it maps a bearer token off the request
// to an agent id (via AGAP_MCP_AGENT_TOKENS, a secret never committed to git),
// then looks up that agent's trust class in agent-registry.yaml (the
// version-controlled source of truth for grants, kb#134) to decide whether
// vault tools (vw_*) may run.
//
// FAIL-CLOSED PRINCIPLE (once enforcement is turned on): no token, an
// unrecognized token, or an agent below `trusted` rank all resolve to "no
// vault access" — there is no default-allow path once AGAP_MCP_ENFORCE_VAULT_TRUST
// is on. See server.js for the off-by-default activation gate: merging this
// module changes zero live behavior until an operator deliberately flips that
// flag AND supplies real per-agent tokens (kb#147 handover step).

import { readFileSync } from 'fs';
import yaml from 'js-yaml';

const VAULT_TOOL_PREFIX = 'vw_';
const DEFAULT_REGISTRY_PATH = '/agent-registry.yaml';
const DEFAULT_TRUSTED_RANK = 2; // matches agent-registry.yaml trust_classes.trusted.rank; used only if the registry can't be read

let _registryCache = null;

export function loadRegistry(path = process.env.AGENT_REGISTRY_PATH || DEFAULT_REGISTRY_PATH) {
  if (_registryCache) return _registryCache;
  try {
    _registryCache = yaml.load(readFileSync(path, 'utf8'));
  } catch (e) {
    // Fail closed, not fail crash: no registry readable means no agent can be
    // proven trusted, so vaultAllowed() below returns false for everyone
    // rather than the process refusing to start (agap-mcp serves gitea/ha/
    // zabbix/radicale/todoist tools too, which don't depend on this file).
    console.error(`trust-gate: could not load agent registry from ${path}: ${e.message}`);
    _registryCache = { agents: [], trust_classes: {} };
  }
  return _registryCache;
}

// Test-only: let unit tests inject a registry object instead of touching the
// filesystem, and let the CLI/tests reset the module-level cache between runs.
export function _resetRegistryCacheForTests(registry = null) {
  _registryCache = registry;
}

export function trustedRankThreshold(registry = loadRegistry()) {
  return registry.trust_classes?.trusted?.rank ?? DEFAULT_TRUSTED_RANK;
}

export function trustRankOf(agentId, registry = loadRegistry()) {
  if (!agentId) return -1; // unauthenticated caller: rank below every real trust class
  const agent = (registry.agents || []).find(a => a.id === agentId);
  if (!agent) return -1; // unknown agent id: fail closed, not "assume trusted"
  const cls = registry.trust_classes?.[agent.trust_class];
  return cls ? cls.rank : -1;
}

// tokenMap: { "<bearer-token>": "<agent-id>" } — parsed once at startup from
// AGAP_MCP_AGENT_TOKENS (JSON), itself sourced from per-agent tokens stored in
// Vaultwarden and injected via this container's .env, never inlined in git.
export function loadTokenMap(raw = process.env.AGAP_MCP_AGENT_TOKENS) {
  if (!raw) return Object.create(null);
  try {
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== 'object') return Object.create(null);
    // Rebuild onto a null-proto object so a token literally named
    // "__proto__"/"constructor"/"prototype" in AGAP_MCP_AGENT_TOKENS can
    // never merge into Object.prototype instead of becoming an own key.
    return Object.assign(Object.create(null), parsed);
  } catch (e) {
    console.error(`trust-gate: AGAP_MCP_AGENT_TOKENS is not valid JSON: ${e.message}`);
    return Object.create(null);
  }
}

export function resolveCallerAgent(bearerToken, tokenMap) {
  if (!bearerToken || !tokenMap) return null;
  // Guard against prototype-key inputs: a bearer token of "__proto__",
  // "constructor", or "prototype" must never resolve via the object's
  // prototype chain (e.g. tokenMap['__proto__'] returning Object.prototype,
  // which is truthy and would silently "authenticate" as a non-existent
  // agent). Object.hasOwn only ever matches an actual own property that was
  // set from AGAP_MCP_AGENT_TOKENS, so a __proto__ probe resolves to null
  // explicitly regardless of the token map's shape (kb#182).
  if (!Object.hasOwn(tokenMap, bearerToken)) return null;
  return tokenMap[bearerToken] || null;
}

export function isVaultTool(toolName) {
  return toolName.startsWith(VAULT_TOOL_PREFIX);
}

export function vaultAllowed(agentId, registry = loadRegistry()) {
  return trustRankOf(agentId, registry) >= trustedRankThreshold(registry);
}

export function authHeaderToken(req) {
  const header = req.headers?.['authorization'] || req.headers?.['Authorization'] || '';
  return header.startsWith('Bearer ') ? header.slice(7).trim() : null;
}
