use crate::db::{models::User, projects, users};
use crate::server::auth::{
    cookie_helpers::{self, CookieSettings},
    token_storage::{
        generate_code_challenge, generate_code_verifier, generate_state_token,
        CompletedAuthSession, OAuth2State,
    },
};
use crate::server::frontend::StaticAssets;
use crate::server::state::AppState;
use axum::{
    extract::{Extension, Query, State},
    http::{uri::Uri, HeaderMap, StatusCode},
    response::{Html, IntoResponse, Redirect, Response},
    Json,
};
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tera::Tera;
use tracing::instrument;

/// Helper function to sync IdP groups after login
///
/// This validates the token and syncs the user's team memberships from IdP groups.
/// Should be called during login flows (code exchange, device exchange, OAuth callback).
async fn sync_groups_after_login(
    state: &AppState,
    id_token: &str,
) -> Result<(), (StatusCode, String)> {
    // Only sync if enabled
    if !state.auth_settings.idp_group_sync_enabled {
        return Ok(());
    }

    // Build expected claims for validation
    let mut expected_claims = HashMap::new();
    expected_claims.insert("aud".to_string(), state.auth_settings.client_id.clone());

    // Validate token to get claims
    let claims_value = state
        .jwt_validator
        .validate(id_token, &state.auth_settings.issuer, &expected_claims)
        .await
        .map_err(|e| {
            tracing::warn!("Failed to validate token for group sync: {:#}", e);
            (StatusCode::UNAUTHORIZED, format!("Invalid token: {}", e))
        })?;

    // Parse claims
    let claims: crate::server::auth::jwt::Claims =
        serde_json::from_value(claims_value).map_err(|e| {
            tracing::warn!("Failed to parse claims for group sync: {:#}", e);
            (
                StatusCode::UNAUTHORIZED,
                format!("Invalid token claims: {}", e),
            )
        })?;

    // Get or create user
    let user = users::find_or_create(&state.db_pool, &claims.email)
        .await
        .map_err(|e| {
            tracing::error!("Failed to find/create user for group sync: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error".to_string(),
            )
        })?;

    // Sync groups if present in claims
    if let Some(ref groups) = claims.groups {
        if !groups.is_empty() {
            tracing::debug!(
                "Syncing {} IdP groups for user {} during login",
                groups.len(),
                user.email
            );

            if let Err(e) =
                crate::server::auth::group_sync::sync_user_groups(&state.db_pool, user.id, groups)
                    .await
            {
                // Log error but don't fail login
                tracing::error!(
                    "Failed to sync IdP groups during login for user {}: {:#}",
                    user.email,
                    e
                );
            } else {
                tracing::info!(
                    "Successfully synced IdP groups during login for user {}",
                    user.email
                );
            }
        }
    }

    Ok(())
}

#[derive(Debug, Deserialize)]
pub struct CodeExchangeRequest {
    pub code: String,
    pub code_verifier: String,
    pub redirect_uri: String,
}

#[derive(Debug, Deserialize)]
pub struct DeviceExchangeRequest {
    pub device_code: String,
}

#[derive(Debug, Serialize)]
pub struct DeviceExchangeResponse {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_description: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct LoginResponse {
    pub token: String,
}

#[derive(Debug, Deserialize)]
pub struct AuthorizeRequest {
    /// For authorization code flow: the redirect URI
    #[serde(default)]
    pub redirect_uri: Option<String>,
    /// For authorization code flow: the PKCE code challenge
    #[serde(default)]
    pub code_challenge: Option<String>,
    /// For authorization code flow: the PKCE code challenge method
    #[serde(default)]
    pub code_challenge_method: Option<String>,
    /// Flow type: "code" for authorization code flow, "device" for device flow
    pub flow: String,
}

#[derive(Debug, Serialize)]
pub struct AuthorizeResponse {
    /// For authorization code flow: the full authorization URL to open in browser
    #[serde(skip_serializing_if = "Option::is_none")]
    pub authorization_url: Option<String>,
    /// For device flow: the device code
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_code: Option<String>,
    /// For device flow: the user code to display
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_code: Option<String>,
    /// For device flow: the verification URI to display
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verification_uri: Option<String>,
    /// For device flow: the complete verification URI (with user code embedded)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verification_uri_complete: Option<String>,
    /// For device flow: how long the device code is valid (seconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expires_in: Option<u64>,
    /// For device flow: how often to poll (seconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub interval: Option<u64>,
}

/// Build OAuth2 authorization URL or initiate device flow (for CLI)
#[instrument(skip(state))]
pub async fn authorize(
    State(state): State<AppState>,
    Json(payload): Json<AuthorizeRequest>,
) -> Result<Json<AuthorizeResponse>, (StatusCode, String)> {
    match payload.flow.as_str() {
        "code" => {
            // Authorization code flow with PKCE
            let redirect_uri = payload.redirect_uri.ok_or_else(|| {
                (
                    StatusCode::BAD_REQUEST,
                    "redirect_uri required for code flow".to_string(),
                )
            })?;
            let code_challenge = payload.code_challenge.ok_or_else(|| {
                (
                    StatusCode::BAD_REQUEST,
                    "code_challenge required for code flow".to_string(),
                )
            })?;
            let code_challenge_method = payload.code_challenge_method.ok_or_else(|| {
                (
                    StatusCode::BAD_REQUEST,
                    "code_challenge_method required for code flow".to_string(),
                )
            })?;

            // Build authorization URL with typed parameters
            let params = crate::server::auth::oauth::AuthorizeParams {
                client_id: &state.auth_settings.client_id,
                redirect_uri: &redirect_uri,
                response_type: "code",
                scope: "openid email profile offline_access",
                code_challenge: &code_challenge,
                code_challenge_method: &code_challenge_method,
                state: None,
            };

            let authorization_url = state.oauth_client.build_authorize_url(&params);

            Ok(Json(AuthorizeResponse {
                authorization_url: Some(authorization_url),
                device_code: None,
                user_code: None,
                verification_uri: None,
                verification_uri_complete: None,
                expires_in: None,
                interval: None,
            }))
        }
        "device" => {
            // Device authorization flow
            let device_response = state.oauth_client.device_flow_start().await.map_err(|e| {
                tracing::error!("Failed to start device flow: {:#}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Failed to start device flow: {}", e),
                )
            })?;

            Ok(Json(AuthorizeResponse {
                authorization_url: None,
                device_code: Some(device_response.device_code),
                user_code: Some(device_response.user_code),
                verification_uri: Some(device_response.verification_uri.clone()),
                verification_uri_complete: Some(device_response.verification_uri_complete),
                expires_in: Some(device_response.expires_in),
                interval: Some(device_response.interval),
            }))
        }
        _ => Err((
            StatusCode::BAD_REQUEST,
            format!("Invalid flow type: {}", payload.flow),
        )),
    }
}

/// Exchange authorization code for token (OAuth2 PKCE flow)
#[instrument(skip(state, payload))]
pub async fn code_exchange(
    State(state): State<AppState>,
    Json(payload): Json<CodeExchangeRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, String)> {
    tracing::debug!(
        "Code exchange request: redirect_uri={}",
        payload.redirect_uri
    );

    // Exchange authorization code for tokens using PKCE
    let token_info = state
        .oauth_client
        .exchange_code_pkce(&payload.code, &payload.code_verifier, &payload.redirect_uri)
        .await
        .map_err(|e| {
            tracing::warn!("OAuth2 code exchange failed: {:#}", e);
            (
                StatusCode::UNAUTHORIZED,
                format!("Code exchange failed: {}", e),
            )
        })?;

    tracing::info!(
        "Code exchange successful, token_type={}, expires_in={}",
        token_info.token_type,
        token_info.expires_in
    );

    // Decode and log token claims for debugging (without validating yet)
    if let Ok(header) = jsonwebtoken::decode_header(&token_info.id_token) {
        tracing::debug!("ID token header: {:?}", header);
    }

    // Try to decode payload for logging (this doesn't validate signature)
    let parts: Vec<&str> = token_info.id_token.split('.').collect();
    if parts.len() == 3 {
        if let Ok(decoded) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(parts[1]) {
            if let Ok(claims_str) = String::from_utf8(decoded) {
                tracing::debug!("ID token claims: {}", claims_str);
            }
        }
    }

    // Sync IdP groups after successful login
    if let Err(e) = sync_groups_after_login(&state, &token_info.id_token).await {
        tracing::warn!("Group sync failed during code exchange: {:?}", e);
        // Don't fail the login if group sync fails
    }

    // Return the ID token (which contains user claims)
    Ok(Json(LoginResponse {
        token: token_info.id_token,
    }))
}

/// Exchange device code for token (Device Flow)
#[instrument(skip(state, payload))]
pub async fn device_exchange(
    State(state): State<AppState>,
    Json(payload): Json<DeviceExchangeRequest>,
) -> Json<DeviceExchangeResponse> {
    tracing::debug!(
        "Device exchange request: device_code={}...",
        &payload.device_code[..8.min(payload.device_code.len())]
    );

    // Poll the identity provider's token endpoint with the device code
    match state
        .oauth_client
        .device_flow_poll(&payload.device_code)
        .await
    {
        Ok(Some(token_info)) => {
            tracing::info!("Device authorization successful");

            // Sync IdP groups after successful login
            if let Err(e) = sync_groups_after_login(&state, &token_info.id_token).await {
                tracing::warn!("Group sync failed during device exchange: {:?}", e);
                // Don't fail the login if group sync fails
            }

            Json(DeviceExchangeResponse {
                token: Some(token_info.id_token),
                error: None,
                error_description: None,
            })
        }
        Ok(None) => {
            // authorization_pending - user hasn't authorized yet
            tracing::debug!("Device authorization pending");
            Json(DeviceExchangeResponse {
                token: None,
                error: Some("authorization_pending".to_string()),
                error_description: None,
            })
        }
        Err(e) => {
            let error_msg = e.to_string();
            tracing::warn!("Device authorization error: {}", error_msg);

            // Check for standard OAuth2 device flow errors
            let (error, description) = if error_msg.contains("slow_down") {
                ("slow_down".to_string(), None)
            } else {
                ("access_denied".to_string(), Some(error_msg))
            };

            Json(DeviceExchangeResponse {
                token: None,
                error: Some(error),
                error_description: description,
            })
        }
    }
}

#[derive(Debug, Serialize)]
pub struct MeResponse {
    pub id: String,
    pub email: String,
    pub is_admin: bool,
}

/// Get current user info from auth middleware
#[instrument(skip(state))]
pub async fn me(
    State(state): State<AppState>,
    Extension(user): Extension<User>,
) -> Result<Json<MeResponse>, (StatusCode, String)> {
    // User is injected by auth middleware
    tracing::debug!("GET /me: user_id={}, email={}", user.id, user.email);
    let is_admin = state.admin_users.contains(&user.email);
    Ok(Json(MeResponse {
        id: user.id.to_string(),
        email: user.email,
        is_admin,
    }))
}

#[derive(Debug, Deserialize)]
pub struct UsersLookupRequest {
    pub emails: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct UsersLookupResponse {
    pub users: Vec<UserInfo>,
}

#[derive(Debug, Serialize)]
pub struct UserInfo {
    pub id: String,
    pub email: String,
}

/// Lookup users by email addresses
#[instrument(skip(state))]
pub async fn users_lookup(
    State(state): State<AppState>,
    Extension(_user): Extension<User>,
    Json(payload): Json<UsersLookupRequest>,
) -> Result<Json<UsersLookupResponse>, (StatusCode, String)> {
    let mut user_infos = Vec::new();

    for email in payload.emails {
        // Query database for user by email
        let user = users::find_by_email(&state.db_pool, &email)
            .await
            .map_err(|e| {
                tracing::error!("Database error looking up user {}: {:#}", email, e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "Database error".to_string(),
                )
            })?;

        match user {
            Some(u) => {
                user_infos.push(UserInfo {
                    id: u.id.to_string(),
                    email: u.email,
                });
            }
            None => {
                return Err((StatusCode::NOT_FOUND, format!("User not found: {}", email)));
            }
        }
    }

    Ok(Json(UsersLookupResponse { users: user_infos }))
}

// ============================================================================
// OAuth2 Proxy Handlers for Ingress Authentication
// ============================================================================

#[derive(Debug, Deserialize)]
pub struct SigninQuery {
    /// Optional redirect URL to return to after authentication (path only)
    pub redirect: Option<String>,
    /// Optional full redirect URL from Nginx ingress (includes host)
    pub rd: Option<String>,
    /// Optional project name for ingress authentication flow
    pub project: Option<String>,
    /// Skip cookie configuration warnings
    pub skip_warning: Option<bool>,
}

/// Pre-authentication page for ingress auth
///
/// Shows the user which project they're about to authenticate for before
/// starting the OAuth flow. This provides better UX by explaining what's happening.
#[instrument(skip(state, params, headers, uri))]
pub async fn signin_page(
    State(state): State<AppState>,
    headers: HeaderMap,
    uri: Uri,
    Query(params): Query<SigninQuery>,
) -> Result<Response, (StatusCode, String)> {
    let project_name = params.project.as_deref().unwrap_or("Unknown");
    let redirect_url = params
        .redirect
        .as_ref()
        .or(params.rd.as_ref())
        .cloned()
        .unwrap_or_else(|| "/".to_string());

    tracing::info!(
        project = %project_name,
        has_redirect = !redirect_url.is_empty(),
        "Signin page requested"
    );

    // Load template from embedded assets
    let template_content = StaticAssets::get("auth-signin.html.tera")
        .ok_or_else(|| {
            tracing::error!("auth-signin.html.tera template not found");
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template not found".to_string(),
            )
        })?
        .data;

    let template_str = std::str::from_utf8(&template_content).map_err(|e| {
        tracing::error!("Failed to parse template as UTF-8: {:#}", e);
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            "Template encoding error".to_string(),
        )
    })?;

    // Create Tera instance and add template
    let mut tera = Tera::default();
    tera.add_raw_template("auth-signin.html.tera", template_str)
        .map_err(|e| {
            tracing::error!("Failed to parse template: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template error".to_string(),
            )
        })?;

    // Determine if this is via `/.rise/auth` path (custom domain Ingress routing)
    let is_rise_path = uri.path().starts_with("/.rise/auth");

    // Build continue URL (to oauth_signin_start)
    let mut continue_params = vec![];
    if let Some(ref project) = params.project {
        continue_params.push(format!("project={}", urlencoding::encode(project)));
    }
    if !redirect_url.is_empty() {
        continue_params.push(format!("redirect={}", urlencoding::encode(&redirect_url)));
    }

    // Use request base URL for continue link when accessed via /.rise/auth path
    let continue_url = if is_rise_path {
        format!(
            "{}/.rise/auth/signin/start?{}",
            extract_request_base_url(&headers, &state),
            continue_params.join("&")
        )
    } else {
        format!(
            "{}/api/v1/auth/signin/start?{}",
            state.public_url.trim_end_matches('/'),
            continue_params.join("&")
        )
    };

    // Render template
    let mut context = tera::Context::new();
    context.insert("project_name", project_name);
    context.insert("continue_url", &continue_url);
    context.insert("redirect_url", &redirect_url);

    let html = tera
        .render("auth-signin.html.tera", &context)
        .map_err(|e| {
            tracing::error!("Failed to render template: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template rendering error".to_string(),
            )
        })?;

    Ok(Html(html).into_response())
}

/// Checks if a hostname is covered by a cookie domain according to RFC 6265.
///
/// This handles both exact matches and subdomain matches. The cookie_domain
/// is normalized by stripping a leading dot if present.
///
/// # Examples
/// - `host_matches_cookie_domain("rise.local", ".rise.local")` -> `true`
/// - `host_matches_cookie_domain("test.rise.local", ".rise.local")` -> `true`
/// - `host_matches_cookie_domain("rise.local", "rise.local")` -> `true`
/// - `host_matches_cookie_domain("other.com", ".rise.local")` -> `false`
///
/// # Note
/// Assumes cookie_domain is non-empty. Empty cookie domains mean "current host only"
/// and require special handling depending on context.
fn host_matches_cookie_domain(hostname: &str, cookie_domain: &str) -> bool {
    let cookie_domain_normalized = cookie_domain.trim_start_matches('.');
    hostname == cookie_domain_normalized
        || hostname.ends_with(&format!(".{}", cookie_domain_normalized))
}

/// Extract base URL (scheme + host) from request headers.
///
/// Used for OAuth callback URL when handling requests via Ingress routing.
/// This allows the OAuth flow to use the actual request host (e.g., custom domain)
/// instead of the configured public_url.
///
/// Falls back to the configured public_url if no valid host header is present.
fn extract_request_base_url(headers: &HeaderMap, state: &AppState) -> String {
    // Get Host header
    if let Some(host) = headers.get("host") {
        if let Ok(host_str) = host.to_str() {
            // Get X-Forwarded-Proto header (set by Nginx ingress)
            let scheme = headers
                .get("x-forwarded-proto")
                .and_then(|v| v.to_str().ok())
                .unwrap_or("http");

            return format!("{}://{}", scheme, host_str);
        }
    }

    // Fallback to configured public URL
    state.public_url.trim_end_matches('/').to_string()
}

/// Render warning page for cookie configuration issues
fn render_warning_page(
    state: &AppState,
    params: &SigninQuery,
    warnings: Vec<String>,
    request_host: &str,
) -> Html<String> {
    // Load template
    let template_content = StaticAssets::get("auth-warning.html.tera")
        .expect("auth-warning.html.tera template not found")
        .data;

    let template_str = std::str::from_utf8(&template_content).expect("Template encoding error");

    // Create Tera instance
    let mut tera = Tera::default();
    tera.add_raw_template("auth-warning.html.tera", template_str)
        .expect("Template parse error");

    // Extract redirect host
    let redirect_url = params.redirect.as_ref().or(params.rd.as_ref());
    let redirect_host = redirect_url.and_then(|url| {
        url::Url::parse(url)
            .ok()
            .and_then(|u| u.host_str().map(|h| h.to_string()))
    });

    // Build continue URL (proceed with OAuth despite warning)
    let mut continue_params = vec![];
    if let Some(ref project) = params.project {
        continue_params.push(format!("project={}", urlencoding::encode(project)));
    }
    if let Some(ref redirect) = params.redirect {
        continue_params.push(format!("redirect={}", urlencoding::encode(redirect)));
    } else if let Some(ref rd) = params.rd {
        continue_params.push(format!("rd={}", urlencoding::encode(rd)));
    }
    continue_params.push("skip_warning=true".to_string());

    let continue_url = format!(
        "{}/api/v1/auth/signin/start?{}",
        state.public_url.trim_end_matches('/'),
        continue_params.join("&")
    );

    // Render template
    let mut context = tera::Context::new();
    context.insert("warnings", &warnings);
    context.insert(
        "project_name",
        &params.project.as_deref().unwrap_or("Unknown"),
    );
    context.insert("request_host", request_host);
    context.insert(
        "cookie_domain",
        &if state.cookie_settings.domain.is_empty() {
            "(empty - current host only)"
        } else {
            &state.cookie_settings.domain
        },
    );
    context.insert("redirect_host", &redirect_host);
    context.insert("redirect_url", &redirect_url);
    context.insert("continue_url", &continue_url);

    let html = tera
        .render("auth-warning.html.tera", &context)
        .expect("Template rendering error");

    Html(html)
}

/// Initiate OAuth2 login flow for ingress auth (start of OAuth flow)
///
/// This handler starts the OAuth2 authorization code flow with PKCE.
/// It generates a PKCE verifier/challenge pair, stores the state, and
/// redirects the user to the OIDC provider for authentication.
#[instrument(skip(state, params, uri))]
pub async fn oauth_signin_start(
    State(state): State<AppState>,
    headers: HeaderMap,
    uri: Uri,
    Query(params): Query<SigninQuery>,
) -> Result<Response, (StatusCode, String)> {
    // Prefer rd (full URL) over redirect (path only)
    let redirect_url = params.rd.as_ref().or(params.redirect.as_ref());
    tracing::info!(
        project = ?params.project,
        has_redirect = redirect_url.is_some(),
        "OAuth signin initiated"
    );

    // Determine if this is via `/.rise/auth` path (custom domain Ingress routing)
    let is_rise_path = uri.path().starts_with("/.rise/auth");

    // Extract request host for validation
    let request_host = headers
        .get("host")
        .and_then(|h| h.to_str().ok())
        .unwrap_or("");

    // Skip validation if skip_warning is set OR if this is via /.rise/auth path
    // (custom domain routing handles cookies differently - always uses current host)
    if !params.skip_warning.unwrap_or(false) && !is_rise_path {
        // Extract redirect URL host (if provided)
        let redirect_host = redirect_url.and_then(|url| {
            url::Url::parse(url)
                .ok()
                .and_then(|u| u.host_str().map(|h| h.to_string()))
        });

        let cookie_domain = &state.cookie_settings.domain;

        // Strip port from request_host for cookie domain comparisons
        // (ports are irrelevant for cookie domain matching)
        let request_host_without_port = request_host.split(':').next().unwrap_or(request_host);

        // Detect potential misconfigurations
        let mut warnings = Vec::new();

        // Scenario 1: Cookie won't be accessible on redirect domain
        if let Some(ref redirect_host_str) = redirect_host {
            let cookie_will_match_redirect = if cookie_domain.is_empty() {
                // Empty domain = current host only (ports irrelevant for cookies)
                redirect_host_str == request_host_without_port
            } else {
                // Check if redirect host is covered by cookie domain
                host_matches_cookie_domain(redirect_host_str, cookie_domain)
            };

            if !cookie_will_match_redirect {
                warnings.push(format!(
                    "Authentication cookies may not work correctly. The redirect target '{}' does not match the configured cookie domain '{}'.",
                    redirect_host_str,
                    if cookie_domain.is_empty() {
                        request_host_without_port
                    } else {
                        cookie_domain
                    }
                ));
            }
        }

        // Scenario 2: Cookie domain doesn't match request host
        if !cookie_domain.is_empty()
            && !request_host_without_port.is_empty()
            && !host_matches_cookie_domain(request_host_without_port, cookie_domain)
        {
            warnings.push(format!(
                "Authentication configuration issue detected. The sign-in page is accessed from '{}' but cookies are configured for domain '{}'.",
                request_host_without_port,
                cookie_domain
            ));
        }

        // Scenario 3: Custom domain without proper cookie configuration
        if let Some(ref redirect_host_str) = redirect_host {
            if request_host_without_port == redirect_host_str.as_str()
                && !cookie_domain.is_empty()
                && !host_matches_cookie_domain(request_host_without_port, cookie_domain)
            {
                warnings.push(format!(
                    "This application ('{}') is not covered by the configured cookie domain '{}'.",
                    request_host_without_port, cookie_domain
                ));
            }
        }

        // Log warnings for troubleshooting
        if !warnings.is_empty() {
            tracing::warn!(
                "Cookie configuration warnings detected for project {:?}: {}",
                params.project,
                warnings.join(" | ")
            );

            // Show warning page to user
            return Ok(render_warning_page(&state, &params, warnings, request_host).into_response());
        }
    }

    // Generate PKCE parameters
    let code_verifier = generate_code_verifier();
    let code_challenge = generate_code_challenge(&code_verifier);
    let state_token = generate_state_token();

    // For custom domain auth routing via /.rise/auth path:
    // - IdP callback always goes to the main Rise domain (only one redirect URI needed)
    // - After callback, we redirect to the custom domain's /.rise/auth/complete endpoint
    let custom_domain_base_url = if is_rise_path {
        Some(extract_request_base_url(&headers, &state))
    } else {
        None
    };

    // Store PKCE state with redirect URL, project name, and custom domain base URL
    let oauth_state = OAuth2State {
        code_verifier: code_verifier.clone(),
        redirect_url: redirect_url.cloned(),
        project_name: params.project.clone(), // For ingress auth flow
        custom_domain_base_url,
    };
    state.token_store.save(state_token.clone(), oauth_state);

    // Build OAuth2 authorization URL
    // IdP callback always uses the main Rise domain (pre-registered with IdP)
    // For custom domains, we'll redirect to them after the callback completes
    let callback_url = format!(
        "{}/api/v1/auth/callback",
        state.public_url.trim_end_matches('/')
    );

    let params = crate::server::auth::oauth::AuthorizeParams {
        client_id: &state.auth_settings.client_id,
        redirect_uri: &callback_url,
        response_type: "code",
        scope: "openid email profile",
        code_challenge: &code_challenge,
        code_challenge_method: "S256",
        state: Some(&state_token),
    };

    let auth_url = state.oauth_client.build_authorize_url(&params);

    tracing::debug!("Redirecting to OIDC provider for authentication");
    Ok(Redirect::to(&auth_url).into_response())
}

#[derive(Debug, Deserialize)]
pub struct CallbackQuery {
    pub code: String,
    pub state: String,
}

/// OAuth2 callback from OIDC provider
///
/// This handler receives the authorization code from the OIDC provider, exchanges it for tokens,
/// sets a session cookie, and redirects the user back to their original URL.
///
/// For custom domain auth routing:
/// - IdP always redirects to the main Rise domain (single pre-registered redirect URI)
/// - If `custom_domain_callback_url` is set in state, we store a one-time token and redirect
///   to the custom domain's `/.rise/auth/complete` endpoint to set cookies there
#[instrument(skip(state, params, headers))]
pub async fn oauth_callback(
    State(state): State<AppState>,
    headers: HeaderMap,
    Query(params): Query<CallbackQuery>,
) -> Result<Response, (StatusCode, String)> {
    tracing::info!("OAuth callback received");

    // Retrieve PKCE state from token store
    let oauth_state = state.token_store.get(&params.state).ok_or_else(|| {
        tracing::warn!("Invalid or expired state token");
        (
            StatusCode::BAD_REQUEST,
            "Invalid or expired state token".to_string(),
        )
    })?;

    // Build callback URL (must match the one used in signin)
    // IdP callback always uses the main Rise domain (pre-registered with IdP)
    let callback_url = format!(
        "{}/api/v1/auth/callback",
        state.public_url.trim_end_matches('/')
    );

    // Exchange authorization code for tokens
    let token_info = state
        .oauth_client
        .exchange_code_pkce(&params.code, &oauth_state.code_verifier, &callback_url)
        .await
        .map_err(|e| {
            tracing::error!("Failed to exchange code: {:#}", e);
            (
                StatusCode::UNAUTHORIZED,
                format!("Code exchange failed: {}", e),
            )
        })?;

    tracing::info!("Successfully exchanged code for tokens");

    // Sync IdP groups after successful login
    if let Err(e) = sync_groups_after_login(&state, &token_info.id_token).await {
        tracing::warn!("Group sync failed during OAuth callback: {:?}", e);
        // Don't fail the login if group sync fails
    }

    // Validate the JWT to extract expiry time
    let mut expected_claims = HashMap::new();
    expected_claims.insert("aud".to_string(), state.auth_settings.client_id.clone());

    let claims = state
        .jwt_validator
        .validate(
            &token_info.id_token,
            &state.auth_settings.issuer,
            &expected_claims,
        )
        .await
        .map_err(|e| {
            tracing::error!("Failed to validate JWT: {:#}", e);
            (StatusCode::UNAUTHORIZED, "Invalid token".to_string())
        })?;

    // Calculate cookie max age from JWT expiry
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let exp = claims["exp"].as_u64().unwrap_or(now + 3600);
    let max_age = if exp > now {
        exp - now
    } else {
        3600 // Default to 1 hour if exp is in the past
    };

    // Determine redirect URL
    let redirect_url = oauth_state.redirect_url.unwrap_or_else(|| "/".to_string());

    // Check if this is an ingress auth flow (has project context) vs UI login flow
    let is_ingress_auth = oauth_state.project_name.is_some();

    // For UI login flow (no project), return an HTML page that stores the token in localStorage
    if !is_ingress_auth {
        tracing::info!("UI login flow - returning token storage page");

        // Create a simple HTML page that stores the token and redirects
        let html = format!(
            r#"<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Login Successful</title>
</head>
<body>
    <script>
        // Store token in localStorage
        localStorage.setItem('rise_token', {});

        // Clean up OAuth params and redirect
        window.history.replaceState({{}}, document.title, '/');
        window.location.href = '{}';
    </script>
    <noscript>
        <p>JavaScript is required to complete the login process.</p>
    </noscript>
</body>
</html>"#,
            serde_json::to_string(&token_info.id_token).unwrap(),
            redirect_url
        );

        return Ok(Html(html).into_response());
    }

    // Determine cookie domain based on request host
    // For Rise subdomains, use the configured cookie domain for subdomain sharing
    // Otherwise, use current host only (empty domain)
    let cookie_settings_for_response = {
        // Check if request host matches configured cookie domain
        let request_host = headers
            .get("host")
            .and_then(|h| h.to_str().ok())
            .unwrap_or("");
        let request_host_without_port = request_host.split(':').next().unwrap_or(request_host);

        if !state.cookie_settings.domain.is_empty()
            && host_matches_cookie_domain(request_host_without_port, &state.cookie_settings.domain)
        {
            // Request is on a Rise subdomain - use configured domain for cookie sharing
            state.cookie_settings.clone()
        } else {
            // Request host doesn't match configured domain - use current host only
            CookieSettings {
                domain: String::new(),
                secure: state.cookie_settings.secure,
            }
        }
    };

    // For ingress auth flow (with project), issue Rise JWT
    // For custom domain auth, we may need to redirect to the custom domain to set cookies there
    if let Some(ref project) = oauth_state.project_name {
        tracing::info!(
            "Issuing Rise JWT for ingress auth (project context: {})",
            project
        );

        // Get user email from claims
        let user_email = claims["email"].as_str().ok_or_else(|| {
            tracing::error!("No email in JWT claims");
            (StatusCode::UNAUTHORIZED, "Invalid token claims".to_string())
        })?;

        // Find or create user to get user_id for team lookup
        let user = users::find_or_create(&state.db_pool, user_email)
            .await
            .map_err(|e| {
                tracing::error!("Failed to find/create user: {:#}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "Database error".to_string(),
                )
            })?;

        // Issue Rise JWT with user's team memberships
        let rise_jwt = state
            .jwt_signer
            .sign_ingress_jwt(&claims, user.id, &state.db_pool, Some(exp))
            .await
            .map_err(|e| {
                tracing::error!("Failed to sign Rise JWT: {:#}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "Failed to create authentication token".to_string(),
                )
            })?;

        // Check if this is a custom domain auth flow that needs redirect
        if let Some(custom_domain_base_url) = oauth_state.custom_domain_base_url {
            // Generate a one-time token for the custom domain callback
            let completion_token = generate_state_token();

            // Store the completed session data
            let completed_session = CompletedAuthSession {
                rise_jwt,
                max_age,
                redirect_url: redirect_url.clone(),
                project_name: project.clone(),
            };
            state
                .token_store
                .save_completed_session(completion_token.clone(), completed_session);

            // Construct the complete URL by appending the path to the base URL
            let complete_url = format!(
                "{}/.rise/auth/complete?token={}",
                custom_domain_base_url.trim_end_matches('/'),
                completion_token
            );

            tracing::info!(
                "Redirecting to custom domain for cookie setting: {}",
                complete_url
            );

            return Ok(Redirect::to(&complete_url).into_response());
        }

        // Normal flow: set cookie directly on main domain
        let cookie = cookie_helpers::create_ingress_jwt_cookie(
            &rise_jwt,
            &cookie_settings_for_response,
            max_age,
        );

        return render_success_page(&state, project, &redirect_url, &cookie);
    }

    // Regular OAuth flow (not ingress auth) - UI login
    tracing::info!("Using IdP token for session");
    let cookie = cookie_helpers::create_session_cookie(
        &token_info.id_token,
        &cookie_settings_for_response,
        max_age,
    );

    tracing::info!("Setting session cookie and redirecting to {}", redirect_url);

    // For regular OAuth flow, immediate redirect
    let response = (
        StatusCode::FOUND,
        [("Location", redirect_url.as_str()), ("Set-Cookie", &cookie)],
    )
        .into_response();

    Ok(response)
}

/// Helper function to render the success page with cookie
fn render_success_page(
    _state: &AppState,
    project_name: &str,
    redirect_url: &str,
    cookie: &str,
) -> Result<Response, (StatusCode, String)> {
    // Load success template
    let template_content = StaticAssets::get("auth-success.html.tera")
        .ok_or_else(|| {
            tracing::error!("auth-success.html.tera template not found");
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template not found".to_string(),
            )
        })?
        .data;

    let template_str = std::str::from_utf8(&template_content).map_err(|e| {
        tracing::error!("Failed to parse template as UTF-8: {:#}", e);
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            "Template encoding error".to_string(),
        )
    })?;

    // Create Tera instance and add template
    let mut tera = Tera::default();
    tera.add_raw_template("auth-success.html.tera", template_str)
        .map_err(|e| {
            tracing::error!("Failed to parse template: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template error".to_string(),
            )
        })?;

    // Render success template
    let mut context = tera::Context::new();
    context.insert("success", &true);
    context.insert("project_name", project_name);
    context.insert("redirect_url", redirect_url);

    let html = tera
        .render("auth-success.html.tera", &context)
        .map_err(|e| {
            tracing::error!("Failed to render template: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Template rendering error".to_string(),
            )
        })?;

    tracing::info!(
        "Setting ingress JWT cookie and showing success page for project: {}",
        project_name
    );

    // Build response with cookie and HTML
    let response = (StatusCode::OK, [("Set-Cookie", cookie)], Html(html)).into_response();

    Ok(response)
}

#[derive(Debug, Deserialize)]
pub struct CompleteQuery {
    pub token: String,
}

/// Complete OAuth flow on custom domain
///
/// This handler is called on the custom domain after the IdP callback completes on the main domain.
/// It receives a one-time token, retrieves the stored auth session, sets the cookie on the
/// custom domain, and shows the success page.
#[instrument(skip(state, params))]
pub async fn oauth_complete(
    State(state): State<AppState>,
    Query(params): Query<CompleteQuery>,
) -> Result<Response, (StatusCode, String)> {
    tracing::info!("Custom domain auth complete received");

    // Retrieve and consume the completed session
    let session = state
        .token_store
        .get_completed_session(&params.token)
        .ok_or_else(|| {
            tracing::warn!("Invalid or expired completion token");
            (
                StatusCode::BAD_REQUEST,
                "Invalid or expired completion token. Please try logging in again.".to_string(),
            )
        })?;

    // Create cookie for the custom domain (empty domain = current host only)
    let cookie_settings = CookieSettings {
        domain: String::new(),
        secure: state.cookie_settings.secure,
    };

    let cookie = cookie_helpers::create_ingress_jwt_cookie(
        &session.rise_jwt,
        &cookie_settings,
        session.max_age,
    );

    render_success_page(
        &state,
        &session.project_name,
        &session.redirect_url,
        &cookie,
    )
}

#[derive(Debug, Deserialize)]
pub struct IngressAuthQuery {
    pub project: String,
}

/// Nginx ingress auth endpoint
///
/// This handler is called by Nginx for every request to a private project.
/// It validates the session cookie, checks JWT validity, and verifies
/// project access authorization.
#[instrument(skip(state, params, headers))]
pub async fn ingress_auth(
    State(state): State<AppState>,
    Query(params): Query<IngressAuthQuery>,
    headers: HeaderMap,
) -> Result<Response, (StatusCode, String)> {
    // Log only safe, relevant information (excluding sensitive cookies, tokens, etc.)
    let request_id = headers
        .get("x-request-id")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("none");

    tracing::debug!(
        project = %params.project,
        request_id = %request_id,
        "Ingress auth check"
    );

    // Allow access to /.rise/* paths without authentication (login page, static assets)
    // This prevents redirect loops when users try to access the signin page
    // Use x-auth-request-redirect header which contains the request path
    if let Some(redirect_path) = headers
        .get("x-auth-request-redirect")
        .and_then(|v| v.to_str().ok())
    {
        if redirect_path.starts_with("/.rise/") {
            tracing::debug!(
                project = %params.project,
                redirect_path = %redirect_path,
                "Allowing unauthenticated access to .rise path"
            );
            return Ok((
                StatusCode::OK,
                [("X-Auth-Request-User", "anonymous".to_string())],
            )
                .into_response());
        }
    }

    // Extract and validate Rise JWT (required)
    let rise_jwt = cookie_helpers::extract_ingress_jwt_cookie(&headers).ok_or_else(|| {
        tracing::debug!("No ingress JWT cookie found");
        (StatusCode::UNAUTHORIZED, "No session cookie".to_string())
    })?;

    let ingress_claims = state
        .jwt_signer
        .verify_ingress_jwt(&rise_jwt)
        .map_err(|e| {
            tracing::warn!("Invalid or expired ingress JWT: {:#}", e);
            (
                StatusCode::UNAUTHORIZED,
                "Invalid or expired session".to_string(),
            )
        })?;

    let email = ingress_claims.email;

    // Find or create user in database
    let user = users::find_or_create(&state.db_pool, &email)
        .await
        .map_err(|e| {
            tracing::error!("Database error finding/creating user: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error".to_string(),
            )
        })?;

    tracing::debug!(
        project = %params.project,
        user_id = %user.id,
        user_email = %user.email,
        "Rise JWT validated"
    );

    // Find project by name
    let project = projects::find_by_name(&state.db_pool, &params.project)
        .await
        .map_err(|e| {
            tracing::error!("Database error finding project: {:#}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error".to_string(),
            )
        })?
        .ok_or_else(|| {
            tracing::debug!("Project not found: {}", params.project);
            (StatusCode::NOT_FOUND, "Project not found".to_string())
        })?;

    // Get project's access class configuration
    use crate::server::settings::AccessRequirement;
    let access_class = state
        .access_classes
        .get(&project.access_class)
        .ok_or_else(|| {
            tracing::error!("Access class '{}' not configured", project.access_class);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Invalid access class".to_string(),
            )
        })?;

    // Handle different access requirements
    match access_class.access_requirement {
        AccessRequirement::None => {
            // Should never be called - None means no nginx auth annotations
            // But if it is called, deny access as a safety measure
            tracing::warn!(
                project = %params.project,
                "Auth endpoint called for AccessRequirement::None project"
            );
            Err((
                StatusCode::FORBIDDEN,
                "This project should not require authentication".to_string(),
            ))
        }

        AccessRequirement::Authenticated => {
            // Allow all authenticated users (no membership check)
            tracing::debug!(
                project = %params.project,
                user_id = %user.id,
                user_email = %user.email,
                access_class = %project.access_class,
                "Access granted - authenticated user"
            );
            Ok((
                StatusCode::OK,
                [
                    ("X-Auth-Request-Email", email),
                    ("X-Auth-Request-User", user.id.to_string()),
                ],
            )
                .into_response())
        }

        AccessRequirement::Member => {
            // Check project membership (owner or team member)
            let has_access = projects::user_can_access(&state.db_pool, project.id, user.id)
                .await
                .map_err(|e| {
                    tracing::error!("Database error checking access: {:#}", e);
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        "Database error".to_string(),
                    )
                })?;

            if has_access {
                tracing::debug!(
                    project = %params.project,
                    user_id = %user.id,
                    user_email = %user.email,
                    "Access granted - project member"
                );
                Ok((
                    StatusCode::OK,
                    [
                        ("X-Auth-Request-Email", email),
                        ("X-Auth-Request-User", user.id.to_string()),
                    ],
                )
                    .into_response())
            } else {
                tracing::warn!(
                    project = %params.project,
                    user_id = %user.id,
                    user_email = %user.email,
                    "Access denied - not a project member"
                );
                Err((
                    StatusCode::FORBIDDEN,
                    "You do not have access to this project".to_string(),
                ))
            }
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct LogoutQuery {
    /// Optional redirect URL after logout
    pub redirect: Option<String>,
}

/// Logout endpoint
///
/// Clears the session cookie and redirects the user.
#[instrument(skip(state))]
pub async fn oauth_logout(
    State(state): State<AppState>,
    Query(params): Query<LogoutQuery>,
) -> Result<Response, (StatusCode, String)> {
    tracing::info!("Logout initiated");

    // Clear the session cookie
    let cookie = cookie_helpers::clear_session_cookie(&state.cookie_settings);

    // Determine redirect URL
    let redirect_url = params.redirect.unwrap_or_else(|| "/".to_string());

    tracing::info!(
        "Clearing session cookie and redirecting to {}",
        redirect_url
    );

    // Build response with Set-Cookie header and redirect
    let response = (
        StatusCode::FOUND,
        [("Location", redirect_url.as_str()), ("Set-Cookie", &cookie)],
    )
        .into_response();

    Ok(response)
}
