use std::collections::HashMap;

use crate::app_config::AppConfig;
use prettytable::{Table, row};

use crate::constants;
use crate::devices::device::{Device, DeviceId, DeviceRelay, DeviceRelayID};
use crate::energy::consumption::EnergyConsumption;
use crate::energy::relay_state_history;
use crate::energy::relay_state_history::RelayStateHistory;
use crate::utils::time_utils::{curr_hour, time_secs};

#[derive(Default)]
pub struct DeviceState {
    pub last_ping: u64,
}

impl DeviceState {
    pub fn record_ping(&mut self) {
        self.last_ping = time_secs();
    }

    pub fn is_online(&self) -> bool {
        (time_secs() - self.last_ping) < constants::DEVICE_MAX_PING_TIME
    }
}

#[derive(Default, Clone)]
pub struct RelayState {
    on: bool,
    since: usize,
}

impl RelayState {
    pub fn is_on(&self) -> bool {
        self.on
    }

    fn is_off(&self) -> bool {
        !self.on
    }

    pub fn state_for(&self) -> usize {
        (time_secs() - self.since as u64) as usize
    }
}

type RelaysState = HashMap<DeviceRelayID, RelayState>;

#[derive(Default)]
pub struct EnergyEngine {
    devices_state: HashMap<DeviceId, DeviceState>,
    relays_state: RelaysState,
}

impl DeviceRelay {
    // Note : this function is not recursive
    fn has_running_dependencies(&self, s: &RelaysState, devices: &[&Device]) -> bool {
        for d in devices {
            for r in &d.relays {
                if r.depends_on.contains(&self.id) && s.get(&r.id).unwrap().is_on() {
                    return true;
                }
            }
        }

        false
    }

    // Note : this function is not recursive
    fn is_missing_dependencies(&self, s: &RelaysState) -> bool {
        self.depends_on.iter().any(|id| s.get(id).unwrap().is_off())
    }

    fn is_having_conflict(&self, s: &RelaysState, devices: &[&Device]) -> bool {
        if self
            .conflicts_with
            .iter()
            .any(|id| s.get(id).unwrap().is_on())
        {
            return true;
        }

        // Reverse search
        for device in devices {
            for r in &device.relays {
                if s.get(&r.id).unwrap().is_on() && r.conflicts_with.contains(&self.id) {
                    return true;
                }
            }
        }

        false
    }
}

fn sum_relays_consumption(state: &RelaysState, devices: &[&Device]) -> usize {
    let mut consumption = 0;

    for d in devices {
        for r in &d.relays {
            if matches!(state.get(&r.id).map(|r| r.on), Some(true)) {
                consumption += r.consumption;
            }
        }
    }

    consumption
}

impl EnergyEngine {
    pub fn device_state(&mut self, dev_id: &DeviceId) -> &mut DeviceState {
        self.devices_state.entry(dev_id.clone()).or_default();
        self.devices_state.get_mut(dev_id).unwrap()
    }

    pub fn relay_state(&mut self, relay_id: DeviceRelayID) -> &mut RelayState {
        self.relays_state.entry(relay_id).or_default();
        self.relays_state.get_mut(&relay_id).unwrap()
    }

    pub fn sum_relays_consumption(&self, devices: &[&Device]) -> usize {
        sum_relays_consumption(&self.relays_state, devices)
    }

    fn print_summary(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) {
        log::info!("Current consumption: {curr_consumption}");

        let mut table = Table::new();
        table.add_row(row![
            "Device",
            "Relay",
            "Consumption",
            "Min downtime / uptime",
            "On",
            "Since",
            "Online",
            "Enabled device / relay"
        ]);
        for d in devices {
            let dev_online = self.device_state(&d.id).is_online();
            for r in &d.relays {
                let status = self.relay_state(r.id);
                table.add_row(row![
                    d.name,
                    r.name,
                    r.consumption,
                    format!("{} / {}", r.minimal_downtime, r.minimal_uptime),
                    status.is_on().to_string(),
                    status.since,
                    match dev_online {
                        true => "Online",
                        false => "Offline",
                    },
                    format!(
                        "{} / {}",
                        match d.enabled {
                            true => "Enabled",
                            false => "Disabled",
                        },
                        match r.enabled {
                            true => "Enabled",
                            false => "Disabled",
                        }
                    )
                ]);
            }
        }
        table.printstd();
    }

    pub fn estimated_consumption_without_relays(
        &self,
        curr_consumption: EnergyConsumption,
        devices: &[&Device],
    ) -> EnergyConsumption {
        curr_consumption - self.sum_relays_consumption(devices) as i32
    }

    /// Refresh energy engine; this method shall never fail !
    pub fn refresh(&mut self, curr_consumption: EnergyConsumption, devices: &[&Device]) {
        let base_production = self.estimated_consumption_without_relays(curr_consumption, devices);
        log::info!("Estimated base production: {base_production}");

        // Force creation of missing relays state
        for d in devices {
            for r in &d.relays {
                // Requesting relay state is enough to trigger relay creation
                self.relay_state(r.id);
            }
        }

        let mut new_relays_state = self.relays_state.clone();

        // Forcefully turn off relays that belongs to offline devices
        for d in devices {
            if !self.device_state(&d.id).is_online() {
                for r in &d.relays {
                    new_relays_state.get_mut(&r.id).unwrap().on = false;
                }
            }
        }

        // Forcefully turn off disabled relays
        for d in devices {
            for r in &d.relays {
                if !r.enabled || !d.enabled {
                    new_relays_state.get_mut(&r.id).unwrap().on = false;
                }
            }
        }

        // Forcefully turn off relays with missing dependency
        loop {
            let mut changed = false;

            for d in devices {
                for r in &d.relays {
                    if new_relays_state.get(&r.id).unwrap().is_off() {
                        continue;
                    }

                    // Check if any dependency of relay is off
                    if r.is_missing_dependencies(&new_relays_state) {
                        new_relays_state.get_mut(&r.id).unwrap().on = false;
                        changed = true;
                    }
                }
            }

            if !changed {
                break;
            }
        }

        // Virtually turn off all relays that can be stopped
        loop {
            let mut changed = false;

            for d in devices {
                for r in &d.relays {
                    let state = new_relays_state.get(&r.id).unwrap();
                    if state.is_off() {
                        continue;
                    }

                    // Check if minimal runtime has not been reached
                    if (state.since + r.minimal_uptime) as i64 > time_secs() as i64 {
                        continue;
                    }

                    // Check that no relay that depends on this relay are turned on
                    if r.has_running_dependencies(&new_relays_state, devices) {
                        continue;
                    }

                    new_relays_state.get_mut(&r.id).unwrap().on = false;
                    changed = true;
                }
            }

            if !changed {
                break;
            }
        }

        // Turn on relays with running constraints (only ENABLED)
        for d in devices {
            for r in &d.relays {
                if !r.enabled || !d.enabled || !self.device_state(&d.id).is_online() {
                    continue;
                }

                if new_relays_state.get(&r.id).unwrap().is_on() {
                    continue;
                }

                let Some(constraints) = &r.daily_runtime else {
                    continue;
                };

                if !constraints.catch_up_hours.contains(&curr_hour()) {
                    continue;
                }

                let total_runtime = relay_state_history::relay_total_runtime_adjusted(r);

                if total_runtime > constraints.min_runtime {
                    continue;
                }

                log::info!(
                    "Forcefully turn on relay {} to catch up running constraints (only {}s this day)",
                    r.name,
                    total_runtime
                );
                new_relays_state.get_mut(&r.id).unwrap().on = true;
            }
        }

        // Order relays
        let mut ordered_relays = devices
            .iter()
            .filter(|d| self.device_state(&d.id).is_online() && d.enabled)
            .flat_map(|d| &d.relays)
            .filter(|r| r.enabled)
            .collect::<Vec<_>>();
        ordered_relays.sort_by_key(|r| r.priority);
        ordered_relays.reverse();

        loop {
            let mut changed = false;
            for relay in &ordered_relays {
                if new_relays_state.get(&relay.id).unwrap().is_on() {
                    continue;
                }

                if !relay.enabled {
                    continue;
                }

                let real_relay_state = self.relays_state.get(&relay.id).unwrap();
                if real_relay_state.is_off()
                    && (real_relay_state.since + relay.minimal_downtime) as u64 > time_secs()
                {
                    continue;
                }

                if relay.is_missing_dependencies(&new_relays_state) {
                    continue;
                }

                if relay.is_having_conflict(&new_relays_state, devices) {
                    continue;
                }

                let new_consumption = base_production
                    + sum_relays_consumption(&new_relays_state, devices) as EnergyConsumption;

                if new_consumption + relay.consumption as i32 > AppConfig::get().production_margin {
                    continue;
                }

                log::info!("Turn on relay {}", relay.name);
                new_relays_state.get_mut(&relay.id).unwrap().on = true;
                changed = true;
            }

            if !changed {
                break;
            }
        }

        // Commit changes
        for (id, new_state) in &new_relays_state {
            let curr_state = self.relay_state(*id);
            if curr_state.on != new_state.on {
                curr_state.on = new_state.on;
                curr_state.since = time_secs() as usize;
                log::info!("Changing state of {id:?} to {}", new_state.on);
            }
        }

        self.print_summary(curr_consumption, devices);
    }

    /// Save relays state to disk
    pub fn persist_relays_state(&mut self, devices: &[&Device]) -> anyhow::Result<()> {
        // Save all relays state
        for d in devices {
            for r in &d.relays {
                let mut file = RelayStateHistory::open(r.id, time_secs())?;
                file.set_state(time_secs(), self.relay_state(r.id).is_on())?;
                file.save()?;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod test {
    use crate::devices::device::{Device, DeviceId, DeviceRelayID};
    use crate::energy::consumption::EnergyConsumption;
    use crate::energy::engine::EnergyEngine;
    use crate::utils::time_utils::time_secs;
    use rust_embed::Embed;

    #[derive(serde::Deserialize)]
    struct TestRelayState {
        id: DeviceRelayID,
        on: bool,
        r#for: usize,
        should_be_on: bool,
    }

    #[derive(serde::Deserialize)]
    struct TestDeviceState {
        id: DeviceId,
        online: bool,
        relays: Vec<TestRelayState>,
    }

    #[derive(serde::Deserialize)]
    struct TestConfig {
        curr_consumption: EnergyConsumption,
        devices: Vec<Device>,
    }

    #[derive(serde::Deserialize)]
    struct TestConfigState {
        devices: Vec<TestDeviceState>,
    }

    fn parse_test_config(
        conf: &str,
    ) -> (
        Vec<Device>,
        EnergyEngine,
        EnergyConsumption,
        Vec<TestDeviceState>,
    ) {
        let config: TestConfig = serde_yml::from_str(conf).unwrap();
        let test_config: TestConfigState = serde_yml::from_str(conf).unwrap();

        let mut engine = EnergyEngine {
            devices_state: Default::default(),
            relays_state: Default::default(),
        };

        for d in &test_config.devices {
            engine.device_state(&d.id).last_ping = match d.online {
                true => time_secs() - 1,
                false => 10,
            };

            for r in &d.relays {
                let s = engine.relay_state(r.id);
                s.on = r.on;
                s.since = time_secs() as usize - r.r#for;
            }
        }

        (
            config.devices,
            engine,
            config.curr_consumption,
            test_config.devices,
        )
    }

    fn run_test(name: &str, conf: &str) {
        let (devices, mut energy_engine, consumption, states) = parse_test_config(conf);

        energy_engine.refresh(consumption, &devices.iter().collect::<Vec<_>>());

        for (device_s, device) in states.iter().zip(&devices) {
            for (relay_s, relay) in device_s.relays.iter().zip(&device.relays) {
                let is_on = energy_engine.relay_state(relay_s.id).on;

                assert_eq!(
                    energy_engine.relay_state(relay_s.id).on,
                    relay_s.should_be_on,
                    "For test {name} on relay {} got state {is_on} instead of {}",
                    relay.name,
                    relay_s.should_be_on
                );
            }
        }
    }

    #[derive(Embed)]
    #[folder = "engine_test/"]
    struct Asset;

    #[test]
    fn test_confs() {
        for file in Asset::iter() {
            let content = Asset::get(&file).unwrap();

            log::info!("Testing {file}");

            run_test(&file, &String::from_utf8(content.data.to_vec()).unwrap())
        }
    }
}