use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::SpecError;

const MIOT_V2_TRANSLATION_URL: &str = "https://miot-spec.org/instance/v2/multiLanguage";

/// Official MIoT translation payload.
///
/// Example shape:
/// `{ "data": { "en": {"service:001": "..."}, "zh_cn": {...} }, "timestamp": "1642573108" }`
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct Translation {
    #[serde(default)]
    pub data: BTreeMap<String, BTreeMap<String, String>>,
    #[serde(default)]
    pub timestamp: String,
}

impl Translation {
    /// Returns the translation value for a given locale and key.
    pub fn text(&self, locale: &str, key: &str) -> Option<&str> {
        self.data.get(locale)?.get(key).map(String::as_str)
    }

    /// Returns all translation pairs for a locale.
    pub fn locale(&self, locale: &str) -> Option<&BTreeMap<String, String>> {
        self.data.get(locale)
    }
}

/// Parses translation payload JSON into `Translation`.
pub fn parse_translation(json: &str) -> Result<Translation, SpecError> {
    let payload: Translation = serde_json::from_str(json)?;
    Ok(payload)
}

/// Fetches MIoT v2 translation payload by device URN in sync mode.
///
/// Requires `sync` and one of `ureq-client` or `reqwest-client` features.
#[cfg(feature = "sync")]
pub fn fetch_translation(urn: &str) -> Result<Translation, SpecError> {
    #[cfg(feature = "ureq-client")]
    let body = {
        let mut response = ureq::get(MIOT_V2_TRANSLATION_URL)
            .query("urn", urn)
            .header("User-Agent", crate::USER_AGENT)
            .call()
            .map_err(|e| match e {
                ureq::Error::StatusCode(code) => SpecError::HttpStatus(code),
                err => SpecError::Http(err.to_string()),
            })?;

        response
            .body_mut()
            .read_to_string()
            .map_err(|e| SpecError::Http(e.to_string()))?
    };

    #[cfg(all(not(feature = "ureq-client"), feature = "reqwest-client"))]
    let body = {
        let url = format!("{}?urn={}", MIOT_V2_TRANSLATION_URL, urn);
        reqwest::blocking::Client::new()
            .get(&url)
            .header("User-Agent", crate::USER_AGENT)
            .send()
            .map_err(|e| SpecError::Http(e.to_string()))?
            .error_for_status()
            .map_err(|e| {
                if let Some(status) = e.status() {
                    SpecError::HttpStatus(status.as_u16())
                } else {
                    SpecError::Http(e.to_string())
                }
            })?
            .text()
            .map_err(|e| SpecError::Http(e.to_string()))?;
    };

    #[cfg(not(any(feature = "ureq-client", feature = "reqwest-client")))]
    return Err(SpecError::Feature(
        "Enable `ureq-client` or `reqwest-client` for MIoT v2 sync translation API",
    ));

    parse_translation(&body)
}

/// Fetches MIoT v2 translation payload by device URN in async mode.
///
/// Requires `async` and `reqwest-client` features.
#[cfg(feature = "async")]
pub async fn fetch_translation_async(urn: &str) -> Result<Translation, SpecError> {
    #[cfg(feature = "reqwest-client")]
    {
        let url = format!("{}?urn={}", MIOT_V2_TRANSLATION_URL, urn);
        let body = reqwest::Client::new()
            .get(&url)
            .header("User-Agent", crate::USER_AGENT)
            .send()
            .await
            .map_err(|e| SpecError::Http(e.to_string()))?
            .error_for_status()
            .map_err(|e| {
                if let Some(status) = e.status() {
                    SpecError::HttpStatus(status.as_u16())
                } else {
                    SpecError::Http(e.to_string())
                }
            })?
            .text()
            .await
            .map_err(|e| SpecError::Http(e.to_string()))?;

        return parse_translation(&body);
    }

    #[cfg(not(feature = "reqwest-client"))]
    {
        let _ = urn;
        Err(SpecError::Feature(
            "Enable `reqwest-client` for MIoT v2 async translation API",
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_translation_with_en_and_zh_cn() {
        let json = r#"{
            "data": {
                "en": {
                    "service:001": "Device Information",
                    "service:001:property:001": "Device Manufacturer"
                },
                "zh_cn": {
                    "service:001": "Device Information",
                    "service:001:property:001": "Device Manufacturer"
                }
            },
            "timestamp": "1642573108"
        }"#;

        let payload = parse_translation(json).expect("translation json should parse");
        assert_eq!(payload.timestamp, "1642573108");
        assert_eq!(
            payload.text("en", "service:001"),
            Some("Device Information")
        );
        assert_eq!(
            payload.text("zh_cn", "service:001:property:001"),
            Some("Device Manufacturer")
        );
    }

    #[test]
    fn locale_lookup_returns_none_for_unknown_locale_or_key() {
        let json = r#"{
            "data": { "en": { "service:001": "Device Information" } },
            "timestamp": "1642573108"
        }"#;

        let payload = parse_translation(json).expect("translation json should parse");
        assert_eq!(payload.text("zh_cn", "service:001"), None);
        assert_eq!(payload.text("en", "service:999"), None);
    }
}
