use axum::{
    extract::State,
    http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization, Cookie},
    typed_header::TypedHeader,
};
use chrono::{Duration as ChronoDuration, Utc};
use diesel::{pg::PgConnection, prelude::*};
use rand::{rngs::OsRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use utoipa::{OpenApi, ToSchema};
use uuid::Uuid;

use crate::{
    auth::{
        api_tokens::{find_active_token_by_secret, touch_api_token},
        passkeys::{
            AuthenticationChallengeResponse, PasskeyLoginFinishPayload, PasskeyLoginStartPayload,
            PasskeyRegistrationFinishPayload, PasskeySummary, RegistrationChallengeResponse,
        },
        AuthenticatedUser,
    },
    error::{AppError, AppResult},
    models::{
        ApiTokenCapability, MagicToken, MagicTokenKind, NewUser, NewUserSession, TenantStatus,
        User, UserSession,
    },
    schema::{
        magic_tokens::dsl as magic_dsl, tenants::dsl as tenant_dsl,
        user_memberships::dsl as memberships_dsl, user_passkeys::dsl as passkey_dsl, user_sessions,
        users::dsl,
    },
    state::AppState,
    tenants::{
        apply_tenant_guc, apply_user_guc, apply_user_session_hash, clear_user_guc,
        clear_user_session_hash,
    },
};

use crate::schema::user_sessions::dsl as session_dsl;
use webauthn_rs::prelude::RegisterPublicKeyCredential;

const SESSION_COOKIE_NAME: &str = "refresh_token";

#[derive(Deserialize, ToSchema)]
pub struct LoginRequest {
    #[serde(default)]
    pub username: String,
    #[serde(default)]
    #[schema(nullable)]
    pub password: Option<String>,
    #[serde(default)]
    #[schema(nullable)]
    pub magic_token: Option<String>,
    #[serde(default)]
    #[schema(nullable)]
    pub preferred_tenant_id: Option<Uuid>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct ApiTokenExchangeRequest {
    pub api_token: String,
}

#[derive(Deserialize, Serialize, ToSchema)]
pub struct LoginResponse {
    pub access_token: String,
    pub token_type: String,
    pub expires_in: i64,
    pub tenant: TenantSnippet,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSnippet {
    pub id: Uuid,
    pub name: String,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantSelectionResponse {
    pub access_token: String,
    pub tenants: Vec<TenantSnippet>,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct TenantListResponse {
    pub tenants: Vec<TenantSnippet>,
}

#[derive(Deserialize, ToSchema)]
pub struct TenantSelectionRequest {
    pub tenant_id: Uuid,
}

#[derive(Deserialize, ToSchema)]
pub struct SignupStartRequest {
    pub username: String,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct SignupStartResponse {
    pub signup_token: String,
    pub challenge: RegistrationChallengeResponse,
}

#[derive(Deserialize, ToSchema)]
pub struct SignupFinishRequest {
    pub signup_token: String,
    #[schema(value_type = Object)]
    pub credential: RegisterPublicKeyCredential,
    #[schema(nullable)]
    pub nickname: Option<String>,
}

#[derive(Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum LoginResponseVariants {
    Token(LoginResponse),
    Selection(TenantSelectionResponse),
}

#[derive(OpenApi)]
#[openapi(
    paths(
        login,
        api_token_exchange,
        signup_start,
        signup_finish,
        refresh,
        logout,
        me,
        list_tenants,
        select_tenant,
        passkey_register_start,
        passkey_register_finish,
        passkey_login_start,
        passkey_login_finish,
    ),
    components(schemas(
        LoginRequest,
        ApiTokenExchangeRequest,
        SignupStartRequest,
        SignupStartResponse,
        SignupFinishRequest,
        LoginResponse,
        LoginResponseVariants,
        TenantSnippet,
        TenantSelectionResponse,
        TenantSelectionRequest,
        TenantListResponse,
        crate::auth::AuthenticatedUser,
        crate::auth::passkeys::RegistrationChallengeResponse,
        crate::auth::passkeys::AuthenticationChallengeResponse,
        crate::auth::passkeys::PasskeySummary,
        crate::auth::passkeys::PasskeyRegistrationFinishPayload,
        crate::auth::passkeys::PasskeyLoginStartPayload,
        crate::auth::passkeys::PasskeyLoginFinishPayload,
        crate::models::ApiTokenCapability,
    ))
)]
pub struct AuthApiDoc;

#[utoipa::path(
    post,
    path = "/api/auth/login",
    request_body = LoginRequest,
    responses(
        (status = 200, description = "Login succeeded", body = LoginResponseVariants),
        (status = 401, description = "Invalid credentials")
    ),
    tag = "Auth"
)]
pub async fn login(
    State(state): State<AppState>,
    Json(payload): Json<LoginRequest>,
) -> AppResult<Response> {
    let magic_token = payload
        .magic_token
        .as_ref()
        .map(|value| value.trim())
        .filter(|value| !value.is_empty());

    if magic_token.is_none() {
        if payload.password.is_some() {
            return Err(AppError::bad_request(
                "password authentication is no longer supported",
            ));
        }

        return Err(AppError::bad_request(
            "magic_token is required for passwordless login",
        ));
    }

    let token_value = magic_token.unwrap();

    let mut conn = state.db_unscoped()?;
    let username_hint = payload.username.trim();
    let preferred_tenant_id = payload.preferred_tenant_id;

    magic_token_login(
        &state,
        &mut conn,
        token_value,
        (!username_hint.is_empty()).then_some(username_hint),
        preferred_tenant_id,
    )
}

#[utoipa::path(
    post,
    path = "/api/auth/exchange-api-token",
    request_body = ApiTokenExchangeRequest,
    responses((status = 200, description = "Access token issued", body = LoginResponse)),
    tag = "Auth"
)]
pub async fn api_token_exchange(
    State(state): State<AppState>,
    Json(payload): Json<ApiTokenExchangeRequest>,
) -> AppResult<Json<LoginResponse>> {
    let secret = payload.api_token.trim();
    if secret.is_empty() {
        return Err(AppError::bad_request("api_token must not be empty"));
    }

    let mut conn = state.db_unscoped()?;

    let token = find_active_token_by_secret(&mut conn, None, secret, ApiTokenCapability::Api)?
        .ok_or_else(AppError::unauthorized)?;

    let user: User = dsl::users.find(token.user_id).first(&mut conn)?;

    apply_user_guc(&mut conn, user.id)?;
    let membership = memberships_dsl::user_memberships
        .filter(memberships_dsl::user_id.eq(user.id))
        .filter(memberships_dsl::tenant_id.eq(token.tenant_id))
        .select(memberships_dsl::tenant_id)
        .first::<Uuid>(&mut conn)
        .optional()?;
    clear_user_guc(&mut conn)?;

    if membership.is_none() {
        return Err(AppError::unauthorized());
    }

    apply_tenant_guc(&mut conn, token.tenant_id)?;
    touch_api_token(&mut conn, token.id)?;

    let access_token = state
        .jwt
        .generate_token(user.id, token.tenant_id, &user.username)
        .map_err(AppError::from)?;

    let tenant_name: String = tenant_dsl::tenants
        .find(token.tenant_id)
        .select(tenant_dsl::name)
        .first(&mut conn)
        .map_err(AppError::from)?;

    let response = LoginResponse {
        access_token,
        token_type: "Bearer".to_string(),
        expires_in: state.config.jwt_expiry_minutes * 60,
        tenant: TenantSnippet {
            id: token.tenant_id,
            name: tenant_name,
        },
    };

    Ok(Json(response))
}

#[utoipa::path(
    post,
    path = "/api/auth/signup/start",
    request_body = SignupStartRequest,
    responses(
        (status = 200, description = "Signup challenge created", body = SignupStartResponse),
        (status = 400, description = "Invalid signup request"),
        (status = 409, description = "Username already exists")
    ),
    tag = "Auth"
)]
pub async fn signup_start(
    State(state): State<AppState>,
    Json(payload): Json<SignupStartRequest>,
) -> AppResult<Json<SignupStartResponse>> {
    let username = payload.username.trim();
    if username.is_empty() {
        return Err(AppError::bad_request("username must not be empty"));
    }

    let mut conn = state.db_unscoped()?;
    let exists: bool = dsl::users
        .filter(dsl::username.eq(username))
        .first::<User>(&mut conn)
        .optional()?
        .is_some();
    if exists {
        return Err(AppError::conflict("username already exists"));
    }

    let user_id = Uuid::new_v4();
    let challenge = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?
        .start_signup_registration(&mut conn, user_id, username)?;

    let signup_token = state
        .jwt
        .generate_signup_token(user_id, challenge.challenge_id, username.to_owned())
        .map_err(AppError::from)?;

    Ok(Json(SignupStartResponse {
        signup_token,
        challenge,
    }))
}

#[utoipa::path(
    post,
    path = "/api/auth/signup/finish",
    request_body = SignupFinishRequest,
    responses(
        (status = 200, description = "Signup completed", body = LoginResponseVariants),
        (status = 400, description = "Invalid signup completion"),
        (status = 409, description = "Username already exists")
    ),
    tag = "Auth"
)]
pub async fn signup_finish(
    State(state): State<AppState>,
    Json(payload): Json<SignupFinishRequest>,
) -> AppResult<Response> {
    let claims = state
        .jwt
        .verify_signup_token(&payload.signup_token)
        .map_err(|_| AppError::unauthorized())?;

    let mut conn = state.db_unscoped()?;

    let exists: bool = dsl::users
        .filter(dsl::username.eq(&claims.username))
        .first::<User>(&mut conn)
        .optional()?
        .is_some();
    if exists {
        return Err(AppError::conflict("username already exists"));
    }

    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let prepared_passkey =
        service.consume_signup_challenge(&mut conn, claims.challenge_id, &payload.credential)?;

    let state_clone = state.clone();
    let response = conn.transaction::<Response, AppError, _>(|conn| {
        insert_user(conn, claims.sub, &claims.username)?;

        let tenant = state_clone.tenants.create_tenant_with_conn(
            conn,
            &claims.username,
            None,
            None,
            TenantStatus::Creating,
            &[claims.sub],
            Some(claims.sub),
        )?;

        let passkey_insert =
            prepared_passkey.into_new_user_passkey(claims.sub, payload.nickname.clone());

        diesel::insert_into(passkey_dsl::user_passkeys)
            .values(&passkey_insert)
            .execute(conn)
            .map_err(AppError::from)?;

        let user: User = dsl::users.find(claims.sub).first(conn)?;
        issue_session(&state_clone, conn, &user, tenant.id)
    })?;

    Ok(response)
}

#[utoipa::path(
    post,
    path = "/api/auth/refresh",
    responses(
        (status = 200, description = "Refreshed access token", body = LoginResponse),
        (status = 401, description = "Missing or invalid refresh token")
    ),
    tag = "Auth"
)]
pub async fn refresh(
    State(state): State<AppState>,
    jar: Option<TypedHeader<Cookie>>,
) -> AppResult<Response> {
    let cookies = jar.ok_or_else(AppError::unauthorized)?;
    let refresh_value = cookies
        .get(SESSION_COOKIE_NAME)
        .ok_or_else(AppError::unauthorized)?;

    let hashed = hash_session_token(refresh_value);
    let mut conn = state.db_unscoped()?;
    let now = Utc::now();
    let now_naive = now.naive_utc();

    apply_user_session_hash(&mut conn, &hashed)?;
    let token = match session_dsl::user_sessions
        .filter(session_dsl::token_hash.eq(&hashed))
        .filter(session_dsl::revoked_at.is_null())
        .filter(session_dsl::expires_at.gt(now_naive))
        .first::<UserSession>(&mut conn)
    {
        Ok(token) => token,
        Err(diesel::result::Error::NotFound) => return Err(AppError::unauthorized()),
        Err(err) => return Err(AppError::from(err)),
    };

    clear_user_session_hash(&mut conn)?;
    apply_tenant_guc(&mut conn, token.tenant_id)?;
    clear_user_guc(&mut conn)?;

    diesel::update(session_dsl::user_sessions.filter(session_dsl::id.eq(token.id)))
        .set((
            session_dsl::revoked_at.eq(now_naive),
            session_dsl::updated_at.eq(now_naive),
        ))
        .execute(&mut conn)?;

    let user: User = dsl::users
        .find(token.user_id)
        .first(&mut conn)
        .map_err(AppError::from)?;

    issue_session(&state, &mut conn, &user, token.tenant_id)
}

fn insert_user(conn: &mut PgConnection, id: Uuid, username: &str) -> AppResult<()> {
    let new_user = NewUser {
        id,
        username: username.to_string(),
    };

    diesel::insert_into(dsl::users)
        .values(&new_user)
        .execute(conn)
        .map(|_| ())
        .map_err(AppError::from)
}

#[utoipa::path(
    post,
    path = "/api/auth/select-tenant",
    request_body = TenantSelectionRequest,
    responses((status = 200, description = "Tenant selected", body = LoginResponse)),
    tag = "Auth"
)]
pub async fn select_tenant(
    State(state): State<AppState>,
    TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
    Json(payload): Json<TenantSelectionRequest>,
) -> AppResult<Response> {
    let user_id = match state.jwt.verify_tenant_selector_token(bearer.token()) {
        Ok(claims) => claims.sub,
        Err(_) => state
            .jwt
            .verify_token(bearer.token())
            .map(|claims| claims.sub)
            .map_err(|_| AppError::unauthorized())?,
    };

    let mut conn = state.db_unscoped()?;
    apply_user_guc(&mut conn, user_id)?;

    let membership_exists = memberships_dsl::user_memberships
        .filter(memberships_dsl::user_id.eq(user_id))
        .filter(memberships_dsl::tenant_id.eq(payload.tenant_id))
        .select(memberships_dsl::tenant_id)
        .first::<Uuid>(&mut conn)
        .optional()?;

    clear_user_guc(&mut conn)?;

    if membership_exists.is_none() {
        return Err(AppError::unauthorized());
    }

    let user: User = dsl::users
        .find(user_id)
        .first(&mut conn)
        .map_err(AppError::from)?;

    issue_session(&state, &mut conn, &user, payload.tenant_id)
}

#[utoipa::path(
    post,
    path = "/api/auth/logout",
    responses((status = 204, description = "Session revoked")),
    tag = "Auth"
)]
pub async fn logout(
    State(state): State<AppState>,
    user: AuthenticatedUser,
    jar: Option<TypedHeader<Cookie>>,
) -> AppResult<(HeaderMap, StatusCode)> {
    let mut conn = state.db_for_tenant(user.tenant_id)?;
    let now = Utc::now().naive_utc();
    let mut rows_affected = 0;

    if let Some(cookies) = jar {
        if let Some(value) = cookies.get(SESSION_COOKIE_NAME) {
            let hashed = hash_session_token(value);
            rows_affected = diesel::update(
                session_dsl::user_sessions
                    .filter(session_dsl::token_hash.eq(hashed))
                    .filter(session_dsl::user_id.eq(user.user_id))
                    .filter(session_dsl::revoked_at.is_null()),
            )
            .set((
                session_dsl::revoked_at.eq(now),
                session_dsl::updated_at.eq(now),
            ))
            .execute(&mut conn)
            .unwrap_or(0);
        }
    }

    if rows_affected == 0 {
        let _ = diesel::update(
            session_dsl::user_sessions
                .filter(session_dsl::user_id.eq(user.user_id))
                .filter(session_dsl::revoked_at.is_null()),
        )
        .set((
            session_dsl::revoked_at.eq(now),
            session_dsl::updated_at.eq(now),
        ))
        .execute(&mut conn);
    }

    let mut headers = HeaderMap::new();
    headers.insert(SET_COOKIE, build_clear_session_cookie(&state));
    Ok((headers, StatusCode::NO_CONTENT))
}

#[utoipa::path(
    get,
    path = "/api/auth/me",
    responses((status = 200, description = "Authenticated principal", body = AuthenticatedUser)),
    tag = "Auth"
)]
pub async fn me(user: AuthenticatedUser) -> Json<AuthenticatedUser> {
    Json(user)
}

#[utoipa::path(
    get,
    path = "/api/auth/tenants",
    responses((status = 200, description = "Available tenants", body = TenantListResponse)),
    tag = "Auth"
)]
pub async fn list_tenants(
    State(state): State<AppState>,
    auth: Option<TypedHeader<Authorization<Bearer>>>,
) -> AppResult<Json<TenantListResponse>> {
    let bearer = auth.ok_or_else(AppError::unauthorized)?;
    let token = bearer.token();

    let user_id = match state.jwt.verify_token(token) {
        Ok(claims) => claims.sub,
        Err(_) => {
            let claims = state
                .jwt
                .verify_tenant_selector_token(token)
                .map_err(|_| AppError::unauthorized())?;
            claims.sub
        }
    };

    let mut conn = state.db_unscoped()?;
    apply_user_guc(&mut conn, user_id)?;

    let tenant_ids: Vec<Uuid> = memberships_dsl::user_memberships
        .filter(memberships_dsl::user_id.eq(user_id))
        .select(memberships_dsl::tenant_id)
        .load(&mut conn)?;

    clear_user_guc(&mut conn)?;
    drop(conn);

    let mut tenants = Vec::with_capacity(tenant_ids.len());
    for tenant_id in tenant_ids {
        let mut tenant_conn = state.db_for_tenant(tenant_id)?;
        let name: String = tenant_dsl::tenants
            .find(tenant_id)
            .select(tenant_dsl::name)
            .first(&mut tenant_conn)
            .map_err(AppError::from)?;
        tenants.push(TenantSnippet {
            id: tenant_id,
            name,
        });
    }

    Ok(Json(TenantListResponse { tenants }))
}

#[utoipa::path(
    post,
    path = "/api/auth/passkeys/register/start",
    responses((status = 200, description = "Passkey registration challenge", body = RegistrationChallengeResponse)),
    tag = "Auth"
)]
pub async fn passkey_register_start(
    State(state): State<AppState>,
    user: AuthenticatedUser,
) -> AppResult<Json<RegistrationChallengeResponse>> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let mut conn = state.db_unscoped()?;
    let current_user: User = dsl::users.find(user.user_id).first(&mut conn)?;
    let challenge = service.start_registration(&mut conn, &current_user)?;
    Ok(Json(challenge))
}

#[utoipa::path(
    post,
    path = "/api/auth/passkeys/register/finish",
    request_body = PasskeyRegistrationFinishPayload,
    responses((status = 200, description = "Passkey registered", body = PasskeySummary)),
    tag = "Auth"
)]
pub async fn passkey_register_finish(
    State(state): State<AppState>,
    user: AuthenticatedUser,
    Json(payload): Json<PasskeyRegistrationFinishPayload>,
) -> AppResult<Json<PasskeySummary>> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let mut conn = state.db_unscoped()?;
    let current_user: User = dsl::users.find(user.user_id).first(&mut conn)?;

    let PasskeyRegistrationFinishPayload {
        challenge_id,
        credential,
        nickname,
    } = payload;

    let passkey = service.finish_registration(
        &mut conn,
        &current_user,
        challenge_id,
        credential,
        nickname,
    )?;

    Ok(Json(PasskeySummary::from(passkey)))
}

#[utoipa::path(
    post,
    path = "/api/auth/passkeys/login/start",
    request_body = PasskeyLoginStartPayload,
    responses((status = 200, description = "Passkey authentication challenge", body = AuthenticationChallengeResponse)),
    tag = "Auth"
)]
pub async fn passkey_login_start(
    State(state): State<AppState>,
    Json(payload): Json<PasskeyLoginStartPayload>,
) -> AppResult<Json<AuthenticationChallengeResponse>> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let username = payload.username.trim();
    if username.is_empty() {
        return Err(AppError::bad_request("username must not be empty"));
    }

    let mut conn = state.db_unscoped()?;
    let user: User = dsl::users
        .filter(dsl::username.eq(username))
        .first(&mut conn)?;

    let challenge = service.start_authentication(&mut conn, &user)?;
    Ok(Json(challenge))
}

#[utoipa::path(
    post,
    path = "/api/auth/passkeys/login/finish",
    request_body = PasskeyLoginFinishPayload,
    responses(
        (status = 200, description = "Passkey login successful", body = LoginResponseVariants),
        (status = 401, description = "Authentication failed")
    ),
    tag = "Auth"
)]
pub async fn passkey_login_finish(
    State(state): State<AppState>,
    Json(payload): Json<PasskeyLoginFinishPayload>,
) -> AppResult<Response> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let mut conn = state.db_unscoped()?;
    let (user, _passkey, auth_result) =
        service.finish_authentication(&mut conn, payload.challenge_id, payload.credential)?;

    if !auth_result.user_verified() {
        return Err(AppError::unauthorized());
    }

    complete_login(&state, &mut conn, &user, None)
}

fn complete_login(
    state: &AppState,
    conn: &mut PgConnection,
    user: &User,
    preferred_tenant_id: Option<Uuid>,
) -> AppResult<Response> {
    apply_user_guc(conn, user.id)?;
    let tenant_ids: Vec<Uuid> = memberships_dsl::user_memberships
        .filter(memberships_dsl::user_id.eq(user.id))
        .select(memberships_dsl::tenant_id)
        .load(conn)?;
    clear_user_guc(conn)?;

    tracing::debug!(user_id = %user.id, tenants = tenant_ids.len(), "passkey login memberships");

    if tenant_ids.is_empty() {
        return Err(AppError::unauthorized());
    }

    if let Some(preferred_id) = preferred_tenant_id {
        if tenant_ids.iter().any(|id| *id == preferred_id) {
            return issue_session(state, conn, user, preferred_id);
        }
    }

    if tenant_ids.len() == 1 {
        return issue_session(state, conn, user, tenant_ids[0]);
    }

    let selection_token = state
        .jwt
        .generate_tenant_selector_token(user.id)
        .map_err(AppError::from)?;

    let mut tenants = Vec::with_capacity(tenant_ids.len());
    for tenant_id in tenant_ids {
        let mut tenant_conn = state.db_for_tenant(tenant_id)?;
        let name: String = tenant_dsl::tenants
            .find(tenant_id)
            .select(tenant_dsl::name)
            .first(&mut tenant_conn)
            .map_err(AppError::from)?;
        tenants.push(TenantSnippet {
            id: tenant_id,
            name,
        });
    }

    Ok(Json(TenantSelectionResponse {
        access_token: selection_token,
        tenants,
    })
    .into_response())
}

fn magic_token_login(
    state: &AppState,
    conn: &mut PgConnection,
    token_value: &str,
    username_hint: Option<&str>,
    preferred_tenant_id: Option<Uuid>,
) -> AppResult<Response> {
    if token_value.is_empty() {
        return Err(AppError::bad_request("magic_token must not be empty"));
    }

    let token_hash = hash_magic_token(token_value);
    let now = Utc::now();
    let now_naive = now.naive_utc();

    conn.transaction::<Response, AppError, _>(|conn| {
        let magic = magic_dsl::magic_tokens
            .filter(magic_dsl::token_hash.eq(&token_hash))
            .filter(magic_dsl::expires_at.gt(now_naive))
            .first::<MagicToken>(conn)
            .map_err(|err| match err {
                diesel::result::Error::NotFound => AppError::unauthorized(),
                _ => AppError::from(err),
            })?;

        if let Some(limit) = magic.max_uses {
            if magic.used_count >= limit {
                return Err(AppError::unauthorized());
            }
        }

        match magic.kind {
            MagicTokenKind::EmailLogin | MagicTokenKind::DemoLogin => {}
        }

        let user: User = dsl::users
            .find(magic.user_id)
            .first(conn)
            .map_err(AppError::from)?;

        if let Some(expected) = username_hint {
            if expected != user.username {
                return Err(AppError::unauthorized());
            }
        }

        diesel::update(magic_dsl::magic_tokens.filter(magic_dsl::id.eq(magic.id)))
            .set((
                magic_dsl::used_count.eq(magic.used_count + 1),
                magic_dsl::last_used_at.eq(Some(now_naive)),
            ))
            .execute(conn)?;

        complete_login(state, conn, &user, preferred_tenant_id)
    })
}

fn issue_session(
    state: &AppState,
    conn: &mut PgConnection,
    user: &User,
    tenant_id: Uuid,
) -> AppResult<Response> {
    apply_tenant_guc(conn, tenant_id)?;
    clear_user_guc(conn)?;
    clear_user_session_hash(conn)?;

    let now = Utc::now();
    let access_token = state
        .jwt
        .generate_token(user.id, tenant_id, &user.username)
        .map_err(AppError::from)?;

    let tenant_name: String = tenant_dsl::tenants
        .find(tenant_id)
        .select(tenant_dsl::name)
        .first(conn)
        .map_err(AppError::from)?;

    let session_value = generate_session_token();
    let session_hash = hash_session_token(&session_value);
    let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days);

    let new_session = NewUserSession {
        id: Uuid::new_v4(),
        user_id: user.id,
        token_hash: session_hash,
        issued_at: now.naive_utc(),
        expires_at: refresh_expires_at.naive_utc(),
        tenant_id,
    };

    diesel::insert_into(user_sessions::table)
        .values(&new_session)
        .execute(conn)?;

    let mut response = Json(LoginResponse {
        access_token,
        token_type: "Bearer".to_string(),
        expires_in: state.config.jwt_expiry_minutes * 60,
        tenant: TenantSnippet {
            id: tenant_id,
            name: tenant_name,
        },
    })
    .into_response();

    response.headers_mut().insert(
        SET_COOKIE,
        build_session_cookie(state, &session_value, refresh_expires_at),
    );

    Ok(response)
}

fn hash_session_token(token: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    hex::encode(hasher.finalize())
}

fn hash_magic_token(token: &str) -> String {
    hash_session_token(token)
}

fn generate_session_token() -> String {
    let mut bytes = [0u8; 32];
    OsRng.fill_bytes(&mut bytes);
    hex::encode(bytes)
}

fn build_session_cookie(
    state: &AppState,
    token: &str,
    expires_at: chrono::DateTime<Utc>,
) -> HeaderValue {
    let max_age = ChronoDuration::days(state.config.refresh_token_expiry_days).num_seconds();

    let mut parts = vec![format!("{}={}", SESSION_COOKIE_NAME, token)];
    parts.push("Path=/".into());
    parts.push("HttpOnly".into());
    parts.push("SameSite=Strict".into());
    parts.push(format!("Max-Age={}", max_age));
    parts.push(format!("Expires={}", expires_at.to_rfc2822()));
    if state.config.refresh_cookie_secure {
        parts.push("Secure".into());
    }
    if let Some(domain) = &state.config.refresh_cookie_domain {
        parts.push(format!("Domain={}", domain));
    }

    HeaderValue::from_str(&parts.join("; ")).expect("valid session cookie")
}

fn build_clear_session_cookie(state: &AppState) -> HeaderValue {
    let mut parts = vec![format!("{}=", SESSION_COOKIE_NAME)];
    parts.push("Path=/".into());
    parts.push("HttpOnly".into());
    parts.push("SameSite=Strict".into());
    parts.push("Max-Age=0".into());
    parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT".into());
    if state.config.refresh_cookie_secure {
        parts.push("Secure".into());
    }
    if let Some(domain) = &state.config.refresh_cookie_domain {
        parts.push(format!("Domain={}", domain));
    }

    HeaderValue::from_str(&parts.join("; ")).expect("valid session cookie")
}
