// Copyright 2026 Cisco Systems, Inc. and its affiliates
// SPDX-License-Identifier: Apache-2.0
//
// Render a Policy into the multiple files the gateway expects on
// disk. This mirrors the on-disk layout in policies/ so the operator
// can paste the output directly under ~/.defenseclaw/policies/.

import yaml from 'js-yaml';
import type { CiscoAIDefenseConfig, CorrelationPattern, Policy } from '../types';
import { projectPolicyToData } from './data-projection';
import { policyFromPreset } from './presets';

export interface EmittedFile {
  path: string;
  contents: string;
  description: string;
}

const YAML_OPTS: yaml.DumpOptions = {
  indent: 2,
  lineWidth: 100,
  noRefs: true,
  sortKeys: false,
};

// Defaults applied only when emit() receives a Policy with these fields
// missing. The hydration boundary (PolicyCreator + decodePolicyFromHash)
// runs normalizeImportedPolicy and SHOULD have backfilled these already;
// this is belt-and-suspenders so a stale draft from any future path
// can't crash the export panel.
const DEFAULT_AID: CiscoAIDefenseConfig = {
  enabled: false,
  endpoint: '',
  api_key_env: '',
  scan_hook_surface: true,
};

function dump(value: unknown): string {
  return yaml.dump(value, YAML_OPTS);
}

export function emit(policy: Policy): EmittedFile[] {
  const files: EmittedFile[] = [];
  const packName = policy.rule_pack.name || policy.name;
  const correlator: CorrelationPattern[] = policy.correlator ?? [];

  // 1) Top-level admission policy YAML.
  const aid: CiscoAIDefenseConfig = policy.cisco_ai_defense ?? DEFAULT_AID;
  const aidBlock =
    aid.enabled || aid.api_key_env || aid.endpoint
      ? {
          cisco_ai_defense: {
            ...(aid.endpoint ? { endpoint: aid.endpoint } : {}),
            ...(aid.api_key_env ? { api_key_env: aid.api_key_env } : {}),
            // Only emit scan_hook_surface when the operator changed it
            // from the default true; HookSurfaceEnabled() handles unset.
            ...(aid.scan_hook_surface === false ? { scan_hook_surface: false } : {}),
          },
        }
      : {};
  const policyYaml = {
    name: policy.name,
    description: policy.description,
    admission: policy.admission,
    skill_actions: policy.skill_actions,
    scanner_overrides: policy.scanner_overrides,
    first_party_allow_list: policy.first_party_allow_list,
    guardrail: {
      block_threshold: policy.guardrail.block_threshold,
      alert_threshold: policy.guardrail.alert_threshold,
      cisco_trust_level: policy.guardrail.cisco_trust_level,
      hilt: policy.guardrail.hilt,
      patterns: policy.guardrail.patterns,
      severity_mappings: policy.guardrail.severity_mappings,
    },
    firewall: policy.firewall,
    watch: policy.watch,
    enforcement: policy.enforcement,
    audit: policy.audit,
    webhooks: policy.webhooks,
    scanners: policy.scanners,
    ...aidBlock,
  };
  files.push({
    path: `~/.defenseclaw/policies/${policy.name}.yaml`,
    contents:
      `# DefenseClaw policy: ${policy.name}\n` +
      `# ${policy.description}\n` +
      `# Generated by docs-site policy creator. Edit by hand or regenerate.\n` +
      `# Activate with: defenseclaw policy activate ${policy.name}\n\n` +
      dump(policyYaml),
    description: 'Top-level admission/severity/firewall/audit policy YAML',
  });

  // 2) The data.json projection that OPA will read.
  const opaData = projectPolicyToData(policy);
  files.push({
    path: `~/.defenseclaw/policies/rego/data.json`,
    contents: JSON.stringify(opaData, null, 2) + '\n',
    description: 'OPA data.json — read by every Rego module at evaluation time',
  });

  // 3) Per-file rule-pack YAMLs.
  for (const rf of policy.rule_pack.files) {
    if (!rf.rules.length) continue;
    files.push({
      path: `~/.defenseclaw/policies/guardrail/${packName}/rules/${rf.filename}.yaml`,
      contents: dump({ version: 1, category: rf.category, rules: rf.rules }),
      description: `Rule pack: ${rf.filename}.yaml (${rf.rules.length} rule${rf.rules.length === 1 ? '' : 's'})`,
    });
  }

  // 4) Suppressions.
  const supp = policy.suppressions;
  if (
    supp.pre_judge_strips.length > 0 ||
    supp.finding_suppressions.length > 0 ||
    supp.tool_suppressions.length > 0
  ) {
    files.push({
      path: `~/.defenseclaw/policies/guardrail/${packName}/suppressions.yaml`,
      contents: dump({
        version: 1,
        pre_judge_strips: supp.pre_judge_strips,
        finding_suppressions: supp.finding_suppressions,
        tool_suppressions: supp.tool_suppressions,
      }),
      description: `Suppressions: ${supp.pre_judge_strips.length} pre-strip · ${supp.finding_suppressions.length} finding · ${supp.tool_suppressions.length} tool`,
    });
  }

  // 5) Sensitive tools.
  if (policy.sensitive_tools.length > 0) {
    files.push({
      path: `~/.defenseclaw/policies/guardrail/${packName}/sensitive-tools.yaml`,
      contents: dump({ version: 1, tools: policy.sensitive_tools }),
      description: `Sensitive tool inspection: ${policy.sensitive_tools.length} tool${policy.sensitive_tools.length === 1 ? '' : 's'}`,
    });
  }

  // 6) Judge configs.
  for (const judge of policy.judges) {
    if (!judge.system_prompt) continue;
    files.push({
      path: `~/.defenseclaw/policies/guardrail/${packName}/judge/${judge.name}.yaml`,
      contents: dump({
        version: 1,
        name: judge.name,
        enabled: judge.enabled,
        system_prompt: judge.system_prompt,
        ...(judge.adjudication_prompt ? { adjudication_prompt: judge.adjudication_prompt } : {}),
        ...(judge.min_categories_for_high
          ? { min_categories_for_high: judge.min_categories_for_high }
          : {}),
        ...(judge.min_categories_for_critical
          ? { min_categories_for_critical: judge.min_categories_for_critical }
          : {}),
        ...(judge.single_category_max_severity
          ? { single_category_max_severity: judge.single_category_max_severity }
          : {}),
        categories: judge.categories,
      }),
      description: `Judge config: ${judge.name}`,
    });
  }

  // 7) Custom Rego snippets (Phase 5).
  for (const snippet of policy.custom_rego) {
    if (!snippet.source.trim()) continue;
    files.push({
      path: `~/.defenseclaw/policies/rego/custom-${snippet.name}.rego`,
      contents:
        `# ${snippet.description}\n` +
        `# Generated by docs-site policy creator. Run \`opa check\` after install.\n` +
        snippet.source.trimEnd() +
        '\n',
      description: `Custom Rego snippet: ${snippet.name}`,
    });
  }

  // 8) Session correlator patterns (Layer 5). Only emit when the
  // operator changed something — if the wizard's set matches the
  // bundled defaults verbatim, the gateway will pick those up
  // automatically and the override file just adds noise. We skip
  // disabled patterns; an empty result still emits the file so the
  // gateway-side defaults are overridden with "no patterns".
  const enabledPatterns = correlator.filter((p) => p.enabled);
  const hasCustomCorrelator = correlatorDiffersFromDefault(policy);
  if (hasCustomCorrelator) {
    files.push({
      path: `~/.defenseclaw/policies/guardrail/${packName}/correlation-patterns.yaml`,
      contents:
        `# Sliding-window correlation patterns for pack ${packName}.\n` +
        `# Overrides the bundled defaults in internal/guardrail/defaults/.\n` +
        `# See https://defenseclaw.dev/docs/policies#layer-5--session-correlator\n` +
        dump({
          patterns: enabledPatterns.map((p) => ({
            id: p.id,
            ...(p.description ? { description: p.description } : {}),
            window_events: p.window_events,
            severity_on_match: p.severity_on_match,
            ...(p.all_of && p.all_of.length > 0 ? { all_of: p.all_of } : {}),
            ...(p.sequence && p.sequence.length > 0 ? { sequence: p.sequence } : {}),
            ...(p.fingerprint_chain && p.fingerprint_chain.length > 0
              ? { fingerprint_chain: p.fingerprint_chain }
              : {}),
          })),
        }),
      description: `Session correlator patterns (${enabledPatterns.length} enabled)`,
    });
  }

  return files;
}

// Compare the wizard's correlator state to the bundled defaults for
// the preset the operator started from. We only emit a YAML override
// when the operator has actually changed something — disabled a
// pattern, edited a window, added a clause — to avoid shadowing
// future upstream pattern updates with stale copies.
//
// Compares canonical signatures (id-keyed JSON, missing fields
// normalized) so reordering, key insertion order, and the presence
// of optional empty arrays don't trip a false "edited".
function correlatorDiffersFromDefault(policy: Policy): boolean {
  const current = canonicalCorrelator(policy.correlator ?? []);
  const baseline = canonicalCorrelator(policyFromPreset(policy.basedOn).correlator);
  return current !== baseline;
}

// Canonical signature of a correlator state. Used by the emit-or-not
// heuristic and exposed for tests. The signature is deterministic
// across re-renders so identical states hash to the same string.
function canonicalCorrelator(patterns: CorrelationPattern[]): string {
  const sorted = [...patterns]
    .map((p) => ({
      id: p.id,
      enabled: p.enabled !== false,
      description: p.description ?? '',
      window_events: p.window_events,
      severity_on_match: p.severity_on_match,
      all_of: (p.all_of ?? []).map(canonicalClause),
      sequence: (p.sequence ?? []).map((s) => ({ severity: s.severity })),
      fingerprint_chain: (p.fingerprint_chain ?? []).map(canonicalClause),
    }))
    .sort((a, b) => a.id.localeCompare(b.id));
  return JSON.stringify(sorted);
}

function canonicalClause(c: {
  axis?: string;
  tool_capability_class?: string;
  with_rule_match?: string[];
  min_severity?: string;
}): Record<string, unknown> {
  return {
    axis: c.axis ?? '',
    tool_capability_class: c.tool_capability_class ?? '',
    with_rule_match: [...(c.with_rule_match ?? [])].sort(),
    min_severity: c.min_severity ?? '',
  };
}

// Exposed for tests so we can assert "edit to a bundled pattern is
// detected as different from the baseline preset".
export const __TEST_INTERNALS = {
  correlatorDiffersFromDefault,
  canonicalCorrelator,
};
