use chrono::{NaiveTime, Weekday};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

// ---------------------------------------------------------------------------
// Identity -- lightweight representation for policy evaluation.
// dome-core doesn't export an Identity type yet, so we define one here that
// the rest of the gateway can construct when handing off to the policy engine.
// ---------------------------------------------------------------------------

/// Caller identity as seen by the policy engine.
#[derive(Debug, Clone)]
pub struct Identity {
    /// Principal identifier, e.g. "uid:501", "oauth:user@example.com".
    pub principal: String,
    /// Free-form labels that policy rules can match against,
    /// e.g. "role:admin", "team:infra", "env:prod".
    pub labels: HashSet<String>,
}

impl Identity {
    pub fn new(principal: impl Into<String>, labels: impl IntoIterator<Item = String>) -> Self {
        Self {
            principal: principal.into(),
            labels: labels.into_iter().collect(),
        }
    }

    /// Anonymous identity with no labels.
    pub fn anonymous() -> Self {
        Self {
            principal: "anonymous".to_string(),
            labels: HashSet::new(),
        }
    }
}

// ---------------------------------------------------------------------------
// Rule types
// ---------------------------------------------------------------------------

/// What happens when a rule matches.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Effect {
    Allow,
    Deny,
    /// Allow, but flag the call for review in the audit log.
    AuditOnly,
}

/// How to match the caller identity.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IdentityMatcher {
    /// Wildcard -- matches any identity.
    Any(String), // expected value: "*"
    /// Structured matcher with optional principals and/or labels.
    Structured {
        #[serde(default)]
        principals: Vec<String>,
        #[serde(default)]
        labels: Vec<String>,
    },
}

impl IdentityMatcher {
    pub fn matches(&self, identity: &Identity) -> bool {
        match self {
            Self::Any(s) if s == "*" => true,
            Self::Any(_) => false,
            Self::Structured { principals, labels } => {
                let principal_ok =
                    principals.is_empty() || principals.iter().any(|p| p == &identity.principal);

                let labels_ok =
                    labels.is_empty() || labels.iter().all(|l| identity.labels.contains(l));

                principal_ok && labels_ok
            }
        }
    }
}

/// How to match the tool being invoked.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolMatcher {
    /// Wildcard -- matches any tool.
    Wildcard(String), // expected value: "*"
    /// Exact list of tool names (or glob patterns).
    List(Vec<String>),
}

impl ToolMatcher {
    pub fn matches(&self, tool: &str) -> bool {
        match self {
            Self::Wildcard(s) if s == "*" => true,
            Self::Wildcard(_) => false,
            Self::List(patterns) => patterns.iter().any(|p| {
                if p.contains('*') || p.contains('?') || p.contains('[') {
                    glob_match::glob_match(p, tool)
                } else {
                    p == tool
                }
            }),
        }
    }
}

/// Constraint on a single tool argument.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgConstraint {
    /// JSON key name in the tool's arguments object, or "*" for all string args.
    pub param: String,
    /// If set, the argument value must match at least one of these glob patterns.
    #[serde(default)]
    pub allow_glob: Option<Vec<String>>,
    /// If set, the argument value must NOT match any of these regexes.
    #[serde(default)]
    pub deny_regex: Option<Vec<String>>,
    /// Maximum allowed string length.
    #[serde(default)]
    pub max_length: Option<usize>,
}

/// Per-rule rate limit override.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimit {
    pub max: u64,
    pub window: String,
}

/// A time-based or calendar-based condition that must be satisfied for a rule to match.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Condition {
    /// Current time must fall within the [after, before) window.
    /// Handles midnight crossing (e.g. after=22:00, before=06:00).
    TimeWindow {
        after: NaiveTime,
        before: NaiveTime,
        timezone: Option<String>,
    },
    /// Current day of week must be in the given list.
    DayOfWeek { days: Vec<Weekday> },
}

/// Intermediate serde representation for `Condition` in TOML.
#[derive(Debug, Deserialize)]
struct ConditionRaw {
    #[serde(rename = "type")]
    kind: String,
    #[serde(default)]
    after: Option<String>,
    #[serde(default)]
    before: Option<String>,
    #[serde(default)]
    timezone: Option<String>,
    #[serde(default)]
    days: Option<Vec<String>>,
}

impl<'de> Deserialize<'de> for Condition {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = ConditionRaw::deserialize(deserializer)?;
        match raw.kind.as_str() {
            "time_window" => {
                let after_str = raw
                    .after
                    .ok_or_else(|| serde::de::Error::missing_field("after"))?;
                let before_str = raw
                    .before
                    .ok_or_else(|| serde::de::Error::missing_field("before"))?;
                let after = NaiveTime::parse_from_str(&after_str, "%H:%M")
                    .map_err(serde::de::Error::custom)?;
                let before = NaiveTime::parse_from_str(&before_str, "%H:%M")
                    .map_err(serde::de::Error::custom)?;
                Ok(Condition::TimeWindow {
                    after,
                    before,
                    timezone: raw.timezone,
                })
            }
            "day_of_week" => {
                let day_strings = raw
                    .days
                    .ok_or_else(|| serde::de::Error::missing_field("days"))?;
                let days = day_strings
                    .iter()
                    .map(|s| {
                        parse_weekday(s).ok_or_else(|| {
                            serde::de::Error::custom(format!("invalid weekday: '{s}'"))
                        })
                    })
                    .collect::<Result<Vec<_>, _>>()?;
                Ok(Condition::DayOfWeek { days })
            }
            other => Err(serde::de::Error::custom(format!(
                "unknown condition type: '{other}'"
            ))),
        }
    }
}

impl Serialize for Condition {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeMap;
        match self {
            Condition::TimeWindow {
                after,
                before,
                timezone,
            } => {
                let mut map = serializer.serialize_map(None)?;
                map.serialize_entry("type", "time_window")?;
                map.serialize_entry("after", &after.format("%H:%M").to_string())?;
                map.serialize_entry("before", &before.format("%H:%M").to_string())?;
                if let Some(tz) = timezone {
                    map.serialize_entry("timezone", tz)?;
                }
                map.end()
            }
            Condition::DayOfWeek { days } => {
                let mut map = serializer.serialize_map(None)?;
                map.serialize_entry("type", "day_of_week")?;
                let day_strs: Vec<&str> = days.iter().map(|d| weekday_to_str(*d)).collect();
                map.serialize_entry("days", &day_strs)?;
                map.end()
            }
        }
    }
}

/// Parse a weekday abbreviation like "Mon", "Tue", etc.
fn parse_weekday(s: &str) -> Option<Weekday> {
    match s {
        "Mon" => Some(Weekday::Mon),
        "Tue" => Some(Weekday::Tue),
        "Wed" => Some(Weekday::Wed),
        "Thu" => Some(Weekday::Thu),
        "Fri" => Some(Weekday::Fri),
        "Sat" => Some(Weekday::Sat),
        "Sun" => Some(Weekday::Sun),
        _ => None,
    }
}

fn weekday_to_str(d: Weekday) -> &'static str {
    match d {
        Weekday::Mon => "Mon",
        Weekday::Tue => "Tue",
        Weekday::Wed => "Wed",
        Weekday::Thu => "Thu",
        Weekday::Fri => "Fri",
        Weekday::Sat => "Sat",
        Weekday::Sun => "Sun",
    }
}

/// A single authorization rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
    pub id: String,
    pub priority: u32,
    pub effect: Effect,
    pub identities: IdentityMatcher,
    pub tools: ToolMatcher,
    #[serde(default)]
    pub arguments: Vec<ArgConstraint>,
    #[serde(default)]
    pub conditions: Vec<Condition>,
    #[serde(default)]
    pub rate_limit: Option<RateLimit>,
}

// ---------------------------------------------------------------------------
// Decision -- the output of policy evaluation
// ---------------------------------------------------------------------------

/// The result of evaluating a request against the policy rule set.
#[derive(Debug, Clone)]
pub struct Decision {
    pub effect: Effect,
    pub rule_id: String,
    pub reason: String,
}

impl Decision {
    /// The implicit decision when no rule matches: deny.
    pub fn default_deny() -> Self {
        Self {
            effect: Effect::Deny,
            rule_id: "__default_deny".to_string(),
            reason: "no matching rule (default deny)".to_string(),
        }
    }

    pub fn is_allowed(&self) -> bool {
        matches!(self.effect, Effect::Allow | Effect::AuditOnly)
    }
}
