//! Implementation of utilities for working with responses in `salvo` and `reqwest`.

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::http::HeaderValue;

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::hyper::header::CONTENT_TYPE;

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::oapi::{EndpointOutRegister, ToSchema};

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::{Depot, Request, Response};

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::Writer as ServerResponseWriter;

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
use salvo::fs::NamedFile;

/// Macro to define the function that called the response.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! fn_name {
  () => {{
    fn f() {}
    fn type_name_of<T>(_: T) -> &'static str {
      std::any::type_name::<T>()
    }
    let name = type_name_of(f);

    // For `#[endpoint]` path can be shortened as follows:
    match name[..name.len() - 3].rsplit("::").nth(2) {
      Some(el) => el,
      None => &name[..name.len() - 3],
    }
  }};
}

/// Macro for automating `EndpointOutRegister` implementations (for simple types)
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
macro_rules! impl_oapi_endpoint_out {
  ($t:tt, $c:expr) => {
    #[cfg(feature = "salvo")]
    impl EndpointOutRegister for $t {
      #[inline]
      fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
        operation.responses.insert(
          "200",
          salvo::oapi::Response::new("Ok").add_content($c, String::to_schema(components)),
        );
      }
    }
  };
}

/// Macro for automating `EndpointOutRegister` implementations (for template types)
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
macro_rules! impl_oapi_endpoint_out_t {
  ($t:tt, $c:expr) => {
    #[cfg(feature = "salvo")]
    impl<T> EndpointOutRegister for $t<T> {
      #[inline]
      fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
        operation.responses.insert(
          "200",
          salvo::oapi::Response::new("Ok").add_content($c, String::to_schema(components)),
        );
      }
    }
  };
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[allow(async_fn_in_trait)]
/// Trait that utilizes only mutable reference to `Response` and makes no need for `Request`/`Depot`.
pub trait ExplicitServerWrite {
  /// Write an actual response in a `Response` object.
  async fn explicit_write(self, res: &mut Response);
}

/// Sends 200 without data.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
pub struct OK(pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out!(OK, "text/plain");

/// Returns empty `200 OK` response.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! ok {
  () => {
    Ok::<impulse_utils::responses::OK, impulse_utils::errors::ServerError>(impulse_utils::responses::OK(
      $crate::fn_name!(),
    ))
  };
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl ExplicitServerWrite for OK {
  async fn explicit_write(self, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    res.render("");
    tracing::trace!("[{}] => Received and sent result 200", self.0);
  }
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl ServerResponseWriter for OK {
  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    ExplicitServerWrite::explicit_write(self, res).await
  }
}

/// Sends 200 and plain text.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[derive(Debug)]
pub struct Plain(pub String, pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out!(Plain, "text/plain");

/// Returns given plain text.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! plain {
  ($plain_text:expr) => {
    Ok::<impulse_utils::responses::Plain, impulse_utils::errors::ServerError>(impulse_utils::responses::Plain(
      $plain_text,
      $crate::fn_name!(),
    ))
  };
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl ExplicitServerWrite for Plain {
  async fn explicit_write(self, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    res.render(&self.0);
    tracing::trace!("[{}] => Received and sent result 200 with text: {}", self.1, self.0);
  }
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl ServerResponseWriter for Plain {
  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    ExplicitServerWrite::explicit_write(self, res).await
  }
}

/// Sends 200 and HTML.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[derive(Debug)]
pub struct Html(pub String, pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out!(Html, "text/html");

/// Returns given HTML code.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! html {
  ($html_data:expr) => {
    Ok::<impulse_utils::responses::Html, impulse_utils::errors::ServerError>(impulse_utils::responses::Html(
      $html_data,
      $crate::fn_name!(),
    ))
  };
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl ExplicitServerWrite for Html {
  async fn explicit_write(self, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    res.render(salvo::writing::Text::Html(&self.0));
    tracing::trace!("[{}] => Received and sent result 200 with HTML", self.1);
  }
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl ServerResponseWriter for Html {
  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    ExplicitServerWrite::explicit_write(self, res).await
  }
}

/// Sends 200 and file.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[derive(Debug)]
pub struct File(pub std::path::PathBuf, pub String, pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out!(File, "application/octet-stream");

/// File response.
///
/// Usage:
///
/// ```rust
/// use impulse_utils::prelude::*;
/// use salvo::prelude::*;
/// use std::path::PathBuf;
///
/// pub async fn some_endpoint() -> MResult<File> {
///   file_upload!(PathBuf::from("filepath.txt"), "Normal file name.txt".to_string())
/// }
/// ```
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! file_upload {
  ($filepath:expr, $attached_filename:expr) => {
    Ok::<impulse_utils::responses::File, impulse_utils::errors::ServerError>(impulse_utils::responses::File(
      $filepath,
      $attached_filename,
      $crate::fn_name!(),
    ))
  };
}

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl ServerResponseWriter for File {
  async fn write(self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    res.headers_mut().append(
      "Cache-Control",
      HeaderValue::from_static("public, max-age=0, must-revalidate"),
    );
    NamedFile::builder(&self.0)
      .attached_name(&self.1)
      .use_etag(true)
      .use_last_modified(true)
      .send(req.headers(), res)
      .await;
    tracing::trace!("[{}] => Received and sent result 200 with file {}", self.2, self.1);
  }
}

/// Sends 200 and JSON.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[derive(Debug)]
pub struct Json<T>(pub T, pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out_t!(Json, "application/json");

/// Serializes to JSON and returns given object.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! json {
  ($json_data:expr) => {
    Ok::<impulse_utils::responses::Json<_>, impulse_utils::errors::ServerError>(impulse_utils::responses::Json(
      $json_data,
      $crate::fn_name!(),
    ))
  };
}

#[cfg(all(feature = "salvo", feature = "mresult"))]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl<T: serde::Serialize + Send> ExplicitServerWrite for Json<T> {
  async fn explicit_write(self, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    match sonic_rs::to_string(&self.0) {
      Ok(s) => {
        res.headers_mut().insert(
          CONTENT_TYPE,
          HeaderValue::from_static("application/json; charset=utf-8"),
        );
        tracing::trace!("[{}] => Sending JSON: {:?}", self.1, s.as_str());
        res.write_body(s).ok();
        tracing::trace!("[{}] => Received and sent result 200 with JSON", self.1);
      }
      Err(e) => {
        tracing::error!("[{}] => Failed to serialize data: {:?}", e, self.1);
        crate::prelude::ServerError::from_private(e)
          .with_public("Failed to serialize data.")
          .with_500()
          .explicit_write(res)
          .await;
      }
    }
  }
}

#[cfg(all(feature = "salvo", feature = "mresult"))]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl<T: serde::Serialize + Send> ServerResponseWriter for Json<T> {
  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    ExplicitServerWrite::explicit_write(self, res).await
  }
}

/// Sends 200 and MsgPack.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[derive(Debug)]
pub struct MsgPack<T>(pub T, pub &'static str);

#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl_oapi_endpoint_out_t!(MsgPack, "application/msgpack");

/// Serializes to MsgPack and returns given object.
#[cfg(feature = "salvo")]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[macro_export]
macro_rules! msgpack {
  ($msgpack_data:expr) => {
    Ok::<impulse_utils::responses::MsgPack<_>, impulse_utils::errors::ServerError>(impulse_utils::responses::MsgPack(
      $msgpack_data,
      $crate::fn_name!(),
    ))
  };
}

#[cfg(all(feature = "salvo", feature = "mresult"))]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
impl<T: serde::Serialize + Send> ExplicitServerWrite for MsgPack<T> {
  async fn explicit_write(self, res: &mut Response) {
    res.status_code(salvo::http::StatusCode::OK);
    match rmp_serde::to_vec(&self.0) {
      Ok(bytes) => {
        res.headers_mut().insert(
          CONTENT_TYPE,
          HeaderValue::from_static("application/msgpack; charset=utf-8"),
        );
        tracing::trace!("[{}] => Sending bytes: {:04X?}", self.1, bytes);
        res.write_body(bytes).ok();
        tracing::trace!("[{}] => Received and sent result 200 with MsgPack", self.1);
      }
      Err(e) => {
        tracing::error!("[{}] => Failed to serialize data: {:?}", e, self.1);
        crate::prelude::ServerError::from_private(e)
          .with_public("Failed to serialize data.")
          .with_500()
          .explicit_write(res)
          .await;
      }
    }
  }
}

#[cfg(all(feature = "salvo", feature = "mresult"))]
#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
#[salvo::async_trait]
impl<T: serde::Serialize + Send> ServerResponseWriter for MsgPack<T> {
  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    ExplicitServerWrite::explicit_write(self, res).await
  }
}

/// Trait to parse MessagePack responses from `reqwest` library.
#[cfg(all(feature = "reqwest", feature = "cresult"))]
#[allow(async_fn_in_trait)]
pub trait MsgPackResponse {
  /// Parses MessagePack from body.
  async fn msgpack<T: serde::de::DeserializeOwned>(self) -> crate::prelude::CResult<T>;
}

#[cfg(all(feature = "reqwest", feature = "cresult"))]
impl MsgPackResponse for reqwest::Response {
  async fn msgpack<T: serde::de::DeserializeOwned>(self) -> crate::prelude::CResult<T> {
    use crate::errors::ClientError;

    let full = self.bytes().await.map_err(ClientError::from)?;
    rmp_serde::from_slice(&full).map_err(ClientError::from)
  }
}

/// Trait to recover public errors from server.
#[cfg(all(feature = "reqwest", feature = "cresult"))]
#[allow(async_fn_in_trait)]
pub trait CollectServerError
where
  Self: Sized,
{
  /// Collects server error.
  async fn collect_server_error(self) -> crate::prelude::CResult<Self>;
}

#[cfg(all(feature = "reqwest", feature = "cresult"))]
impl CollectServerError for reqwest::Response {
  async fn collect_server_error(self) -> crate::prelude::CResult<Self> {
    use crate::errors::ClientError;

    let status_code = self.status().as_u16();
    if status_code >= 400 {
      let ctype = self
        .headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.split(';').next())
        .map(|v| v.to_string());

      if ctype.as_ref().is_some_and(|ct| ct.as_str().eq("application/json")) {
        let err_json = self
          .json::<crate::errors::ErrorResponse>()
          .await
          .map_err(|_| ClientError::from_str(crate::errors::public_msg_from(&Some(status_code))))?;
        return Err(ClientError::from_str(err_json.err));
      }

      Err(ClientError::from_str(crate::errors::public_msg_from(&Some(
        status_code,
      ))))
    } else {
      Ok(self)
    }
  }
}

/// Trait to recover public errors from server.
#[cfg(all(feature = "reqwest", feature = "mresult"))]
#[allow(async_fn_in_trait)]
pub trait RedirectServerError
where
  Self: Sized,
{
  /// Redirects server error.
  async fn redirect_server_error(self) -> crate::prelude::MResult<Self>;
}

#[cfg(all(feature = "reqwest", feature = "mresult"))]
impl RedirectServerError for reqwest::Response {
  async fn redirect_server_error(self) -> crate::prelude::MResult<Self> {
    use crate::errors::ServerError;

    let status_code = self.status();
    if status_code.as_u16() >= 400 {
      let ctype = self
        .headers()
        .get(reqwest::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.split(';').next())
        .map(|v| v.to_string());

      if ctype.as_ref().is_some_and(|ct| ct.as_str().eq("application/json")) {
        let err_json = self.json::<crate::errors::ErrorResponse>().await.map_err(|e| {
          ServerError::from_private(e)
            .with_public(crate::errors::public_msg_from(&Some(status_code.as_u16())))
            .with_code(status_code)
        })?;
        return Err(ServerError::from_public(err_json.err).with_code(status_code));
      }

      Err(ServerError::from_public(crate::errors::public_msg_from(&Some(status_code.as_u16()))).with_code(status_code))
    } else {
      Ok(self)
    }
  }
}
