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

use crate::models::cdn_mp3::CdnMp3;

#[derive(Clone, Debug)]
pub struct GetCdnMp3Request {
    pub base_url: String,
    pub clip_id: Uuid,
}

#[derive(Debug, Error)]
pub enum GetCdnMp3Error {
    #[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,
}

#[derive(Clone)]
pub struct GetCdnMp3<S> {
    inner: S,
}

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

impl<S, SB> Service<GetCdnMp3Request> for GetCdnMp3<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 = CdnMp3;
    type Error = GetCdnMp3Error;
    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| GetCdnMp3Error::Transport(e.to_string()))
    }

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

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

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

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

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

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

            Ok(CdnMp3 { data: body_bytes })
        })
    }
}
