use anyhow::Result;
use clap::ValueEnum;
#[cfg(not(coverage))]
use inquire::Select;
use serde::{Deserialize, Serialize};

use crate::{
    detection,
    policy::{AccessRequest, PolicyEvaluation},
};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum ApprovalScope {
    Once,
    Session,
    Branch,
    Always,
    Deny,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ApprovalSource {
    LocalTty,
    ManualAllow,
    AgentMediated,
    Grant,
    PolicyAuto,
    PolicyDeny,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalDecision {
    pub approved: bool,
    pub scope: ApprovalScope,
    pub approved_env: Vec<String>,
    pub denied_env: Vec<String>,
    pub source: ApprovalSource,
    pub grant_id: Option<uuid::Uuid>,
}

pub fn prompt_for_approval(
    request: &AccessRequest,
    evaluation: &PolicyEvaluation,
) -> Result<ApprovalDecision> {
    print_request_summary(request, evaluation);
    let critical = detection::has_critical_findings(&evaluation.findings);
    let suspicious_action = detection::has_suspicious_action_findings(&evaluation.findings);
    if has_action_injection_findings(&evaluation.findings) {
        print_action_warning();
    }
    if critical {
        print_critical_warning();
    }

    if let Some(scope) = test_approval_scope()? {
        validate_scope_for_findings(scope, &evaluation.findings)?;
        return Ok(decision_for_scope(request, scope));
    }

    let choices = if critical {
        critical_approval_choices()
    } else {
        approval_choices(request.branch.is_some(), !suspicious_action)
    };
    let selected = select_approval_scope(choices)?;
    Ok(decision_for_scope(request, selected))
}

pub(crate) fn approval_choices(include_branch: bool, include_always: bool) -> Vec<ApprovalScope> {
    let mut choices = vec![ApprovalScope::Once, ApprovalScope::Session];
    if include_branch {
        choices.push(ApprovalScope::Branch);
    }
    if include_always {
        choices.push(ApprovalScope::Always);
    }
    choices.push(ApprovalScope::Deny);
    choices
}

pub(crate) fn critical_approval_choices() -> Vec<ApprovalScope> {
    vec![ApprovalScope::Deny, ApprovalScope::Once]
}

pub(crate) fn validate_scope_for_critical(scope: ApprovalScope, critical: bool) -> Result<()> {
    if critical && !matches!(scope, ApprovalScope::Deny | ApprovalScope::Once) {
        anyhow::bail!("critical requests can only be approved with --scope once");
    }
    Ok(())
}

pub(crate) fn validate_scope_for_findings(
    scope: ApprovalScope,
    findings: &[detection::Finding],
) -> Result<()> {
    validate_scope_for_critical(scope, detection::has_critical_findings(findings))?;
    if scope == ApprovalScope::Always && detection::has_suspicious_action_findings(findings) {
        anyhow::bail!("suspicious action text cannot be approved with --scope always");
    }
    Ok(())
}

#[cfg(not(coverage))]
fn select_approval_scope(choices: Vec<ApprovalScope>) -> Result<ApprovalScope> {
    Ok(Select::new("Approve scoped env access?", choices).prompt()?)
}

#[cfg(coverage)]
fn select_approval_scope(choices: Vec<ApprovalScope>) -> Result<ApprovalScope> {
    choices
        .into_iter()
        .next()
        .ok_or_else(|| anyhow::anyhow!("approval prompt has no choices"))
}

pub(crate) fn test_approval_scope() -> Result<Option<ApprovalScope>> {
    let Ok(value) = std::env::var("WARD_UNSAFE_TEST_APPROVAL") else {
        return Ok(None);
    };
    let scope = match value.trim().to_ascii_lowercase().as_str() {
        "once" => ApprovalScope::Once,
        "session" => ApprovalScope::Session,
        "branch" => ApprovalScope::Branch,
        "always" => ApprovalScope::Always,
        "deny" => ApprovalScope::Deny,
        other => anyhow::bail!("invalid WARD_UNSAFE_TEST_APPROVAL value: {other}"),
    };
    Ok(Some(scope))
}

pub(crate) fn decision_for_scope(
    request: &AccessRequest,
    scope: ApprovalScope,
) -> ApprovalDecision {
    let approved = scope != ApprovalScope::Deny;
    ApprovalDecision {
        approved,
        scope,
        approved_env: if approved {
            request.env.clone()
        } else {
            Vec::new()
        },
        denied_env: if approved {
            Vec::new()
        } else {
            request.env.clone()
        },
        source: ApprovalSource::LocalTty,
        grant_id: None,
    }
}

pub fn auto_approval(evaluation: &PolicyEvaluation) -> ApprovalDecision {
    ApprovalDecision {
        approved: evaluation.denied_env.is_empty(),
        scope: ApprovalScope::Once,
        approved_env: evaluation.approved_env.clone(),
        denied_env: evaluation.denied_env.clone(),
        source: ApprovalSource::PolicyAuto,
        grant_id: None,
    }
}

fn print_request_summary(request: &AccessRequest, evaluation: &PolicyEvaluation) {
    eprintln!("Ward access request");
    eprintln!("Project: {}", request.project);
    if let Some(agent) = &request.agent {
        eprintln!("Agent: {agent}");
    }
    if let Some(branch) = &request.branch {
        eprintln!("Branch: {branch}");
    }
    if let Some(action) = &request.action {
        eprintln!("Declared action: {action}");
    }
    eprintln!("Command: {}", request.command);
    eprintln!("Requested env: {}", request.env.join(", "));

    if let Some(preset) = &evaluation.matched_preset {
        eprintln!("Matched preset: {preset}");
    }
    if let Some(profile) = &evaluation.matched_profile {
        eprintln!("Matched profile: {profile}");
    }

    for finding in &evaluation.findings {
        eprintln!(
            "[{:?}] {}: {}",
            finding.severity, finding.code, finding.message
        );
    }
}

fn print_critical_warning() {
    eprintln!();
    eprintln!("CRITICAL Ward warning");
    eprintln!("This command matched known secret-exfiltration patterns.");
    eprintln!("Deny unless you explicitly expect this exact command to inspect secrets.");
}

fn print_action_warning() {
    eprintln!();
    eprintln!("Ward action warning");
    eprintln!("The declared action contains suspicious or coercive text.");
    eprintln!("Review the exact command and requested env vars before approving.");
}

fn has_action_injection_findings(findings: &[detection::Finding]) -> bool {
    detection::has_suspicious_action_findings(findings)
        || findings
            .iter()
            .any(|finding| finding.code == "action.secret_exfil_hint")
}

impl std::fmt::Display for ApprovalScope {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ApprovalScope::Once => write!(formatter, "Allow once"),
            ApprovalScope::Session => write!(formatter, "Allow for session"),
            ApprovalScope::Branch => write!(formatter, "Allow for branch"),
            ApprovalScope::Always => write!(formatter, "Always allow"),
            ApprovalScope::Deny => write!(formatter, "Deny"),
        }
    }
}

impl ApprovalSource {
    pub fn is_persistable_approval(self) -> bool {
        matches!(
            self,
            ApprovalSource::LocalTty | ApprovalSource::ManualAllow | ApprovalSource::AgentMediated
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        detection::Finding,
        policy::{AccessRequest, ApprovalMode, PolicyEvaluation},
    };
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }

    fn access() -> AccessRequest {
        AccessRequest {
            project: "demo".to_string(),
            agent: Some("codex".to_string()),
            branch: Some("feature/test".to_string()),
            action: Some("Run dev".to_string()),
            command: "pnpm dev".to_string(),
            env: vec!["DATABASE_URL".to_string()],
        }
    }

    fn evaluation() -> PolicyEvaluation {
        PolicyEvaluation {
            matched_profile: None,
            matched_preset: Some("Next.js Dev Server".to_string()),
            matched_mode: None,
            approval_mode: ApprovalMode::Prompt,
            requested_env: vec!["DATABASE_URL".to_string()],
            approved_env: vec!["DATABASE_URL".to_string()],
            denied_env: Vec::new(),
            requires_prompt: true,
            findings: vec![Finding {
                severity: crate::detection::Severity::Info,
                code: "test.info".to_string(),
                message: "test finding".to_string(),
            }],
        }
    }

    #[test]
    fn approval_choices_include_branch_when_available() {
        assert_eq!(
            approval_choices(true, true),
            vec![
                ApprovalScope::Once,
                ApprovalScope::Session,
                ApprovalScope::Branch,
                ApprovalScope::Always,
                ApprovalScope::Deny
            ]
        );
        assert_eq!(
            approval_choices(false, true),
            vec![
                ApprovalScope::Once,
                ApprovalScope::Session,
                ApprovalScope::Always,
                ApprovalScope::Deny
            ]
        );
        assert_eq!(
            approval_choices(true, false),
            vec![
                ApprovalScope::Once,
                ApprovalScope::Session,
                ApprovalScope::Branch,
                ApprovalScope::Deny
            ]
        );
        assert_eq!(
            critical_approval_choices(),
            vec![ApprovalScope::Deny, ApprovalScope::Once]
        );
    }

    #[test]
    fn decision_for_scope_handles_allowed_and_denied_scopes() {
        let access = access();
        let allowed = decision_for_scope(&access, ApprovalScope::Always);
        let denied = decision_for_scope(&access, ApprovalScope::Deny);

        assert!(allowed.approved);
        assert_eq!(allowed.approved_env, vec!["DATABASE_URL"]);
        assert!(allowed.denied_env.is_empty());
        assert!(!denied.approved);
        assert!(denied.approved_env.is_empty());
        assert_eq!(denied.denied_env, vec!["DATABASE_URL"]);
    }

    #[test]
    #[serial_test::serial]
    fn parses_all_test_approval_scopes_and_rejects_invalid_values() {
        let _guard = env_lock();

        for (value, expected) in [
            ("once", ApprovalScope::Once),
            ("session", ApprovalScope::Session),
            ("branch", ApprovalScope::Branch),
            ("always", ApprovalScope::Always),
            ("deny", ApprovalScope::Deny),
        ] {
            std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", value);
            assert_eq!(test_approval_scope().unwrap(), Some(expected));
        }

        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "bad");
        assert!(test_approval_scope().is_err());
        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");
        assert_eq!(test_approval_scope().unwrap(), None);
    }

    #[test]
    fn auto_approval_uses_policy_evaluation() {
        let mut evaluation = evaluation();
        evaluation.requires_prompt = false;
        let decision = auto_approval(&evaluation);

        assert!(decision.approved);
        assert_eq!(decision.source, ApprovalSource::PolicyAuto);
        assert_eq!(decision.approved_env, vec!["DATABASE_URL"]);
    }

    #[test]
    fn display_strings_are_user_facing() {
        assert_eq!(ApprovalScope::Once.to_string(), "Allow once");
        assert_eq!(ApprovalScope::Session.to_string(), "Allow for session");
        assert_eq!(ApprovalScope::Branch.to_string(), "Allow for branch");
        assert_eq!(ApprovalScope::Always.to_string(), "Always allow");
        assert_eq!(ApprovalScope::Deny.to_string(), "Deny");
    }

    #[test]
    #[serial_test::serial]
    fn prompt_for_approval_uses_test_scope() {
        let _guard = env_lock();
        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "deny");

        let decision = prompt_for_approval(&access(), &evaluation()).unwrap();

        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");
        assert!(!decision.approved);
        assert_eq!(decision.scope, ApprovalScope::Deny);
    }

    #[test]
    #[serial_test::serial]
    fn prompt_for_approval_restricts_critical_test_scope() {
        let _guard = env_lock();
        let mut evaluation = evaluation();
        evaluation.findings = vec![Finding::critical("critical.test", "critical")];
        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "session");

        assert!(prompt_for_approval(&access(), &evaluation).is_err());

        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "once");
        let decision = prompt_for_approval(&access(), &evaluation).unwrap();
        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");
        assert_eq!(decision.scope, ApprovalScope::Once);
        assert!(decision.approved);
        assert!(validate_scope_for_critical(ApprovalScope::Always, true).is_err());
        assert!(validate_scope_for_critical(ApprovalScope::Always, false).is_ok());
    }

    #[test]
    #[serial_test::serial]
    fn prompt_for_approval_restricts_suspicious_action_always_scope() {
        let _guard = env_lock();
        let mut evaluation = evaluation();
        evaluation.findings = vec![Finding::warning(
            "action.approval_coercion",
            "coercive action",
        )];
        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "always");

        assert!(prompt_for_approval(&access(), &evaluation).is_err());

        std::env::set_var("WARD_UNSAFE_TEST_APPROVAL", "session");
        let decision = prompt_for_approval(&access(), &evaluation).unwrap();
        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");
        assert_eq!(decision.scope, ApprovalScope::Session);
    }

    #[test]
    fn suspicious_action_findings_reject_always_scope() {
        let findings = vec![Finding::warning(
            "action.prompt_injection",
            "suspicious action",
        )];

        assert!(validate_scope_for_findings(ApprovalScope::Always, &findings).is_err());
        assert!(validate_scope_for_findings(ApprovalScope::Session, &findings).is_ok());
    }

    #[cfg(coverage)]
    #[test]
    #[serial_test::serial]
    fn prompt_for_approval_uses_coverage_terminal_stub_without_test_scope() {
        let _guard = env_lock();
        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");

        let decision = prompt_for_approval(&access(), &evaluation()).unwrap();

        assert!(decision.approved);
        assert_eq!(decision.scope, ApprovalScope::Once);
    }

    #[cfg(coverage)]
    #[test]
    #[serial_test::serial]
    fn critical_prompt_uses_coverage_terminal_stub_without_test_scope() {
        let _guard = env_lock();
        std::env::remove_var("WARD_UNSAFE_TEST_APPROVAL");
        let mut evaluation = evaluation();
        evaluation.findings = vec![Finding::critical("critical.test", "critical")];

        let decision = prompt_for_approval(&access(), &evaluation).unwrap();

        assert!(!decision.approved);
        assert_eq!(decision.scope, ApprovalScope::Deny);
    }

    #[cfg(coverage)]
    #[test]
    fn coverage_prompt_rejects_empty_choice_list() {
        assert!(select_approval_scope(Vec::new()).is_err());
    }
}
