use anyhow::Result;
use serde::Deserialize;
use serde_json::from_str;

pub struct Tasmota {
    device: String,
}

impl Tasmota {
    pub fn new(device: impl ToString) -> Self {
        Self {
            device: device.to_string(),
        }
    }

    pub fn name(&self) -> &str {
        &self.device
    }

    fn command<'a>(&self, command: impl IntoIterator<Item = &'a str>) -> String {
        let mut str = String::new();

        for s in command.into_iter() {
            str += s;
            str += "%20";
        }

        format!("http://{}/cm?cmnd={}", self.device, str)
    }

    async fn post<'a>(&self, command: impl IntoIterator<Item = &'a str>) -> Result<String> {
        Ok(reqwest::Client::new()
            .post(&self.command(command))
            .send()
            .await?
            .text()
            .await?)
    }

    async fn get<'a>(&self, command: impl IntoIterator<Item = &'a str>) -> Result<String> {
        Ok(reqwest::Client::new()
            .get(&self.command(command))
            .send()
            .await?
            .text()
            .await?)
    }

    pub async fn turn_on_led(&self) -> Result<()> {
        self.post(["LedPower", "1"]).await?;

        Ok(())
    }

    pub async fn turn_off_led(&self) -> Result<()> {
        self.post(["LedPower", "0"]).await?;

        Ok(())
    }

    pub async fn switch_on(&self) -> Result<()> {
        self.post(["Power0", "1"]).await?;

        Ok(())
    }

    pub async fn switch_off(&self) -> Result<()> {
        self.post(["Power0", "0"]).await?;

        Ok(())
    }

    pub async fn power_state(&self) -> Result<bool> {
        let res = self.get(["Power0"]).await?;

        let state = PowerState::from_str(&res)?;

        Ok(state.is_on())
    }

    pub async fn read_power_usage(&self) -> Result<f32> {
        let res = self.get(["Status", "8"]).await?;

        let status = Status::from_str(&res)?;

        Ok(status.StatusSNS.ENERGY.Power)
    }

    pub async fn led_state(&self) -> Result<bool> {
        let res = self.get(["LedState"]).await?;

        let state = LedState::from_str(&res)?;

        Ok(state.LedState != 0)
    }
}

#[cfg(test)]
mod test {
    use std::{thread, time::Duration};

    use super::*;

    use anyhow::Result;

    #[tokio::test]
    async fn test_connection() -> Result<()> {
        let dev = Tasmota::new("Tasmota-Plug-1");

        let power = dev.read_power_usage().await?;

        println!("{power}");

        Ok(())
    }

    #[tokio::test]
    async fn test_toggle() -> Result<()> {
        let dev = Tasmota::new("Tasmota-Plug-4");

        dev.switch_off().await?;
        assert_eq!(dev.power_state().await?, false);

        thread::sleep(Duration::from_secs(5));

        dev.switch_on().await?;
        assert_eq!(dev.power_state().await?, true);

        Ok(())
    }

    #[tokio::test]
    async fn test_led() -> Result<()> {
        let dev = Tasmota::new("Tasmota-Plug-4");

        dev.turn_off_led().await?;
        assert_eq!(dev.led_state().await?, false);

        thread::sleep(Duration::from_secs(5));

        dev.turn_on_led().await?;
        assert_eq!(dev.led_state().await?, true);

        Ok(())
    }
}

#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Status {
    pub StatusSNS: StatusSNS,
}

impl Status {
    fn from_str(s: &str) -> Result<Self> {
        Ok(from_str(s)?)
    }
}

#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct StatusSNS {
    pub Time: String,
    pub ENERGY: Energy,
}

#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct Energy {
    pub TotalStartTime: String,
    pub Total: f32,
    pub Yesterday: f32,
    pub Today: f32,
    pub Power: f32,
    pub ApparentPower: u32,
    pub ReactivePower: u32,
    pub Factor: f32,
    pub Voltage: u32,
    pub Current: f32,
}

#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct LedState {
    LedState: u8,
}

impl LedState {
    fn from_str(s: &str) -> Result<Self> {
        Ok(from_str(s)?)
    }
}

#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]
pub struct PowerState {
    POWER: String,
}

impl PowerState {
    fn from_str(s: &str) -> Result<Self> {
        Ok(from_str(s)?)
    }

    pub fn is_on(&self) -> bool {
        self.POWER == "ON"
    }
}