use std::collections::{HashSet, VecDeque};
use std::path::Path;

use glob::Pattern;
use serde::Deserialize;

/// Loaded RBAC policy. Derives `Default` so a missing / unreadable file
/// degrades gracefully.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct FileRbacConfig {
    #[serde(default)]
    pub groups: Vec<FileRbacGroup>,
    #[serde(default)]
    pub user_overrides: Vec<FileRbacUserOverride>,
    #[serde(default)]
    pub defaults: FileRbacDefaults,
}

#[derive(Debug, Clone, Deserialize)]
pub struct FileRbacGroup {
    pub name: String,
    #[serde(default)]
    pub parent_groups: Vec<String>,
    #[serde(default)]
    pub permissions: Vec<PolicyRule>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct FileRbacUserOverride {
    pub id: String,
    #[serde(default)]
    pub permissions: Vec<PolicyRule>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PolicyRule {
    pub resource: ResourceKind,
    pub pattern: String,
    pub outcome: RbacOutcome,
    #[serde(default)]
    pub methods: Vec<String>,
    #[serde(default)]
    pub effects: Vec<PolicyEffect>,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResourceKind {
    Endpoint,
    Page,
    Asset,
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum RbacOutcome {
    Allow,
    Deny,
}

/// Side-effecting actions executed when a rule matches.
/// `type` is the serde discriminator tag matching Nickel's `type` field.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PolicyEffect {
    Redirect {
        to: String,
    },
    Log {
        #[serde(default = "default_level")]
        level: String,
        message: Option<String>,
    },
    AuditLog {
        #[serde(default = "default_level")]
        level: String,
        event: Option<String>,
    },
    Notify {
        channel: String,
        message: Option<String>,
    },
    Response {
        #[serde(default = "default_403")]
        status: u16,
        body: Option<serde_json::Value>,
    },
}

fn default_level() -> String {
    "info".to_string()
}
fn default_403() -> u16 {
    403
}

#[derive(Debug, Clone, Deserialize)]
pub struct FileRbacDefaults {
    #[serde(default)]
    pub unauthenticated_allow: Vec<PolicyRule>,
    #[serde(default)]
    pub unauthenticated_deny: Vec<PolicyRule>,
    #[serde(default = "deny_outcome")]
    pub authenticated_fallback: RbacOutcome,
    #[serde(default = "deny_outcome")]
    pub unauthenticated_fallback: RbacOutcome,
    #[serde(default)]
    pub fallback_effects: Vec<PolicyEffect>,
}

fn deny_outcome() -> RbacOutcome {
    RbacOutcome::Deny
}

impl Default for FileRbacDefaults {
    fn default() -> Self {
        Self {
            unauthenticated_allow: Vec::new(),
            unauthenticated_deny: Vec::new(),
            authenticated_fallback: RbacOutcome::Deny,
            unauthenticated_fallback: RbacOutcome::Deny,
            fallback_effects: Vec::new(),
        }
    }
}

/// The outcome of a policy evaluation: what the server should do.
pub struct PolicyDecision {
    pub outcome: RbacOutcome,
    pub effects: Vec<PolicyEffect>,
}

impl FileRbacConfig {
    /// Load policy from an `.ncl` or `.toml` file via `rustelo_config`.
    pub fn load(path: &Path) -> anyhow::Result<Self> {
        let json_value = rustelo_config::format::load_config(path)
            .map_err(|e| anyhow::anyhow!("failed to load {}: {}", path.display(), e))?;
        serde_json::from_value(json_value)
            .map_err(|e| anyhow::anyhow!("failed to deserialize FileRbacConfig: {}", e))
    }

    /// Evaluate the policy for an incoming request.
    ///
    /// - `user_groups`: group names from the authenticated user's JWT roles, or
    ///   `None` for unauthenticated requests.
    /// - `user_id`: the user's identity (email or sub) used for per-user
    ///   overrides.
    /// - First-match wins within each evaluation tier.
    pub fn evaluate(
        &self,
        user_groups: Option<&[String]>,
        user_id: Option<&str>,
        kind: ResourceKind,
        path: &str,
        method: &str,
    ) -> PolicyDecision {
        match user_groups {
            None => self.evaluate_unauthenticated(&kind, path, method),
            Some(groups) => self.evaluate_authenticated(groups, user_id, &kind, path, method),
        }
    }

    fn evaluate_unauthenticated(
        &self,
        kind: &ResourceKind,
        path: &str,
        method: &str,
    ) -> PolicyDecision {
        for rule in &self.defaults.unauthenticated_allow {
            if matches_rule(rule, kind, path, method) {
                return PolicyDecision {
                    outcome: rule.outcome.clone(),
                    effects: rule.effects.clone(),
                };
            }
        }
        for rule in &self.defaults.unauthenticated_deny {
            if matches_rule(rule, kind, path, method) {
                return PolicyDecision {
                    outcome: rule.outcome.clone(),
                    effects: rule.effects.clone(),
                };
            }
        }
        PolicyDecision {
            outcome: self.defaults.unauthenticated_fallback.clone(),
            effects: self.defaults.fallback_effects.clone(),
        }
    }

    fn evaluate_authenticated(
        &self,
        user_groups: &[String],
        user_id: Option<&str>,
        kind: &ResourceKind,
        path: &str,
        method: &str,
    ) -> PolicyDecision {
        // Tier 1: per-user overrides (highest priority)
        if let Some(id) = user_id {
            if let Some(entry) = self.user_overrides.iter().find(|o| o.id == id) {
                for rule in &entry.permissions {
                    if matches_rule(rule, kind, path, method) {
                        return PolicyDecision {
                            outcome: rule.outcome.clone(),
                            effects: rule.effects.clone(),
                        };
                    }
                }
            }
        }

        // Tier 2: group rules with BFS inheritance expansion
        let all_groups = expand_groups(user_groups, &self.groups);
        for group_name in &all_groups {
            if let Some(group) = self.groups.iter().find(|g| g.name == *group_name) {
                for rule in &group.permissions {
                    if matches_rule(rule, kind, path, method) {
                        return PolicyDecision {
                            outcome: rule.outcome.clone(),
                            effects: rule.effects.clone(),
                        };
                    }
                }
            }
        }

        // Tier 3: fallback
        PolicyDecision {
            outcome: self.defaults.authenticated_fallback.clone(),
            effects: self.defaults.fallback_effects.clone(),
        }
    }
}

/// Returns `true` when `rule` matches the given resource, path and method.
fn matches_rule(rule: &PolicyRule, kind: &ResourceKind, path: &str, method: &str) -> bool {
    if rule.resource != *kind {
        return false;
    }
    let pattern = match Pattern::new(&rule.pattern) {
        Ok(p) => p,
        Err(_) => return false,
    };
    if !pattern.matches(path) {
        return false;
    }
    rule.methods.is_empty() || rule.methods.iter().any(|m| m.eq_ignore_ascii_case(method))
}

/// BFS expansion of group names following `parent_groups` links.
/// Uses a `HashSet` as cycle guard, so circular references are silently
/// ignored.
fn expand_groups(user_groups: &[String], all_groups: &[FileRbacGroup]) -> Vec<String> {
    let mut seen: HashSet<String> = HashSet::new();
    let mut order: Vec<String> = Vec::new();
    let mut queue: VecDeque<String> = user_groups.iter().cloned().collect();

    while let Some(g) = queue.pop_front() {
        if seen.insert(g.clone()) {
            order.push(g.clone());
            if let Some(group) = all_groups.iter().find(|gr| gr.name == g) {
                for parent in &group.parent_groups {
                    if !seen.contains(parent.as_str()) {
                        queue.push_back(parent.clone());
                    }
                }
            }
        }
    }
    order
}

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

    fn make_config() -> FileRbacConfig {
        FileRbacConfig {
            groups: vec![
                FileRbacGroup {
                    name: "admin".to_string(),
                    parent_groups: vec![],
                    permissions: vec![PolicyRule {
                        resource: ResourceKind::Endpoint,
                        pattern: "/api/*".to_string(),
                        outcome: RbacOutcome::Allow,
                        methods: vec![],
                        effects: vec![],
                    }],
                },
                FileRbacGroup {
                    name: "user".to_string(),
                    parent_groups: vec![],
                    permissions: vec![PolicyRule {
                        resource: ResourceKind::Page,
                        pattern: "/*".to_string(),
                        outcome: RbacOutcome::Allow,
                        methods: vec![],
                        effects: vec![],
                    }],
                },
                FileRbacGroup {
                    name: "moderator".to_string(),
                    parent_groups: vec!["user".to_string()],
                    permissions: vec![],
                },
            ],
            user_overrides: vec![FileRbacUserOverride {
                id: "blocked@test.com".to_string(),
                permissions: vec![PolicyRule {
                    resource: ResourceKind::Page,
                    pattern: "/*".to_string(),
                    outcome: RbacOutcome::Deny,
                    methods: vec![],
                    effects: vec![],
                }],
            }],
            defaults: FileRbacDefaults {
                unauthenticated_allow: vec![
                    PolicyRule {
                        resource: ResourceKind::Page,
                        pattern: "/login".to_string(),
                        outcome: RbacOutcome::Allow,
                        methods: vec![],
                        effects: vec![],
                    },
                    PolicyRule {
                        resource: ResourceKind::Asset,
                        pattern: "/*".to_string(),
                        outcome: RbacOutcome::Allow,
                        methods: vec![],
                        effects: vec![],
                    },
                ],
                unauthenticated_deny: vec![],
                authenticated_fallback: RbacOutcome::Deny,
                unauthenticated_fallback: RbacOutcome::Deny,
                fallback_effects: vec![],
            },
        }
    }

    #[test]
    fn unauthenticated_allow_login() {
        let cfg = make_config();
        let d = cfg.evaluate(None, None, ResourceKind::Page, "/login", "GET");
        assert_eq!(d.outcome, RbacOutcome::Allow);
    }

    #[test]
    fn unauthenticated_deny_protected_page() {
        let cfg = make_config();
        let d = cfg.evaluate(None, None, ResourceKind::Page, "/dashboard", "GET");
        assert_eq!(d.outcome, RbacOutcome::Deny);
    }

    #[test]
    fn unauthenticated_allow_asset() {
        let cfg = make_config();
        let d = cfg.evaluate(None, None, ResourceKind::Asset, "/styles/main.css", "GET");
        assert_eq!(d.outcome, RbacOutcome::Allow);
    }

    #[test]
    fn admin_can_access_api() {
        let cfg = make_config();
        let groups = vec!["admin".to_string()];
        let d = cfg.evaluate(
            Some(&groups),
            Some("admin@test.com"),
            ResourceKind::Endpoint,
            "/api/users",
            "GET",
        );
        assert_eq!(d.outcome, RbacOutcome::Allow);
    }

    #[test]
    fn user_cannot_access_api() {
        let cfg = make_config();
        let groups = vec!["user".to_string()];
        let d = cfg.evaluate(
            Some(&groups),
            Some("user@test.com"),
            ResourceKind::Endpoint,
            "/api/users",
            "GET",
        );
        assert_eq!(d.outcome, RbacOutcome::Deny);
    }

    #[test]
    fn moderator_inherits_user_page_access() {
        let cfg = make_config();
        let groups = vec!["moderator".to_string()];
        // moderator has no page rules, inherits from user via parent_groups
        let d = cfg.evaluate(
            Some(&groups),
            Some("mod@test.com"),
            ResourceKind::Page,
            "/blog",
            "GET",
        );
        assert_eq!(d.outcome, RbacOutcome::Allow);
    }

    #[test]
    fn user_override_blocks_all_pages() {
        let cfg = make_config();
        let groups = vec!["user".to_string()];
        let d = cfg.evaluate(
            Some(&groups),
            Some("blocked@test.com"),
            ResourceKind::Page,
            "/blog",
            "GET",
        );
        assert_eq!(d.outcome, RbacOutcome::Deny);
    }

    #[test]
    fn expand_groups_respects_inheritance() {
        let groups = vec![
            FileRbacGroup {
                name: "child".to_string(),
                parent_groups: vec!["parent".to_string()],
                permissions: vec![],
            },
            FileRbacGroup {
                name: "parent".to_string(),
                parent_groups: vec!["grandparent".to_string()],
                permissions: vec![],
            },
            FileRbacGroup {
                name: "grandparent".to_string(),
                parent_groups: vec![],
                permissions: vec![],
            },
        ];
        let expanded = expand_groups(&["child".to_string()], &groups);
        assert!(expanded.contains(&"child".to_string()));
        assert!(expanded.contains(&"parent".to_string()));
        assert!(expanded.contains(&"grandparent".to_string()));
    }

    #[test]
    fn expand_groups_no_infinite_loop_on_cycle() {
        let groups = vec![
            FileRbacGroup {
                name: "a".to_string(),
                parent_groups: vec!["b".to_string()],
                permissions: vec![],
            },
            FileRbacGroup {
                name: "b".to_string(),
                parent_groups: vec!["a".to_string()],
                permissions: vec![],
            },
        ];
        let expanded = expand_groups(&["a".to_string()], &groups);
        assert_eq!(expanded.len(), 2);
    }
}
