use serde::ser::SerializeSeq;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::SpecError;

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

/// Official MIoT v2 device instance.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceInstance {
    #[serde(rename = "type")]
    pub urn: String,
    pub description: String,
    #[serde(default)]
    pub services: Vec<Service>,
}

/// Official MIoT v2 service.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Service {
    pub iid: u32,
    #[serde(rename = "type")]
    pub urn: String,
    pub description: String,
    #[serde(default)]
    pub properties: Vec<Property>,
    #[serde(default)]
    pub actions: Vec<Action>,
    #[serde(default)]
    pub events: Vec<Event>,
}

/// Official MIoT v2 property.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Property {
    pub iid: u32,
    #[serde(rename = "type")]
    pub urn: String,
    pub description: String,
    pub format: String,
    pub access: Vec<String>,

    #[serde(default)]
    pub unit: Option<String>,

    // The official API encodes value-range as [min, max, step].
    #[serde(rename = "value-range", default)]
    pub value_range: Option<ValueRange>,

    #[serde(rename = "value-list", default)]
    pub value_list: Option<Vec<ValueItem>>,
}

impl Property {
    pub fn is_readable(&self) -> bool {
        self.access.iter().any(|a| a.eq_ignore_ascii_case("read"))
    }
    pub fn is_writable(&self) -> bool {
        self.access.iter().any(|a| a.eq_ignore_ascii_case("write"))
    }
    pub fn is_notifiable(&self) -> bool {
        self.access.iter().any(|a| a.eq_ignore_ascii_case("notify"))
    }
}

/// Official MIoT v2 action.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Action {
    pub iid: u32,
    #[serde(rename = "type")]
    pub urn: String,
    pub description: String,

    // Related property iids.
    #[serde(rename = "in", default)]
    pub in_args: Vec<u32>,
    #[serde(rename = "out", default)]
    pub out_args: Vec<u32>,
}

/// Official MIoT v2 event.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Event {
    pub iid: u32,
    #[serde(rename = "type")]
    pub urn: String,
    pub description: String,

    // Related property iids.
    #[serde(rename = "arguments", default)]
    pub arguments: Vec<u32>,
}

/// Official MIoT v2 value range.
#[derive(Debug, Clone, PartialEq)]
pub struct ValueRange {
    pub min: f64,
    pub max: f64,
    pub step: f64,
}

// Custom serialization because official value-range is [min, max, step].
impl Serialize for ValueRange {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(3))?;
        seq.serialize_element(&self.min)?;
        seq.serialize_element(&self.max)?;
        seq.serialize_element(&self.step)?;
        seq.end()
    }
}

// Custom deserialization because official value-range is [min, max, step].
impl<'de> Deserialize<'de> for ValueRange {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let arr: Vec<f64> = Vec::deserialize(deserializer)?;
        if arr.len() >= 3 {
            Ok(ValueRange {
                min: arr[0],
                max: arr[1],
                step: arr[2],
            })
        } else {
            Err(serde::de::Error::custom("Invalid value-range array length"))
        }
    }
}

/// Official MIoT v2 value list item.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ValueItem {
    pub value: Value,
    pub description: String,
}

/// Fetches the official MIoT v2 instance by device URN in sync mode.
///
/// Requires `sync` and one of `ureq-client` or `reqwest-client` features.
#[cfg(feature = "sync")]
pub fn fetch_spec(urn: &str) -> Result<DeviceInstance, SpecError> {
    let url = format!("{}?type={}", MIOT_V2_INSTANCE_URL, urn);

    #[cfg(feature = "ureq-client")]
    let body = {
        let mut response = ureq::get(&url)
            .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 = 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 API",
    ));

    let instance: DeviceInstance = serde_json::from_str(&body)?;
    Ok(instance)
}

/// Fetches the official MIoT v2 instance by device URN in async mode.
///
/// Requires `async` and `reqwest-client` features.
#[cfg(feature = "async")]
pub async fn fetch_spec_async(urn: &str) -> Result<DeviceInstance, SpecError> {
    #[cfg(feature = "reqwest-client")]
    {
        let url = format!("{}?type={}", MIOT_V2_INSTANCE_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()))?;

        let instance: DeviceInstance = serde_json::from_str(&body)?;

        return Ok(instance);
    }

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

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

    #[test]
    fn value_range_serializes_to_official_array_shape() {
        let range = ValueRange {
            min: 0.0,
            max: 100.0,
            step: 1.0,
        };

        let value = serde_json::to_value(&range).expect("value range should serialize");
        assert_eq!(value, serde_json::json!([0.0, 100.0, 1.0]));
    }

    #[test]
    fn device_instance_roundtrips_value_range_through_json() {
        let raw = r#"{
            "type": "urn:miot-spec-v2:device:light:0000A001:test-device:1",
            "description": "Test Device",
            "services": [{
                "iid": 2,
                "type": "urn:miot-spec-v2:service:light:00000001:test-device:1",
                "description": "Light Service",
                "properties": [{
                    "iid": 1,
                    "type": "urn:miot-spec-v2:property:brightness:0000000D:test-device:1",
                    "description": "Brightness",
                    "format": "uint8",
                    "access": ["read", "write"],
                    "unit": "percentage",
                    "value-range": [0, 100, 1],
                    "value-list": null
                }],
                "actions": [],
                "events": []
            }]
        }"#;

        let parsed: DeviceInstance = serde_json::from_str(raw)
            .expect("instance json should parse from official payload shape");

        let encoded = serde_json::to_string(&parsed).expect("instance should serialize");
        let encoded_json: serde_json::Value =
            serde_json::from_str(&encoded).expect("encoded instance should be valid json");

        assert_eq!(
            encoded_json["services"][0]["properties"][0]["value-range"],
            serde_json::json!([0.0, 100.0, 1.0])
        );

        let reparsed: DeviceInstance =
            serde_json::from_str(&encoded).expect("serialized output should remain parseable");
        assert_eq!(reparsed, parsed);
    }
}
