use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    Json,
};
use chrono::{DateTime, NaiveDateTime};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;

use crate::auth::{
    api_tokens::{
        create_api_token as issue_token, list_api_tokens as load_tokens,
        regenerate_api_token as rotate_token, revoke_api_token as revoke_token,
        update_api_token_capabilities as update_capabilities,
    },
    passkeys::PasskeySummary,
    TenantScopedConn,
};
use crate::error::{AppError, AppResult};
use crate::models::{ApiToken, ApiTokenCapability};
use crate::state::AppState;
use crate::utils::{db::no_content, time::to_iso};

#[derive(Debug, Serialize, ToSchema)]
pub struct ApiTokenResponse {
    pub id: Uuid,
    pub tenant_id: Uuid,
    #[schema(nullable)]
    pub label: Option<String>,
    pub capabilities: Vec<ApiTokenCapability>,
    pub created_at: String,
    #[schema(nullable)]
    pub last_used_at: Option<String>,
    #[schema(nullable)]
    pub expires_at: Option<String>,
    #[schema(nullable)]
    pub revoked_at: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ApiTokenCreatedResponse {
    pub token: String,
    pub token_info: ApiTokenResponse,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateApiTokenRequest {
    #[schema(nullable)]
    pub label: Option<String>,
    #[schema(nullable)]
    pub expires_at: Option<String>,
    #[serde(default)]
    #[schema(nullable)]
    pub capabilities: Option<Vec<ApiTokenCapability>>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateApiTokenCapabilitiesRequest {
    pub capabilities: Vec<ApiTokenCapability>,
}

#[derive(Debug, Deserialize, ToSchema)]
pub struct RevokePasskeyQuery {
    #[serde(default)]
    #[schema(nullable)]
    pub reason: Option<String>,
}

#[utoipa::path(
    get,
    path = "/api/profile/passkeys",
    responses((status = 200, description = "List registered passkeys", body = [PasskeySummary])),
    tag = "Profile"
)]
pub async fn list_passkeys(
    State(state): State<AppState>,
    TenantScopedConn {
        mut conn, user_id, ..
    }: TenantScopedConn,
) -> AppResult<Json<Vec<PasskeySummary>>> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let passkeys = service.list_for_user(&mut conn, user_id)?;
    Ok(Json(passkeys))
}

#[utoipa::path(
    get,
    path = "/api/profile/api-tokens",
    responses((status = 200, description = "List API tokens", body = [ApiTokenResponse])),
    tag = "Profile"
)]
pub async fn list_api_tokens(
    TenantScopedConn {
        mut conn,
        tenant_id,
        user_id,
        ..
    }: TenantScopedConn,
) -> AppResult<Json<Vec<ApiTokenResponse>>> {
    let tokens = load_tokens(&mut conn, user_id, Some(tenant_id))?;
    let responses = tokens.into_iter().map(api_token_to_response).collect();
    Ok(Json(responses))
}

#[utoipa::path(
    post,
    path = "/api/profile/api-tokens",
    request_body = CreateApiTokenRequest,
    responses((status = 201, description = "API token created", body = ApiTokenCreatedResponse)),
    tag = "Profile"
)]
pub async fn create_api_token(
    TenantScopedConn {
        mut conn,
        tenant_id,
        user_id,
        ..
    }: TenantScopedConn,
    Json(payload): Json<CreateApiTokenRequest>,
) -> AppResult<(StatusCode, Json<ApiTokenCreatedResponse>)> {
    let expires_at = match payload.expires_at {
        Some(ref value) => Some(parse_timestamp(value)?),
        None => None,
    };

    let capabilities = payload
        .capabilities
        .unwrap_or_else(|| vec![ApiTokenCapability::Webdav]);

    let issued = issue_token(
        &mut conn,
        user_id,
        tenant_id,
        payload.label.clone(),
        expires_at,
        capabilities,
    )?;

    let response = ApiTokenCreatedResponse {
        token: issued.token,
        token_info: api_token_to_response(issued.record),
    };

    Ok((StatusCode::CREATED, Json(response)))
}

#[utoipa::path(
    post,
    path = "/api/profile/api-tokens/{id}/regenerate",
    params(("id" = Uuid, Path, description = "API token ID")),
    responses((status = 200, description = "API token regenerated", body = ApiTokenCreatedResponse)),
    tag = "Profile"
)]
pub async fn regenerate_api_token(
    TenantScopedConn {
        mut conn,
        tenant_id,
        user_id,
        ..
    }: TenantScopedConn,
    Path(token_id): Path<Uuid>,
) -> AppResult<Json<ApiTokenCreatedResponse>> {
    let issued = rotate_token(&mut conn, token_id, user_id, Some(tenant_id))?;
    let response = ApiTokenCreatedResponse {
        token: issued.token,
        token_info: api_token_to_response(issued.record),
    };

    Ok(Json(response))
}

#[utoipa::path(
    patch,
    path = "/api/profile/api-tokens/{id}",
    params(("id" = Uuid, Path, description = "API token ID")),
    request_body = UpdateApiTokenCapabilitiesRequest,
    responses((status = 200, description = "API token updated", body = ApiTokenResponse)),
    tag = "Profile"
)]
pub async fn update_api_token(
    TenantScopedConn {
        mut conn,
        tenant_id,
        user_id,
        ..
    }: TenantScopedConn,
    Path(token_id): Path<Uuid>,
    Json(payload): Json<UpdateApiTokenCapabilitiesRequest>,
) -> AppResult<Json<ApiTokenResponse>> {
    let updated = update_capabilities(
        &mut conn,
        token_id,
        user_id,
        Some(tenant_id),
        payload.capabilities,
    )?;

    Ok(Json(api_token_to_response(updated)))
}

#[utoipa::path(
    delete,
    path = "/api/profile/api-tokens/{id}",
    params(("id" = Uuid, Path, description = "API token ID")),
    responses((status = 204, description = "API token revoked")),
    tag = "Profile"
)]
pub async fn delete_api_token(
    TenantScopedConn {
        mut conn, user_id, ..
    }: TenantScopedConn,
    Path(token_id): Path<Uuid>,
) -> AppResult<StatusCode> {
    revoke_token(&mut conn, token_id, user_id)?;
    no_content()
}

#[utoipa::path(
    delete,
    path = "/api/profile/passkeys/{id}",
    params(
        ("id" = Uuid, Path, description = "Passkey ID"),
        ("reason" = Option<String>, Query, description = "Optional reason for revoking the passkey")
    ),
    responses((status = 204, description = "Passkey revoked")),
    tag = "Profile"
)]
pub async fn delete_passkey(
    State(state): State<AppState>,
    TenantScopedConn {
        mut conn, user_id, ..
    }: TenantScopedConn,
    Path(passkey_id): Path<Uuid>,
    Query(query): Query<RevokePasskeyQuery>,
) -> AppResult<StatusCode> {
    let service = state
        .passkeys
        .as_ref()
        .ok_or_else(|| AppError::bad_request("passkey support is disabled"))?;

    let active_count = service.active_passkey_count(&mut conn, user_id)?;
    if active_count <= 1 {
        return Err(AppError::bad_request(
            "cannot revoke the last remaining passkey",
        ));
    }

    service.revoke_passkey(&mut conn, user_id, passkey_id, query.reason)?;
    no_content()
}

fn api_token_to_response(token: ApiToken) -> ApiTokenResponse {
    let ApiToken {
        id,
        tenant_id,
        label,
        created_at,
        last_used_at,
        expires_at,
        revoked_at,
        capabilities,
        ..
    } = token;

    ApiTokenResponse {
        id,
        tenant_id,
        label,
        capabilities,
        created_at: to_iso(created_at),
        last_used_at: last_used_at.map(to_iso),
        expires_at: expires_at.map(to_iso),
        revoked_at: revoked_at.map(to_iso),
    }
}

fn parse_timestamp(value: &str) -> AppResult<NaiveDateTime> {
    let dt = DateTime::parse_from_rfc3339(value)
        .map_err(|_| AppError::bad_request("invalid expires_at timestamp"))?;
    Ok(dt.naive_utc())
}

#[derive(utoipa::OpenApi)]
#[openapi(
    paths(
        crate::routes::profile::list_api_tokens,
        crate::routes::profile::create_api_token,
        crate::routes::profile::regenerate_api_token,
        crate::routes::profile::update_api_token,
        crate::routes::profile::delete_api_token,
        crate::routes::profile::list_passkeys,
        crate::routes::profile::delete_passkey
    ),
    components(schemas(
        crate::models::ApiTokenCapability,
        crate::routes::profile::ApiTokenResponse,
        crate::routes::profile::ApiTokenCreatedResponse,
        crate::routes::profile::CreateApiTokenRequest,
        crate::routes::profile::UpdateApiTokenCapabilitiesRequest,
        crate::routes::profile::RevokePasskeyQuery,
        crate::auth::passkeys::PasskeySummary
    ))
)]
pub struct ProfileApiDoc;
