//! Compliance narrative format for exec audit explanation.
//!
//! Formats audit operations into a three-question narrative structure:
//! 1. "granted:" — What authority was granted to this command?
//! 2. "target:" — How was the target command evaluated and resolved?
//! 3. "denied/stripped:" — What was denied or stripped, and why?
//!
//! This format is designed for compliance review and operator diagnosis without
//! leaking plaintext secret values.

use serde::{Deserialize, Serialize};

use crate::audit::{AuditExecContext, AuditStatus};
use crate::audit_explain::ExecutionAuthoritySummary;

/// Compliance narrative explaining an exec operation in three questions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ComplianceNarrative {
    /// What authority was granted to this command?
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub granted: Option<GrantedAuthority>,

    /// How was the target command evaluated and resolved?
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<TargetDecisionChain>,

    /// What was denied or stripped, and why?
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub denied_stripped: Option<DeniedAndStripped>,
}

/// Q1: What authority was granted?
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantedAuthority {
    /// The contract name (if provided) or a summary of the mode/policy
    pub policy_source: String,

    /// Number of secrets granted (by name hash ref)
    pub secret_count: usize,

    /// Trust level applied (standard / hardened / custom)
    pub trust_level: String,

    /// Environment inheritance mode (full / minimal / custom)
    pub inherit_mode: String,

    /// Whether output redaction is active
    pub output_redacted: bool,
}

/// Q2: How was the target evaluated?
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetDecisionChain {
    /// The command that was attempted
    pub command: String,

    /// The decision code (UNCONSTRAINED, ALLOWED_EXACT, ALLOWED_BASENAME, MISSING, DENIED)
    pub decision_code: String,

    /// Human-readable explanation of the decision
    pub decision_explanation: String,

    /// If denied, the reason code and message
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub denial_reason: Option<String>,
}

/// Q3: What was denied or stripped?
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeniedAndStripped {
    /// Environment variables stripped (those not in --only or --inherit policy)
    pub stripped_env_names: Vec<String>,

    /// Secrets that were blocked (in --allowed but not injected)
    pub blocked_secrets: Vec<String>,

    /// Top-level deny reason if the operation failed
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub operation_denied_reason: Option<String>,

    /// Access profile enforcement notes (e.g. read-only violations)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub access_profile_notes: Option<String>,
}

impl ComplianceNarrative {
    /// Create a compliance narrative from exec context and summary.
    pub fn from_exec_context(
        ctx: &AuditExecContext,
        _summary: Option<&ExecutionAuthoritySummary>,
        _status: &AuditStatus,
    ) -> Self {
        let granted = ctx.trust_level.as_ref().map(|trust| GrantedAuthority {
            policy_source: ctx
                .contract_name
                .clone()
                .unwrap_or_else(|| "explicit flags".to_string()),
            secret_count: ctx.injected_secrets.len(),
            trust_level: trust.as_str().to_string(),
            inherit_mode: ctx
                .inherit
                .map(|m| format!("{m:?}").to_lowercase())
                .unwrap_or_else(|| "unknown".to_string()),
            output_redacted: ctx.redact_output.unwrap_or(false),
        });

        let target = ctx.target.as_ref().map(|cmd| {
            let (decision_code, decision_explanation) = match ctx.target_decision {
                Some(td) => (format!("{td:?}"), format!("{td:?}")),
                None => (
                    "UNKNOWN".to_string(),
                    "no target decision recorded".to_string(),
                ),
            };

            let denial_reason = ctx
                .deny_reason
                .map(|r| format!("{}: {}", r.code(), r.message()));

            TargetDecisionChain {
                command: cmd.clone(),
                decision_code,
                decision_explanation,
                denial_reason,
            }
        });

        let denied_stripped = Some(DeniedAndStripped {
            stripped_env_names: ctx.dropped_env_names.clone(),
            blocked_secrets: ctx
                .allowed_secrets
                .iter()
                .filter(|s| !ctx.injected_secrets.contains(s))
                .cloned()
                .collect(),
            operation_denied_reason: ctx
                .deny_reason
                .map(|r| format!("{}: {}", r.code(), r.message())),
            access_profile_notes: None,
        });

        Self {
            granted,
            target,
            denied_stripped,
        }
    }

    /// Format as plaintext for human review.
    pub fn to_plaintext(&self) -> String {
        let mut output = String::new();

        // Q1: Granted
        if let Some(grant) = &self.granted {
            output.push_str("granted:\n");
            output.push_str(&format!("  policy_source: {}\n", grant.policy_source));
            output.push_str(&format!("  secret_count: {}\n", grant.secret_count));
            output.push_str(&format!("  trust_level: {}\n", grant.trust_level));
            output.push_str(&format!("  inherit_mode: {}\n", grant.inherit_mode));
            output.push_str(&format!("  output_redacted: {}\n", grant.output_redacted));
        }

        // Q2: Target
        if let Some(tgt) = &self.target {
            output.push_str("target:\n");
            output.push_str(&format!("  command: {}\n", tgt.command));
            output.push_str(&format!("  decision: {}\n", tgt.decision_code));
            if let Some(reason) = &tgt.denial_reason {
                output.push_str(&format!("  denial_reason: {reason}\n"));
            }
        }

        // Q3: Denied/Stripped
        if let Some(ds) = &self.denied_stripped {
            output.push_str("denied_stripped:\n");
            if !ds.stripped_env_names.is_empty() {
                output.push_str(&format!(
                    "  stripped_env: {}\n",
                    ds.stripped_env_names.join(", ")
                ));
            }
            if !ds.blocked_secrets.is_empty() {
                output.push_str(&format!(
                    "  blocked_secrets: {}\n",
                    ds.blocked_secrets.len()
                ));
            }
            if let Some(reason) = &ds.operation_denied_reason {
                output.push_str(&format!("  denial_reason: {reason}\n"));
            }
        }

        output
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compliance_narrative_plaintext_format_is_readable() {
        let narrative = ComplianceNarrative {
            granted: Some(GrantedAuthority {
                policy_source: "deploy".to_string(),
                secret_count: 2,
                trust_level: "hardened".to_string(),
                inherit_mode: "minimal".to_string(),
                output_redacted: true,
            }),
            target: Some(TargetDecisionChain {
                command: "terraform".to_string(),
                decision_code: "AllowedExact".to_string(),
                decision_explanation: "matched allowlist by exact name".to_string(),
                denial_reason: None,
            }),
            denied_stripped: Some(DeniedAndStripped {
                stripped_env_names: vec!["HOME".to_string(), "PATH".to_string()],
                blocked_secrets: vec![],
                operation_denied_reason: None,
                access_profile_notes: None,
            }),
        };

        let text = narrative.to_plaintext();
        assert!(text.contains("granted:"));
        assert!(text.contains("policy_source: deploy"));
        assert!(text.contains("target:"));
        assert!(text.contains("command: terraform"));
        assert!(text.contains("denied_stripped:"));
        assert!(text.contains("stripped_env: HOME, PATH"));
    }
}
