use std::fmt::Display;

use http_client::reqwest::StatusCode;
use meilisearch_types::batches::BatchId;
use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::milli::index::RollbackOutcome;
use meilisearch_types::milli::DocumentId;
use meilisearch_types::tasks::network::{ReceiveImportFinishedError, ReceiveTaskError};
use meilisearch_types::tasks::{Kind, Status};
use meilisearch_types::{heed, milli};
use thiserror::Error;
use uuid::Uuid;

use crate::TaskId;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DateField {
    BeforeEnqueuedAt,
    AfterEnqueuedAt,
    BeforeStartedAt,
    AfterStartedAt,
    BeforeFinishedAt,
    AfterFinishedAt,
}

impl Display for DateField {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DateField::BeforeEnqueuedAt => write!(f, "beforeEnqueuedAt"),
            DateField::AfterEnqueuedAt => write!(f, "afterEnqueuedAt"),
            DateField::BeforeStartedAt => write!(f, "beforeStartedAt"),
            DateField::AfterStartedAt => write!(f, "afterStartedAt"),
            DateField::BeforeFinishedAt => write!(f, "beforeFinishedAt"),
            DateField::AfterFinishedAt => write!(f, "afterFinishedAt"),
        }
    }
}

impl From<DateField> for Code {
    fn from(date: DateField) -> Self {
        match date {
            DateField::BeforeEnqueuedAt => Code::InvalidTaskBeforeEnqueuedAt,
            DateField::AfterEnqueuedAt => Code::InvalidTaskAfterEnqueuedAt,
            DateField::BeforeStartedAt => Code::InvalidTaskBeforeStartedAt,
            DateField::AfterStartedAt => Code::InvalidTaskAfterStartedAt,
            DateField::BeforeFinishedAt => Code::InvalidTaskBeforeFinishedAt,
            DateField::AfterFinishedAt => Code::InvalidTaskAfterFinishedAt,
        }
    }
}

#[allow(clippy::large_enum_variant)]
#[derive(Error, Debug)]
pub enum Error {
    #[error("{1}")]
    WithCustomErrorCode(Code, Box<Self>),
    #[error("Received bad task id: {received} should be >= to {expected}.")]
    BadTaskId { received: TaskId, expected: TaskId },
    #[error("Index `{0}` not found.")]
    IndexNotFound(String),
    #[error("Index `{0}` already exists.")]
    IndexAlreadyExists(String),
    #[error(
        "Indexes must be declared only once during a swap. `{0}` was specified several times."
    )]
    SwapDuplicateIndexFound(String),
    #[error(
        "Indexes must be declared only once during a swap. {} were specified several times.",
        .0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
    )]
    SwapDuplicateIndexesFound(Vec<String>),
    #[error("Index `{0}` not found.")]
    SwapIndexNotFound(String),
    #[error("Cannot rename `{0}` to `{1}` as the index already exists. Hint: You can remove `{1}` first and then do your remove.")]
    SwapIndexFoundDuringRename(String, String),
    #[error("Meilisearch cannot receive write operations because the limit of the task database has been reached. Please delete tasks to continue performing write operations.")]
    NoSpaceLeftInTaskQueue,
    #[error(
        "Indexes {} not found.",
        .0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
    )]
    SwapIndexesNotFound(Vec<String>),
    #[error("The following indexes are being renamed but cannot because their new name conflicts with an already existing index: {}. Renaming doesn't overwrite the other index name.",
        .0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
    )]
    SwapIndexesFoundDuringRename(Vec<String>),
    #[error("Corrupted dump.")]
    CorruptedDump,
    #[error(
        "Task `{field}` `{date}` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."
    )]
    InvalidTaskDate { field: DateField, date: String },
    #[error("Task uid `{task_uid}` is invalid. It should only contain numeric characters.")]
    InvalidTaskUid { task_uid: String },
    #[error("Batch uid `{batch_uid}` is invalid. It should only contain numeric characters.")]
    InvalidBatchUid { batch_uid: String },
    #[error(
        "Task status `{status}` is invalid. Available task statuses are {}.",
            enum_iterator::all::<Status>()
                .map(|s| format!("`{s}`"))
                .collect::<Vec<String>>()
                .join(", ")
    )]
    InvalidTaskStatuses { status: String },
    #[error(
        "Task type `{type_}` is invalid. Available task types are {}",
            enum_iterator::all::<Kind>()
                .map(|s| format!("`{s}`"))
                .collect::<Vec<String>>()
                .join(", ")
    )]
    InvalidTaskTypes { type_: String },
    #[error(
        "Task canceledBy `{canceled_by}` is invalid. It should only contains numeric characters separated by `,` character."
    )]
    InvalidTaskCanceledBy { canceled_by: String },
    #[error(
        "{index_uid} is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_), and can not be more than 400 bytes."
    )]
    InvalidIndexUid { index_uid: String },
    #[error("Task `{0}` not found.")]
    TaskNotFound(TaskId),
    #[error("Task `{0}` does not contain any documents. Only `documentAdditionOrUpdate` tasks with the statuses `enqueued` or `processing` contain documents")]
    TaskFileNotFound(TaskId),
    #[error("Batch `{0}` not found.")]
    BatchNotFound(BatchId),
    #[error("Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")]
    TaskDeletionWithEmptyQuery,
    #[error("Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.")]
    TaskCancelationWithEmptyQuery,
    #[error("Aborted task")]
    AbortedTask,

    #[error("S3 error: status: {status}, body: {body}")]
    S3Error { status: StatusCode, body: String },
    #[error("S3 HTTP error: {0}")]
    S3HttpError(http_client::reqwest::Error),
    #[error("S3 XML error: {0}")]
    S3XmlError(Box<dyn std::error::Error + Send + Sync>),
    #[error("S3 bucket error: {0}")]
    S3BucketError(rusty_s3::BucketError),
    #[error(transparent)]
    Dump(#[from] dump::Error),
    #[error(transparent)]
    Heed(#[from] heed::Error),
    #[error("{}", match .index_uid {
        Some(uid) if !uid.is_empty() => format!("Index `{}`: {error}", uid),
        _ => format!("{error}")
    })]
    Milli { error: milli::Error, index_uid: Option<String> },
    #[error("An unexpected crash occurred when processing the task: {0}")]
    ProcessBatchPanicked(String),
    #[error(transparent)]
    FileStore(#[from] file_store::Error),
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error(transparent)]
    Persist(#[from] tempfile::PersistError),
    #[error(transparent)]
    FeatureNotEnabled(#[from] FeatureNotEnabledError),

    #[error(transparent)]
    Anyhow(#[from] anyhow::Error),

    // Irrecoverable errors:
    #[error(transparent)]
    CreateBatch(Box<Self>),
    #[error("Corrupted task queue.")]
    CorruptedTaskQueue,
    #[error(transparent)]
    DatabaseUpgrade(Box<Self>),
    #[error(transparent)]
    Export(Box<Self>),
    #[error("Failed to export documents to remote server {code} ({type}): {message} <{link}>")]
    FromRemoteWhenExporting { message: String, code: String, r#type: String, link: String },
    #[error("Failed to rollback for index `{index}`: {rollback_outcome} ")]
    RollbackFailed { index: String, rollback_outcome: RollbackOutcome },
    #[error(transparent)]
    UnrecoverableError(Box<Self>),
    #[error("The index scheduler is in version v{}.{}.{}, but Meilisearch is in version v{}.{}.{}.\n  - hint: start the correct version of Meilisearch, or consider updating your database. See also <https://www.meilisearch.com/docs/learn/update_and_migration/updating>",
    index_scheduler_version.0, index_scheduler_version.1, index_scheduler_version.2,
    package_version.0, package_version.1, package_version.2)]
    IndexSchedulerVersionMismatch {
        index_scheduler_version: (u32, u32, u32),
        package_version: (u32, u32, u32),
    },
    #[error("Index `{index}` is in version v{}.{}.{}, but Meilisearch is in version v{}.{}.{}.\n  - note: this is an internal error, please consider filing a bug report: <https://github.com/meilisearch/meilisearch/issues/new?template=bug_report.md>",
    index_version.0, index_version.1, index_version.2, package_version.0, package_version.1, package_version.2)]
    IndexVersionMismatch {
        index: String,
        index_version: (u32, u32, u32),
        package_version: (u32, u32, u32),
    },
    #[error(transparent)]
    HeedTransaction(heed::Error),

    #[error("No network topology change task is currently enqueued or processing")]
    ImportTaskWithoutNetworkTask,
    #[error("The network task version (`{network_task}`) does not match the import task version (`{import_task}`)")]
    NetworkVersionMismatch { network_task: Uuid, import_task: Uuid },
    #[error("The import task emanates from an unknown remote `{0}`")]
    ImportTaskUnknownRemote(String),
    #[error("The finished import notification emanates from an unknown remote `{0}`")]
    ReceiveImportFinishedUnknownRemote(String),
    #[error("The import task with key `{0}` was already received")]
    ImportTaskAlreadyReceived(DocumentId),
    #[error("Invalid remote url `{url}`: {cause}")]
    InvalidRemoteUrl { url: String, cause: String },
    #[error("{action} requires the Enterprise Edition")]
    RequiresEnterpriseEdition { action: &'static str },

    #[cfg(test)]
    #[error("Planned failure for tests.")]
    PlannedFailure,
}

#[derive(Debug, thiserror::Error)]
#[error(
    "{disabled_action} requires enabling the `{feature}` experimental feature. See {issue_link}"
)]
pub struct FeatureNotEnabledError {
    pub disabled_action: &'static str,
    pub feature: &'static str,
    pub issue_link: &'static str,
}

impl Error {
    pub fn is_recoverable(&self) -> bool {
        match self {
            Error::IndexNotFound(_)
            | Error::WithCustomErrorCode(_, _)
            | Error::BadTaskId { .. }
            | Error::IndexAlreadyExists(_)
            | Error::SwapDuplicateIndexFound(_)
            | Error::SwapDuplicateIndexesFound(_)
            | Error::SwapIndexNotFound(_)
            | Error::NoSpaceLeftInTaskQueue
            | Error::SwapIndexesNotFound(_)
            | Error::SwapIndexFoundDuringRename(_, _)
            | Error::SwapIndexesFoundDuringRename(_)
            | Error::CorruptedDump
            | Error::InvalidTaskDate { .. }
            | Error::InvalidTaskUid { .. }
            | Error::InvalidBatchUid { .. }
            | Error::InvalidTaskStatuses { .. }
            | Error::InvalidTaskTypes { .. }
            | Error::InvalidTaskCanceledBy { .. }
            | Error::InvalidIndexUid { .. }
            | Error::TaskNotFound(_)
            | Error::TaskFileNotFound(_)
            | Error::BatchNotFound(_)
            | Error::TaskDeletionWithEmptyQuery
            | Error::TaskCancelationWithEmptyQuery
            | Error::FromRemoteWhenExporting { .. }
            | Error::AbortedTask
            | Error::S3Error { .. }
            | Error::S3HttpError(_)
            | Error::S3XmlError(_)
            | Error::S3BucketError(_)
            | Error::Dump(_)
            | Error::Heed(_)
            | Error::Milli { .. }
            | Error::ProcessBatchPanicked(_)
            | Error::FileStore(_)
            | Error::IoError(_)
            | Error::Persist(_)
            | Error::FeatureNotEnabled(_)
            | Error::Export(_)
            | Error::ImportTaskWithoutNetworkTask
            | Error::NetworkVersionMismatch { .. }
            | Error::ImportTaskAlreadyReceived(_)
            | Error::ImportTaskUnknownRemote(_)
            | Error::RequiresEnterpriseEdition { .. }
            | Error::Anyhow(_) => true,
            Error::CreateBatch(_)
            | Error::CorruptedTaskQueue
            | Error::DatabaseUpgrade(_)
            | Error::UnrecoverableError(_)
            | Error::IndexSchedulerVersionMismatch { .. }
            | Error::IndexVersionMismatch { .. }
            | Error::RollbackFailed { .. }
            | Error::ReceiveImportFinishedUnknownRemote(_)
            | Error::InvalidRemoteUrl { .. }
            | Error::HeedTransaction(_) => false,
            #[cfg(test)]
            Error::PlannedFailure => false,
        }
    }

    pub fn with_custom_error_code(self, code: Code) -> Self {
        Self::WithCustomErrorCode(code, Box::new(self))
    }

    pub fn from_milli(err: milli::Error, index_uid: Option<String>) -> Self {
        match err {
            milli::Error::UserError(milli::UserError::InvalidFilter(_)) => {
                Self::Milli { error: err, index_uid }
                    .with_custom_error_code(Code::InvalidDocumentFilter)
            }
            milli::Error::UserError(milli::UserError::InvalidFilterExpression { .. }) => {
                Self::Milli { error: err, index_uid }
                    .with_custom_error_code(Code::InvalidDocumentFilter)
            }
            _ => Self::Milli { error: err, index_uid },
        }
    }
}

impl ErrorCode for Error {
    fn error_code(&self) -> Code {
        match self {
            Error::WithCustomErrorCode(code, _) => *code,
            Error::BadTaskId { .. } => Code::BadRequest,
            Error::IndexNotFound(_) => Code::IndexNotFound,
            Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists,
            Error::SwapDuplicateIndexesFound(_) => Code::InvalidSwapDuplicateIndexFound,
            Error::SwapDuplicateIndexFound(_) => Code::InvalidSwapDuplicateIndexFound,
            Error::SwapIndexNotFound(_) => Code::IndexNotFound,
            Error::SwapIndexesNotFound(_) => Code::IndexNotFound,
            Error::SwapIndexFoundDuringRename(_, _) => Code::IndexAlreadyExists,
            Error::SwapIndexesFoundDuringRename(_) => Code::IndexAlreadyExists,
            Error::InvalidTaskDate { field, .. } => (*field).into(),
            Error::InvalidTaskUid { .. } => Code::InvalidTaskUids,
            Error::InvalidBatchUid { .. } => Code::InvalidBatchUids,
            Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatuses,
            Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypes,
            Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledBy,
            Error::InvalidIndexUid { .. } => Code::InvalidIndexUid,
            Error::TaskNotFound(_) => Code::TaskNotFound,
            Error::TaskFileNotFound(_) => Code::TaskFileNotFound,
            Error::BatchNotFound(_) => Code::BatchNotFound,
            Error::TaskDeletionWithEmptyQuery => Code::MissingTaskFilters,
            Error::TaskCancelationWithEmptyQuery => Code::MissingTaskFilters,
            Error::NoSpaceLeftInTaskQueue => Code::NoSpaceLeftOnDevice,
            Error::ImportTaskWithoutNetworkTask => Code::ImportTaskWithoutNetworkTask,
            Error::NetworkVersionMismatch { .. } => Code::NetworkVersionMismatch,
            Error::ImportTaskAlreadyReceived(_) => Code::ImportTaskAlreadyReceived,
            Error::ImportTaskUnknownRemote(_) => Code::ImportTaskUnknownRemote,
            Error::InvalidRemoteUrl { .. } => Code::InvalidNetworkRemotes,
            Error::ReceiveImportFinishedUnknownRemote(_) => {
                Code::ReceiveImportFinishedUnknownRemote
            }
            Error::RequiresEnterpriseEdition { .. } => Code::RequiresEnterpriseEdition,
            Error::S3Error { status, .. } if status.is_client_error() => {
                Code::InvalidS3SnapshotRequest
            }
            Error::S3Error { .. } => Code::S3SnapshotServerError,
            Error::S3HttpError(_) => Code::S3SnapshotServerError,
            Error::S3XmlError(_) => Code::S3SnapshotServerError,
            Error::S3BucketError(_) => Code::InvalidS3SnapshotParameters,
            Error::Dump(e) => e.error_code(),
            Error::Milli { error, .. } => error.error_code(),
            Error::ProcessBatchPanicked(_) => Code::Internal,
            Error::FromRemoteWhenExporting { .. } => Code::Internal,
            Error::Heed(e) => e.error_code(),
            Error::HeedTransaction(e) => e.error_code(),
            Error::FileStore(e) => e.error_code(),
            Error::IoError(e) => e.error_code(),
            Error::Persist(e) => e.error_code(),
            Error::FeatureNotEnabled(_) => Code::FeatureNotEnabled,

            // Irrecoverable errors
            Error::Anyhow(_) => Code::Internal,
            Error::CorruptedTaskQueue => Code::Internal,
            Error::CorruptedDump => Code::Internal,
            Error::DatabaseUpgrade(_) => Code::Internal,
            Error::Export(_) => Code::Internal,
            Error::RollbackFailed { .. } => Code::Internal,
            Error::UnrecoverableError(_) => Code::Internal,
            Error::IndexSchedulerVersionMismatch { .. } => Code::Internal,
            Error::IndexVersionMismatch { .. } => Code::Internal,
            Error::CreateBatch(_) => Code::Internal,

            // This one should never be seen by the end user
            Error::AbortedTask => Code::Internal,

            #[cfg(test)]
            Error::PlannedFailure => Code::Internal,
        }
    }
}

impl From<ReceiveTaskError> for Error {
    fn from(value: ReceiveTaskError) -> Self {
        match value {
            ReceiveTaskError::UnknownRemote(unknown) => Error::ImportTaskUnknownRemote(unknown),
            ReceiveTaskError::DuplicateTask(dup) => Error::ImportTaskAlreadyReceived(dup),
        }
    }
}

impl From<ReceiveImportFinishedError> for Error {
    fn from(value: ReceiveImportFinishedError) -> Self {
        match value {
            ReceiveImportFinishedError::UnknownRemote(unknown) => {
                Error::ReceiveImportFinishedUnknownRemote(unknown)
            }
        }
    }
}
