use crate::PolicyError;
use serde::{Deserialize, Serialize};

const UNKNOWN_PERMISSION_ATOM_PREFIX: &str = "unknown permission atom: ";

/// String constants for every recognised permission atom.
pub mod atom {
    /// Create a workflow template.
    pub const WORKFLOW_TEMPLATE_CREATE: &str = "workflow:template_create";
    /// Read a workflow template.
    pub const WORKFLOW_TEMPLATE_READ: &str = "workflow:template_read";
    /// Retire a workflow template.
    pub const WORKFLOW_TEMPLATE_RETIRE: &str = "workflow:template_retire";

    /// Create a workflow instance.
    pub const WORKFLOW_INSTANCE_CREATE: &str = "workflow:instance_create";
    /// Read a workflow instance.
    pub const WORKFLOW_INSTANCE_READ: &str = "workflow:instance_read";
    /// Execute a step on a workflow instance.
    pub const WORKFLOW_INSTANCE_EXECUTE: &str = "workflow:instance_execute";
    /// Cancel a workflow instance.
    pub const WORKFLOW_INSTANCE_CANCEL: &str = "workflow:instance_cancel";

    /// Create an endpoint configuration.
    pub const ENDPOINT_CREATE: &str = "endpoint:create";
    /// Rotate an endpoint configuration key.
    pub const ENDPOINT_ROTATE: &str = "endpoint:rotate";
    /// Retire an endpoint configuration.
    pub const ENDPOINT_RETIRE: &str = "endpoint:retire";
    /// Read endpoint metadata (non-decrypted).
    pub const ENDPOINT_READ_METADATA: &str = "endpoint:read_metadata";
    /// Read decrypted endpoint configuration.
    pub const ENDPOINT_READ_DECRYPTED: &str = "endpoint:read_decrypted";

    /// Create embedding datasets.
    pub const EMBED_DATASET_CREATE: &str = "embed_dataset:create";
    /// Read embedding datasets.
    pub const EMBED_DATASET_READ: &str = "embed_dataset:read";
    /// Update embedding datasets.
    pub const EMBED_DATASET_UPDATE: &str = "embed_dataset:update";
    /// Retire embedding datasets.
    pub const EMBED_DATASET_RETIRE: &str = "embed_dataset:retire";

    /// Manage principals within a tenant.
    pub const TENANT_PRINCIPAL_MANAGE: &str = "tenant:principal_manage";
    /// Manage roles within a tenant.
    pub const TENANT_ROLE_MANAGE: &str = "tenant:role_manage";

    /// Manage minting authorities within a tenant.
    pub const TENANT_MINTING_MANAGE: &str = "tenant:minting_manage";
    /// Mint ephemeral API tokens.
    pub const MINT_EPHEMERAL_TOKEN: &str = "mint:ephemeral_token";

    /// Read tenant settings.
    pub const TENANT_SETTINGS_READ: &str = "tenant:settings_read";
    /// Manage tenant settings.
    pub const TENANT_SETTINGS_MANAGE: &str = "tenant:settings_manage";

    /// Read audit events.
    pub const AUDIT_READ: &str = "audit:read";

    /// Manage tenants at the deployment level.
    pub const DEPLOYMENT_TENANT_MANAGE: &str = "deployment:tenant_manage";
    /// Manage realms at the deployment level.
    pub const DEPLOYMENT_REALM_MANAGE: &str = "deployment:realm_manage";
    /// Read audit events at the deployment level.
    pub const DEPLOYMENT_AUDIT_READ: &str = "deployment:audit_read";
}

/// Complete list of recognised permission atoms.
pub const ALL_ATOMS: [&str; 26] = [
    atom::WORKFLOW_TEMPLATE_CREATE,
    atom::WORKFLOW_TEMPLATE_READ,
    atom::WORKFLOW_TEMPLATE_RETIRE,
    atom::WORKFLOW_INSTANCE_CREATE,
    atom::WORKFLOW_INSTANCE_READ,
    atom::WORKFLOW_INSTANCE_EXECUTE,
    atom::WORKFLOW_INSTANCE_CANCEL,
    atom::ENDPOINT_CREATE,
    atom::ENDPOINT_ROTATE,
    atom::ENDPOINT_RETIRE,
    atom::ENDPOINT_READ_METADATA,
    atom::ENDPOINT_READ_DECRYPTED,
    atom::EMBED_DATASET_CREATE,
    atom::EMBED_DATASET_READ,
    atom::EMBED_DATASET_UPDATE,
    atom::EMBED_DATASET_RETIRE,
    atom::TENANT_PRINCIPAL_MANAGE,
    atom::TENANT_ROLE_MANAGE,
    atom::TENANT_MINTING_MANAGE,
    atom::MINT_EPHEMERAL_TOKEN,
    atom::TENANT_SETTINGS_READ,
    atom::TENANT_SETTINGS_MANAGE,
    atom::AUDIT_READ,
    atom::DEPLOYMENT_TENANT_MANAGE,
    atom::DEPLOYMENT_REALM_MANAGE,
    atom::DEPLOYMENT_AUDIT_READ,
];

/// Validated set of permission atoms attached to a role.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PermissionDocument {
    permissions: Vec<String>,
}

impl PermissionDocument {
    /// Check whether this document includes the given permission atom.
    pub fn contains(&self, atom: &str) -> bool {
        self.permissions.iter().any(|value| value == atom)
    }

    /// Return the contained permission atom strings.
    pub fn permissions(&self) -> &[String] {
        &self.permissions
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
enum PermissionDocumentWire {
    Bare(Vec<String>),
    Wrapped { permissions: Vec<String> },
}

impl<'de> Deserialize<'de> for PermissionDocument {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let wire = PermissionDocumentWire::deserialize(deserializer)?;
        let permissions = match wire {
            PermissionDocumentWire::Bare(values) => values,
            PermissionDocumentWire::Wrapped { permissions } => permissions,
        };

        for atom in &permissions {
            if !ALL_ATOMS.contains(&atom.as_str()) {
                return Err(serde::de::Error::custom(format!(
                    "{UNKNOWN_PERMISSION_ATOM_PREFIX}{atom}"
                )));
            }
        }

        Ok(Self { permissions })
    }
}

pub(crate) fn parse_permission_document(bytes: &[u8]) -> Result<PermissionDocument, PolicyError> {
    serde_json::from_slice(bytes).map_err(map_permission_document_parse_error)
}

fn map_permission_document_parse_error(error: serde_json::Error) -> PolicyError {
    if let Some(atom) = unknown_permission_atom_from_parse_error(&error) {
        return PolicyError::UnknownPermissionAtom { atom };
    }

    PolicyError::PermissionDocumentParse(error)
}

fn unknown_permission_atom_from_parse_error(error: &serde_json::Error) -> Option<String> {
    let message = error.to_string();
    let atom_and_location = message.strip_prefix(UNKNOWN_PERMISSION_ATOM_PREFIX)?;
    let atom = atom_and_location
        .split(" at line ")
        .next()
        .unwrap_or(atom_and_location);
    Some(atom.to_owned())
}

#[cfg(test)]
mod tests {
    use super::{PermissionDocument, atom};

    #[test]
    fn permission_document_parses_bare_array() {
        let doc: PermissionDocument =
            serde_json::from_slice(br#"["workflow:template_read","audit:read"]"#).unwrap();

        assert_eq!(doc.permissions(), ["workflow:template_read", "audit:read"]);
    }

    #[test]
    fn permission_document_parses_wrapped_shape() {
        let doc: PermissionDocument = serde_json::from_slice(
            br#"{"permissions":["workflow:instance_execute","tenant:role_manage"]}"#,
        )
        .unwrap();

        assert_eq!(
            doc.permissions(),
            ["workflow:instance_execute", "tenant:role_manage"]
        );
    }

    #[test]
    fn permission_document_contains_checks_membership() {
        let doc: PermissionDocument =
            serde_json::from_slice(br#"["workflow:template_read","audit:read"]"#).unwrap();

        assert!(doc.contains("audit:read"));
        assert!(!doc.contains("workflow:instance_cancel"));
    }

    #[test]
    fn permission_document_rejects_unknown_atom_bare() {
        let err = serde_json::from_slice::<PermissionDocument>(
            br#"["workflow:template_read","totally:made_up"]"#,
        )
        .unwrap_err();

        assert!(
            err.to_string()
                .contains("unknown permission atom: totally:made_up")
        );
    }

    #[test]
    fn permission_document_rejects_unknown_atom_wrapped() {
        let err = serde_json::from_slice::<PermissionDocument>(
            br#"{"permissions":["workflow:template_read","totally:made_up"]}"#,
        )
        .unwrap_err();

        assert!(
            err.to_string()
                .contains("unknown permission atom: totally:made_up")
        );
    }

    #[test]
    fn permission_document_accepts_empty_array() {
        let doc: PermissionDocument = serde_json::from_slice(br#"[]"#).unwrap();

        assert!(doc.permissions().is_empty());
    }

    #[test]
    fn permission_document_accepts_embed_dataset_atoms() {
        let doc: PermissionDocument = serde_json::from_slice(
            br#"[
                "embed_dataset:create",
                "embed_dataset:read",
                "embed_dataset:update",
                "embed_dataset:retire"
            ]"#,
        )
        .unwrap();

        assert_eq!(
            doc.permissions(),
            [
                atom::EMBED_DATASET_CREATE,
                atom::EMBED_DATASET_READ,
                atom::EMBED_DATASET_UPDATE,
                atom::EMBED_DATASET_RETIRE
            ]
        );
    }
}
