use bytes::Bytes;
use http::{Request, Response, StatusCode, header};
use http_body::Body;
use http_body_util::{BodyExt, Empty, Full};
use thiserror::Error;
use tower::{Service, ServiceExt};

use crate::models::discover::{DiscoverRequest, DiscoverResponse, ShortcutResponse};

// =============================================================================
// GetHomepageSections
// =============================================================================

/// Input to the homepage sections endpoint service
#[derive(Clone, Debug)]
pub struct GetHomePageSectionsRequest {
    pub base_url: String,
    pub discover_request: DiscoverRequest,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum GetHomePageSectionsError {
    #[error("bad url: {0}")]
    BadUrl(String),
    #[error("transport: {0}")]
    Transport(String),
    #[error("http {status}, head: {body_head}")]
    Http {
        status: StatusCode,
        body_head: String,
    },
    #[error("read body")]
    ReadBody,
    #[error("json: {0}")]
    Json(String),
    #[error("request body: {0}")]
    RequestBody(String),
}

/// A Tower service that turns `GetHomePageSectionsRequest` into a `DiscoverResponse` by calling:
///   POST {base}/api/discover
#[derive(Clone)]
pub struct GetHomePageSections<S> {
    inner: S,
}

impl<S> GetHomePageSections<S> {
    pub fn new(inner: S) -> Self {
        Self { inner }
    }
}

impl<S, SB> Service<GetHomePageSectionsRequest> for GetHomePageSections<S>
where
    S: Service<Request<Full<Bytes>>, Response = Response<SB>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    S::Error: std::error::Error + Send + Sync + 'static,
    SB: Body<Data = Bytes> + Send + 'static,
    SB::Error: std::error::Error + Send + Sync + 'static,
{
    type Response = DiscoverResponse;
    type Error = GetHomePageSectionsError;
    type Future = futures_util::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner
            .poll_ready(cx)
            .map_err(|e| GetHomePageSectionsError::Transport(e.to_string()))
    }

    fn call(&mut self, req: GetHomePageSectionsRequest) -> Self::Future {
        let client = self.inner.clone();
        let url = format!("{}/api/discover", req.base_url.trim_end_matches('/'));

        // Serialize request body
        let body_bytes = match serde_json::to_vec(&req.discover_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(async move {
                    Err(GetHomePageSectionsError::RequestBody(e.to_string()))
                });
            }
        };

        let mut builder = Request::builder()
            .method("POST")
            .uri(url.clone())
            .header(header::CONTENT_TYPE, "application/json");

        if let Some(k) = req.api_key.clone() {
            let headers = builder.headers_mut().expect("request builder usable");
            headers.insert(
                header::AUTHORIZATION,
                format!("Bearer {k}").parse().expect("valid Authorization"),
            );
            headers.insert("X-Api-Key", k.parse().expect("valid X-Api-Key"));
        }

        let request = match builder.body(Full::new(Bytes::from(body_bytes))) {
            Ok(r) => r,
            Err(e) => {
                return Box::pin(
                    async move { Err(GetHomePageSectionsError::BadUrl(e.to_string())) },
                );
            }
        };

        Box::pin(async move {
            let resp = client
                .oneshot(request)
                .await
                .map_err(|e| GetHomePageSectionsError::Transport(e.to_string()))?;

            let status = resp.status();
            let body_bytes = resp
                .into_body()
                .collect()
                .await
                .map_err(|_| GetHomePageSectionsError::ReadBody)?
                .to_bytes();

            if !status.is_success() {
                let head = String::from_utf8_lossy(&body_bytes)
                    .chars()
                    .take(400)
                    .collect::<String>();
                return Err(GetHomePageSectionsError::Http {
                    status,
                    body_head: head,
                });
            }

            serde_json::from_slice::<DiscoverResponse>(&body_bytes)
                .map_err(|e| GetHomePageSectionsError::Json(e.to_string()))
        })
    }
}

// =============================================================================
// GetDiscoverSection
// =============================================================================

/// Input to the get specific discover section endpoint service
#[derive(Clone, Debug)]
pub struct GetDiscoverSectionRequest {
    pub base_url: String,
    pub section_name: String,
    pub discover_request: Option<DiscoverRequest>,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum GetDiscoverSectionError {
    #[error("bad url: {0}")]
    BadUrl(String),
    #[error("transport: {0}")]
    Transport(String),
    #[error("http {status}, head: {body_head}")]
    Http {
        status: StatusCode,
        body_head: String,
    },
    #[error("read body")]
    ReadBody,
    #[error("json: {0}")]
    Json(String),
}

/// Response is a single section (one item from DiscoverResponse.sections)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GetDiscoverSectionResponse(pub crate::models::discover::DiscoverSection);

/// A Tower service that turns `GetDiscoverSectionRequest` into a section by calling:
///   GET {base}/api/discover/sections/{section_name}
#[derive(Clone)]
pub struct GetDiscoverSection<S> {
    inner: S,
}

impl<S> GetDiscoverSection<S> {
    pub fn new(inner: S) -> Self {
        Self { inner }
    }
}

impl<S, SB> Service<GetDiscoverSectionRequest> for GetDiscoverSection<S>
where
    S: Service<Request<Empty<Bytes>>, Response = Response<SB>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    S::Error: std::error::Error + Send + Sync + 'static,
    SB: Body<Data = Bytes> + Send + 'static,
    SB::Error: std::error::Error + Send + Sync + 'static,
{
    type Response = GetDiscoverSectionResponse;
    type Error = GetDiscoverSectionError;
    type Future = futures_util::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner
            .poll_ready(cx)
            .map_err(|e| GetDiscoverSectionError::Transport(e.to_string()))
    }

    fn call(&mut self, req: GetDiscoverSectionRequest) -> Self::Future {
        let client = self.inner.clone();
        let url = format!(
            "{}/api/discover/sections/{}",
            req.base_url.trim_end_matches('/'),
            &req.section_name
        );

        let mut builder = Request::builder().method("GET").uri(url.clone());

        if let Some(k) = req.api_key.clone() {
            let headers = builder.headers_mut().expect("request builder usable");
            headers.insert(
                header::AUTHORIZATION,
                format!("Bearer {k}").parse().expect("valid Authorization"),
            );
            headers.insert("X-Api-Key", k.parse().expect("valid X-Api-Key"));
        }

        let request = match builder.body(Empty::new()) {
            Ok(r) => r,
            Err(e) => {
                return Box::pin(
                    async move { Err(GetDiscoverSectionError::BadUrl(e.to_string())) },
                );
            }
        };

        Box::pin(async move {
            let resp = client
                .oneshot(request)
                .await
                .map_err(|e| GetDiscoverSectionError::Transport(e.to_string()))?;

            let status = resp.status();
            let body_bytes = resp
                .into_body()
                .collect()
                .await
                .map_err(|_| GetDiscoverSectionError::ReadBody)?
                .to_bytes();

            if !status.is_success() {
                let head = String::from_utf8_lossy(&body_bytes)
                    .chars()
                    .take(400)
                    .collect::<String>();
                return Err(GetDiscoverSectionError::Http {
                    status,
                    body_head: head,
                });
            }

            serde_json::from_slice::<crate::models::discover::DiscoverSection>(&body_bytes)
                .map(GetDiscoverSectionResponse)
                .map_err(|e| GetDiscoverSectionError::Json(e.to_string()))
        })
    }
}

// =============================================================================
// GetShortcuts
// =============================================================================

/// Input to the get shortcuts endpoint service
#[derive(Clone, Debug)]
pub struct GetShortcutsRequest {
    pub base_url: String,
    pub shortcut_type: String,
    pub context_song_id: Option<String>,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum GetShortcutsError {
    #[error("bad url: {0}")]
    BadUrl(String),
    #[error("transport: {0}")]
    Transport(String),
    #[error("http {status}, head: {body_head}")]
    Http {
        status: StatusCode,
        body_head: String,
    },
    #[error("read body")]
    ReadBody,
    #[error("json: {0}")]
    Json(String),
}

/// A Tower service that turns `GetShortcutsRequest` into a `ShortcutResponse` by calling:
///   GET {base}/api/discover/shortcuts_songs?shortcut_type={type}&context_song_id={id}
#[derive(Clone)]
pub struct GetShortcuts<S> {
    inner: S,
}

impl<S> GetShortcuts<S> {
    pub fn new(inner: S) -> Self {
        Self { inner }
    }
}

impl<S, SB> Service<GetShortcutsRequest> for GetShortcuts<S>
where
    S: Service<Request<Empty<Bytes>>, Response = Response<SB>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    S::Error: std::error::Error + Send + Sync + 'static,
    SB: Body<Data = Bytes> + Send + 'static,
    SB::Error: std::error::Error + Send + Sync + 'static,
{
    type Response = ShortcutResponse;
    type Error = GetShortcutsError;
    type Future = futures_util::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner
            .poll_ready(cx)
            .map_err(|e| GetShortcutsError::Transport(e.to_string()))
    }

    fn call(&mut self, req: GetShortcutsRequest) -> Self::Future {
        let client = self.inner.clone();
        let mut url = format!(
            "{}/api/discover/shortcuts_songs?shortcut_type={}",
            req.base_url.trim_end_matches('/'),
            &req.shortcut_type
        );

        if let Some(context_id) = req.context_song_id.as_ref() {
            url.push_str(&format!("&context_song_id={}", context_id));
        }

        let mut builder = Request::builder().method("GET").uri(url.clone());

        if let Some(k) = req.api_key.clone() {
            let headers = builder.headers_mut().expect("request builder usable");
            headers.insert(
                header::AUTHORIZATION,
                format!("Bearer {k}").parse().expect("valid Authorization"),
            );
            headers.insert("X-Api-Key", k.parse().expect("valid X-Api-Key"));
        }

        let request = match builder.body(Empty::new()) {
            Ok(r) => r,
            Err(e) => {
                return Box::pin(async move { Err(GetShortcutsError::BadUrl(e.to_string())) });
            }
        };

        Box::pin(async move {
            let resp = client
                .oneshot(request)
                .await
                .map_err(|e| GetShortcutsError::Transport(e.to_string()))?;

            let status = resp.status();
            let body_bytes = resp
                .into_body()
                .collect()
                .await
                .map_err(|_| GetShortcutsError::ReadBody)?
                .to_bytes();

            if !status.is_success() {
                let head = String::from_utf8_lossy(&body_bytes)
                    .chars()
                    .take(400)
                    .collect::<String>();
                return Err(GetShortcutsError::Http {
                    status,
                    body_head: head,
                });
            }

            serde_json::from_slice::<ShortcutResponse>(&body_bytes)
                .map_err(|e| GetShortcutsError::Json(e.to_string()))
        })
    }
}
