//! Scope types for IAM
//!
//! Scopes define the hierarchical boundary for permissions.
//! The hierarchy is: System > Org > Project > Resource

use serde::{Deserialize, Serialize};
use std::fmt;

/// Scope represents the hierarchical boundary for permissions
///
/// Hierarchy: System > Org > Project > Resource
///
/// Each scope level includes context about its parent scopes,
/// enabling proper hierarchical permission evaluation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[derive(Default)]
pub enum Scope {
    /// System-wide scope (affects entire cluster)
    #[default]
    System,

    /// Organization scope
    Org {
        /// Organization ID
        id: String,
    },

    /// Project scope (belongs to an organization)
    Project {
        /// Project ID
        id: String,
        /// Parent organization ID
        org_id: String,
    },

    /// Resource-specific scope (belongs to a project)
    Resource {
        /// Resource ID
        id: String,
        /// Parent project ID
        project_id: String,
        /// Parent organization ID
        org_id: String,
    },
}

impl Scope {
    /// Create a system scope
    pub fn system() -> Self {
        Scope::System
    }

    /// Create an organization scope
    pub fn org(id: impl Into<String>) -> Self {
        Scope::Org { id: id.into() }
    }

    /// Create a project scope
    pub fn project(id: impl Into<String>, org_id: impl Into<String>) -> Self {
        Scope::Project {
            id: id.into(),
            org_id: org_id.into(),
        }
    }

    /// Create a resource scope
    pub fn resource(
        id: impl Into<String>,
        project_id: impl Into<String>,
        org_id: impl Into<String>,
    ) -> Self {
        Scope::Resource {
            id: id.into(),
            project_id: project_id.into(),
            org_id: org_id.into(),
        }
    }

    /// Check if this scope contains another scope
    ///
    /// A scope contains another if:
    /// - They are equal
    /// - This scope is a parent of the other scope in the hierarchy
    ///
    /// Examples:
    /// - System contains everything
    /// - Org("acme") contains Project { org_id: "acme", ... }
    /// - Project { id: "proj1", org_id: "acme" } contains Resource { project_id: "proj1", org_id: "acme", ... }
    pub fn contains(&self, other: &Scope) -> bool {
        match (self, other) {
            // System contains everything
            (Scope::System, _) => true,

            // Org contains itself
            (Scope::Org { id: self_id }, Scope::Org { id: other_id }) => self_id == other_id,
            // Org contains projects within it
            (
                Scope::Org { id: org_id },
                Scope::Project {
                    org_id: proj_org_id,
                    ..
                },
            ) => org_id == proj_org_id,
            // Org contains resources within its projects
            (
                Scope::Org { id: org_id },
                Scope::Resource {
                    org_id: res_org_id, ..
                },
            ) => org_id == res_org_id,
            (Scope::Org { .. }, Scope::System) => false,

            // Project contains itself
            (
                Scope::Project {
                    id: self_id,
                    org_id: self_org_id,
                },
                Scope::Project {
                    id: other_id,
                    org_id: other_org_id,
                },
            ) => self_id == other_id && self_org_id == other_org_id,
            // Project contains resources within it
            (
                Scope::Project {
                    id: proj_id,
                    org_id: proj_org_id,
                },
                Scope::Resource {
                    project_id: res_proj_id,
                    org_id: res_org_id,
                    ..
                },
            ) => proj_id == res_proj_id && proj_org_id == res_org_id,
            (Scope::Project { .. }, _) => false,

            // Resource only contains itself
            (
                Scope::Resource {
                    id: self_id,
                    project_id: self_proj_id,
                    org_id: self_org_id,
                },
                Scope::Resource {
                    id: other_id,
                    project_id: other_proj_id,
                    org_id: other_org_id,
                },
            ) => {
                self_id == other_id && self_proj_id == other_proj_id && self_org_id == other_org_id
            }
            (Scope::Resource { .. }, _) => false,
        }
    }

    /// Check whether this scope can be applied to another scope.
    ///
    /// Unlike `contains`, this matcher understands `*` wildcard segments.
    /// It is intended for validating role applicability at bind time.
    pub fn applies_to(&self, other: &Scope) -> bool {
        match (self, other) {
            (Scope::System, _) => true,
            (Scope::Org { id }, Scope::Org { id: other_id }) => segment_matches(id, other_id),
            (
                Scope::Org { id },
                Scope::Project {
                    org_id: other_org_id,
                    ..
                },
            ) => segment_matches(id, other_org_id),
            (
                Scope::Org { id },
                Scope::Resource {
                    org_id: other_org_id,
                    ..
                },
            ) => segment_matches(id, other_org_id),
            (
                Scope::Project { id, org_id },
                Scope::Project {
                    id: other_id,
                    org_id: other_org_id,
                },
            ) => segment_matches(org_id, other_org_id) && segment_matches(id, other_id),
            (
                Scope::Project { id, org_id },
                Scope::Resource {
                    project_id: other_project_id,
                    org_id: other_org_id,
                    ..
                },
            ) => segment_matches(org_id, other_org_id) && segment_matches(id, other_project_id),
            (
                Scope::Resource {
                    id,
                    project_id,
                    org_id,
                },
                Scope::Resource {
                    id: other_id,
                    project_id: other_project_id,
                    org_id: other_org_id,
                },
            ) => {
                segment_matches(org_id, other_org_id)
                    && segment_matches(project_id, other_project_id)
                    && segment_matches(id, other_id)
            }
            _ => false,
        }
    }

    /// Get the parent scope, if any
    ///
    /// - System has no parent
    /// - Org's parent is System
    /// - Project's parent is its Org
    /// - Resource's parent is its Project
    pub fn parent(&self) -> Option<Scope> {
        match self {
            Scope::System => None,
            Scope::Org { .. } => Some(Scope::System),
            Scope::Project { org_id, .. } => Some(Scope::Org { id: org_id.clone() }),
            Scope::Resource {
                project_id, org_id, ..
            } => Some(Scope::Project {
                id: project_id.clone(),
                org_id: org_id.clone(),
            }),
        }
    }

    /// Get all ancestor scopes, from immediate parent to System
    ///
    /// Does not include self.
    pub fn ancestors(&self) -> Vec<Scope> {
        let mut result = Vec::new();
        let mut current = self.parent();
        while let Some(parent) = current {
            let next_parent = parent.parent();
            result.push(parent);
            current = next_parent;
        }
        result
    }

    /// Get all ancestor scopes including self, from self to System
    pub fn ancestors_including_self(&self) -> Vec<Scope> {
        let mut result = vec![self.clone()];
        result.extend(self.ancestors());
        result
    }

    /// Get the scope level (lower = broader)
    ///
    /// - System: 0
    /// - Org: 1
    /// - Project: 2
    /// - Resource: 3
    pub fn level(&self) -> u8 {
        match self {
            Scope::System => 0,
            Scope::Org { .. } => 1,
            Scope::Project { .. } => 2,
            Scope::Resource { .. } => 3,
        }
    }

    /// Get the organization ID if this scope is within an organization
    pub fn org_id(&self) -> Option<&str> {
        match self {
            Scope::System => None,
            Scope::Org { id } => Some(id),
            Scope::Project { org_id, .. } => Some(org_id),
            Scope::Resource { org_id, .. } => Some(org_id),
        }
    }

    /// Get the project ID if this scope is within a project
    pub fn project_id(&self) -> Option<&str> {
        match self {
            Scope::System | Scope::Org { .. } => None,
            Scope::Project { id, .. } => Some(id),
            Scope::Resource { project_id, .. } => Some(project_id),
        }
    }

    /// Convert to a key string for storage
    ///
    /// Format:
    /// - System: "system"
    /// - Org: "org/{org_id}"
    /// - Project: "org/{org_id}/project/{project_id}"
    /// - Resource: "org/{org_id}/project/{project_id}/resource/{resource_id}"
    pub fn to_key(&self) -> String {
        match self {
            Scope::System => "system".to_string(),
            Scope::Org { id } => format!("org/{}", id),
            Scope::Project { id, org_id } => format!("org/{}/project/{}", org_id, id),
            Scope::Resource {
                id,
                project_id,
                org_id,
            } => format!("org/{}/project/{}/resource/{}", org_id, project_id, id),
        }
    }

    /// Parse from a key string
    pub fn from_key(s: &str) -> Option<Self> {
        if s == "system" {
            return Some(Scope::System);
        }

        let parts: Vec<&str> = s.split('/').collect();

        match parts.as_slice() {
            ["org", org_id] => Some(Scope::Org {
                id: (*org_id).to_string(),
            }),
            ["org", org_id, "project", proj_id] => Some(Scope::Project {
                id: (*proj_id).to_string(),
                org_id: (*org_id).to_string(),
            }),
            ["org", org_id, "project", proj_id, "resource", res_id] => Some(Scope::Resource {
                id: (*res_id).to_string(),
                project_id: (*proj_id).to_string(),
                org_id: (*org_id).to_string(),
            }),
            _ => None,
        }
    }
}

fn segment_matches(pattern: &str, value: &str) -> bool {
    pattern == "*" || pattern == value
}

impl fmt::Display for Scope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Scope::System => write!(f, "system"),
            Scope::Org { id } => write!(f, "org:{}", id),
            Scope::Project { id, org_id } => write!(f, "org:{}:project:{}", org_id, id),
            Scope::Resource {
                id,
                project_id,
                org_id,
            } => write!(f, "org:{}:project:{}:resource:{}", org_id, project_id, id),
        }
    }
}

impl std::str::FromStr for Scope {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "system" {
            return Ok(Scope::System);
        }

        let parts: Vec<&str> = s.split(':').collect();

        match parts.as_slice() {
            ["org", org_id] => Ok(Scope::Org {
                id: (*org_id).to_string(),
            }),
            ["org", org_id, "project", proj_id] => Ok(Scope::Project {
                id: (*proj_id).to_string(),
                org_id: (*org_id).to_string(),
            }),
            ["org", org_id, "project", proj_id, "resource", res_id] => Ok(Scope::Resource {
                id: (*res_id).to_string(),
                project_id: (*proj_id).to_string(),
                org_id: (*org_id).to_string(),
            }),
            _ => Err(format!("invalid scope format: {}", s)),
        }
    }
}

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

    #[test]
    fn test_scope_constructors() {
        let sys = Scope::system();
        assert_eq!(sys, Scope::System);

        let org = Scope::org("acme");
        assert_eq!(org, Scope::Org { id: "acme".into() });

        let proj = Scope::project("proj1", "acme");
        assert_eq!(
            proj,
            Scope::Project {
                id: "proj1".into(),
                org_id: "acme".into()
            }
        );

        let res = Scope::resource("res1", "proj1", "acme");
        assert_eq!(
            res,
            Scope::Resource {
                id: "res1".into(),
                project_id: "proj1".into(),
                org_id: "acme".into()
            }
        );
    }

    #[test]
    fn test_scope_contains() {
        let system = Scope::system();
        let org = Scope::org("acme");
        let proj = Scope::project("proj1", "acme");
        let res = Scope::resource("res1", "proj1", "acme");

        // System contains everything
        assert!(system.contains(&system));
        assert!(system.contains(&org));
        assert!(system.contains(&proj));
        assert!(system.contains(&res));

        // Org contains itself and its children
        assert!(org.contains(&org));
        assert!(org.contains(&proj));
        assert!(org.contains(&res));
        assert!(!org.contains(&system));
        assert!(!org.contains(&Scope::org("other")));
        assert!(!org.contains(&Scope::project("proj1", "other")));

        // Project contains itself and its resources
        assert!(proj.contains(&proj));
        assert!(proj.contains(&res));
        assert!(!proj.contains(&system));
        assert!(!proj.contains(&org));
        assert!(!proj.contains(&Scope::resource("res1", "proj2", "acme")));

        // Resource only contains itself
        assert!(res.contains(&res));
        assert!(!res.contains(&system));
        assert!(!res.contains(&org));
        assert!(!res.contains(&proj));
    }

    #[test]
    fn test_scope_parent() {
        let system = Scope::system();
        let org = Scope::org("acme");
        let proj = Scope::project("proj1", "acme");
        let res = Scope::resource("res1", "proj1", "acme");

        assert_eq!(system.parent(), None);
        assert_eq!(org.parent(), Some(system.clone()));
        assert_eq!(proj.parent(), Some(org.clone()));
        assert_eq!(res.parent(), Some(proj.clone()));
    }

    #[test]
    fn test_scope_ancestors() {
        let res = Scope::resource("res1", "proj1", "acme");
        let ancestors = res.ancestors();

        assert_eq!(ancestors.len(), 3);
        assert_eq!(ancestors[0], Scope::project("proj1", "acme"));
        assert_eq!(ancestors[1], Scope::org("acme"));
        assert_eq!(ancestors[2], Scope::system());
    }

    #[test]
    fn test_scope_parse() {
        let scope: Scope = "system".parse().unwrap();
        assert_eq!(scope, Scope::System);

        let scope: Scope = "org:acme".parse().unwrap();
        assert_eq!(scope, Scope::org("acme"));

        let scope: Scope = "org:acme:project:proj1".parse().unwrap();
        assert_eq!(scope, Scope::project("proj1", "acme"));

        let scope: Scope = "org:acme:project:proj1:resource:res1".parse().unwrap();
        assert_eq!(scope, Scope::resource("res1", "proj1", "acme"));
    }

    #[test]
    fn test_scope_key() {
        assert_eq!(Scope::System.to_key(), "system");
        assert_eq!(Scope::org("acme").to_key(), "org/acme");
        assert_eq!(
            Scope::project("proj1", "acme").to_key(),
            "org/acme/project/proj1"
        );
        assert_eq!(
            Scope::resource("res1", "proj1", "acme").to_key(),
            "org/acme/project/proj1/resource/res1"
        );

        assert_eq!(Scope::from_key("system"), Some(Scope::System));
        assert_eq!(Scope::from_key("org/acme"), Some(Scope::org("acme")));
        assert_eq!(
            Scope::from_key("org/acme/project/proj1"),
            Some(Scope::project("proj1", "acme"))
        );
        assert_eq!(
            Scope::from_key("org/acme/project/proj1/resource/res1"),
            Some(Scope::resource("res1", "proj1", "acme"))
        );
    }

    #[test]
    fn test_scope_display() {
        assert_eq!(Scope::System.to_string(), "system");
        assert_eq!(Scope::org("acme").to_string(), "org:acme");
        assert_eq!(
            Scope::project("proj1", "acme").to_string(),
            "org:acme:project:proj1"
        );
        assert_eq!(
            Scope::resource("res1", "proj1", "acme").to_string(),
            "org:acme:project:proj1:resource:res1"
        );
    }

    #[test]
    fn test_scope_level() {
        assert_eq!(Scope::system().level(), 0);
        assert_eq!(Scope::org("acme").level(), 1);
        assert_eq!(Scope::project("p", "o").level(), 2);
        assert_eq!(Scope::resource("r", "p", "o").level(), 3);
    }

    #[test]
    fn test_scope_org_id() {
        assert_eq!(Scope::system().org_id(), None);
        assert_eq!(Scope::org("acme").org_id(), Some("acme"));
        assert_eq!(Scope::project("p", "acme").org_id(), Some("acme"));
        assert_eq!(Scope::resource("r", "p", "acme").org_id(), Some("acme"));
    }

    #[test]
    fn test_scope_project_id() {
        assert_eq!(Scope::system().project_id(), None);
        assert_eq!(Scope::org("acme").project_id(), None);
        assert_eq!(Scope::project("proj1", "acme").project_id(), Some("proj1"));
        assert_eq!(
            Scope::resource("r", "proj1", "acme").project_id(),
            Some("proj1")
        );
    }

    #[test]
    fn test_scope_applies_to_with_wildcards() {
        assert!(Scope::System.applies_to(&Scope::project("proj-1", "org-1")));
        assert!(Scope::org("*").applies_to(&Scope::project("proj-1", "org-1")));
        assert!(Scope::org("org-1").applies_to(&Scope::resource("res-1", "proj-1", "org-1")));
        assert!(Scope::project("*", "*").applies_to(&Scope::project("proj-1", "org-1")));
        assert!(Scope::project("proj-1", "org-1")
            .applies_to(&Scope::resource("res-1", "proj-1", "org-1")));
        assert!(!Scope::project("proj-2", "org-1").applies_to(&Scope::project("proj-1", "org-1")));
        assert!(!Scope::org("org-2").applies_to(&Scope::project("proj-1", "org-1")));
    }
}
