use std::fmt;

use actix_web::{HttpMessage, error::ParseError, http::header, post, web};
use ariadne::ids::base62_impl::parse_base62;
use base64::Engine;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::trace;

use crate::database::redis::RedisPool;
use crate::models::ids::PayoutId;
use crate::routes::ApiError;
use crate::util::error::Context;
use crate::util::gotenberg::{
    GeneratedPdfType, MODRINTH_GENERATED_PDF_TYPE, MODRINTH_PAYMENT_ID,
    PAYMENT_STATEMENTS_NAMESPACE,
};
use crate::util::guards::internal_network_guard;

/// Document generated by Gotenberg and returned to us.
#[derive(Debug, Serialize, Deserialize)]
pub struct GotenbergDocument {
    /// Body of the document as a base 64 string.
    pub body: String,
}

pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
    cfg.service(success_callback).service(error_callback);
}

#[post("/gotenberg/success", guard = "internal_network_guard")]
pub async fn success_callback(
    web::Header(header::ContentDisposition {
        disposition,
        parameters: disposition_parameters,
    }): web::Header<header::ContentDisposition>,
    web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
    web::Header(ModrinthGeneratedPdfType(r#type)): web::Header<
        ModrinthGeneratedPdfType,
    >,
    maybe_payment_id: Option<web::Header<ModrinthPaymentId>>,
    body: web::Bytes,
    redis: web::Data<RedisPool>,
) -> Result<(), ApiError> {
    trace!(
        %trace,
        %disposition,
        ?disposition_parameters,
        r#type = r#type.as_str(),
        ?maybe_payment_id,
        body.len = body.len(),
        "Received Gotenberg generated PDF"
    );

    let payout_id = maybe_payment_id
        .wrap_request_err("no payout ID for document")?
        .0
        .0;

    let mut redis = redis
        .connect()
        .await
        .wrap_internal_err("failed to get Redis connection")?;

    let body = base64::engine::general_purpose::STANDARD.encode(&body);

    let redis_msg = serde_json::to_string(&Ok::<
        GotenbergDocument,
        GotenbergError,
    >(GotenbergDocument { body }))
    .wrap_internal_err("failed to serialize document to JSON")?;

    redis
        .lpush(
            PAYMENT_STATEMENTS_NAMESPACE,
            &payout_id.to_string(),
            &redis_msg,
        )
        .await
        .wrap_internal_err("failed to send document over Redis")?;

    Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize, Error)]
pub struct GotenbergError {
    pub status: Option<String>,
    pub message: Option<String>,
}

impl fmt::Display for GotenbergError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match (&self.status, &self.message) {
            (None, None) => write!(f, "(unknown error)"),
            (Some(status), None) => write!(f, "status {status}"),
            (None, Some(message)) => write!(f, "(no status) {message}"),
            (Some(status), Some(message)) => {
                write!(f, "status {status}: {message}")
            }
        }
    }
}

#[post("/gotenberg/error", guard = "internal_network_guard")]
pub async fn error_callback(
    web::Header(GotenbergTrace(trace)): web::Header<GotenbergTrace>,
    web::Header(ModrinthGeneratedPdfType(r#type)): web::Header<
        ModrinthGeneratedPdfType,
    >,
    maybe_payment_id: Option<web::Header<ModrinthPaymentId>>,
    web::Json(error_body): web::Json<GotenbergError>,
    redis: web::Data<RedisPool>,
) -> Result<(), ApiError> {
    trace!(
        %trace,
        r#type = r#type.as_str(),
        ?maybe_payment_id,
        ?error_body,
        "Received Gotenberg error webhook"
    );

    let payout_id = maybe_payment_id
        .wrap_request_err("no payout ID for document")?
        .0
        .0;
    let mut redis = redis
        .connect()
        .await
        .wrap_internal_err("failed to get Redis connection")?;

    let redis_msg = serde_json::to_string(&Err::<
        GotenbergDocument,
        GotenbergError,
    >(error_body))
    .wrap_internal_err("failed to serialize error to JSON")?;

    redis
        .lpush(
            PAYMENT_STATEMENTS_NAMESPACE,
            &payout_id.to_string(),
            &redis_msg,
        )
        .await
        .wrap_internal_err("failed to send error over Redis")?;

    Ok(())
}

#[derive(Debug)]
struct GotenbergTrace(String);

impl header::TryIntoHeaderValue for GotenbergTrace {
    type Error = header::InvalidHeaderValue;

    fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
        header::HeaderValue::from_str(&self.0)
    }
}

impl header::Header for GotenbergTrace {
    fn name() -> header::HeaderName {
        header::HeaderName::from_static("gotenberg-trace")
    }

    fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
        m.headers()
            .get(Self::name())
            .ok_or(ParseError::Header)?
            .to_str()
            .map_err(|_| ParseError::Header)
            .map(ToOwned::to_owned)
            .map(GotenbergTrace)
    }
}

#[derive(Debug)]
struct ModrinthGeneratedPdfType(GeneratedPdfType);

impl header::TryIntoHeaderValue for ModrinthGeneratedPdfType {
    type Error = header::InvalidHeaderValue;

    fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
        header::HeaderValue::from_str(self.0.as_str())
    }
}

impl header::Header for ModrinthGeneratedPdfType {
    fn name() -> header::HeaderName {
        MODRINTH_GENERATED_PDF_TYPE
    }

    fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
        m.headers()
            .get(Self::name())
            .ok_or(ParseError::Header)?
            .to_str()
            .map_err(|_| ParseError::Header)?
            .parse()
            .map_err(|_| ParseError::Header)
            .map(ModrinthGeneratedPdfType)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ModrinthPaymentId(pub PayoutId);

impl header::TryIntoHeaderValue for ModrinthPaymentId {
    type Error = header::InvalidHeaderValue;

    fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
        header::HeaderValue::from_str(&self.0.to_string())
    }
}

impl header::Header for ModrinthPaymentId {
    fn name() -> header::HeaderName {
        MODRINTH_PAYMENT_ID
    }

    fn parse<M: HttpMessage>(m: &M) -> Result<Self, ParseError> {
        m.headers()
            .get(Self::name())
            .ok_or(ParseError::Header)?
            .to_str()
            .map_err(|_| ParseError::Header)
            .and_then(|s| parse_base62(s).map_err(|_| ParseError::Header))
            .map(|id| Self(PayoutId(id)))
    }
}
