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

use crate::models::search::{
    HashtagSearchRequest, HashtagSearchResponse, HashtagTrendingRequest,
    LyricsSearchRequest as ModelLyricsSearchRequest, LyricsSearchResponse, OmnisearchRequest,
    OmnisearchResponse, SearchRequest, SearchRequestBuilder, SearchResponse, UserSearchRequest,
    ValidationError,
};

// =============================================================================
// CompoundSearch
// =============================================================================

/// Input to the compound search endpoint service
#[derive(Clone, Debug)]
pub struct CompoundSearchRequest {
    pub base_url: String,
    pub search_request: SearchRequest,
    pub api_key: Option<String>,
}

impl CompoundSearchRequest {
    /// Create a simple public song search request
    ///
    /// # Examples
    /// ```rust,no_run
    /// let request = CompoundSearchRequest::simple_search(
    ///     "https://api.example.com",
    ///     "lo-fi beats",
    ///     Some("api_key".to_string()),
    /// )?;
    /// ```
    pub fn simple_search(
        base_url: impl Into<String>,
        term: impl Into<String>,
        api_key: Option<String>,
    ) -> Result<Self, ValidationError> {
        Ok(Self {
            base_url: base_url.into(),
            search_request: SearchRequest::simple_public_song(term)?,
            api_key,
        })
    }

    /// Create from a search request builder
    ///
    /// # Examples
    /// ```rust,no_run
    /// let request = CompoundSearchRequest::from_builder(
    ///     "https://api.example.com",
    ///     SearchRequest::builder()
    ///         .public_song("jazz")
    ///         .user("artist"),
    ///     Some("api_key".to_string()),
    /// )?;
    /// ```
    pub fn from_builder(
        base_url: impl Into<String>,
        builder: SearchRequestBuilder,
        api_key: Option<String>,
    ) -> Result<Self, ValidationError> {
        Ok(Self {
            base_url: base_url.into(),
            search_request: builder.build()?,
            api_key,
        })
    }
}

#[derive(Debug, Error)]
pub enum CompoundSearchError {
    #[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 `CompoundSearchRequest` into a `SearchResponse` by calling:
///   POST {base}/api/search
#[derive(Clone)]
pub struct CompoundSearch<S> {
    inner: S,
}

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

impl<S, SB> Service<CompoundSearchRequest> for CompoundSearch<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 = SearchResponse;
    type Error = CompoundSearchError;
    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| CompoundSearchError::Transport(e.to_string()))
    }

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

        // Serialize request body
        let body_bytes = match serde_json::to_vec(&req.search_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(
                    async move { Err(CompoundSearchError::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(CompoundSearchError::BadUrl(e.to_string())) });
            }
        };

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

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

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

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

// =============================================================================
// SearchUsers
// =============================================================================

/// Input to the user search endpoint service
#[derive(Clone, Debug)]
pub struct SearchUsersRequest {
    pub base_url: String,
    pub user_search_request: UserSearchRequest,
    pub api_key: Option<String>,
}

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

/// User search response is a list of profiles
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SearchUsersResponse(pub Vec<crate::models::search::SearchProfile>);

/// A Tower service that turns `SearchUsersRequest` into user profiles by calling:
///   POST {base}/api/search/users
#[derive(Clone)]
pub struct SearchUsers<S> {
    inner: S,
}

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

impl<S, SB> Service<SearchUsersRequest> for SearchUsers<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 = SearchUsersResponse;
    type Error = SearchUsersError;
    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| SearchUsersError::Transport(e.to_string()))
    }

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

        // Serialize request body
        let body_bytes = match serde_json::to_vec(&req.user_search_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(async move { Err(SearchUsersError::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(SearchUsersError::BadUrl(e.to_string())) }),
        };

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

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

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

            serde_json::from_slice::<Vec<crate::models::search::SearchProfile>>(&body_bytes)
                .map(SearchUsersResponse)
                .map_err(|e| SearchUsersError::Json(e.to_string()))
        })
    }
}

// =============================================================================
// SearchHashtags
// =============================================================================

/// Input to the hashtag search endpoint service
#[derive(Clone, Debug)]
pub struct SearchHashtagsRequest {
    pub base_url: String,
    pub hashtag_search_request: HashtagSearchRequest,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum SearchHashtagsError {
    #[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 `SearchHashtagsRequest` into `HashtagSearchResponse` by calling:
///   POST {base}/api/search/hashtag_suggestions
#[derive(Clone)]
pub struct SearchHashtags<S> {
    inner: S,
}

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

impl<S, SB> Service<SearchHashtagsRequest> for SearchHashtags<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 = HashtagSearchResponse;
    type Error = SearchHashtagsError;
    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| SearchHashtagsError::Transport(e.to_string()))
    }

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

        // Serialize request body
        let body_bytes = match serde_json::to_vec(&req.hashtag_search_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(
                    async move { Err(SearchHashtagsError::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(SearchHashtagsError::BadUrl(e.to_string())) });
            }
        };

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

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

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

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

// =============================================================================
// TrendingHashtags
// =============================================================================

/// Input to the trending hashtag endpoint service
#[derive(Clone, Debug)]
pub struct TrendingHashtagsRequest {
    pub base_url: String,
    pub trending_request: HashtagTrendingRequest,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum TrendingHashtagsError {
    #[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 `TrendingHashtagsRequest` into `HashtagSearchResponse` by calling:
///   POST {base}/api/search/hashtag_trending
#[derive(Clone)]
pub struct TrendingHashtags<S> {
    inner: S,
}

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

impl<S, SB> Service<TrendingHashtagsRequest> for TrendingHashtags<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 = HashtagSearchResponse;
    type Error = TrendingHashtagsError;
    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| TrendingHashtagsError::Transport(e.to_string()))
    }

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

        let body_bytes = match serde_json::to_vec(&req.trending_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(
                    async move { Err(TrendingHashtagsError::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(TrendingHashtagsError::BadUrl(e.to_string())) });
            }
        };

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

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

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

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

// =============================================================================
// Omnisearch
// =============================================================================

/// Input to the omnisearch endpoint service
#[derive(Clone, Debug)]
pub struct OmnisearchServiceRequest {
    pub base_url: String,
    pub omnisearch_request: OmnisearchRequest,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum OmnisearchError {
    #[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 `OmnisearchServiceRequest` into `OmnisearchResponse` by calling:
///   POST {base}/api/search/omnisearch
#[derive(Clone)]
pub struct Omnisearch<S> {
    inner: S,
}

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

impl<S, SB> Service<OmnisearchServiceRequest> for Omnisearch<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 = OmnisearchResponse;
    type Error = OmnisearchError;
    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| OmnisearchError::Transport(e.to_string()))
    }

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

        let body_bytes = match serde_json::to_vec(&req.omnisearch_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(async move { Err(OmnisearchError::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(OmnisearchError::BadUrl(e.to_string())) }),
        };

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

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

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

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

// =============================================================================
// LyricsSearch (Is This Us)
// =============================================================================

/// Input to the lyrics search endpoint service
#[derive(Clone, Debug)]
pub struct LyricsSearchServiceRequest {
    pub base_url: String,
    pub lyrics_search_request: ModelLyricsSearchRequest,
    pub api_key: Option<String>,
}

#[derive(Debug, Error)]
pub enum LyricsSearchError {
    #[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 `LyricsSearchRequest` into clips by calling:
///   POST {base}/api/search/is_this_us
#[derive(Clone)]
pub struct LyricsSearch<S> {
    inner: S,
}

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

impl<S, SB> Service<LyricsSearchServiceRequest> for LyricsSearch<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 = LyricsSearchResponse;
    type Error = LyricsSearchError;
    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| LyricsSearchError::Transport(e.to_string()))
    }

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

        // Serialize request body
        let body_bytes = match serde_json::to_vec(&req.lyrics_search_request) {
            Ok(b) => b,
            Err(e) => {
                return Box::pin(async move { Err(LyricsSearchError::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(LyricsSearchError::BadUrl(e.to_string())) });
            }
        };

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

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

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

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

// =============================================================================
// High-level client facade
// =============================================================================

/// Ergonomic façade around the low-level Tower search services.
///
/// Applications can share a single `SearchClient` instead of wiring each service
/// individually. The client keeps track of the base URL, optional API key, and
/// exposes convenience helpers for common search flows.
pub mod client {
    use super::{
        CompoundSearch, CompoundSearchError, CompoundSearchRequest, HashtagSearchRequest,
        HashtagSearchResponse, LyricsSearch, LyricsSearchError, LyricsSearchResponse,
        LyricsSearchServiceRequest, ModelLyricsSearchRequest, Omnisearch, OmnisearchError,
        OmnisearchResponse, OmnisearchServiceRequest, SearchHashtags, SearchHashtagsError,
        SearchHashtagsRequest, SearchRequestBuilder, SearchResponse, SearchUsers, SearchUsersError,
        SearchUsersRequest, SearchUsersResponse, TrendingHashtags, TrendingHashtagsError,
        TrendingHashtagsRequest, ValidationError,
    };
    use crate::models::search::{
        HashtagTrendingRequest, OmnisearchRequest, SearchQuery, SearchQueryBuilder, SearchRequest,
    };
    use crate::validation::env::{self, EnvValidationError};
    use bytes::Bytes;
    use http::{Request, Response};
    use http_body_util::Full;
    use thiserror::Error;
    use tower::{Service, ServiceExt};

    /// Errors that can occur while constructing or using the [`SearchClient`].
    #[derive(Debug, Error)]
    pub enum SearchClientError {
        #[error("invalid search request: {0}")]
        Validation(#[from] ValidationError),
        #[error("compound search failed: {0}")]
        Compound(#[from] CompoundSearchError),
        #[error("user search failed: {0}")]
        Users(#[from] SearchUsersError),
        #[error("hashtag search failed: {0}")]
        Hashtags(#[from] SearchHashtagsError),
        #[error("lyrics search failed: {0}")]
        Lyrics(#[from] LyricsSearchError),
        #[error("trending hashtags failed: {0}")]
        TrendingHashtags(#[from] TrendingHashtagsError),
        #[error("omnisearch failed: {0}")]
        Omnisearch(#[from] OmnisearchError),
    }

    /// Errors when building a [`SearchClient`], typically due to environment or configuration.
    #[derive(Debug, Error)]
    pub enum SearchClientBuildError {
        #[error("environment validation failure: {0}")]
        Env(#[from] EnvValidationError),
        #[error("base URL must not be empty")]
        EmptyBaseUrl,
    }

    /// High-level entry point for Suno search endpoints.
    ///
    /// The client owns an HTTP transport that implements [`tower::Service`] for `http::Request`
    /// carrying fully buffered bodies (`Full<Bytes>`). This keeps the client agnostic of the
    /// concrete HTTP implementation (`reqwest`, `hyper`, etc.) while still providing an easy API.
    #[derive(Clone)]
    pub struct SearchClient<S> {
        transport: S,
        base_url: String,
        api_key: Option<String>,
    }

    impl<S> SearchClient<S>
    where
        S: Service<Request<Full<Bytes>>, Response = Response<Full<Bytes>>> + Clone + Send + 'static,
        S::Future: Send + 'static,
        S::Error: std::error::Error + Send + Sync + 'static,
    {
        /// Create a client from an explicit base URL and transport.
        pub fn new(
            base_url: impl Into<String>,
            transport: S,
        ) -> Result<Self, SearchClientBuildError> {
            let sanitized = sanitize_base_url(base_url);
            if sanitized.is_empty() {
                return Err(SearchClientBuildError::EmptyBaseUrl);
            }

            Ok(Self {
                transport,
                base_url: sanitized,
                api_key: None,
            })
        }

        /// Construct a client using environment variables validated by `sunocore::validation::env`.
        pub fn try_from_env(transport: S) -> Result<Self, SearchClientBuildError> {
            let base_url = env::suno_base_url()?;
            let api_key = env::suno_api_key()?;
            let mut client = Self::new(base_url, transport)?;
            client.api_key = api_key;
            Ok(client)
        }

        /// Borrow the configured base URL.
        pub fn base_url(&self) -> &str {
            &self.base_url
        }

        /// Borrow the configured API key if present.
        pub fn api_key(&self) -> Option<&str> {
            self.api_key.as_deref()
        }

        /// Set the API key used for subsequent calls.
        pub fn with_api_key(mut self, api_key: Option<String>) -> Self {
            self.api_key = api_key;
            self
        }

        /// Convenience setter for string-like API keys.
        pub fn with_api_key_str(mut self, api_key: impl Into<String>) -> Self {
            self.api_key = Some(api_key.into());
            self
        }

        /// Build a compound search request from the provided builder and execute it.
        pub async fn compound_from_builder(
            &self,
            builder: SearchRequestBuilder,
        ) -> Result<SearchResponse, SearchClientError> {
            let request = builder.build()?;
            self.compound(request).await
        }

        /// Execute a previously built compound search request.
        pub async fn compound(
            &self,
            request: SearchRequest,
        ) -> Result<SearchResponse, SearchClientError> {
            let svc = CompoundSearch::new(self.transport.clone());
            let response = svc
                .oneshot(CompoundSearchRequest {
                    base_url: self.base_url.clone(),
                    search_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }

        /// Convenience helper for a single public song search by term.
        pub async fn public_song_search(
            &self,
            term: impl Into<String>,
        ) -> Result<SearchResponse, SearchClientError> {
            let query = SearchQuery::public_song(term).build()?;
            self.compound(SearchRequest {
                search_queries: vec![query],
            })
            .await
        }

        /// Execute an arbitrary query builder.
        pub async fn execute_query_builder(
            &self,
            builder: SearchQueryBuilder,
        ) -> Result<SearchResponse, SearchClientError> {
            let query = builder.build()?;
            self.compound(SearchRequest {
                search_queries: vec![query],
            })
            .await
        }

        /// Execute a user search request.
        pub async fn search_users(
            &self,
            request: crate::models::search::UserSearchRequest,
        ) -> Result<SearchUsersResponse, SearchClientError> {
            let svc = SearchUsers::new(self.transport.clone());
            let response = svc
                .oneshot(SearchUsersRequest {
                    base_url: self.base_url.clone(),
                    user_search_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }

        /// Execute a hashtag search request.
        pub async fn search_hashtags(
            &self,
            request: HashtagSearchRequest,
        ) -> Result<HashtagSearchResponse, SearchClientError> {
            let svc = SearchHashtags::new(self.transport.clone());
            let response = svc
                .oneshot(SearchHashtagsRequest {
                    base_url: self.base_url.clone(),
                    hashtag_search_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }

        /// Execute a trending hashtag search request.
        pub async fn search_hashtag_trending(
            &self,
            request: HashtagTrendingRequest,
        ) -> Result<HashtagSearchResponse, SearchClientError> {
            let svc = TrendingHashtags::new(self.transport.clone());
            let response = svc
                .oneshot(TrendingHashtagsRequest {
                    base_url: self.base_url.clone(),
                    trending_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }

        /// Execute a lyrics similarity search request.
        pub async fn search_lyrics(
            &self,
            request: ModelLyricsSearchRequest,
        ) -> Result<LyricsSearchResponse, SearchClientError> {
            let svc = LyricsSearch::new(self.transport.clone());
            let response = svc
                .oneshot(LyricsSearchServiceRequest {
                    base_url: self.base_url.clone(),
                    lyrics_search_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }

        /// Construct a request for searching songs similar to the provided clip ID.
        pub async fn similar_to(
            &self,
            song_id: impl Into<String>,
        ) -> Result<SearchResponse, SearchClientError> {
            let query = SearchQuery::similar_to(song_id).build()?;
            self.compound(SearchRequest {
                search_queries: vec![query],
            })
            .await
        }

        /// Execute an omnisearch request that aggregates multiple content types.
        pub async fn omnisearch(
            &self,
            request: OmnisearchRequest,
        ) -> Result<OmnisearchResponse, SearchClientError> {
            let svc = Omnisearch::new(self.transport.clone());
            let response = svc
                .oneshot(OmnisearchServiceRequest {
                    base_url: self.base_url.clone(),
                    omnisearch_request: request,
                    api_key: self.api_key.clone(),
                })
                .await?;
            Ok(response)
        }
    }

    fn sanitize_base_url(base_url: impl Into<String>) -> String {
        base_url.into().trim_end_matches('/').to_owned()
    }

    #[cfg(test)]
    mod tests {
        use super::*;
        use crate::models::search::SearchResult;
        use bytes::Bytes;
        use futures_util::future::ready;
        use http::{Response, StatusCode, header};
        use http_body_util::Full;
        use std::collections::BTreeMap;
        use std::convert::Infallible;
        use std::sync::{Arc, Mutex};

        #[derive(Clone)]
        struct MockOkService {
            captured_uris: Arc<Mutex<Vec<String>>>,
            response_body: Arc<Vec<u8>>,
        }

        impl MockOkService {
            fn new(body: Vec<u8>) -> Self {
                Self {
                    captured_uris: Arc::new(Mutex::new(Vec::new())),
                    response_body: Arc::new(body),
                }
            }

            fn uris(&self) -> Vec<String> {
                self.captured_uris.lock().unwrap().clone()
            }
        }

        impl Service<Request<Full<Bytes>>> for MockOkService {
            type Response = Response<Full<Bytes>>;
            type Error = Infallible;
            type Future = futures_util::future::Ready<Result<Self::Response, Self::Error>>;

            fn poll_ready(
                &mut self,
                _cx: &mut std::task::Context<'_>,
            ) -> std::task::Poll<Result<(), Self::Error>> {
                std::task::Poll::Ready(Ok(()))
            }

            fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
                self.captured_uris
                    .lock()
                    .unwrap()
                    .push(req.uri().to_string());

                let body = self.response_body.clone();
                let response = Response::builder()
                    .status(StatusCode::OK)
                    .header(header::CONTENT_TYPE, "application/json")
                    .body(Full::new(Bytes::from(body.as_ref().clone())))
                    .expect("build http response");

                ready(Ok(response))
            }
        }

        #[tokio::test]
        async fn compound_from_builder_success() {
            let response_body = serde_json::to_vec(&SearchResponse {
                result: BTreeMap::<String, SearchResult>::new(),
            })
            .expect("serialize response");

            let service = MockOkService::new(response_body);
            let client =
                SearchClient::new("https://example.com/", service.clone()).expect("client builds");

            let response = client
                .compound_from_builder(SearchRequest::builder().public_song("jazz"))
                .await
                .expect("compound search succeeds");

            assert!(response.result.is_empty(), "expected empty result map");

            let uris = service.uris();
            assert_eq!(uris.len(), 1);
            assert_eq!(uris[0], "https://example.com/api/search");
        }

        #[derive(Clone)]
        struct MockErrorService;

        impl Service<Request<Full<Bytes>>> for MockErrorService {
            type Response = Response<Full<Bytes>>;
            type Error = Infallible;
            type Future = futures_util::future::Ready<Result<Self::Response, Self::Error>>;

            fn poll_ready(
                &mut self,
                _cx: &mut std::task::Context<'_>,
            ) -> std::task::Poll<Result<(), Self::Error>> {
                std::task::Poll::Ready(Ok(()))
            }

            fn call(&mut self, _req: Request<Full<Bytes>>) -> Self::Future {
                let response = Response::builder()
                    .status(StatusCode::INTERNAL_SERVER_ERROR)
                    .body(Full::new(Bytes::from_static(b"nope")))
                    .expect("build http response");
                ready(Ok(response))
            }
        }

        #[tokio::test]
        async fn compound_propagates_errors() {
            let client =
                SearchClient::new("https://example.com", MockErrorService).expect("client builds");

            let query = SearchQuery::public_song("jazz")
                .build()
                .expect("query builds");

            let err = client
                .compound(SearchRequest {
                    search_queries: vec![query],
                })
                .await
                .expect_err("compound search should fail");

            match err {
                SearchClientError::Compound(CompoundSearchError::Http { status, .. }) => {
                    assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
                }
                other => panic!("unexpected error variant: {other:?}"),
            }
        }

        #[tokio::test]
        async fn try_from_env_uses_validated_vars() {
            let _guard_base = EnvGuard::set("SUNO_BASE_URL", "https://unit.test/");
            let _guard_key = EnvGuard::set("SUNO_API_KEY", "test-key");

            let client =
                SearchClient::try_from_env(MockOkService::new(Vec::new())).expect("client builds");

            assert_eq!(client.base_url(), "https://unit.test");
            assert_eq!(client.api_key(), Some("test-key"));
        }

        #[tokio::test]
        async fn search_hashtag_trending_hits_endpoint() {
            let response_body = serde_json::to_vec(&HashtagSearchResponse { hashtags: vec![] })
                .expect("serialize response");

            let service = MockOkService::new(response_body);
            let client =
                SearchClient::new("https://example.com", service.clone()).expect("client builds");

            let _ = client
                .search_hashtag_trending(HashtagTrendingRequest {
                    days: Some(7),
                    size: Some(10),
                })
                .await
                .expect("trending search succeeds");

            let uris = service.uris();
            assert_eq!(uris.len(), 1);
            assert_eq!(uris[0], "https://example.com/api/search/hashtag_trending");
        }

        #[tokio::test]
        async fn omnisearch_hits_endpoint() {
            let response_body = serde_json::to_vec(&OmnisearchResponse {
                top_result: None,
                top_result_type: None,
                sections: Vec::new(),
            })
            .expect("serialize response");

            let service = MockOkService::new(response_body);
            let client =
                SearchClient::new("https://example.com", service.clone()).expect("client builds");

            let _ = client
                .omnisearch(OmnisearchRequest {
                    term: "ambient".into(),
                })
                .await
                .expect("omnisearch succeeds");

            let uris = service.uris();
            assert_eq!(uris.len(), 1);
            assert_eq!(uris[0], "https://example.com/api/search/omnisearch");
        }

        struct EnvGuard {
            key: &'static str,
        }

        impl EnvGuard {
            fn set(key: &'static str, value: &str) -> Self {
                unsafe { std::env::set_var(key, value) };
                Self { key }
            }
        }

        impl Drop for EnvGuard {
            fn drop(&mut self) {
                unsafe { std::env::remove_var(self.key) };
            }
        }
    }
}

pub use client::{SearchClient, SearchClientBuildError, SearchClientError};
