use actix_web as aweb;
use aweb::error::{JsonPayloadError, QueryPayloadError};
use byte_unit::{Byte, UnitType};
use meilisearch_types::document_formats::{DocumentFormatError, PayloadType};
use meilisearch_types::error::{Code, ErrorCode, ResponseError};
use meilisearch_types::index_uid::{IndexUid, IndexUidFormatError};
use meilisearch_types::milli;
use meilisearch_types::milli::OrderBy;
use meilisearch_types::tasks::network::headers::{
    PROXY_IMPORT_DOCS_HEADER, PROXY_IMPORT_INDEX_COUNT_HEADER, PROXY_IMPORT_INDEX_HEADER,
    PROXY_IMPORT_REMOTE_HEADER, PROXY_IMPORT_TASK_KEY_HEADER, PROXY_IMPORT_TOTAL_INDEX_DOCS_HEADER,
    PROXY_ORIGIN_REMOTE_HEADER, PROXY_ORIGIN_TASK_UID_HEADER,
};
use serde_json::Value;
use tokio::task::JoinError;
use uuid::Uuid;

#[derive(Debug, thiserror::Error)]
#[allow(clippy::large_enum_variant)]
pub enum MeilisearchHttpError {
    #[error("A Content-Type header is missing. Accepted values for the Content-Type header are: {}",
            .0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", "))]
    MissingContentType(Vec<String>),
    #[error("The `/logs/stream` route is currently in use by someone else.")]
    AlreadyUsedLogRoute,
    #[error("The Content-Type `{0}` does not support the use of a csv delimiter. The csv delimiter can only be used with the Content-Type `text/csv`.")]
    CsvDelimiterWithWrongContentType(String),
    #[error(
        "The Content-Type `{}` is invalid. Accepted values for the Content-Type header are: {}",
        .0, .1.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
    )]
    InvalidContentType(String, Vec<String>),
    #[error("Document `{0}` not found.")]
    DocumentNotFound(String),
    #[error("Sending an empty filter is forbidden.")]
    EmptyFilter,
    #[error("Invalid syntax for the filter parameter: `expected {}, found: {}`.", .0.join(", "), .1)]
    InvalidExpression(&'static [&'static str], Value),
    #[error("Using `federationOptions` is not allowed in a non-federated search.\n - Hint: remove `federationOptions` from query #{0} or add `federation` to the request.")]
    FederationOptionsInNonFederatedRequest(usize),
    #[error("Inside `.queries[{0}]`: Using pagination options is not allowed in federated queries.\n - Hint: remove `{1}` from query #{0} or remove `federation` from the request\n - Hint: pass `federation.limit` and `federation.offset` for pagination in federated search")]
    PaginationInFederatedQuery(usize, &'static str),
    #[error("Inside `.queries[{0}]`: Using facet options is not allowed in federated queries.\n - Hint: remove `facets` from query #{0} or remove `federation` from the request\n - Hint: pass `federation.facetsByIndex.{1}: {2:?}` for facets in federated search")]
    FacetsInFederatedQuery(usize, String, Vec<String>),
    #[error("Inside `.queries[{0}]`: Using `.personalize` is not allowed in federated queries.\n - Hint: remove `personalize` from query #{0} or remove `federation` from the request")]
    PersonalizationInFederatedQuery(usize),
    #[error("Inside `.queries[{0}]`: Using `.showPerformanceDetails` is not allowed in federated queries.\n - Hint: remove `showPerformanceDetails` from query #{0} or remove `federation` from the request")]
    ShowPerformanceDetailsInFederatedQuery(usize),
    #[error("Inside `.queries[{0}]`: Using `.useNetwork` is not allowed as the same time as `.federationOptions.remote`.\n  - Hint: to perform an explicit query against a remote, remove `.useNetwork`.\n  - Hint: to automatically perform queries against the entire network, remove `.federationOptions.remote`.")]
    RemoteAndUseNetwork(usize),
    #[error("Inconsistent order for values in facet `{facet}`: index `{previous_uid}` orders {previous_facet_order}, but index `{current_uid}` orders {index_facet_order}.\n - Hint: Remove `federation.mergeFacets` or change `faceting.sortFacetValuesBy` to be consistent in settings.")]
    InconsistentFacetOrder {
        facet: String,
        previous_facet_order: OrderBy,
        previous_uid: String,
        index_facet_order: OrderBy,
        current_uid: String,
    },
    #[error("A {0} payload is missing.")]
    MissingPayload(PayloadType),
    #[error("Too many search requests running at the same time: {0}. Retry after 10s.")]
    TooManySearchRequests(usize),
    #[error("Internal error: Search limiter is down.")]
    SearchLimiterIsDown,
    #[error("The provided payload reached the size limit. The maximum accepted payload size is {}.", Byte::from_u64(*.0 as u64).get_appropriate_unit(if *.0 % 1024 == 0 { UnitType::Binary } else { UnitType::Decimal }))]
    PayloadTooLarge(usize),
    #[error("Two indexes must be given for each swap. The list `[{}]` contains {} indexes.",
        .0.iter().map(|uid| format!("\"{uid}\"")).collect::<Vec<_>>().join(", "), .0.len()
    )]
    SwapIndexPayloadWrongLength(Vec<IndexUid>),
    #[error(transparent)]
    IndexUid(#[from] IndexUidFormatError),
    #[error(transparent)]
    SerdeJson(#[from] serde_json::Error),
    #[error(transparent)]
    HeedError(#[from] meilisearch_types::heed::Error),
    #[error(transparent)]
    IndexScheduler(#[from] index_scheduler::Error),
    #[error("{}", match .index_name {
        Some(name) if !name.is_empty() => format!("Index `{}`: {error}", name),
        _ => format!("{error}")
    })]
    Milli { error: milli::Error, index_name: Option<String> },
    #[error(transparent)]
    Payload(#[from] PayloadError),
    #[error(transparent)]
    FileStore(#[from] file_store::Error),
    #[error(transparent)]
    DocumentFormat(#[from] DocumentFormatError),
    #[error(transparent)]
    Join(#[from] JoinError),
    #[error("Invalid request: missing `hybrid` parameter when `vector` or `media` are present.")]
    MissingSearchHybrid,
    #[error("Invalid request: both `media` and `vector` parameters are present.")]
    MediaAndVector,
    #[error("Inconsistent `Origin` headers: {} was provided but {} is missing.\n  - Hint: Either both headers should be provided, or none of them", if *is_remote_missing {
        PROXY_ORIGIN_TASK_UID_HEADER
    } else { PROXY_ORIGIN_REMOTE_HEADER },
    if *is_remote_missing {
        PROXY_ORIGIN_REMOTE_HEADER
    } else { PROXY_ORIGIN_TASK_UID_HEADER }
)]
    InconsistentOriginHeaders { is_remote_missing: bool },
    #[error("Inconsistent `Import` headers: {remote}: {remote_status}, {index}: {index_status}, {docs}: {docs_status}.\n - Hint: either all three headers should be provided, or none of them",
        remote = PROXY_IMPORT_REMOTE_HEADER,
        remote_status = if *is_remote_missing { "missing" } else{ "provided" },
        index = PROXY_IMPORT_INDEX_HEADER,
        index_status = if *is_index_missing { "missing" } else { "provided" },
        docs = PROXY_IMPORT_DOCS_HEADER,
        docs_status = if *is_docs_missing { "missing" } else { "provided" }
    )]
    InconsistentImportHeaders {
        is_remote_missing: bool,
        is_index_missing: bool,
        is_docs_missing: bool,
    },
    #[error("Inconsistent `Import-Metadata` headers: {index_count}: {index_count_status}, {task_key}: {task_key_status}, {total_index_documents}: {total_index_documents_status}.\n - Hint: either all three headers should be provided, or none of them",
        index_count = PROXY_IMPORT_INDEX_COUNT_HEADER,
        index_count_status = if *is_index_count_missing { "missing" } else { "provided"},
        task_key = PROXY_IMPORT_TASK_KEY_HEADER,
        task_key_status = if *is_task_key_missing { "missing" } else { "provided"},
        total_index_documents = PROXY_IMPORT_TOTAL_INDEX_DOCS_HEADER,
        total_index_documents_status = if *is_total_index_documents_missing { "missing" } else { "provided"},
    )]
    InconsistentImportMetadataHeaders {
        is_index_count_missing: bool,
        is_task_key_missing: bool,
        is_total_index_documents_missing: bool,
    },

    #[error(
        "Inconsistent task network headers: origin headers: {origin_status}, import headers: {import_status}, import metadata: {import_metadata_status}",
        origin_status =  if *is_missing_origin { "missing"} else { "present" },
        import_status =  if *is_missing_import { "missing"} else { "present" },
        import_metadata_status =  if *is_missing_import_metadata { "missing"} else { "present" })]
    InconsistentTaskNetworkHeaders {
        is_missing_origin: bool,
        is_missing_import: bool,
        is_missing_import_metadata: bool,
    },
    #[error("Invalid value for header `{header_name}`: {msg}")]
    InvalidHeaderValue { header_name: &'static str, msg: String },
    #[error("This remote is not the leader of the network.\n  - Note: only the leader `{leader}` can receive new tasks.")]
    NotLeader { leader: String },
    #[error("Unexpected `previousRemotes` in network call.\n  - Note: `previousRemote` is reserved for internal use.")]
    UnexpectedNetworkPreviousRemotes,
    #[error("The network version in request is too old.\n  - Received: {received}\n  - Expected at least: {expected_at_least}")]
    NetworkVersionTooOld { received: Uuid, expected_at_least: Uuid },
    #[error("Remote `{remote}` encountered an error: {error}")]
    RemoteIndexScheduler { remote: String, error: index_scheduler::Error },
    #[error("{if_remote}Already has a pending network task with uid {task_uid}.\n  - Note: No network task can be registered while any previous network task is not done processing.\n  - Hint: Wait for task {task_uid} to complete or cancel it.",
if_remote=if let Some(remote) = remote {
    format!("Remote `{remote}` encountered an error: ")
} else {"".into()} )]
    UnprocessedNetworkTask { remote: Option<String>, task_uid: meilisearch_types::tasks::TaskId },
    #[error("Inside `.queries[{0}]`: Using `distinct` options is not allowed in federated queries when it also appears in `.federation.distinct`.\n - Hint: remove `distinct` from query #{0} or remove `federation` from the request\n  - Note: `distinct` at the query level is discouraged in federated search.")]
    DistinctInFederatedQueryAndFederation(usize),
}

impl MeilisearchHttpError {
    pub(crate) fn from_milli(error: milli::Error, index_name: Option<String>) -> Self {
        Self::Milli { error, index_name }
    }
}

impl ErrorCode for MeilisearchHttpError {
    fn error_code(&self) -> Code {
        match self {
            MeilisearchHttpError::MissingContentType(_) => Code::MissingContentType,
            MeilisearchHttpError::AlreadyUsedLogRoute => Code::BadRequest,
            MeilisearchHttpError::CsvDelimiterWithWrongContentType(_) => Code::InvalidContentType,
            MeilisearchHttpError::MissingPayload(_) => Code::MissingPayload,
            MeilisearchHttpError::InvalidContentType(_, _) => Code::InvalidContentType,
            MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound,
            MeilisearchHttpError::EmptyFilter => Code::InvalidDocumentFilter,
            MeilisearchHttpError::InvalidExpression(_, _) => Code::InvalidSearchFilter,
            MeilisearchHttpError::PayloadTooLarge(_) => Code::PayloadTooLarge,
            MeilisearchHttpError::TooManySearchRequests(_) => Code::TooManySearchRequests,
            MeilisearchHttpError::SearchLimiterIsDown => Code::Internal,
            MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::InvalidSwapIndexes,
            MeilisearchHttpError::IndexUid(e) => e.error_code(),
            MeilisearchHttpError::SerdeJson(_) => Code::Internal,
            MeilisearchHttpError::HeedError(_) => Code::Internal,
            MeilisearchHttpError::IndexScheduler(e) => e.error_code(),
            MeilisearchHttpError::RemoteIndexScheduler { error, .. } => error.error_code(),
            MeilisearchHttpError::Milli { error, .. } => error.error_code(),
            MeilisearchHttpError::Payload(e) => e.error_code(),
            MeilisearchHttpError::FileStore(_) => Code::Internal,
            MeilisearchHttpError::DocumentFormat(e) => e.error_code(),
            MeilisearchHttpError::Join(_) => Code::Internal,
            MeilisearchHttpError::MissingSearchHybrid => Code::MissingSearchHybrid,
            MeilisearchHttpError::MediaAndVector => Code::InvalidSearchMediaAndVector,
            MeilisearchHttpError::FederationOptionsInNonFederatedRequest(_)
            | MeilisearchHttpError::RemoteAndUseNetwork(_) => {
                Code::InvalidMultiSearchFederationOptions
            }
            MeilisearchHttpError::PaginationInFederatedQuery(_, _) => {
                Code::InvalidMultiSearchQueryPagination
            }
            MeilisearchHttpError::DistinctInFederatedQueryAndFederation(..) => {
                Code::InvalidMultiSearchDistinct
            }
            MeilisearchHttpError::FacetsInFederatedQuery(..) => Code::InvalidMultiSearchQueryFacets,
            MeilisearchHttpError::InconsistentFacetOrder { .. } => {
                Code::InvalidMultiSearchFacetOrder
            }
            MeilisearchHttpError::PersonalizationInFederatedQuery(_) => {
                Code::InvalidMultiSearchQueryPersonalization
            }
            MeilisearchHttpError::ShowPerformanceDetailsInFederatedQuery(_) => {
                Code::InvalidMultiSearchQueryShowPerformanceDetails
            }
            MeilisearchHttpError::InconsistentOriginHeaders { .. }
            | MeilisearchHttpError::InconsistentImportHeaders { .. }
            | MeilisearchHttpError::InconsistentImportMetadataHeaders { .. }
            | MeilisearchHttpError::InconsistentTaskNetworkHeaders { .. } => {
                Code::InconsistentDocumentChangeHeaders
            }
            MeilisearchHttpError::InvalidHeaderValue { .. } => Code::InvalidHeaderValue,
            MeilisearchHttpError::NotLeader { .. } => Code::NotLeader,
            MeilisearchHttpError::UnexpectedNetworkPreviousRemotes => {
                Code::UnexpectedNetworkPreviousRemotes
            }
            MeilisearchHttpError::NetworkVersionTooOld { .. } => Code::NetworkVersionTooOld,
            MeilisearchHttpError::UnprocessedNetworkTask { .. } => Code::UnprocessedNetworkTask,
        }
    }
}

impl From<MeilisearchHttpError> for aweb::Error {
    fn from(other: MeilisearchHttpError) -> Self {
        aweb::Error::from(ResponseError::from(other))
    }
}

impl From<aweb::error::PayloadError> for MeilisearchHttpError {
    fn from(error: aweb::error::PayloadError) -> Self {
        match error {
            aweb::error::PayloadError::Incomplete(_) => MeilisearchHttpError::Payload(
                PayloadError::Payload(ActixPayloadError::IncompleteError),
            ),
            _ => MeilisearchHttpError::Payload(PayloadError::Payload(
                ActixPayloadError::OtherError(error),
            )),
        }
    }
}

impl<T: meilisearch_types::tasks::network::headers::GetHeader>
    From<meilisearch_types::tasks::network::headers::DecodeError<T>> for MeilisearchHttpError
{
    fn from(value: meilisearch_types::tasks::network::headers::DecodeError<T>) -> Self {
        Self::InvalidHeaderValue { header_name: value.header(), msg: value.to_string() }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ActixPayloadError {
    #[error("The provided payload is incomplete and cannot be parsed")]
    IncompleteError,
    #[error(transparent)]
    OtherError(aweb::error::PayloadError),
}

#[derive(Debug, thiserror::Error)]
pub enum PayloadError {
    #[error(transparent)]
    Payload(ActixPayloadError),
    #[error(transparent)]
    Json(JsonPayloadError),
    #[error(transparent)]
    Query(QueryPayloadError),
    #[error("The json payload provided is malformed. `{0}`.")]
    MalformedPayload(serde_json::error::Error),
    #[error("A json payload is missing.")]
    MissingPayload,
    #[error("Error while receiving the playload. `{0}`.")]
    ReceivePayload(Box<dyn std::error::Error + Send + Sync + 'static>),
}

impl ErrorCode for PayloadError {
    fn error_code(&self) -> Code {
        match self {
            PayloadError::Payload(e) => match e {
                ActixPayloadError::IncompleteError => Code::BadRequest,
                ActixPayloadError::OtherError(error) => match error {
                    aweb::error::PayloadError::EncodingCorrupted => Code::BadRequest,
                    aweb::error::PayloadError::Overflow => Code::PayloadTooLarge,
                    aweb::error::PayloadError::UnknownLength => Code::BadRequest,
                    aweb::error::PayloadError::Http2Payload(_) => Code::BadRequest,
                    aweb::error::PayloadError::Io(_) => Code::Internal,
                    aweb::error::PayloadError::Incomplete(_) => Code::BadRequest,
                    _ => Code::Internal,
                },
            },
            PayloadError::Json(err) => match err {
                JsonPayloadError::Overflow { .. } => Code::PayloadTooLarge,
                JsonPayloadError::ContentType => Code::UnsupportedMediaType,
                JsonPayloadError::Payload(aweb::error::PayloadError::Overflow) => {
                    Code::PayloadTooLarge
                }
                JsonPayloadError::Payload(_) => Code::BadRequest,
                JsonPayloadError::Deserialize(_) => Code::BadRequest,
                JsonPayloadError::Serialize(_) => Code::Internal,
                _ => Code::Internal,
            },
            PayloadError::Query(err) => match err {
                QueryPayloadError::Deserialize(_) => Code::BadRequest,
                _ => Code::Internal,
            },
            PayloadError::MissingPayload => Code::MissingPayload,
            PayloadError::MalformedPayload(_) => Code::MalformedPayload,
            PayloadError::ReceivePayload(_) => Code::Internal,
        }
    }
}

impl From<JsonPayloadError> for PayloadError {
    fn from(other: JsonPayloadError) -> Self {
        match other {
            JsonPayloadError::Deserialize(e)
                if e.classify() == serde_json::error::Category::Eof
                    && e.line() == 1
                    && e.column() == 0 =>
            {
                Self::MissingPayload
            }
            JsonPayloadError::Deserialize(e)
                if e.classify() != serde_json::error::Category::Data =>
            {
                Self::MalformedPayload(e)
            }
            _ => Self::Json(other),
        }
    }
}

impl From<QueryPayloadError> for PayloadError {
    fn from(other: QueryPayloadError) -> Self {
        Self::Query(other)
    }
}

impl From<PayloadError> for aweb::Error {
    fn from(other: PayloadError) -> Self {
        aweb::Error::from(ResponseError::from(other))
    }
}
