/*
 * Author: Ruben Fiszel
 * Copyright: Windmill Labs, Inc 2022
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the included NOTICE for copyright information and
 * LICENSE-AGPL for a copy of the license.
 */

use std::panic::Location;

use axum::body::Body;
use axum::response::Response;
use axum::{response::IntoResponse, response::Json};

use hyper::StatusCode;
use sqlx::migrate::MigrateError;
use thiserror::Error;
use tokio::io;

pub type Result<T> = std::result::Result<T, Error>;
pub type JsonResult<T> = std::result::Result<Json<T>, Error>;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Bad gateway: {0}")]
    BadGateway(String),
    #[error("Bad config: {0}")]
    BadConfig(String),
    #[error("Connecting to database: {0}")]
    ConnectingToDatabase(String),
    #[error("Not found: {0}")]
    NotFound(String),
    #[error("Not authorized: {0}")]
    NotAuthorized(String),
    #[error("Metric not found: {0}")]
    MetricNotFound(String),
    #[error("Permission denied: {0}")]
    PermissionDenied(String),
    #[error("Require Admin privileges for {0}")]
    RequireAdmin(String),
    #[error("{0}")]
    ExecutionErr(String),
    #[error("{0}")]
    ResultTooLarge(String),
    #[error("IoErr: {error:#} @{location:#}")]
    IoErr { error: io::Error, location: String },
    #[error("Utf8Err: {error:#} @{location:#}")]
    Utf8Err { error: std::string::FromUtf8Error, location: String },
    #[error("UuidErr: {error:#} @{location:#}")]
    UuidErr { error: uuid::Error, location: String },
    #[error("SqlErr: {error:#} @{location:#}")]
    SqlErr { error: sqlx::Error, location: String },
    #[error("SerdeJson: {error:#} @{location:#}")]
    SerdeJson { error: serde_json::Error, location: String },
    #[error("Bad request: {0}")]
    BadRequest(String),
    #[error("Quota exceeded: {0}")]
    QuotaExceeded(String),
    #[error("Internal: {0}")]
    InternalErr(String),
    #[error("Internal: {message} @{location}")]
    InternalErrLoc { message: String, location: String },
    #[error("Internal: {0}: {1}")]
    InternalErrAt(&'static Location<'static>, String),
    #[error("HexErr: {error:#} @{location:#}")]
    HexErr { error: hex::FromHexError, location: String },
    #[error("Migrating database: {0}")]
    DatabaseMigration(#[from] MigrateError),
    #[error("Non-zero exit status for {0}: {1}")]
    ExitStatus(String, i32),
    #[error("ExecutionRawError: {0}")]
    ExecutionRawError(Box<serde_json::value::RawValue>),
    #[error("Error: {error:#} @{location:#}")]
    Anyhow { error: anyhow::Error, location: String },
    #[error("{}", format_json_err_message(.0))]
    JsonErr(serde_json::Value),
    #[error("{0}")]
    AIError(String),
    #[error("{0}")]
    AlreadyCompleted(String),
    #[error("WAC job suspended: {0}")]
    WacSuspended(String),
    #[error("Find python error: {0}")]
    FindPythonError(String),
    #[error("Problem with arguments: {0}")]
    ArgumentErr(String),
    #[error("{1}")]
    Generic(StatusCode, String),
    #[error("{feature} is unavailable due to some workers being behind. Do not use the feature or make sure all workers run at least {min_version}")]
    WorkersAreBehind { feature: String, min_version: String },
    #[error(
        "Breaking change was introduced in v{version} ({feature}). Follow this migration guide: {guide_url}"
    )]
    MigrationNeeded { version: String, feature: String, guide_url: url::Url },
    #[error("{0} is unavailable. It is possible for this worker to be behind.")]
    FeatureUnavailable(String),
}

impl Error {
    pub fn name(&self) -> &str {
        match self {
            Self::ExecutionErr(_) => "ExecutionErr",
            Self::ResultTooLarge(_) => "ResultTooLarge",
            Self::BadRequest(_) => "BadRequest",
            Self::QuotaExceeded(_) => "QuotaExceeded",
            Self::InternalErr(_) => "InternalErr",
            Self::InternalErrLoc { .. } => "InternalErr",
            Self::InternalErrAt(_, _) => "InternalErr",
            Self::Anyhow { .. } => "Anyhow",
            Self::JsonErr(_) => "JsonErr",
            Self::AIError(_) => "AIError",
            Self::AlreadyCompleted(_) => "AlreadyCompleted",
            Self::WacSuspended(_) => "WacSuspended",
            Self::FindPythonError(_) => "FindPythonError",
            Self::ArgumentErr(_) => "ArgumentErr",
            Self::Generic(_, _) => "Generic",
            Self::IoErr { .. } => "IoErr",
            Self::Utf8Err { .. } => "Utf8Err",
            Self::UuidErr { .. } => "UuidErr",
            Self::SqlErr { .. } => "SqlErr",
            Self::SerdeJson { .. } => "SerdeJson",
            Self::HexErr { .. } => "HexErr",
            Self::DatabaseMigration(_) => "DatabaseMigration",
            Self::ExitStatus(_, _) => "ExitStatus",
            Self::ExecutionRawError(_) => "ExecutionRawError",
            Self::BadGateway(_) => "BadGateway",
            Self::BadConfig(_) => "BadConfig",
            Self::ConnectingToDatabase(_) => "ConnectingToDatabase",
            Self::NotFound(_) => "NotFound",
            Self::NotAuthorized(_) => "NotAuthorized",
            Self::MetricNotFound(_) => "MetricNotFound",
            Self::PermissionDenied(_) => "PermissionDenied",
            _ => "InternalErr",
        }
    }
}

fn prettify_location(location: &'static Location<'static>) -> String {
    location
        .to_string()
        .split("/")
        .last()
        .unwrap_or("unknown")
        .to_string()
}
impl From<anyhow::Error> for Error {
    #[track_caller]
    fn from(e: anyhow::Error) -> Self {
        Self::Anyhow { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<sqlx::Error> for Error {
    #[track_caller]
    fn from(e: sqlx::Error) -> Self {
        Self::SqlErr { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<uuid::Error> for Error {
    #[track_caller]
    fn from(e: uuid::Error) -> Self {
        Self::UuidErr { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<std::string::FromUtf8Error> for Error {
    #[track_caller]
    fn from(e: std::string::FromUtf8Error) -> Self {
        Self::Utf8Err { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<io::Error> for Error {
    #[track_caller]
    fn from(e: io::Error) -> Self {
        Self::IoErr { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<hex::FromHexError> for Error {
    #[track_caller]
    fn from(e: hex::FromHexError) -> Self {
        Self::HexErr { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<serde_json::Error> for Error {
    #[track_caller]
    fn from(e: serde_json::Error) -> Self {
        Self::SerdeJson { error: e, location: prettify_location(std::panic::Location::caller()) }
    }
}

impl From<url::ParseError> for Error {
    #[track_caller]
    fn from(e: url::ParseError) -> Self {
        Self::ArgumentErr(format!("Cannot parse provided url. \ne: {e}"))
    }
}

impl From<tokio::time::error::Elapsed> for Error {
    fn from(value: tokio::time::error::Elapsed) -> Self {
        Self::InternalErr(value.to_string())
    }
}

impl From<semver::Error> for Error {
    fn from(e: semver::Error) -> Self {
        Self::ArgumentErr(format!("Cannot parse provided semver: {e}"))
    }
}

impl Error {
    /// https://docs.rs/anyhow/1/anyhow/struct.Error.html#display-representations
    pub fn alt(&self) -> String {
        format!("{:#}", self)
    }

    pub fn dbg(&self) -> String {
        format!("{:?}", self)
    }

    pub fn relocate_internal(self, loc: &'static Location<'static>) -> Self {
        match self {
            Self::InternalErrLoc { message, .. }
            | Self::InternalErrAt(_, message)
            | Self::InternalErr(message) => Self::InternalErrAt(loc, message),
            _ => self,
        }
    }

    #[track_caller]
    pub fn internal_err<T: AsRef<str>>(msg: T) -> Self {
        Self::InternalErrLoc {
            message: msg.as_ref().to_string(),
            location: prettify_location(std::panic::Location::caller()),
        }
    }
}

pub fn relocate_internal(loc: &'static Location<'static>) -> impl FnOnce(Error) -> Error {
    move |e| e.relocate_internal(loc)
}

pub fn to_anyhow<T: 'static + std::error::Error + Send + Sync>(e: T) -> anyhow::Error {
    From::from(e)
}

impl IntoResponse for Error {
    fn into_response(self) -> axum::response::Response {
        let status = match self {
            Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
            Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
            Self::RequireAdmin(_) | Self::PermissionDenied(_) => axum::http::StatusCode::FORBIDDEN,
            Self::SqlErr { .. }
            | Self::BadRequest(_)
            | Self::AIError(_)
            | Self::JsonErr(_)
            | Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
            Self::BadGateway(_) => axum::http::StatusCode::BAD_GATEWAY,
            Self::Generic(status_code, _) => status_code,
            _ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        };

        let e = &self;

        if matches!(status, axum::http::StatusCode::NOT_FOUND) {
            tracing::warn!(message = e.to_string());
        } else {
            tracing::error!(message = e.to_string(), error = ?e);
        };

        let body = Body::from(e.to_string());

        axum::response::Response::builder()
            .header("Content-Type", "text/plain")
            .status(status)
            .body(body)
            .unwrap()
    }
}

/// Render a `JsonErr` payload as a readable message suitable for direct
/// display in a toast: surface the `error` field as the headline, append a
/// short summary of `details` (e.g. duplicate paths) when present, and fall
/// back to pretty JSON for unknown shapes. Avoids the Rust `Debug` output
/// (`Object { "error": String("..."), ... }`) that previously leaked to users.
fn format_json_err_message(v: &serde_json::Value) -> String {
    if let Some(obj) = v.as_object() {
        let headline = obj
            .get("error")
            .and_then(|e| e.as_str())
            .map(|s| s.to_string());
        let details_summary = obj.get("details").and_then(|d| {
            let arr = d.as_array()?;
            if arr.is_empty() {
                return None;
            }
            let preview = arr
                .iter()
                .take(5)
                .map(|item| match item {
                    serde_json::Value::Object(o) => {
                        let parts: Vec<String> = o
                            .iter()
                            .map(|(k, val)| match val {
                                serde_json::Value::String(s) => format!("{k}={s}"),
                                _ => format!("{k}={val}"),
                            })
                            .collect();
                        format!("- {}", parts.join(", "))
                    }
                    serde_json::Value::String(s) => format!("- {s}"),
                    other => format!("- {other}"),
                })
                .collect::<Vec<_>>()
                .join("\n");
            let suffix = if arr.len() > 5 {
                format!("\n... ({} more)", arr.len() - 5)
            } else {
                String::new()
            };
            Some(format!("{preview}{suffix}"))
        });

        match (headline, details_summary) {
            (Some(h), Some(d)) => return format!("{h}\n{d}"),
            (Some(h), None) => return h,
            (None, Some(d)) => return d,
            (None, None) => {}
        }
    }
    serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string())
}

pub trait OrElseNotFound<T> {
    fn or_else_not_found(self, s: impl ToString) -> Result<T>;
}

impl<T> OrElseNotFound<T> for Option<T> {
    fn or_else_not_found(self, s: impl ToString) -> Result<T> {
        self.ok_or_else(|| Error::NotFound(s.to_string()))
    }
}

// Make our own error that wraps `anyhow::Error`.
pub struct AppError(anyhow::Error);

// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let body = Body::from(self.0.to_string());
        tracing::error!(error = self.0.to_string());
        axum::response::Response::builder()
            .header("Content-Type", "text/plain")
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .body(body)
            .unwrap()
    }
}

// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually
impl<E> From<E> for AppError
where
    E: Into<anyhow::Error>,
{
    fn from(err: E) -> Self {
        Self(err.into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn json_err_message_error_and_details() {
        let v = json!({
            "error": "Duplicate HTTP route paths detected",
            "details": [
                { "route_path": "a", "workspace_id": "admins", "http_method": "post" },
                { "route_path": "a", "workspace_id": "starter", "http_method": "post" },
            ],
        });
        let rendered = Error::JsonErr(v).to_string();
        assert_eq!(
            rendered,
            "Duplicate HTTP route paths detected\n\
             - route_path=a, workspace_id=admins, http_method=post\n\
             - route_path=a, workspace_id=starter, http_method=post"
        );
    }

    #[test]
    fn json_err_message_error_only() {
        let v = json!({ "error": "Something went wrong" });
        assert_eq!(Error::JsonErr(v).to_string(), "Something went wrong");
    }

    #[test]
    fn json_err_message_truncates_long_details() {
        let details: Vec<_> = (0..8).map(|i| json!({ "k": i })).collect();
        let v = json!({ "error": "boom", "details": details });
        let rendered = Error::JsonErr(v).to_string();
        assert!(rendered.starts_with("boom\n- k=0\n- k=1\n- k=2\n- k=3\n- k=4"));
        assert!(rendered.ends_with("... (3 more)"));
        // Items beyond the cap aren't enumerated.
        assert!(!rendered.contains("- k=5"));
    }

    #[test]
    fn json_err_message_fallback_to_pretty_json() {
        let v = json!([1, 2, 3]);
        // Non-object payload falls back to pretty JSON instead of leaking
        // Rust `Debug` syntax.
        let rendered = Error::JsonErr(v).to_string();
        assert_eq!(rendered, "[\n  1,\n  2,\n  3\n]");
    }
}
