use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};

use crate::error::AuthResult;
use crate::session::{ExtractedSessionToken, SessionTokenSource};

pub const DEFAULT_SESSION_COOKIE_NAME: &str = "athena-auth.session-token";
const ATHENA_MODERN_SESSION_VALIDATION_SQL: &str = r#"
    SELECT
        s.id AS session_id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        COALESCE(
            array_agg(DISTINCT m.organization_id) FILTER (WHERE m.organization_id IS NOT NULL),
            ARRAY[]::text[]
        ) AS organization_ids,
        COALESCE(
            array_agg(DISTINCT m.role) FILTER (WHERE m.role IS NOT NULL),
            ARRAY[]::text[]
        ) AS member_roles,
        COALESCE(
            array_agg(DISTINCT ups.scope) FILTER (WHERE ups.enabled = true AND ups.scope IS NOT NULL),
            ARRAY[]::text[]
        ) AS permissions,
        COALESCE(
            BOOL_OR(
                ups.enabled = true AND (
                    ups.global = true
                    OR ups.scope IN ('admin', 'storage.admin', 'storage:admin', 'storage:*')
                )
            ),
            false
        ) AS has_admin_scope,
        s.expires_at
    FROM athena.sessions AS s
    INNER JOIN athena.users AS u
        ON u.id = s.user_id
    LEFT JOIN athena.member AS m
        ON m.user_id = s.user_id
    LEFT JOIN athena.user_permission_scopes AS ups
        ON ups.user_id = s.user_id
    WHERE s.token = $1
      AND s.active = true
      AND s.expires_at > $2
      AND COALESCE(u.banned, false) = false
    GROUP BY
        s.id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        s.expires_at
    LIMIT 1
"#;
const ATHENA_LEGACY_SESSION_VALIDATION_SQL: &str = r#"
    SELECT
        s.id AS session_id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        COALESCE(
            array_agg(DISTINCT m.organization_id) FILTER (WHERE m.organization_id IS NOT NULL),
            ARRAY[]::text[]
        ) AS organization_ids,
        COALESCE(
            array_agg(DISTINCT m.role) FILTER (WHERE m.role IS NOT NULL),
            ARRAY[]::text[]
        ) AS member_roles,
        COALESCE(
            array_agg(DISTINCT ups.scope) FILTER (WHERE ups.enabled = true AND ups.scope IS NOT NULL),
            ARRAY[]::text[]
        ) AS permissions,
        COALESCE(
            BOOL_OR(
                ups.enabled = true AND (
                    ups.global = true
                    OR ups.scope IN ('admin', 'storage.admin', 'storage:admin', 'storage:*')
                )
            ),
            false
        ) AS has_admin_scope,
        s.expires_at
    FROM athena.sessions AS s
    INNER JOIN athena.user AS u
        ON u.id = s.user_id
    LEFT JOIN athena.member AS m
        ON m.user_id = s.user_id
    LEFT JOIN athena.user_permission_scopes AS ups
        ON ups.user_id = s.user_id
    WHERE s.token = $1
      AND s.expires_at > $2
      AND COALESCE(u.banned, false) = false
    GROUP BY
        s.id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        s.expires_at
    LIMIT 1
"#;
const PUBLIC_MODERN_SESSION_VALIDATION_SQL: &str = r#"
    SELECT
        s.id AS session_id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        COALESCE(
            array_agg(DISTINCT m.organization_id) FILTER (WHERE m.organization_id IS NOT NULL),
            ARRAY[]::text[]
        ) AS organization_ids,
        COALESCE(
            array_agg(DISTINCT m.role) FILTER (WHERE m.role IS NOT NULL),
            ARRAY[]::text[]
        ) AS member_roles,
        COALESCE(
            array_agg(DISTINCT ups.scope) FILTER (WHERE ups.enabled = true AND ups.scope IS NOT NULL),
            ARRAY[]::text[]
        ) AS permissions,
        COALESCE(
            BOOL_OR(
                ups.enabled = true AND (
                    ups.global = true
                    OR ups.scope IN ('admin', 'storage.admin', 'storage:admin', 'storage:*')
                )
            ),
            false
        ) AS has_admin_scope,
        s.expires_at
    FROM athena.sessions AS s
    INNER JOIN public.users AS u
        ON u.id = s.user_id
    LEFT JOIN public.member AS m
        ON m.user_id = s.user_id
    LEFT JOIN public.user_permission_scopes AS ups
        ON ups.user_id = s.user_id
    WHERE s.token = $1
      AND s.active = true
      AND s.expires_at > $2
      AND COALESCE(u.banned, false) = false
    GROUP BY
        s.id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        s.expires_at
    LIMIT 1
"#;
const PUBLIC_LEGACY_SESSION_VALIDATION_SQL: &str = r#"
    SELECT
        s.id AS session_id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        COALESCE(
            array_agg(DISTINCT m.organization_id) FILTER (WHERE m.organization_id IS NOT NULL),
            ARRAY[]::text[]
        ) AS organization_ids,
        COALESCE(
            array_agg(DISTINCT m.role) FILTER (WHERE m.role IS NOT NULL),
            ARRAY[]::text[]
        ) AS member_roles,
        COALESCE(
            array_agg(DISTINCT ups.scope) FILTER (WHERE ups.enabled = true AND ups.scope IS NOT NULL),
            ARRAY[]::text[]
        ) AS permissions,
        COALESCE(
            BOOL_OR(
                ups.enabled = true AND (
                    ups.global = true
                    OR ups.scope IN ('admin', 'storage.admin', 'storage:admin', 'storage:*')
                )
            ),
            false
        ) AS has_admin_scope,
        s.expires_at
    FROM athena.sessions AS s
    INNER JOIN public.user AS u
        ON u.id = s.user_id
    LEFT JOIN public.member AS m
        ON m.user_id = s.user_id
    LEFT JOIN public.user_permission_scopes AS ups
        ON ups.user_id = s.user_id
    WHERE s.token = $1
      AND s.expires_at > $2
      AND COALESCE(u.banned, false) = false
    GROUP BY
        s.id,
        s.user_id,
        u.email,
        u.name,
        u.role,
        s.active_organization_id,
        s.expires_at
    LIMIT 1
"#;

#[derive(Debug, Clone)]
pub struct ValidateSessionInput<'a> {
    pub token: &'a str,
    pub now: DateTime<Utc>,
    pub include_user: bool,
    pub include_organization: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ValidatedSessionContext {
    pub session_id: String,
    pub user_id: String,
    pub email: Option<String>,
    pub name: Option<String>,
    pub role: Option<String>,
    pub active_organization_id: Option<String>,
    pub organization_ids: Vec<String>,
    pub member_roles: Vec<String>,
    pub permissions: Vec<String>,
    pub has_admin_scope: bool,
    pub expires_at: DateTime<Utc>,
}

impl ValidatedSessionContext {
    pub fn merged_roles(&self) -> Vec<String> {
        let mut roles = Vec::new();

        if let Some(role) = normalize_optional_string(self.role.as_deref()) {
            roles.push(role);
        }

        for role in &self.member_roles {
            if let Some(role) = normalize_optional_string(Some(role.as_str()))
                && !roles.iter().any(|existing| existing == &role)
            {
                roles.push(role);
            }
        }

        roles
    }
}

#[derive(Debug, FromRow)]
struct SessionValidationRow {
    session_id: String,
    user_id: String,
    email: Option<String>,
    name: Option<String>,
    role: Option<String>,
    active_organization_id: Option<String>,
    organization_ids: Vec<String>,
    member_roles: Vec<String>,
    permissions: Vec<String>,
    has_admin_scope: bool,
    expires_at: DateTime<Utc>,
}

pub fn extract_session_token_from_headers(
    authorization_header: Option<&str>,
    cookie_header: Option<&str>,
    cookie_name: Option<&str>,
) -> Option<ExtractedSessionToken> {
    if let Some(auth_header) =
        authorization_header.and_then(|value| normalize_optional_string(Some(value)))
        && let Some(token) = auth_header.strip_prefix("Bearer ")
    {
        return normalize_optional_string(Some(token)).map(|token| ExtractedSessionToken {
            token,
            source: SessionTokenSource::AuthorizationBearer,
        });
    }

    let cookie_name = cookie_name
        .and_then(|value| normalize_optional_string(Some(value)))
        .unwrap_or_else(|| DEFAULT_SESSION_COOKIE_NAME.to_string());

    let cookie_header = cookie_header.and_then(|value| normalize_optional_string(Some(value)))?;
    for cookie in cookie::Cookie::split_parse(cookie_header).flatten() {
        if cookie.name() == cookie_name && !cookie.value().is_empty() {
            return Some(ExtractedSessionToken {
                token: cookie.value().to_string(),
                source: SessionTokenSource::Cookie,
            });
        }
    }

    None
}

pub async fn validate_session(
    pool: &PgPool,
    input: ValidateSessionInput<'_>,
) -> AuthResult<Option<ValidatedSessionContext>> {
    let row = match fetch_validation_row(pool, ATHENA_MODERN_SESSION_VALIDATION_SQL, &input).await {
        Ok(row) => row,
        Err(error) if should_retry_validation_schema(&error) => {
            fetch_validation_row_with_fallbacks(
                pool,
                &input,
                &[
                    ATHENA_LEGACY_SESSION_VALIDATION_SQL,
                    PUBLIC_MODERN_SESSION_VALIDATION_SQL,
                    PUBLIC_LEGACY_SESSION_VALIDATION_SQL,
                ],
            )
            .await?
        }
        Err(error) => return Err(error.into()),
    };

    Ok(row.map(|row| map_validated_session(row, input)))
}

pub fn require_role(context: &ValidatedSessionContext, role: &str) -> bool {
    let required = match normalize_optional_string(Some(role)) {
        Some(required) => required,
        None => return false,
    };

    context
        .merged_roles()
        .iter()
        .any(|candidate| candidate.eq_ignore_ascii_case(required.as_str()))
}

pub fn require_permission(context: &ValidatedSessionContext, permission: &str) -> bool {
    if context.has_admin_scope {
        return true;
    }

    let required = match normalize_optional_string(Some(permission)) {
        Some(required) => required,
        None => return false,
    };

    context
        .permissions
        .iter()
        .any(|scope| scope_matches(scope, &required))
}

fn map_validated_session(
    row: SessionValidationRow,
    input: ValidateSessionInput<'_>,
) -> ValidatedSessionContext {
    let mut organization_ids = if input.include_organization {
        row.organization_ids
    } else {
        Vec::new()
    };

    if input.include_organization
        && let Some(active_organization_id) = row.active_organization_id.as_ref()
        && !organization_ids
            .iter()
            .any(|candidate| candidate == active_organization_id)
    {
        organization_ids.push(active_organization_id.clone());
    }

    ValidatedSessionContext {
        session_id: row.session_id,
        user_id: row.user_id,
        email: input.include_user.then_some(row.email).flatten(),
        name: input.include_user.then_some(row.name).flatten(),
        role: input.include_user.then_some(row.role).flatten(),
        active_organization_id: input
            .include_organization
            .then_some(row.active_organization_id)
            .flatten(),
        organization_ids,
        member_roles: if input.include_organization {
            row.member_roles
        } else {
            Vec::new()
        },
        permissions: row.permissions,
        has_admin_scope: row.has_admin_scope,
        expires_at: row.expires_at,
    }
}

fn normalize_optional_string(value: Option<&str>) -> Option<String> {
    value
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

async fn fetch_validation_row(
    pool: &PgPool,
    sql: &str,
    input: &ValidateSessionInput<'_>,
) -> Result<Option<SessionValidationRow>, sqlx::Error> {
    sqlx::query_as::<_, SessionValidationRow>(sql)
        .bind(input.token)
        .bind(input.now)
        .fetch_optional(pool)
        .await
}

async fn fetch_validation_row_with_fallbacks(
    pool: &PgPool,
    input: &ValidateSessionInput<'_>,
    fallbacks: &[&str],
) -> Result<Option<SessionValidationRow>, sqlx::Error> {
    let mut last_error = None;

    for sql in fallbacks {
        match fetch_validation_row(pool, sql, input).await {
            Ok(row) => return Ok(row),
            Err(error) if should_retry_validation_schema(&error) => {
                last_error = Some(error);
            }
            Err(error) => return Err(error),
        }
    }

    Err(last_error.expect("fallback validation queries require at least one SQL variant"))
}

fn should_retry_validation_schema(error: &sqlx::Error) -> bool {
    let Some(database_error) = error.as_database_error() else {
        return false;
    };

    matches!(database_error.code().as_deref(), Some("42P01" | "42703"))
}

fn scope_matches(scope: &str, required: &str) -> bool {
    let Some(scope) = normalize_optional_string(Some(scope)) else {
        return false;
    };
    let Some(required) = normalize_optional_string(Some(required)) else {
        return false;
    };

    let canonical_scope = canonicalize_scope(scope.as_str());
    let canonical_required = canonicalize_scope(required.as_str());

    if canonical_scope == "*" || canonical_scope.eq_ignore_ascii_case(canonical_required.as_str()) {
        return true;
    }

    canonical_scope
        .strip_suffix('*')
        .is_some_and(|prefix| canonical_required.starts_with(prefix))
}

fn canonicalize_scope(value: &str) -> String {
    value.replace('.', ":")
}

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

    #[test]
    fn extracts_cookie_when_authorization_is_absent() {
        let token = extract_session_token_from_headers(
            None,
            Some("foo=bar; athena-auth.session-token=session_123; baz=qux"),
            None,
        )
        .expect("cookie token should be extracted");

        assert_eq!(token.source, SessionTokenSource::Cookie);
        assert_eq!(token.token, "session_123");
    }

    #[test]
    fn authorization_bearer_takes_precedence_when_enabled() {
        let token = extract_session_token_from_headers(
            Some("Bearer session_abc"),
            Some("athena-auth.session-token=session_cookie"),
            None,
        )
        .expect("authorization bearer should be extracted");

        assert_eq!(token.source, SessionTokenSource::AuthorizationBearer);
        assert_eq!(token.token, "session_abc");
    }

    #[test]
    fn permission_matching_supports_exact_and_wildcard_scopes() {
        let context = ValidatedSessionContext {
            session_id: "session-1".to_string(),
            user_id: "user-1".to_string(),
            email: Some("user@example.com".to_string()),
            name: Some("User".to_string()),
            role: Some("member".to_string()),
            active_organization_id: Some("org-1".to_string()),
            organization_ids: vec!["org-1".to_string()],
            member_roles: vec!["owner".to_string()],
            permissions: vec!["storage:*".to_string(), "admin.read".to_string()],
            has_admin_scope: false,
            expires_at: Utc::now(),
        };

        assert!(require_permission(&context, "storage.delete"));
        assert!(require_permission(&context, "admin.read"));
        assert!(!require_permission(&context, "admin.write"));
    }

    #[test]
    fn merged_roles_deduplicates_user_and_member_roles() {
        let context = ValidatedSessionContext {
            session_id: "session-1".to_string(),
            user_id: "user-1".to_string(),
            email: None,
            name: None,
            role: Some("admin".to_string()),
            active_organization_id: Some("org-1".to_string()),
            organization_ids: vec!["org-1".to_string()],
            member_roles: vec!["admin".to_string(), "owner".to_string()],
            permissions: Vec::new(),
            has_admin_scope: false,
            expires_at: Utc::now(),
        };

        assert_eq!(
            context.merged_roles(),
            vec!["admin".to_string(), "owner".to_string()]
        );
    }
}
