//! Authentication routes

use axum::{
    extract::{Query, State},
    http::HeaderMap,
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
    Json, Router,
};

use validator::Validate;

use attune_common::repositories::{
    identity::{CreateIdentityInput, IdentityRepository},
    Create, FindById,
};

use crate::{
    auth::{
        hash_password,
        jwt::{
            generate_access_token, generate_refresh_token, generate_sensor_token, validate_token,
            TokenType,
        },
        middleware::RequireAuth,
        oidc::{
            apply_cookies_to_headers, build_login_redirect, build_logout_redirect,
            cookie_authenticated_user, get_cookie_value, oidc_callback_redirect_response,
            OidcCallbackQuery, REFRESH_COOKIE_NAME,
        },
        verify_password,
    },
    dto::{
        ApiResponse, AuthSettingsResponse, ChangePasswordRequest, CurrentUserResponse,
        LoginRequest, RefreshTokenRequest, RegisterRequest, SuccessResponse, TokenResponse,
    },
    middleware::error::ApiError,
    state::SharedState,
};

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Request body for creating sensor tokens
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
pub struct CreateSensorTokenRequest {
    /// Sensor reference (e.g., "core.timer")
    #[validate(length(min = 1, max = 255))]
    pub sensor_ref: String,

    /// List of trigger types this sensor can create events for
    #[validate(length(min = 1))]
    pub trigger_types: Vec<String>,

    /// Optional TTL in seconds (default: 86400 = 24 hours, max: 259200 = 72 hours)
    #[validate(range(min = 3600, max = 259200))]
    pub ttl_seconds: Option<i64>,
}

/// Response for sensor token creation
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SensorTokenResponse {
    pub identity_id: i64,
    pub sensor_ref: String,
    pub token: String,
    pub expires_at: String,
    pub trigger_types: Vec<String>,
}

/// Create authentication routes
pub fn routes() -> Router<SharedState> {
    Router::new()
        .route("/settings", get(auth_settings))
        .route("/login", post(login))
        .route("/oidc/login", get(oidc_login))
        .route("/callback", get(oidc_callback))
        .route("/ldap/login", post(ldap_login))
        .route("/logout", get(logout))
        .route("/register", post(register))
        .route("/refresh", post(refresh_token))
        .route("/me", get(get_current_user))
        .route("/change-password", post(change_password))
        .route("/sensor-token", post(create_sensor_token))
        .route("/internal/sensor-token", post(create_sensor_token_internal))
}

/// Authentication settings endpoint
///
/// GET /auth/settings
#[utoipa::path(
    get,
    path = "/auth/settings",
    tag = "auth",
    responses(
        (status = 200, description = "Authentication settings", body = inline(ApiResponse<AuthSettingsResponse>))
    )
)]
pub async fn auth_settings(
    State(state): State<SharedState>,
) -> Result<Json<ApiResponse<AuthSettingsResponse>>, ApiError> {
    let oidc = state
        .config
        .security
        .oidc
        .as_ref()
        .filter(|oidc| oidc.enabled);

    let ldap = state
        .config
        .security
        .ldap
        .as_ref()
        .filter(|ldap| ldap.enabled);

    let response = AuthSettingsResponse {
        authentication_enabled: state.config.security.enable_auth,
        local_password_enabled: state.config.security.enable_auth,
        local_password_visible_by_default: state.config.security.enable_auth
            && state.config.security.login_page.show_local_login,
        oidc_enabled: oidc.is_some(),
        oidc_visible_by_default: oidc.is_some() && state.config.security.login_page.show_oidc_login,
        oidc_provider_name: oidc.map(|oidc| oidc.provider_name.clone()),
        oidc_provider_label: oidc.map(|oidc| {
            oidc.provider_label
                .clone()
                .unwrap_or_else(|| oidc.provider_name.clone())
        }),
        oidc_provider_icon_url: oidc.and_then(|oidc| oidc.provider_icon_url.clone()),
        ldap_enabled: ldap.is_some(),
        ldap_visible_by_default: ldap.is_some() && state.config.security.login_page.show_ldap_login,
        ldap_provider_name: ldap.map(|ldap| ldap.provider_name.clone()),
        ldap_provider_label: ldap.map(|ldap| {
            ldap.provider_label
                .clone()
                .unwrap_or_else(|| ldap.provider_name.clone())
        }),
        ldap_provider_icon_url: ldap.and_then(|ldap| ldap.provider_icon_url.clone()),
        self_registration_enabled: state.config.security.allow_self_registration,
    };

    Ok(Json(ApiResponse::new(response)))
}

/// Login endpoint
///
/// POST /auth/login
#[utoipa::path(
    post,
    path = "/auth/login",
    tag = "auth",
    request_body = LoginRequest,
    responses(
        (status = 200, description = "Successfully logged in", body = inline(ApiResponse<TokenResponse>)),
        (status = 401, description = "Invalid credentials"),
        (status = 400, description = "Validation error")
    )
)]
pub async fn login(
    State(state): State<SharedState>,
    Json(payload): Json<LoginRequest>,
) -> Result<Json<ApiResponse<TokenResponse>>, ApiError> {
    // Validate request
    payload
        .validate()
        .map_err(|e| ApiError::ValidationError(format!("Invalid login request: {}", e)))?;

    // Find identity by login
    let identity = IdentityRepository::find_by_login(&state.db, &payload.login)
        .await?
        .ok_or_else(|| ApiError::Unauthorized("Invalid login or password".to_string()))?;

    if identity.frozen {
        return Err(ApiError::Forbidden(
            "Identity is frozen and cannot authenticate".to_string(),
        ));
    }

    // Check if identity has a password set
    let password_hash = identity
        .password_hash
        .as_ref()
        .ok_or_else(|| ApiError::Unauthorized("Invalid login or password".to_string()))?;

    // Verify password
    let is_valid = verify_password(&payload.password, password_hash)
        .map_err(|_| ApiError::Unauthorized("Invalid login or password".to_string()))?;

    if !is_valid {
        return Err(ApiError::Unauthorized(
            "Invalid login or password".to_string(),
        ));
    }

    // Generate tokens
    let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
    let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;

    let response = TokenResponse::new(
        access_token,
        refresh_token,
        state.jwt_config.access_token_expiration,
    )
    .with_user(
        identity.id,
        identity.login.clone(),
        identity.display_name.clone(),
    );

    Ok(Json(ApiResponse::new(response)))
}

/// Register endpoint
///
/// POST /auth/register
#[utoipa::path(
    post,
    path = "/auth/register",
    tag = "auth",
    request_body = RegisterRequest,
    responses(
        (status = 200, description = "Successfully registered", body = inline(ApiResponse<TokenResponse>)),
        (status = 409, description = "User already exists"),
        (status = 400, description = "Validation error")
    )
)]
pub async fn register(
    State(state): State<SharedState>,
    Json(payload): Json<RegisterRequest>,
) -> Result<Json<ApiResponse<TokenResponse>>, ApiError> {
    if !state.config.security.allow_self_registration {
        return Err(ApiError::Forbidden(
            "Self-service registration is disabled; identities must be provisioned by an administrator or identity provider".to_string(),
        ));
    }

    // Validate request
    payload
        .validate()
        .map_err(|e| ApiError::ValidationError(format!("Invalid registration request: {}", e)))?;

    // Check if login already exists
    if IdentityRepository::find_by_login(&state.db, &payload.login)
        .await?
        .is_some()
    {
        return Err(ApiError::Conflict(format!(
            "Identity with login '{}' already exists",
            payload.login
        )));
    }

    // Hash password
    let password_hash = hash_password(&payload.password)?;

    // Registration creates an identity only; permission assignments are managed separately.
    let input = CreateIdentityInput {
        login: payload.login.clone(),
        display_name: payload.display_name,
        password_hash: Some(password_hash),
        attributes: serde_json::json!({}),
    };

    let identity = IdentityRepository::create(&state.db, input).await?;

    // Generate tokens
    let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
    let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;

    let response = TokenResponse::new(
        access_token,
        refresh_token,
        state.jwt_config.access_token_expiration,
    )
    .with_user(
        identity.id,
        identity.login.clone(),
        identity.display_name.clone(),
    );

    Ok(Json(ApiResponse::new(response)))
}

/// Refresh token endpoint
///
/// POST /auth/refresh
#[utoipa::path(
    post,
    path = "/auth/refresh",
    tag = "auth",
    request_body = RefreshTokenRequest,
    responses(
        (status = 200, description = "Successfully refreshed token", body = inline(ApiResponse<TokenResponse>)),
        (status = 401, description = "Invalid or expired refresh token"),
        (status = 400, description = "Validation error")
    )
)]
pub async fn refresh_token(
    State(state): State<SharedState>,
    headers: HeaderMap,
    payload: Option<Json<RefreshTokenRequest>>,
) -> Result<Response, ApiError> {
    let browser_cookie_refresh = payload.is_none();
    let refresh_token = if let Some(Json(payload)) = payload {
        payload.validate().map_err(|e| {
            ApiError::ValidationError(format!("Invalid refresh token request: {}", e))
        })?;
        payload.refresh_token
    } else {
        get_cookie_value(&headers, REFRESH_COOKIE_NAME)
            .ok_or_else(|| ApiError::Unauthorized("Missing refresh token".to_string()))?
    };

    // Validate refresh token
    let claims = validate_token(&refresh_token, &state.jwt_config)
        .map_err(|_| ApiError::Unauthorized("Invalid or expired refresh token".to_string()))?;

    // Ensure it's a refresh token
    if claims.token_type != TokenType::Refresh {
        return Err(ApiError::Unauthorized("Invalid token type".to_string()));
    }

    // Parse identity ID
    let identity_id: i64 = claims
        .sub
        .parse()
        .map_err(|_| ApiError::Unauthorized("Invalid token".to_string()))?;

    // Verify identity still exists
    let identity = IdentityRepository::find_by_id(&state.db, identity_id)
        .await?
        .ok_or_else(|| ApiError::Unauthorized("Identity not found".to_string()))?;

    if identity.frozen {
        return Err(ApiError::Forbidden(
            "Identity is frozen and cannot authenticate".to_string(),
        ));
    }

    // Generate new tokens
    let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
    let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;

    let response = TokenResponse::new(
        access_token,
        refresh_token,
        state.jwt_config.access_token_expiration,
    );
    let response_body = Json(ApiResponse::new(response.clone()));

    if browser_cookie_refresh {
        let mut http_response = response_body.into_response();
        apply_cookies_to_headers(
            http_response.headers_mut(),
            &crate::auth::oidc::build_auth_cookies(&state, &response, ""),
        )?;
        return Ok(http_response);
    }

    Ok(response_body.into_response())
}

/// Get current user endpoint
///
/// GET /auth/me
#[utoipa::path(
    get,
    path = "/auth/me",
    tag = "auth",
    responses(
        (status = 200, description = "Current user information", body = inline(ApiResponse<CurrentUserResponse>)),
        (status = 401, description = "Unauthorized"),
        (status = 404, description = "Identity not found")
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn get_current_user(
    State(state): State<SharedState>,
    headers: HeaderMap,
    user: Result<RequireAuth, crate::auth::middleware::AuthError>,
) -> Result<Json<ApiResponse<CurrentUserResponse>>, ApiError> {
    let authenticated_user = match user {
        Ok(RequireAuth(user)) => user,
        Err(_) => cookie_authenticated_user(&headers, &state)?
            .ok_or_else(|| ApiError::Unauthorized("Unauthorized".to_string()))?,
    };
    let identity_id = authenticated_user.identity_id()?;

    // Fetch identity from database
    let identity = IdentityRepository::find_by_id(&state.db, identity_id)
        .await?
        .ok_or_else(|| ApiError::NotFound("Identity not found".to_string()))?;

    if identity.frozen {
        return Err(ApiError::Forbidden(
            "Identity is frozen and cannot authenticate".to_string(),
        ));
    }

    let response = CurrentUserResponse {
        id: identity.id,
        login: identity.login,
        display_name: identity.display_name,
    };

    Ok(Json(ApiResponse::new(response)))
}

/// Request body for LDAP login.
#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
pub struct LdapLoginRequest {
    /// User login name (uid, sAMAccountName, etc.)
    #[validate(length(min = 1, max = 255))]
    pub login: String,
    /// User password
    #[validate(length(min = 1, max = 512))]
    pub password: String,
}

#[derive(Debug, Deserialize)]
pub struct OidcLoginParams {
    pub redirect_to: Option<String>,
}

/// Begin browser OIDC login by redirecting to the provider.
pub async fn oidc_login(
    State(state): State<SharedState>,
    Query(params): Query<OidcLoginParams>,
) -> Result<Response, ApiError> {
    let login_redirect = build_login_redirect(&state, params.redirect_to.as_deref()).await?;
    let mut response = Redirect::temporary(&login_redirect.authorization_url).into_response();
    apply_cookies_to_headers(response.headers_mut(), &login_redirect.cookies)?;
    Ok(response)
}

/// Handle the OIDC authorization code callback.
pub async fn oidc_callback(
    State(state): State<SharedState>,
    headers: HeaderMap,
    Query(query): Query<OidcCallbackQuery>,
) -> Result<Response, ApiError> {
    let redirect_to = get_cookie_value(&headers, crate::auth::oidc::OIDC_REDIRECT_COOKIE_NAME);
    let authenticated = crate::auth::oidc::handle_callback(&state, &headers, &query).await?;
    oidc_callback_redirect_response(
        &state,
        &authenticated.token_response,
        redirect_to,
        &authenticated.id_token,
    )
}

/// Authenticate via LDAP directory.
///
/// POST /auth/ldap/login
#[utoipa::path(
    post,
    path = "/auth/ldap/login",
    tag = "auth",
    request_body = LdapLoginRequest,
    responses(
        (status = 200, description = "Successfully authenticated via LDAP", body = inline(ApiResponse<TokenResponse>)),
        (status = 401, description = "Invalid LDAP credentials"),
        (status = 501, description = "LDAP not configured")
    )
)]
pub async fn ldap_login(
    State(state): State<SharedState>,
    Json(payload): Json<LdapLoginRequest>,
) -> Result<Json<ApiResponse<TokenResponse>>, ApiError> {
    payload
        .validate()
        .map_err(|e| ApiError::ValidationError(format!("Invalid LDAP login request: {e}")))?;

    let authenticated =
        crate::auth::ldap::authenticate(&state, &payload.login, &payload.password).await?;

    Ok(Json(ApiResponse::new(authenticated.token_response)))
}

/// Logout the current browser session and optionally redirect through the provider logout flow.
pub async fn logout(
    State(state): State<SharedState>,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    let oidc_enabled = state
        .config
        .security
        .oidc
        .as_ref()
        .is_some_and(|oidc| oidc.enabled);

    let response = if oidc_enabled {
        let logout_redirect = build_logout_redirect(&state, &headers).await?;
        let mut response = Redirect::temporary(&logout_redirect.redirect_url).into_response();
        apply_cookies_to_headers(response.headers_mut(), &logout_redirect.cookies)?;
        response
    } else {
        let mut response = Redirect::temporary("/login").into_response();
        apply_cookies_to_headers(
            response.headers_mut(),
            &crate::auth::oidc::clear_auth_cookies(&state),
        )?;
        response
    };

    Ok(response)
}

/// Change password endpoint
///
/// POST /auth/change-password
#[utoipa::path(
    post,
    path = "/auth/change-password",
    tag = "auth",
    request_body = ChangePasswordRequest,
    responses(
        (status = 200, description = "Password changed successfully", body = inline(ApiResponse<SuccessResponse>)),
        (status = 401, description = "Invalid current password or unauthorized"),
        (status = 400, description = "Validation error"),
        (status = 404, description = "Identity not found")
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn change_password(
    State(state): State<SharedState>,
    RequireAuth(user): RequireAuth,
    Json(payload): Json<ChangePasswordRequest>,
) -> Result<Json<ApiResponse<SuccessResponse>>, ApiError> {
    // Validate request
    payload.validate().map_err(|e| {
        ApiError::ValidationError(format!("Invalid change password request: {}", e))
    })?;

    let identity_id = user.identity_id()?;

    // Fetch identity from database
    let identity = IdentityRepository::find_by_id(&state.db, identity_id)
        .await?
        .ok_or_else(|| ApiError::NotFound("Identity not found".to_string()))?;

    // Get current password hash
    let current_password_hash = identity
        .password_hash
        .as_ref()
        .ok_or_else(|| ApiError::Unauthorized("No password set".to_string()))?;

    // Verify current password
    let is_valid = verify_password(&payload.current_password, current_password_hash)
        .map_err(|_| ApiError::Unauthorized("Invalid current password".to_string()))?;

    if !is_valid {
        return Err(ApiError::Unauthorized(
            "Invalid current password".to_string(),
        ));
    }

    // Hash new password
    let new_password_hash = hash_password(&payload.new_password)?;

    // Update identity in database with new password hash
    use attune_common::repositories::identity::UpdateIdentityInput;
    use attune_common::repositories::Update;

    let update_input = UpdateIdentityInput {
        display_name: None,
        password_hash: Some(new_password_hash),
        attributes: None,
        frozen: None,
    };

    IdentityRepository::update(&state.db, identity_id, update_input).await?;

    Ok(Json(ApiResponse::new(SuccessResponse::new(
        "Password changed successfully",
    ))))
}

/// Create sensor token endpoint (internal use by sensor service)
///
/// POST /auth/sensor-token
#[utoipa::path(
    post,
    path = "/auth/sensor-token",
    tag = "auth",
    request_body = CreateSensorTokenRequest,
    responses(
        (status = 200, description = "Sensor token created successfully", body = inline(ApiResponse<SensorTokenResponse>)),
        (status = 400, description = "Validation error"),
        (status = 401, description = "Unauthorized")
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn create_sensor_token(
    State(state): State<SharedState>,
    RequireAuth(_user): RequireAuth,
    Json(payload): Json<CreateSensorTokenRequest>,
) -> Result<Json<ApiResponse<SensorTokenResponse>>, ApiError> {
    create_sensor_token_impl(state, payload).await
}

/// Create sensor token endpoint for internal service use (no auth required)
///
/// POST /auth/internal/sensor-token
///
/// This endpoint is intended for internal use by the sensor service to provision
/// tokens for standalone sensors. In production, this should be restricted by
/// network policies or replaced with proper service-to-service authentication.
#[utoipa::path(
    post,
    path = "/auth/internal/sensor-token",
    tag = "auth",
    request_body = CreateSensorTokenRequest,
    responses(
        (status = 200, description = "Sensor token created successfully", body = inline(ApiResponse<SensorTokenResponse>)),
        (status = 400, description = "Validation error")
    )
)]
pub async fn create_sensor_token_internal(
    State(state): State<SharedState>,
    Json(payload): Json<CreateSensorTokenRequest>,
) -> Result<Json<ApiResponse<SensorTokenResponse>>, ApiError> {
    create_sensor_token_impl(state, payload).await
}

/// Shared implementation for sensor token creation
async fn create_sensor_token_impl(
    state: SharedState,
    payload: CreateSensorTokenRequest,
) -> Result<Json<ApiResponse<SensorTokenResponse>>, ApiError> {
    // Validate request
    payload
        .validate()
        .map_err(|e| ApiError::ValidationError(format!("Invalid sensor token request: {}", e)))?;

    // Create or find sensor identity
    let sensor_login = format!("sensor:{}", payload.sensor_ref);

    let identity = match IdentityRepository::find_by_login(&state.db, &sensor_login).await? {
        Some(identity) => identity,
        None => {
            // Create new sensor identity
            let input = CreateIdentityInput {
                login: sensor_login.clone(),
                display_name: Some(format!("Sensor: {}", payload.sensor_ref)),
                password_hash: None, // Sensors don't use passwords
                attributes: serde_json::json!({
                    "type": "sensor",
                    "sensor_ref": payload.sensor_ref,
                    "trigger_types": payload.trigger_types,
                }),
            };
            IdentityRepository::create(&state.db, input).await?
        }
    };

    // Generate sensor token
    let ttl_seconds = payload.ttl_seconds.unwrap_or(86400); // Default: 24 hours
    let token = generate_sensor_token(
        identity.id,
        &payload.sensor_ref,
        payload.trigger_types.clone(),
        &state.jwt_config,
        Some(ttl_seconds),
    )?;

    // Calculate expiration time
    let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds);

    let response = SensorTokenResponse {
        identity_id: identity.id,
        sensor_ref: payload.sensor_ref,
        token,
        expires_at: expires_at.to_rfc3339(),
        trigger_types: payload.trigger_types,
    };

    Ok(Json(ApiResponse::new(response)))
}
