Documentation
use http_api_client::{
    http::{HeaderMap, Method, StatusCode},
    Client,
};
use serde::{de::DeserializeOwned, Serialize};
use url::Url;

use crate::endpoints::v4::{
    parse_response, render_request, AccessToken, ErrorResponseBody, ParseResponseError,
    RenderRequestError,
};

//
pub struct V4Client<C>
where
    C: Client,
{
    inner: C,
    pub access_token: Option<AccessToken>,
    pub base_url: Option<Url>,
}

impl<C> Clone for V4Client<C>
where
    C: Client + Clone,
{
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            access_token: self.access_token.clone(),
            base_url: self.base_url.clone(),
        }
    }
}

impl<C> V4Client<C>
where
    C: Client,
{
    pub fn new(inner: C, access_token: Option<AccessToken>, base_url: Option<Url>) -> Self {
        Self {
            inner,
            access_token,
            base_url,
        }
    }

    pub async fn respond<ReqQuery, ReqBody, RespBody>(
        &self,
        method: Method,
        path: &str,
        query: Option<&ReqQuery>,
        body: Option<&ReqBody>,
    ) -> Result<(StatusCode, RespBody, HeaderMap), V4ClientRespondError<C>>
    where
        ReqQuery: Serialize,
        ReqBody: Serialize,
        RespBody: DeserializeOwned,
    {
        let req = render_request::<ReqQuery, ReqBody>(
            method,
            path,
            query,
            body,
            self.access_token.as_ref(),
            self.base_url.as_ref(),
        )
        .map_err(V4ClientRespondError::RenderRequestError)?;

        let resp = self
            .inner
            .respond(req)
            .await
            .map_err(V4ClientRespondError::RespondError)?;

        let (resp_status, resp_body, resp_headers) =
            parse_response::<RespBody>(resp).map_err(V4ClientRespondError::ParseResponseError)?;

        match resp_body {
            Ok(body) => Ok((resp_status, body, resp_headers)),
            Err(body) => Err(V4ClientRespondError::ResponseStatusCodeNoSuccess(
                resp_status,
                body,
                resp_headers,
            )),
        }
    }
}

//
#[derive(Debug)]
pub enum V4ClientRespondError<C>
where
    C: Client,
{
    RenderRequestError(RenderRequestError),
    RespondError(C::RespondError),
    ParseResponseError(ParseResponseError),
    ResponseStatusCodeNoSuccess(StatusCode, ErrorResponseBody, HeaderMap),
}
impl<C> core::fmt::Display for V4ClientRespondError<C>
where
    C: Client + core::fmt::Debug,
{
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{self:?}")
    }
}
impl<C> std::error::Error for V4ClientRespondError<C> where C: Client + core::fmt::Debug {}

mod ext {
    use super::*;

    use crate::{
        endpoints::v4::{
            linode_instances::{
                config_create, disk_create, disk_view, ip_addresses_info, linode_boot,
                linode_create, linode_reboot, linode_view, linodes_list,
            },
            linode_types::types_list,
            EmptyMapResponseBody, XListRequestQuery,
        },
        objects::v4::linode_instances::{ConfigId, DiskId, LinodeId},
    };

    impl<C> V4Client<C>
    where
        C: Client,
    {
        //
        //
        //
        pub async fn linode_instances_linodes_list(
            &self,
            page: impl Into<Option<usize>>,
            page_size: impl Into<Option<usize>>,
        ) -> Result<linodes_list::ResponseBody, V4ClientRespondError<C>> {
            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<_, (), _>(
                    Method::GET,
                    "/linode/instances",
                    Some(&XListRequestQuery::new(page, page_size)),
                    None,
                )
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_linode_create<F>(
            &self,
            region: &str,
            r#type: &str,
            mut f: F,
        ) -> Result<linode_create::ResponseBody, V4ClientRespondError<C>>
        where
            F: FnMut(&mut linode_create::RequestBody),
        {
            let mut req_body = linode_create::RequestBody {
                region: region.into(),
                r#type: r#type.into(),
                ..Default::default()
            };
            f(&mut req_body);

            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), _, _>(Method::POST, "/linode/instances", None, Some(&req_body))
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_linode_delete(
            &self,
            linode_id: LinodeId,
        ) -> Result<(), V4ClientRespondError<C>> {
            let (_resp_status, _resp_body, _resp_headers) = self
                .respond::<(), (), EmptyMapResponseBody>(
                    Method::DELETE,
                    format!("/linode/instances/{}", linode_id).as_str(),
                    None,
                    None,
                )
                .await?;
            Ok(())
        }

        pub async fn linode_instances_linode_view(
            &self,
            linode_id: LinodeId,
        ) -> Result<linode_view::ResponseBody, V4ClientRespondError<C>> {
            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), (), _>(
                    Method::GET,
                    format!("/linode/instances/{}", linode_id).as_str(),
                    None,
                    None,
                )
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_linode_boot(
            &self,
            linode_id: LinodeId,
            config_id: ConfigId,
        ) -> Result<(), V4ClientRespondError<C>> {
            let req_body = linode_boot::RequestBody { config_id };
            let (_resp_status, _resp_body, _resp_headers) = self
                .respond::<(), _, EmptyMapResponseBody>(
                    Method::POST,
                    format!("/linode/instances/{}/boot", linode_id).as_str(),
                    None,
                    Some(&req_body),
                )
                .await?;
            Ok(())
        }

        pub async fn linode_instances_linode_reboot(
            &self,
            linode_id: LinodeId,
            config_id: Option<ConfigId>,
        ) -> Result<(), V4ClientRespondError<C>> {
            if let Some(config_id) = config_id {
                let req_body = linode_reboot::RequestBody {
                    config_id: Some(config_id),
                };
                let (_resp_status, _resp_body, _resp_headers) = self
                    .respond::<(), _, EmptyMapResponseBody>(
                        Method::POST,
                        format!("/linode/instances/{}/reboot", linode_id).as_str(),
                        None,
                        Some(&req_body),
                    )
                    .await?;
            } else {
                let (_resp_status, _resp_body, _resp_headers) = self
                    .respond::<(), (), EmptyMapResponseBody>(
                        Method::POST,
                        format!("/linode/instances/{}/reboot", linode_id).as_str(),
                        None,
                        None,
                    )
                    .await?;
            }

            Ok(())
        }

        pub async fn linode_instances_ip_addresses_info(
            &self,
            linode_id: LinodeId,
        ) -> Result<ip_addresses_info::ResponseBody, V4ClientRespondError<C>> {
            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), (), _>(
                    Method::GET,
                    format!("/linode/instances/{}/ips", linode_id).as_str(),
                    None,
                    None,
                )
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_config_create<F>(
            &self,
            linode_id: LinodeId,
            label: &str,
            mut f: F,
        ) -> Result<config_create::ResponseBody, V4ClientRespondError<C>>
        where
            F: FnMut(&mut config_create::RequestBody),
        {
            let mut req_body = config_create::RequestBody {
                label: label.into(),
                ..Default::default()
            };
            f(&mut req_body);

            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), _, _>(
                    Method::POST,
                    format!("/linode/instances/{}/configs", linode_id).as_str(),
                    None,
                    Some(&req_body),
                )
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_disk_create_with_image<F>(
            &self,
            linode_id: LinodeId,
            size: usize,
            image: &str,
            root_pass: &str,
            mut f: F,
        ) -> Result<disk_create::ResponseBody, V4ClientRespondError<C>>
        where
            F: FnMut(&mut disk_create::RequestBodyWithImage),
        {
            let mut req_body = disk_create::RequestBodyWithImage {
                size,
                image: image.into(),
                root_pass: root_pass.into(),
                ..Default::default()
            };
            f(&mut req_body);

            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), _, _>(
                    Method::POST,
                    format!("/linode/instances/{}/disks", linode_id).as_str(),
                    None,
                    Some(&req_body),
                )
                .await?;
            Ok(resp_body)
        }

        pub async fn linode_instances_disk_view(
            &self,
            linode_id: LinodeId,
            disk_id: DiskId,
        ) -> Result<disk_view::ResponseBody, V4ClientRespondError<C>> {
            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), (), _>(
                    Method::GET,
                    format!("/linode/instances/{}/disks/{}", linode_id, disk_id).as_str(),
                    None,
                    None,
                )
                .await?;
            Ok(resp_body)
        }

        //
        //
        //
        pub async fn linode_types_types_list(
            &self,
        ) -> Result<types_list::ResponseBody, V4ClientRespondError<C>> {
            let (_resp_status, resp_body, _resp_headers) = self
                .respond::<(), (), types_list::ResponseBody>(
                    Method::GET,
                    "/linode/types",
                    None,
                    None,
                )
                .await?;
            Ok(resp_body)
        }
    }
}