use std::borrow::Cow;
use std::collections::{HashMap, HashSet};

use futures::future::join_all;
use reqwest::StatusCode;
use serde::Deserialize;
use serde::de::DeserializeOwned;

use crate::error::BioMcpError;
use crate::sources::{RequestPlan, request_from_plan};

const HPO_BASE: &str = "https://ontology.jax.org/api/hp";
const HPO_API: &str = "hpo";
const HPO_BASE_ENV: &str = "BIOMCP_HPO_BASE";

pub struct HpoClient {
    client: reqwest_middleware::ClientWithMiddleware,
    base: Cow<'static, str>,
}

impl HpoClient {
    pub fn new() -> Result<Self, BioMcpError> {
        Ok(Self {
            client: crate::sources::shared_client()?,
            base: crate::sources::env_base(HPO_BASE, HPO_BASE_ENV),
        })
    }

    async fn get_json<T: DeserializeOwned>(
        &self,
        req: reqwest_middleware::RequestBuilder,
    ) -> Result<T, BioMcpError> {
        let resp = crate::sources::apply_cache_mode(req).send().await?;
        let status = resp.status();
        let bytes = crate::sources::read_limited_body(resp, HPO_API).await?;
        Self::decode_json_response(status, &bytes)
    }

    pub(crate) fn decode_json_response<T: DeserializeOwned>(
        status: StatusCode,
        bytes: &[u8],
    ) -> Result<T, BioMcpError> {
        if status == StatusCode::NOT_FOUND {
            return Err(BioMcpError::NotFound {
                entity: "hpo".into(),
                id: "term".into(),
                suggestion: "Use an HPO ID like HP:0001653".into(),
            });
        }
        if !status.is_success() {
            let excerpt = crate::sources::body_excerpt(bytes);
            return Err(BioMcpError::Api {
                api: HPO_API.to_string(),
                message: format!("HTTP {status}: {excerpt}"),
            });
        }
        serde_json::from_slice(bytes).map_err(|source| BioMcpError::ApiJson {
            api: HPO_API.to_string(),
            source,
        })
    }

    pub(crate) fn term_plan(hpo_id: &str) -> Result<RequestPlan, BioMcpError> {
        let hpo_id = normalize_hpo_id(hpo_id).ok_or_else(|| {
            BioMcpError::InvalidArgument("HPO term ID is required (e.g., HP:0001653)".into())
        })?;
        Ok(RequestPlan::get(format!("terms/{hpo_id}")))
    }

    pub async fn term(&self, hpo_id: &str) -> Result<HpoTerm, BioMcpError> {
        let plan = Self::term_plan(hpo_id)?;
        self.get_json(request_from_plan(&self.client, self.base.as_ref(), &plan))
            .await
    }

    fn normalize_term_ids(ids: &[String], max_terms: usize) -> Vec<String> {
        let mut normalized = ids
            .iter()
            .filter_map(|id| normalize_hpo_id(id))
            .collect::<Vec<_>>();
        normalized.sort();
        normalized.dedup();
        normalized.truncate(max_terms.clamp(1, 20));
        normalized
    }

    pub async fn resolve_terms(
        &self,
        ids: &[String],
        max_terms: usize,
    ) -> Result<HashMap<String, String>, BioMcpError> {
        let normalized = Self::normalize_term_ids(ids, max_terms);

        let lookups = normalized
            .iter()
            .map(|id| async move { (id.clone(), self.term(id).await) })
            .collect::<Vec<_>>();

        let mut out: HashMap<String, String> = HashMap::new();
        for (id, result) in join_all(lookups).await {
            match result {
                Ok(term) => {
                    let name = term.name.trim();
                    if !name.is_empty() {
                        out.insert(id, name.to_string());
                    }
                }
                Err(BioMcpError::NotFound { .. }) => {}
                Err(err) => return Err(err),
            }
        }
        Ok(out)
    }

    pub(crate) fn search_term_ids_plan(query: &str) -> Option<RequestPlan> {
        let query = query.trim();
        if query.is_empty() {
            return None;
        }
        Some(RequestPlan::get("search").query("q", query))
    }

    fn decode_search_term_ids(response: HpoSearchResponse, max_terms: usize) -> Vec<String> {
        let limit = max_terms.clamp(1, 20);
        let mut out: Vec<String> = Vec::new();
        let mut seen: HashSet<String> = HashSet::new();
        for row in response.terms {
            if let Some(id) = normalize_hpo_id(&row.id)
                && seen.insert(id.clone())
            {
                out.push(id);
                if out.len() >= limit {
                    break;
                }
            }
        }
        out
    }

    pub async fn search_term_ids(
        &self,
        query: &str,
        max_terms: usize,
    ) -> Result<Vec<String>, BioMcpError> {
        let Some(plan) = Self::search_term_ids_plan(query) else {
            return Ok(Vec::new());
        };
        let response: HpoSearchResponse = self
            .get_json(request_from_plan(&self.client, self.base.as_ref(), &plan))
            .await?;
        Ok(Self::decode_search_term_ids(response, max_terms))
    }
}

fn normalize_hpo_id(value: &str) -> Option<String> {
    let mut id = value.trim().to_ascii_uppercase();
    if id.is_empty() {
        return None;
    }
    id = id.replace('_', ":");
    if !id.starts_with("HP:") {
        return None;
    }
    let suffix = id.trim_start_matches("HP:");
    if suffix.is_empty() || !suffix.chars().all(|c| c.is_ascii_digit()) {
        return None;
    }
    Some(format!("HP:{suffix}"))
}

#[derive(Debug, Clone, Deserialize)]
pub struct HpoTerm {
    #[allow(dead_code)]
    pub id: String,
    pub name: String,
}

#[derive(Debug, Clone, Deserialize)]
struct HpoSearchResponse {
    #[serde(default)]
    terms: Vec<HpoSearchTerm>,
}

#[derive(Debug, Clone, Deserialize)]
struct HpoSearchTerm {
    id: String,
}

#[cfg(test)]
mod tests;
