// 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,
    escalation_window: 5,
  },
  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,
    cumulative_drift_threshold: 0.25,
  },
  audit: {
    log_path: null,
    events: ['trifecta', 'injection', 'secrets', 'destructive'],
  },
  ci: {
    failOn: null,
    compact: false,
  },
  entropy: {
    thresholds: {
      critical: { entropy: 5.4, minLen: 128 },
      high:     { entropy: 5.1, minLen: 64 },
      medium:   { entropy: 4.7, minLen: 40 },
    },
    // User-extensible extension skip list — merged with built-in defaults.
    suppress_extensions: [],
    // Additional line-level regex sources (string or array of strings compiled at load).
    suppress_line_patterns: [],
    // Substring matches against relative path — plain contains, no glob.
    suppress_paths: [],
  },
});

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

// Module-scoped Set of env-var names already warned about — dedupes to one
// stderr line per env-var per process, regardless of how many call-sites
// invoke getPolicyValueWithEnvWarn for the same name.
const _warnedEnvVars = new Set();

/**
 * 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;
}

/**
 * Resolve a policy value with an overlapping env-var, emitting a one-time
 * stderr deprecation warning when both the env-var AND the policy.json key
 * are explicitly set.
 *
 * Resolution order (env-wins is unchanged from getPolicyValue contract):
 *   1. If LLM_SECURITY_DEPRECATION_QUIET=1, suppress warning logic entirely
 *      and return env-value (if defined) else policy-value.
 *   2. Otherwise, if env-var is set AND policy-value differs from
 *      defaultValue (heuristic: user wrote the key in policy.json),
 *      emit one stderr warning per envVarName per process.
 *   3. Return env-value if defined, else policy-value.
 *
 * Why "differs from defaultValue" rather than parsing the raw policy file:
 * loadPolicy() deep-merges DEFAULT_POLICY so `key in policy[section]` is
 * always true. Comparing the resolved value to the caller's defaultValue
 * is a reliable proxy for "user explicitly overrode this in policy.json"
 * because callers pass defaults that match DEFAULT_POLICY.
 *
 * @param {string} section - Policy section (e.g. 'injection', 'trifecta')
 * @param {string} key - Key within section (e.g. 'mode')
 * @param {string} envVarName - Overlapping env-var (e.g. 'LLM_SECURITY_INJECTION_MODE')
 * @param {*} defaultValue - Hardcoded default (must match DEFAULT_POLICY value)
 * @param {string} [projectRoot] - Explicit root
 * @returns {*}
 */
export function getPolicyValueWithEnvWarn(section, key, envVarName, defaultValue, projectRoot) {
  const envValue = process.env[envVarName];

  if (process.env.LLM_SECURITY_DEPRECATION_QUIET === '1') {
    if (envValue !== undefined) return envValue;
    return getPolicyValue(section, key, defaultValue, projectRoot);
  }

  const policyValue = getPolicyValue(section, key, defaultValue, projectRoot);

  if (envValue !== undefined && policyValue !== defaultValue) {
    if (!_warnedEnvVars.has(envVarName)) {
      _warnedEnvVars.add(envVarName);
      process.stderr.write(
        `[llm-security] Deprecation: env-var ${envVarName} will be removed in v8.0.0; ` +
        `policy.json key ${section}.${key} also set — env wins for now. ` +
        `Suppress with LLM_SECURITY_DEPRECATION_QUIET=1.\n`
      );
    }
  }

  if (envValue !== undefined) return envValue;
  return policyValue;
}

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

/**
 * Reset cache and warning dedup state (for testing only).
 */
export function _resetCacheForTest() {
  cache.clear();
  _warnedEnvVars.clear();
}
