use std::sync::{atomic::AtomicBool, Arc, Mutex};

use chrono::{DateTime, Datelike, Duration, NaiveDateTime, Timelike};
use chrono_tz::{Europe::Berlin, Tz};

use esp_idf_hal::delay::Delay;
use esp_idf_sys::{
    esp_deep_sleep, esp_ota_get_app_partition_count, esp_ota_get_running_partition, esp_ota_get_state_partition, esp_ota_img_states_t, esp_ota_img_states_t_ESP_OTA_IMG_ABORTED, esp_ota_img_states_t_ESP_OTA_IMG_INVALID, esp_ota_img_states_t_ESP_OTA_IMG_NEW, esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY, esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED, esp_ota_img_states_t_ESP_OTA_IMG_VALID, esp_restart, gpio_deep_sleep_hold_dis, gpio_deep_sleep_hold_en, vTaskDelay, CONFIG_FREERTOS_HZ
};
use log::error;
use once_cell::sync::Lazy;
use plant_hal::{CreatePlantHal, PlantCtrlBoard, PlantCtrlBoardInteraction, PlantHal, PLANT_COUNT};
use serde::{Deserialize, Serialize};

use crate::{
    config::{Config, WifiConfig},
    espota::{mark_app_valid, rollback_and_reboot},
    webserver::webserver::{httpd, httpd_initial},
};
mod config;
pub mod espota;
pub mod plant_hal;

const MOIST_SENSOR_MAX_FREQUENCY: u32 = 5200; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: u32 = 500; // 0.5kHz (500Hz margin)

const FROM: (f32, f32) = (
    MOIST_SENSOR_MIN_FREQUENCY as f32,
    MOIST_SENSOR_MAX_FREQUENCY as f32,
);
const TO: (f32, f32) = (0_f32, 100_f32);

pub static BOARD_ACCESS: Lazy<Mutex<PlantCtrlBoard>> = Lazy::new(|| PlantHal::create().unwrap());
pub static STAY_ALIVE: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));

mod webserver {
    pub mod webserver;
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
enum OnlineMode {
    Offline,
    Wifi,
    SnTp,
    Online,
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
enum WaitType {
    InitialConfig,
    FlashError,
    NormalConfig,
    StayAlive,
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
struct LightState {
    active: bool,
    out_of_work_hour: bool,
    battery_low: bool,
    is_day: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Default)]
struct PlantState {
    a: Option<u8>,
    b: Option<u8>,
    p: Option<u8>,
    after_p: Option<u8>,
    do_water: bool,
    frozen: bool,
    dry: bool,
    active: bool,
    pump_error: bool,
    not_effective: bool,
    cooldown: bool,
    no_water: bool,
    sensor_error_a: Option<SensorError>,
    sensor_error_b: Option<SensorError>,
    sensor_error_p: Option<SensorError>,
    out_of_work_hour: bool,
    next_pump: Option<DateTime<Tz>>,
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]
enum SensorError {
    Unknown,
    ShortCircuit { hz: f32, max: f32 },
    OpenCircuit { hz: f32, min: f32 },
}

#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Default)]
struct TankState {
    enough_water: bool,
    left_ml: u32,
    sensor_error: bool,
    raw: u16,
}

fn safe_main() -> anyhow::Result<()> {
    // It is necessary to call this function once. Otherwise some patches to the runtime
    // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
    esp_idf_svc::sys::link_patches();

    // Bind the log crate to the ESP Logging facilities
    esp_idf_svc::log::EspLogger::initialize_default();

    if esp_idf_sys::CONFIG_MAIN_TASK_STACK_SIZE < 20000 {
        error!(
            "stack too small: {} bail!",
            esp_idf_sys::CONFIG_MAIN_TASK_STACK_SIZE
        );
        return Ok(());
    }

    log::info!("Startup Rust");

    let git_hash = env!("VERGEN_GIT_DESCRIBE");
    let build_timestamp = env!("VERGEN_BUILD_TIMESTAMP");
    println!(
        "Version useing git has {} build on {}",
        git_hash, build_timestamp
    );

    let count = unsafe { esp_ota_get_app_partition_count() };
    println!("Partition count is {}", count);
    let mut ota_state: esp_ota_img_states_t = 0;
    let running_partition = unsafe { esp_ota_get_running_partition() };
    let address = unsafe {
        (*running_partition).address
        
    };
    println!("Partition address is {}", address);
    
    let ota_state_string = unsafe {
        esp_ota_get_state_partition(running_partition, &mut ota_state);
        if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_NEW {
            format!("Partition state is {}", "ESP_OTA_IMG_NEW")
        } else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY {
            format!("Partition state is {}", "ESP_OTA_IMG_PENDING_VERIFY")
        } else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_VALID {
            format!("Partition state is {}", "ESP_OTA_IMG_VALID")
        } else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_INVALID {
            format!("Partition state is {}", "ESP_OTA_IMG_INVALID")
        } else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_ABORTED {
            format!("Partition state is {}", "ESP_OTA_IMG_ABORTED")
        } else if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_UNDEFINED {
            format!("Partition state is {}", "ESP_OTA_IMG_UNDEFINED")
        } else {
            format!("Partition state is {}", ota_state)
        }
    };
    println!("{}", ota_state_string);

    println!("Board hal init");
    let mut board: std::sync::MutexGuard<'_, PlantCtrlBoard<'_>> = BOARD_ACCESS.lock().unwrap();

    println!("Mounting filesystem");
    board.mount_file_system()?;
    let free_space = board.file_system_size()?;
    println!(
        "Mounted, total space {} used {} free {}",
        free_space.total_size, free_space.used_size, free_space.free_size
    );

    let time = board.time();
    let mut cur = match time {
        Ok(cur) => cur,
        Err(err) => {
            log::error!("time error {}", err);
            NaiveDateTime::from_timestamp_millis(0).unwrap().and_utc()
        }
    };
    //check if we know the time current > 2020
    if cur.year() < 2020 {
        if board.is_day() {
            //assume TZ safe times ;)
            cur = *cur.with_hour(15).get_or_insert(cur);
        } else {
            cur = *cur.with_hour(3).get_or_insert(cur);
        }
    }

    println!("cur is {}", cur);

    if board.is_config_reset() {
        board.general_fault(true);
        println!("Reset config is pressed, waiting 5s");
        for _i in 0..25 {
            board.general_fault(true);
            Delay::new_default().delay_ms(50);
            board.general_fault(false);
            Delay::new_default().delay_ms(50);
        }

        if board.is_config_reset() {
            println!("Reset config is still pressed, deleting configs and reboot");
            match board.remove_configs() {
                Ok(case) => {
                    println!("Succeeded in deleting config {}", case);
                }
                Err(err) => {
                    println!("Could not remove config files, system borked {}", err);
                    //terminate main app and freeze

                    wait_infinity(WaitType::FlashError, Arc::new(AtomicBool::new(false)));
                }
            }
        } else {
            board.general_fault(false);
        }
    }

    let mut online_mode = OnlineMode::Offline;
    let wifi_conf = board.get_wifi();
    let wifi: WifiConfig;
    match wifi_conf {
        Ok(conf) => {
            wifi = conf;
        }
        Err(err) => {
            if board.is_wifi_config_file_existant() {
                if ota_state == esp_ota_img_states_t_ESP_OTA_IMG_PENDING_VERIFY {
                    println!("Config seem to be unparsable after upgrade, reverting");
                    rollback_and_reboot()?;
                }
            }
            println!("Missing wifi config, entering initial config mode {}", err);
            board.wifi_ap().unwrap();
            //config upload will trigger reboot!
            drop(board);
            let reboot_now = Arc::new(AtomicBool::new(false));
            let _webserver = httpd_initial(reboot_now.clone());
            wait_infinity(WaitType::InitialConfig, reboot_now.clone());
        }
    };

    println!("attempting to connect wifi");
    let mut ip_address: Option<String>  = None;
    match board.wifi(wifi.ssid, wifi.password, 10000) {
        Ok(ip_info) => {
            ip_address  = Some(ip_info.ip.to_string());
            online_mode = OnlineMode::Wifi;
        }
        Err(_) => {
            println!("Offline mode");
            board.general_fault(true);
        }
    }

    if online_mode == OnlineMode::Wifi {
        match board.sntp(1000 * 5) {
            Ok(new_time) => {
                cur = new_time;
                online_mode = OnlineMode::SnTp;
            }
            Err(err) => {
                println!("sntp error: {}", err);
                board.general_fault(true);
            }
        }
    }

    println!("Running logic at utc {}", cur);
    let europe_time = cur.with_timezone(&Berlin);
    println!("Running logic at europe/berlin {}", europe_time);

    let config: Config;
    match board.get_config() {
        Ok(valid) => {
            config = valid;
        }
        Err(err) => {
            println!("Missing normal config, entering config mode {}", err);
            //config upload will trigger reboot!
            drop(board);
            let reboot_now = Arc::new(AtomicBool::new(false));
            let _webserver = httpd(reboot_now.clone());
            wait_infinity(WaitType::NormalConfig, reboot_now.clone());
        }
    }

    //do mqtt before config check, as mqtt might configure
    if online_mode == OnlineMode::SnTp {
        match board.mqtt(&config) {
            Ok(_) => {
                println!("Mqtt connection ready");
                online_mode = OnlineMode::Online;
            }
            Err(err) => {
                println!("Could not connect mqtt due to {}", err);
            }
        }
    }

    if online_mode == OnlineMode::Online {
        match ip_address {
            Some(add_some) => {
                let _ = board.mqtt_publish(&config, "/firmware/address", add_some.as_bytes());
            },
            None => {
                let _ = board.mqtt_publish(&config, "/firmware/address", "N/A?".as_bytes());
            },
        }
        let _ = board.mqtt_publish(&config, "/firmware/githash", git_hash.as_bytes());
        let _ = board.mqtt_publish(&config, "/firmware/buildtime", build_timestamp.as_bytes());
        let _ = board.mqtt_publish(&config, "/firmware/last_online", europe_time.to_rfc3339().as_bytes());
        let _ = board.mqtt_publish(&config, "/firmware/ota_state", ota_state_string.as_bytes());
        let _ = board.mqtt_publish(&config, "/firmware/partition_address", format!("{:#06x}",address).as_bytes());
        let _ = board.mqtt_publish(&config, "/state", "online".as_bytes());

        publish_battery_state(&mut board, &config);
    }

    let tank_state = determine_tank_state(&mut board, &config);
    if online_mode == OnlineMode::Online {
        if tank_state.sensor_error {
            let _ = board.mqtt_publish(&config, "/water/ml", "error".to_string().as_bytes());
        } else {
            let _ = board.mqtt_publish(&config, "/water/ml", tank_state.left_ml.to_string().as_bytes());
            let _ = board.mqtt_publish(&config, "/water/enough_water", tank_state.enough_water.to_string().as_bytes());
            let _ = board.mqtt_publish(&config, "/water/raw", tank_state.raw.to_string().as_bytes());
        } 
    }


    let mut water_frozen = false;
    for _attempt in 0..5 {
        let water_temperature = board.water_temperature_c();
        match water_temperature {
            Ok(temp) => {
                if online_mode == OnlineMode::Online {
                    let _ = board.mqtt_publish(
                        &config,
                        "/water/temperature",
                        temp.to_string().as_bytes(),
                    );
                }
                //FIXME mqtt here
                println!("Water temp is {}", temp);
                if temp < 4_f32 {
                    water_frozen = true;
                }
                break;
            }
            Err(err) => {
                if online_mode == OnlineMode::Online {
                    let _ = board.mqtt_publish(&config, "/water/temperature", "Error".as_bytes());
                }
                println!("Could not get water temp {}", err)
            }
        }
    }

    let mut plantstate = [PlantState {
        ..Default::default()
    }; PLANT_COUNT];
    let plant_to_pump = determine_next_plant(
        online_mode,
        &mut plantstate,
        europe_time,
        &tank_state,
        water_frozen,
        &config,
        &mut board,
    );

    if STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed) {
        drop(board);
        let reboot_now = Arc::new(AtomicBool::new(false));
        let _webserver = httpd(reboot_now.clone());
        wait_infinity(WaitType::StayAlive, reboot_now.clone());
    }

    match plant_to_pump {
        Some(plant) => {
            let mut state = plantstate[plant];
            let consecutive_pump_count = board.consecutive_pump_count(plant) + 1;
            board.store_consecutive_pump_count(plant, consecutive_pump_count);
            let plant_config = config.plants[plant];

            if plant_config.sensor_p {
                match map_range_moisture(
                    board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
                ) {
                    Ok(p) => state.p = Some(p),
                    Err(err) => {
                        board.fault(plant, true);
                        state.sensor_error_p = Some(err);
                    }
                }
            }

            println!(
                "Trying to pump for {}s with pump {} now",
                plant_config.pump_time_s, plant
            );

            board.any_pump(true)?;
            board.store_last_pump_time(plant, cur);
            board.pump(plant, true)?;
            board.last_pump_time(plant);
            state.active = true;
            for _ in 0..plant_config.pump_time_s {
                unsafe { vTaskDelay(CONFIG_FREERTOS_HZ) };
                if plant_config.sensor_p {
                    let moist = map_range_moisture(
                        board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
                    );
                    if online_mode == OnlineMode::Online {
                        let _ = board.mqtt_publish(
                            &config,
                            format!("/plant{}/Sensor P after", plant + 1).as_str(),
                            option_to_string(moist.ok()).as_bytes(),
                        );
                    }
                } else {
                    if online_mode == OnlineMode::Online {
                        let _ = board.mqtt_publish(
                            &config,
                            format!("/plant{}/Sensor P after", plant + 1).as_str(),
                            "disabled".as_bytes(),
                        );
                    }
                }
            }

            board.pump(plant, false)?;
            if plant_config.sensor_p {
                match map_range_moisture(
                    board.measure_moisture_hz(plant, plant_hal::Sensor::PUMP)? as f32,
                ) {
                    Ok(p) => state.after_p = Some(p),
                    Err(err) => {
                        board.fault(plant, true);
                        state.sensor_error_p = Some(err);
                    }
                }
                if state.after_p.is_none()
                    || state.p.is_none()
                    || state.after_p.unwrap() < state.p.unwrap() + 5
                {
                    state.pump_error = true;
                    board.fault(plant, true);
                    //mqtt sync pump error value
                }
            }
        }
        None => {
            println!("Nothing to do");
        }
    }
    if online_mode == OnlineMode::Online {
        update_plant_state(&mut plantstate, &mut board, &config);
    }
    let mut light_state = LightState {
        ..Default::default()
    };
    let is_day = board.is_day();
    light_state.is_day = is_day;
    light_state.out_of_work_hour = !in_time_range(
        europe_time,
        config.night_lamp_hour_start,
        config.night_lamp_hour_end,
    );

    let state_of_charge = board.state_charge_percent().unwrap_or(0);
    if state_of_charge < 30 {
        board.set_low_voltage_in_cycle();
    } else if state_of_charge > 50 {
        board.clear_low_voltage_in_cycle();
    }
    light_state.battery_low = board.low_voltage_in_cycle();

    if !light_state.out_of_work_hour {
        if config.night_lamp_only_when_dark {
            if !light_state.is_day {
                if light_state.battery_low {
                    board.light(false).unwrap();
                } else {
                    light_state.active = true;
                    board.light(true).unwrap();
                }
            }
        } else {
            if light_state.battery_low {
                board.light(false).unwrap();
            } else {
                light_state.active = true;
                board.light(true).unwrap();
            }
        }
    } else {
        light_state.active = false;
        board.light(false).unwrap();
    }

    println!("Lightstate is {:?}", light_state);

    if online_mode == OnlineMode::Online {
        match serde_json::to_string(&light_state) {
            Ok(state) => {
                let _ = board.mqtt_publish(&config, "/light/active", state.as_bytes());
            }
            Err(err) => {
                println!("Error publishing lightstate {}", err);
            }
        };
    }

    //relatch
    unsafe { gpio_deep_sleep_hold_dis() };
    unsafe { gpio_deep_sleep_hold_en() };

    let deep_sleep_duration_minutes: u32 = if state_of_charge < 10 {
        if online_mode == OnlineMode::Online {
            let _ = board.mqtt_publish(
                &config,
                "/deepsleep",
                "Entering low voltage long deep sleep".as_bytes(),
            );
        }
        12 * 60
    } else if is_day {
        if online_mode == OnlineMode::Online {
            let _ = board.mqtt_publish(
                &config,
                "/deepsleep",
                "Entering normal mode 20m deep sleep".as_bytes(),
            );
        }
        20
    } else {
        if online_mode == OnlineMode::Online {
            let _ = board.mqtt_publish(
                &config,
                "/deepsleep",
                "Entering night mode 1h deep sleep".as_bytes(),
            );
        }
        60
    };
    //determine next event
    //is light out of work trigger soon?
    //is battery low ??
    //is deep sleep

    mark_app_valid();

    unsafe { esp_deep_sleep(1000 * 1000 * 60 * deep_sleep_duration_minutes as u64) };
}

fn publish_battery_state(
    board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
    config: &Config,
) {
    match board.voltage_milli_volt() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/voltage_milli_volt",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(&config, "/battery/voltage_milli_volt", "-1".as_bytes());
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.average_current_milli_ampere() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/average_current_milli_ampere",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/average_current_milli_ampere",
                "-1".as_bytes(),
            );
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.cycle_count() {
        Ok(v) => {
            let _ = board.mqtt_publish(&config, "/battery/cycle_count", v.to_string().as_bytes());
        }
        Err(err) => {
            let _ = board.mqtt_publish(&config, "/battery/cycle_count", "-1".as_bytes());
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.design_milli_ampere_hour() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/design_milli_ampere_hour",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/design_milli_ampere_hour",
                "-1".as_bytes(),
            );
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.max_milli_ampere_hour() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/max_milli_ampere_hour",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(&config, "/battery/max_milli_ampere_hour", "-1".as_bytes());
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.remaining_milli_ampere_hour() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/remaining_milli_ampere_hour",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/remaining_milli_ampere_hour",
                "-1".as_bytes(),
            );
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.state_charge_percent() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/state_charge_percent",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(&config, "/battery/state_charge_percent", "-1".as_bytes());
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
    match board.state_health_percent() {
        Ok(v) => {
            let _ = board.mqtt_publish(
                &config,
                "/battery/state_health_percent",
                v.to_string().as_bytes(),
            );
        }
        Err(err) => {
            let _ = board.mqtt_publish(&config, "/battery/state_health_percent", "-1".as_bytes());
            let _ = board.mqtt_publish(&config, "/errorlog", format!("{:?}", err).as_bytes());
        }
    };
}

fn determine_tank_state(
    board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
    config: &Config,
) -> TankState {
    if config.tank_sensor_enabled {
        let mut rv: TankState = TankState {
            ..Default::default()
        };
        let success = board
            .tank_sensor_percent()
            .and_then(|raw| {
                rv.raw = raw;
                return map_range(
                    (
                        config.tank_empty_percent as f32,
                        config.tank_full_percent as f32,
                    ),
                    raw as f32,
                );
            })
            .and_then(|percent| {
                rv.left_ml = ((percent * config.tank_useable_ml as f32) / 100_f32) as u32;
                println!(
                    "Tank sensor returned mv {} as {}% leaving {} ml useable",
                    rv.raw, percent as u8, rv.left_ml
                );
                if config.tank_warn_percent > percent as u8 {
                    board.general_fault(true);
                    println!(
                        "Low water, current percent is {}, minimum warn level is {}",
                        percent as u8, config.tank_warn_percent
                    );
                }
                if config.tank_empty_percent < percent as u8 {
                    println!(
                        "Enough water, current percent is {}, minimum empty level is {}",
                        percent as u8, config.tank_empty_percent
                    );
                    rv.enough_water = true;
                }
                return Ok(());
            });
        match success {
            Err(err) => {
                println!("Could not determine tank value due to {}", err);
                board.general_fault(true);
                rv.sensor_error = true;
            }
            Ok(_) => {}
        }
        return rv;
    }
    return TankState {
        enough_water: true,
        left_ml: 1337,
        sensor_error: false,
        raw: 0,
    };
}

fn map_range(from_range: (f32, f32), s: f32) -> anyhow::Result<f32> {
    if s < from_range.0 {
        anyhow::bail!(
            "Value out of range, min {} but current is {}",
            from_range.0,
            s
        );
    }
    if s > from_range.1 {
        anyhow::bail!(
            "Value out of range, max {} but current is {}",
            from_range.1,
            s
        );
    }
    return Ok(TO.0 + (s - from_range.0) * (TO.1 - TO.0) / (from_range.1 - from_range.0));
}

fn map_range_moisture(s: f32) -> Result<u8, SensorError> {
    if s < FROM.0 {
        return Err(SensorError::OpenCircuit { hz: s, min: FROM.0 });
    }
    if s > FROM.1 {
        return Err(SensorError::ShortCircuit { hz: s, max: FROM.1 });
    }
    let tmp = TO.0 + (s - FROM.0) * (TO.1 - TO.0) / (FROM.1 - FROM.0);

    return Ok(tmp as u8);
}

fn in_time_range(cur: DateTime<Tz>, start: u8, end: u8) -> bool {
    let curhour = cur.hour() as u8;
    //eg 10-14
    if start < end {
        return curhour > start && curhour < end;
    } else {
        //eg 20-05
        return curhour > start || curhour < end;
    }
}

fn option_to_string(value: Option<u8>) -> String {
    match value {
        Some(v) => v.to_string(),
        None => "Error".to_owned(),
    }
}

fn determine_state_target_moisture_for_plant(
    board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
    plant: usize,
    state: &mut PlantState,
    config: &Config,
    tank_state: &TankState,
    water_frozen: bool,
    cur: DateTime<Tz>,
) {
    let plant_config = &config.plants[plant];
    match board.measure_moisture_hz(plant, plant_hal::Sensor::A) {
        Ok(a) => {
            let mapped = map_range_moisture(a as f32);
            match mapped {
                Ok(result) => state.a = Some(result),
                Err(err) => {
                    state.sensor_error_a = Some(err);
                }
            }
        }
        Err(_) => {
            state.sensor_error_a = Some(SensorError::Unknown);
        }
    }
    if plant_config.sensor_b {
        match board.measure_moisture_hz(plant, plant_hal::Sensor::B) {
            Ok(b) => {
                let mapped = map_range_moisture(b as f32);
                match mapped {
                    Ok(result) => state.b = Some(result),
                    Err(err) => {
                        state.sensor_error_b = Some(err);
                    }
                }
            }
            Err(_) => {
                state.sensor_error_b = Some(SensorError::Unknown);
            }
        }
    }

    //FIXME how to average analyze whatever?
    let a_low = state.a.is_some() && state.a.unwrap() < plant_config.target_moisture;
    let b_low = state.b.is_some() && state.b.unwrap() < plant_config.target_moisture;

    if a_low || b_low {
        state.dry = true;
        if tank_state.sensor_error && !config.tank_allow_pumping_if_sensor_error {
            //ignore is ok
        }
        else if !tank_state.enough_water {
            state.no_water = true;
        }
    }
    let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
    let next_pump = board.last_pump_time(plant) + duration;
    if next_pump > cur {
        let europe_time = next_pump.with_timezone(&Berlin);
        state.next_pump = Some(europe_time);
        state.cooldown = true;
    }
    if !in_time_range(
        cur,
        plant_config.pump_hour_start,
        plant_config.pump_hour_end,
    ) {
        state.out_of_work_hour = true;
    }
    if water_frozen {
        state.frozen = true;
    }
    if state.dry && !state.no_water && !state.cooldown && !state.out_of_work_hour {
        if water_frozen {
            state.frozen = true;
        } else {
            state.do_water = true;
        }
    }
}

fn determine_next_plant(
    online_mode: OnlineMode,
    plantstate: &mut [PlantState; PLANT_COUNT],
    cur: DateTime<Tz>,
    tank_state: &TankState,
    water_frozen: bool,
    config: &Config,
    board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
) -> Option<usize> {
    for plant in 0..PLANT_COUNT {
        let state = &mut plantstate[plant];
        let plant_config = &config.plants[plant];
        match plant_config.mode {
            config::Mode::OFF => {}
            config::Mode::TargetMoisture => {
                determine_state_target_moisture_for_plant(
                    board,
                    plant,
                    state,
                    config,
                    tank_state,
                    water_frozen,
                    cur,
                );
            }
            config::Mode::TimerOnly => {
                let duration = Duration::minutes((plant_config.pump_cooldown_min).into());
                let next_pump = board.last_pump_time(plant) + duration;
                if next_pump > cur {
                    let europe_time = next_pump.with_timezone(&Berlin);
                    state.next_pump = Some(europe_time);
                    state.cooldown = true;
                } else {
                    if water_frozen {
                        state.frozen = true;
                    } else {
                        state.do_water = true;
                    }
                }
            }
            config::Mode::TimerAndDeadzone => {
                let duration = Duration::minutes((60 * plant_config.pump_cooldown_min).into());
                let next_pump = board.last_pump_time(plant) + duration;
                if next_pump > cur {
                    let europe_time = next_pump.with_timezone(&Berlin);
                    state.next_pump = Some(europe_time);
                    state.cooldown = true;
                }
                if !in_time_range(
                    cur,
                    plant_config.pump_hour_start,
                    plant_config.pump_hour_end,
                ) {
                    state.out_of_work_hour = true;
                }
                if !state.cooldown && !state.out_of_work_hour {
                    if water_frozen {
                        state.frozen = true;
                    } else {
                        state.do_water = true;
                    }
                }
            }
        }

        if state.sensor_error_a.is_some()
            || state.sensor_error_b.is_some()
            || state.sensor_error_p.is_some()
        {
            board.fault(plant, true);
        }
        if state.do_water {
            if board.consecutive_pump_count(plant) > config.max_consecutive_pump_count.into() {
                state.not_effective = true;
                board.fault(plant, true);
            }
        } else {
            board.store_consecutive_pump_count(plant, 0);
        }
        println!("Plant {} state is {:?}", plant, state);
    }
    if online_mode == OnlineMode::Online {
        update_plant_state(plantstate, board, config);
    }
    for plant in 0..PLANT_COUNT {
        let state = &plantstate[plant];
        println!(
            "Checking for water plant {} with state {}",
            plant, state.do_water
        );
        if state.do_water {
            return Some(plant);
        }
    }
    println!("No plant needs water");
    return None;
}

fn update_plant_state(
    plantstate: &mut [PlantState; PLANT_COUNT],
    board: &mut std::sync::MutexGuard<'_, PlantCtrlBoard<'_>>,
    config: &Config,
) {
    for plant in 0..PLANT_COUNT {
        let state = &plantstate[plant];
        let plant_config = config.plants[plant];

        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/mode", plant + 1).as_str(),
            match plant_config.mode {
                config::Mode::OFF => "OFF".as_bytes(),
                config::Mode::TargetMoisture => "TargetMoisture".as_bytes(),
                config::Mode::TimerOnly => "TimerOnly".as_bytes(),
                config::Mode::TimerAndDeadzone => "TimerAndDeadzone".as_bytes(),
            },
        );

        let last_time = board.last_pump_time(plant);
        let europe_time = last_time.with_timezone(&Berlin);
        if europe_time.year() > 2023 {
            let time = europe_time.to_rfc3339();
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/last pump", plant + 1).as_str(),
                time.as_bytes(),
            );
        } else {
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/last pump", plant + 1).as_str(),
                "N/A".as_bytes(),
            );
        }

        match state.next_pump {
            Some(next) => {
                let time = next.to_rfc3339();
                let _ = board.mqtt_publish(
                    &config,
                    format!("/plant{}/next pump", plant + 1).as_str(),
                    time.as_bytes(),
                );
            }
            None => {
                let _ = board.mqtt_publish(
                    &config,
                    format!("/plant{}/next pump", plant + 1).as_str(),
                    "N/A".as_bytes(),
                );
            }
        }

        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Sensor A", plant + 1).as_str(),
            option_to_string(state.a).as_bytes(),
        );
        if plant_config.sensor_b {
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor B", plant + 1).as_str(),
                option_to_string(state.b).as_bytes(),
            );
        } else {
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor B", plant + 1).as_str(),
                "disabled".as_bytes(),
            );
        }

        if plant_config.sensor_p {
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor P before", plant + 1).as_str(),
                option_to_string(state.p).as_bytes(),
            );
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor P after", plant + 1).as_str(),
                option_to_string(state.after_p).as_bytes(),
            );
        } else {
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor P before", plant + 1).as_str(),
                "disabled".as_bytes(),
            );
            let _ = board.mqtt_publish(
                &config,
                format!("/plant{}/Sensor P after", plant + 1).as_str(),
                "disabled".as_bytes(),
            );
        }

        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Should water", plant + 1).as_str(),
            state.do_water.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Is frozen", plant + 1).as_str(),
            state.frozen.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Is dry", plant + 1).as_str(),
            state.dry.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Pump Error", plant + 1).as_str(),
            state.pump_error.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Pump Ineffective", plant + 1).as_str(),
            state.not_effective.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Is in Cooldown", plant + 1).as_str(),
            state.cooldown.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/No Water", plant + 1).as_str(),
            state.no_water.to_string().as_bytes(),
        );
        let _ = board.mqtt_publish(
            &config,
            format!("/plant{}/Out of Work Hour", plant + 1).as_str(),
            state.out_of_work_hour.to_string().as_bytes(),
        );
    }
}

fn wait_infinity(wait_type: WaitType, reboot_now: Arc<AtomicBool>) -> ! {
    let delay = match wait_type {
        WaitType::InitialConfig => 250_u32,
        WaitType::FlashError => 100_u32,
        WaitType::NormalConfig => 500_u32,
        WaitType::StayAlive => 1000_u32,
    };
    let led_count = match wait_type {
        WaitType::InitialConfig => 8,
        WaitType::FlashError => 8,
        WaitType::NormalConfig => 4,
        WaitType::StayAlive => 2,
    };
    loop {
        unsafe {
            //do not trigger watchdog
            for i in 0..8 {
                BOARD_ACCESS.lock().unwrap().fault(i, i < led_count);
            }
            BOARD_ACCESS.lock().unwrap().general_fault(true);
            vTaskDelay(delay);
            BOARD_ACCESS.lock().unwrap().general_fault(false);
            for i in 0..8 {
                BOARD_ACCESS.lock().unwrap().fault(i, false);
            }
            vTaskDelay(delay);
            if wait_type == WaitType::StayAlive
                && !STAY_ALIVE.load(std::sync::atomic::Ordering::Relaxed)
            {
                reboot_now.store(true, std::sync::atomic::Ordering::Relaxed);
            }
            if reboot_now.load(std::sync::atomic::Ordering::Relaxed) {
                println!("Rebooting");
                esp_restart();
            }
        }
    }
}

fn main() {
    let result = safe_main();
    match result {
        Ok(_) => {
            println!("Main app finished, restarting");
            unsafe { esp_restart() };
        },
        Err(err) => {
            println!("Failed main {}", err);
            let rollback_successful = rollback_and_reboot();
            println!("Failed to rollback :(");
            rollback_successful.unwrap();
        },
    }
}
//error codes
//error_reading_config_after_upgrade
//error_no_config_after_upgrade
//error_tank_sensor_fault
