// policy-loader.mjs — Central policy file reader for distributable hook configuration
// Reads .llm-security/policy.json from project root. Falls back to defaults
// matching existing hardcoded behavior when no policy file exists.
// Zero external dependencies.

import { readFileSync } from 'node:fs';
import { join } from 'node:path';

// ---------------------------------------------------------------------------
// Default policy — matches all existing hardcoded values exactly
// ---------------------------------------------------------------------------

const DEFAULT_POLICY = Object.freeze({
  version: '1.0',
  injection: {
    mode: 'block',
    medium_advisory: true,
    custom_patterns: [],
  },
  trifecta: {
    mode: 'warn',
    window_size: 20,
    long_horizon_window: 100,
  },
  secrets: {
    additional_patterns: [],
    allowed_paths: [],
  },
  destructive: {
    additional_blocked: [],
    allowed_commands: [],
  },
  pathguard: {
    additional_protected: [],
    allowed_paths: [],
  },
  supply_chain: {
    additional_blocked_packages: [],
    trusted_registries: [],
  },
  mcp: {
    trusted_servers: [],
    volume_threshold_bytes: 100_000,
  },
  audit: {
    log_path: null,
    events: ['trifecta', 'injection', 'secrets', 'destructive'],
  },
  ci: {
    failOn: null,
    compact: false,
  },
});

// Cache loaded policy per project root
const cache = new Map();

/**
 * Resolve project root from env or cwd.
 * @param {string} [explicitRoot]
 * @returns {string}
 */
function resolveRoot(explicitRoot) {
  return explicitRoot || process.env.CLAUDE_PROJECT_ROOT || process.cwd();
}

/**
 * Deep merge two objects (source overrides target).
 * @param {object} target
 * @param {object} source
 * @returns {object}
 */
function deepMerge(target, source) {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    if (
      source[key] !== null &&
      typeof source[key] === 'object' &&
      !Array.isArray(source[key]) &&
      typeof target[key] === 'object' &&
      !Array.isArray(target[key])
    ) {
      result[key] = deepMerge(target[key], source[key]);
    } else {
      result[key] = source[key];
    }
  }
  return result;
}

/**
 * Load policy from .llm-security/policy.json.
 * Returns defaults if no policy file exists or if parsing fails.
 * Cached per project root (per process).
 *
 * @param {string} [projectRoot] - Explicit root, or derived from env/cwd
 * @returns {object} Merged policy with defaults
 */
export function loadPolicy(projectRoot) {
  const root = resolveRoot(projectRoot);

  if (cache.has(root)) return cache.get(root);

  const policyPath = join(root, '.llm-security', 'policy.json');
  let policy;

  try {
    const raw = readFileSync(policyPath, 'utf-8');
    const parsed = JSON.parse(raw);
    policy = deepMerge(DEFAULT_POLICY, parsed);
  } catch {
    // No policy file or invalid JSON — use defaults
    policy = { ...DEFAULT_POLICY };
  }

  cache.set(root, policy);
  return policy;
}

/**
 * Get a specific policy value with fallback.
 * Environment variables ALWAYS take precedence over policy file values.
 *
 * @param {string} section - Policy section (e.g. 'injection', 'trifecta')
 * @param {string} key - Key within section (e.g. 'mode', 'window_size')
 * @param {*} defaultValue - Fallback if neither policy nor default has the value
 * @param {string} [projectRoot] - Explicit root
 * @returns {*}
 */
export function getPolicyValue(section, key, defaultValue, projectRoot) {
  const policy = loadPolicy(projectRoot);
  const sectionObj = policy[section];
  if (sectionObj && key in sectionObj) return sectionObj[key];
  return defaultValue;
}

/**
 * Get the full default policy (for documentation/example generation).
 * @returns {object}
 */
export function getDefaultPolicy() {
  return JSON.parse(JSON.stringify(DEFAULT_POLICY));
}

/**
 * Reset cache (for testing only).
 */
export function _resetCacheForTest() {
  cache.clear();
}
