use std::path::Path;

use cedar_policy::{Authorizer, Context as CedarContext, Entities, PolicySet, Request};
use tracing::debug;

use crate::error::{Result, WorkflowError};

/// Cedar policy authorizer for per-stage execution control.
///
/// Loads `.cedar` policy files from a directory and evaluates authorization
/// decisions before each stage dispatch. Authorization is
/// principal/action/resource based: `User::"vapora-orchestrator"`,
/// `Action::"execute-stage"`, `Stage::"<name>"`.
pub struct CedarAuthorizer {
    policy_set: PolicySet,
    authorizer: Authorizer,
}

impl CedarAuthorizer {
    /// Load all `.cedar` policy files from `dir`, combining them into a single
    /// policy set.
    pub fn load_from_dir(dir: &Path) -> Result<Self> {
        let entries = std::fs::read_dir(dir).map_err(|e| {
            WorkflowError::Internal(format!("reading Cedar policy dir '{}': {e}", dir.display()))
        })?;

        let mut combined = String::new();

        for entry in entries {
            let entry = entry
                .map_err(|e| WorkflowError::Internal(format!("reading Cedar dir entry: {e}")))?;
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("cedar") {
                continue;
            }
            let content = std::fs::read_to_string(&path).map_err(|e| {
                WorkflowError::Internal(format!("reading Cedar policy '{}': {e}", path.display()))
            })?;
            combined.push_str(&content);
            combined.push('\n');
        }

        if combined.trim().is_empty() {
            return Err(WorkflowError::Internal(format!(
                "no .cedar policy files found in '{}'",
                dir.display()
            )));
        }

        let policy_set: PolicySet = combined
            .parse()
            .map_err(|e| WorkflowError::Internal(format!("parsing Cedar policies: {e}")))?;

        Ok(Self {
            policy_set,
            authorizer: Authorizer::new(),
        })
    }

    /// Returns `Ok(())` if permitted, `Err(Unauthorized)` if denied.
    pub fn authorize(&self, principal: &str, action: &str, resource: &str) -> Result<()> {
        let principal_entity: cedar_policy::EntityUid =
            format!("User::\"{principal}\"").parse().map_err(|e| {
                WorkflowError::Internal(format!("parsing Cedar principal EntityUid: {e}"))
            })?;

        let action_entity: cedar_policy::EntityUid = format!("Action::\"{action}\"")
            .parse()
            .map_err(|e| WorkflowError::Internal(format!("parsing Cedar action EntityUid: {e}")))?;

        let resource_entity: cedar_policy::EntityUid = resource.parse().map_err(|e| {
            WorkflowError::Internal(format!(
                "parsing Cedar resource EntityUid '{resource}': {e}"
            ))
        })?;

        let request = Request::new(
            principal_entity,
            action_entity,
            resource_entity,
            CedarContext::empty(),
            None,
        )
        .map_err(|e| WorkflowError::Internal(format!("building Cedar request: {e}")))?;

        let response =
            self.authorizer
                .is_authorized(&request, &self.policy_set, &Entities::empty());

        debug!(
            "Cedar: {principal} {action} {resource} → {:?}",
            response.decision()
        );

        match response.decision() {
            cedar_policy::Decision::Allow => Ok(()),
            cedar_policy::Decision::Deny => Err(WorkflowError::Unauthorized(format!(
                "Cedar denied: principal '{principal}' action '{action}' resource '{resource}'"
            ))),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::io::Write;

    use tempfile::TempDir;

    use super::*;

    fn write_policy(dir: &TempDir, name: &str, content: &str) {
        let path = dir.path().join(name);
        let mut f = std::fs::File::create(path).unwrap();
        f.write_all(content.as_bytes()).unwrap();
    }

    const PERMIT_ORCHESTRATOR: &str = r#"
permit(
    principal == User::"vapora-orchestrator",
    action == Action::"execute-stage",
    resource == Stage::"deploy"
);"#;

    const FORBID_ALL: &str = r#"forbid(principal, action, resource);"#;

    #[test]
    fn test_permit_allows() {
        let dir = TempDir::new().unwrap();
        write_policy(&dir, "permit.cedar", PERMIT_ORCHESTRATOR);

        let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap();
        authz
            .authorize("vapora-orchestrator", "execute-stage", "Stage::\"deploy\"")
            .unwrap();
    }

    #[test]
    fn test_deny_returns_unauthorized() {
        let dir = TempDir::new().unwrap();
        write_policy(&dir, "forbid.cedar", FORBID_ALL);

        let authz = CedarAuthorizer::load_from_dir(dir.path()).unwrap();
        let err = authz
            .authorize("vapora-orchestrator", "execute-stage", "Stage::\"deploy\"")
            .unwrap_err();
        assert!(matches!(err, WorkflowError::Unauthorized(_)));
        assert!(err.to_string().contains("Cedar denied"));
    }

    #[test]
    fn test_empty_dir_fails() {
        let dir = TempDir::new().unwrap();
        let result = CedarAuthorizer::load_from_dir(dir.path());
        assert!(result.is_err());
    }
}
