use std::str;

use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse};
use deserr::actix_web::{AwebJson, AwebQueryParameter};
use deserr::Deserr;
use meilisearch_auth::error::AuthControllerError;
use meilisearch_auth::AuthController;
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;

use super::{PaginationView, PAGINATION_DEFAULT_LIMIT, PAGINATION_DEFAULT_LIMIT_FN};
use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData;
use crate::routes::Pagination;

#[routes::routes(
    routes(
        "" => [post(create_api_key), get(list_api_keys)],
        "/{key}" => [get(get_api_key), patch(patch_api_key), delete(delete_api_key)],
    ),
    tag = "Keys",
    tags((
        name = "Keys",
        description = "Manage API `keys` for a Meilisearch instance. Each key has a given set of permissions.
You must have the master key or the default admin key to access the keys route. More information about the keys and their rights.
Accessing any route under `/keys` without having set a master key will result in an error.",
    )),
)]
pub struct ApiKeyApi;

/// Create API key
///
/// Create a new API key with the specified name, description, actions, and index scopes. The key value is returned only once at creation time; store it securely.
///
/// **Required fields:** `actions`, `indexes`, `expiresAt`.
/// **Optional fields:** `name`, `description`, `uid`.
///
/// Set `expiresAt` to `null` to create a key that never expires.
/// Use `"*"` in the `actions` array to grant all permissions (not recommended for production).
/// Use `["*"]` in the `indexes` array to grant access to all indexes.
#[routes::path(
    security(("Bearer" = ["keys.create", "keys.*", "*"])),
    request_body = CreateApiKey,
    responses(
        (status = 201, description = "Key has been created.", body = KeyView, content_type = "application/json", example = json!(
            {
                "uid": "01b4bc42-eb33-4041-b481-254d00cce834",
                "key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
                "name": "Indexing Products API key",
                "description": null,
                "actions": [
                    "documents.add"
                ],
                "indexes": [
                    "products"
                ],
                "expiresAt": "2021-11-13T00:00:00Z",
                "createdAt": "2021-11-12T10:00:00Z",
                "updatedAt": "2021-11-12T10:00:00Z"
            }
        )),
        (status = 401, description = "The route has been hit on an unprotected instance.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
                "code": "missing_master_key",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_master_key"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
    )
)]
pub async fn create_api_key(
    auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
    body: AwebJson<CreateApiKey, DeserrJsonError>,
    _req: HttpRequest,
) -> Result<HttpResponse, ResponseError> {
    let v = body.into_inner();
    let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
        let key = auth_controller.create_key(v)?;
        Ok(KeyView::from_key(key, &auth_controller))
    })
    .await
    .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;

    Ok(HttpResponse::Created().json(res))
}

#[derive(Deserr, Debug, Clone, Copy, IntoParams)]
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[into_params(rename_all = "camelCase", parameter_in = Query)]
pub struct ListApiKeys {
    /// Number of keys to skip. Use with `limit` for pagination. Defaults to 0.
    #[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
    #[param(required = false, value_type = usize, default = 0)]
    pub offset: Param<usize>,
    /// Maximum number of keys to return. Use with `offset` for pagination. Defaults to 20.
    #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
    #[param(required = false, value_type = usize, default = PAGINATION_DEFAULT_LIMIT_FN)]
    pub limit: Param<usize>,
}

impl ListApiKeys {
    fn as_pagination(self) -> Pagination {
        Pagination { offset: self.offset.0, limit: self.limit.0 }
    }
}

/// List API keys
///
/// Return all API keys configured on the instance. Results are paginated and can be filtered by offset and limit. The key value itself is never returned after creation.
///
/// **Note:** Expired keys are included in the response but deleted keys are not.
/// Keys are returned in descending order of creation date.
#[routes::path(
    security(("Bearer" = ["keys.get", "keys.*", "*"])),
    params(ListApiKeys),
    responses(
        (status = 200, description = "List of keys.", body = PaginationView<KeyView>, content_type = "application/json", example = json!(
            {
                "results": [
                    {
                        "uid": "01b4bc42-eb33-4041-b481-254d00cce834",
                        "key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
                        "name": "An API Key",
                        "description": null,
                        "actions": [
                            "documents.add"
                        ],
                        "indexes": [
                            "movies"
                        ],
                        "expiresAt": "2022-11-12T10:00:00Z",
                        "createdAt": "2021-11-12T10:00:00Z",
                        "updatedAt": "2021-11-12T10:00:00Z"
                    }
                ],
                "limit": 20,
                "offset": 0,
                "total": 1
            }
        )),
        (status = 401, description = "The route has been hit on an unprotected instance.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
                "code": "missing_master_key",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_master_key"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
    )
)]
pub async fn list_api_keys(
    auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
    list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> {
    let paginate = list_api_keys.into_inner().as_pagination();
    let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
        let keys = auth_controller.list_keys()?;
        let page_view = paginate
            .auto_paginate_sized(keys.into_iter().map(|k| KeyView::from_key(k, &auth_controller)));

        Ok(page_view)
    })
    .await
    .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;

    Ok(HttpResponse::Ok().json(page_view))
}

/// Get API key
///
/// Retrieve a single API key by its `uid` or by its `key` value.
#[routes::path(
    security(("Bearer" = ["keys.get", "keys.*", "*"])),
    params(("key" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key.", nullable = false)),
    responses(
        (status = 200, description = "The key is returned.", body = KeyView, content_type = "application/json", example = json!(
            {
                "uid": "01b4bc42-eb33-4041-b481-254d00cce834",
                "key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
                "name": "An API Key",
                "description": null,
                "actions": [
                    "documents.add"
                ],
                "indexes": [
                    "movies"
                ],
                "expiresAt": "2022-11-12T10:00:00Z",
                "createdAt": "2021-11-12T10:00:00Z",
                "updatedAt": "2021-11-12T10:00:00Z"
            }
        )),
        (status = 401, description = "The route has been hit on an unprotected instance.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
                "code": "missing_master_key",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_master_key"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
        (status = 404, description = "API key not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The API key was not found.",
                "code": "api_key_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#api_key_not_found"
            }
        )),
    )
)]
pub async fn get_api_key(
    auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
    path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
    let key = path.into_inner().key;

    let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
        let uid =
            Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
        let key = auth_controller.get_key(uid)?;

        Ok(KeyView::from_key(key, &auth_controller))
    })
    .await
    .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;

    Ok(HttpResponse::Ok().json(res))
}

/// Update API key
///
/// Update the name and description of an API key.
///
/// Updates are partial: only the fields you send are changed, and any fields not present in the payload remain unchanged.
///
/// **Note:** Only `name` and `description` can be updated. The fields `actions`, `indexes`,
/// `expiresAt`, and `uid` cannot be modified after key creation.
#[routes::path(
    security(("Bearer" = ["keys.update", "keys.*", "*"])),
    params(("key" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key.", nullable = false)),
    request_body = PatchApiKey,
    responses(
        (status = 200, description = "The key has been updated.", body = KeyView, content_type = "application/json", example = json!(
            {
                "uid": "01b4bc42-eb33-4041-b481-254d00cce834",
                "key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
                "name": "An API Key",
                "description": null,
                "actions": [
                    "documents.add"
                ],
                "indexes": [
                    "movies"
                ],
                "expiresAt": "2022-11-12T10:00:00Z",
                "createdAt": "2021-11-12T10:00:00Z",
                "updatedAt": "2021-11-12T10:00:00Z"
            }
        )),
        (status = 401, description = "The route has been hit on an unprotected instance.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
                "code": "missing_master_key",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_master_key"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
        (status = 404, description = "API key not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The API key was not found.",
                "code": "api_key_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#api_key_not_found"
            }
        )),
    )
)]
pub async fn patch_api_key(
    auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>,
    body: AwebJson<PatchApiKey, DeserrJsonError>,
    path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
    let key = path.into_inner().key;
    let patch_api_key = body.into_inner();
    let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
        let uid =
            Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
        let key = auth_controller.update_key(uid, patch_api_key)?;

        Ok(KeyView::from_key(key, &auth_controller))
    })
    .await
    .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;

    Ok(HttpResponse::Ok().json(res))
}

/// Delete API key
///
/// Permanently delete the specified API key. The key will no longer be valid for authentication.
#[routes::path(
    security(("Bearer" = ["keys.delete", "keys.*", "*"])),
    params(("key" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key.", nullable = false)),
    responses(
        (status = NO_CONTENT, description = "The key has been removed."),
        (status = 404, description = "API key not found.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The API key was not found.",
                "code": "api_key_not_found",
                "type": "invalid_request",
                "link": "https://docs.meilisearch.com/errors#api_key_not_found"
            }
        )),
        (status = 401, description = "The route has been hit on an unprotected instance.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
                "code": "missing_master_key",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_master_key"
            }
        )),
        (status = 401, description = "The authorization header is missing.", body = ResponseError, content_type = "application/json", example = json!(
            {
                "message": "The Authorization header is missing. It must use the bearer authorization method.",
                "code": "missing_authorization_header",
                "type": "auth",
                "link": "https://docs.meilisearch.com/errors#missing_authorization_header"
            }
        )),
    )
)]
pub async fn delete_api_key(
    auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>,
    path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> {
    let key = path.into_inner().key;
    tokio::task::spawn_blocking(move || {
        let uid =
            Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
        auth_controller.delete_key(uid)
    })
    .await
    .map_err(|e| ResponseError::from_msg(e.to_string(), Code::Internal))??;

    Ok(HttpResponse::NoContent().finish())
}

#[derive(Deserialize)]
pub struct AuthParam {
    key: String,
}

/// Represents an API key used for authenticating requests to Meilisearch.
/// Each key has specific permissions defined by its actions and can be scoped
/// to particular indexes. Keys provide fine-grained access control for your
/// Meilisearch instance.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct KeyView {
    /// A human-readable name for the API key. Use this to identify the purpose
    /// of each key, such as "Frontend Search Key" or "Admin Key for CI/CD".
    /// This is optional and can be `null`.
    name: Option<String>,
    /// A longer description explaining the purpose or usage of this API key.
    /// Useful for documenting why the key was created and how it should be
    /// used. This is optional and can be `null`.
    description: Option<String>,
    /// The actual API key string to use in the `Authorization: Bearer <key>`
    /// header when making requests to Meilisearch. Keep this value secret and
    /// never expose it in client-side code.
    key: String,
    /// The unique identifier (UUID) for this API key. Use this to update or
    /// delete the key. The UID remains constant even if the key's name or
    /// description changes.
    uid: Uuid,
    /// The list of actions (permissions) this key is allowed to perform.
    /// Examples include `documents.add`, `search`, `indexes.create`,
    /// `settings.update`, etc. Use `*` to grant all permissions.
    actions: Vec<Action>,
    /// The list of index UIDs this key can access. Use `*` to grant access to
    /// all indexes, including future ones. Patterns are also supported, e.g.,
    /// `movies_*` matches any index starting with "movies_".
    indexes: Vec<String>,
    /// The expiration date and time of the key in RFC 3339 format. After this
    /// time, the key will no longer be valid for authentication. Set to `null`
    /// for keys that never expire.
    #[serde(serialize_with = "time::serde::rfc3339::option::serialize")]
    expires_at: Option<OffsetDateTime>,
    /// The date and time when this API key was created, formatted as an
    /// RFC 3339 date-time string. This is automatically set by Meilisearch
    /// and cannot be modified.
    #[schema(read_only)]
    #[serde(serialize_with = "time::serde::rfc3339::serialize")]
    created_at: OffsetDateTime,
    /// The date and time when this API key was last modified, formatted as an
    /// RFC 3339 date-time string. This is automatically updated by Meilisearch
    /// when the key's name or description changes.
    #[schema(read_only)]
    #[serde(serialize_with = "time::serde::rfc3339::serialize")]
    updated_at: OffsetDateTime,
}

impl KeyView {
    fn from_key(key: Key, auth: &AuthController) -> Self {
        let generated_key = auth.generate_key(key.uid).unwrap_or_default();

        KeyView {
            name: key.name,
            description: key.description,
            key: generated_key,
            uid: key.uid,
            actions: key.actions,
            indexes: key.indexes.into_iter().map(|x| x.to_string()).collect(),
            expires_at: key.expires_at,
            created_at: key.created_at,
            updated_at: key.updated_at,
        }
    }
}
