use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};

use crate::{config::PlantConfig, in_time_range, plant_hal};
use crate::plant_hal::BoardInteraction;

const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels

#[derive(Debug, PartialEq, Serialize)]
pub enum MoistureSensorError {
    ShortCircuit { hz: f32, max: f32 },
    OpenLoop { hz: f32, min: f32 },
    BoardError(String),
}

#[derive(Debug, PartialEq, Serialize)]
pub enum MoistureSensorState {
    Disabled,
    MoistureValue { raw_hz: f32, moisture_percent: f32 },
    SensorError(MoistureSensorError),
}

impl MoistureSensorState {
    pub fn is_err(&self) -> Option<&MoistureSensorError> {
        match self {
            MoistureSensorState::SensorError(moisture_sensor_error) => Some(moisture_sensor_error),
            _ => None,
        }
    }

    pub fn moisture_percent(&self) -> Option<f32> {
        if let MoistureSensorState::MoistureValue {
            raw_hz: _,
            moisture_percent,
        } = self
        {
            Some(*moisture_percent)
        } else {
            None
        }
    }
}

impl MoistureSensorState {}

#[derive(Debug, PartialEq, Serialize)]
pub enum PumpError {
    PumpNotWorking {
        failed_attempts: usize,
        max_allowed_failures: usize,
    },
}

#[derive(Debug, Serialize)]
pub struct PumpState {
    consecutive_pump_count: u32,
    previous_pump: Option<DateTime<Utc>>,
}

impl PumpState {
    fn is_err(&self, plant_config: &PlantConfig) -> Option<PumpError> {
        if self.consecutive_pump_count > plant_config.max_consecutive_pump_count as u32 {
            Some(PumpError::PumpNotWorking {
                failed_attempts: self.consecutive_pump_count as usize,
                max_allowed_failures: plant_config.max_consecutive_pump_count as usize,
            })
        } else {
            None
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
pub enum PlantWateringMode {
    OFF,
    TargetMoisture,
    TimerOnly,
}

pub struct PlantState {
    pub sensor_a: MoistureSensorState,
    pub sensor_b: MoistureSensorState,
    pub pump: PumpState,
}

fn map_range_moisture(
    s: f32,
    min_frequency: Option<f32>,
    max_frequency: Option<f32>,
) -> Result<f32, MoistureSensorError> {
    // Use overrides if provided, otherwise fallback to defaults
    let min_freq = min_frequency.unwrap_or(MOIST_SENSOR_MIN_FREQUENCY);
    let max_freq = max_frequency.unwrap_or(MOIST_SENSOR_MAX_FREQUENCY);

    if s < min_freq {
        return Err(MoistureSensorError::OpenLoop {
            hz: s,
            min: min_freq,
        });
    }
    if s > max_freq {
        return Err(MoistureSensorError::ShortCircuit {
            hz: s,
            max: max_freq,
        });
    }
    let moisture_percent = (s - min_freq) * 100.0 / (max_freq - min_freq);

    Ok(moisture_percent)
}

impl PlantState {
    pub fn read_hardware_state(
        plant_id: usize,
        board: &mut plant_hal::HAL,
        config: &PlantConfig,
    ) -> Self {
        let sensor_a = if config.sensor_a {
            match board.measure_moisture_hz(plant_id, plant_hal::Sensor::A) {
                Ok(raw) => match map_range_moisture(
                    raw,
                    config.moisture_sensor_min_frequency,
                    config.moisture_sensor_max_frequency,
                ) {
                    Ok(moisture_percent) => MoistureSensorState::MoistureValue {
                        raw_hz: raw,
                        moisture_percent,
                    },
                    Err(err) => MoistureSensorState::SensorError(err),
                },
                Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
                    err.to_string(),
                )),
            }
        } else {
            MoistureSensorState::Disabled
        };

        let sensor_b = if config.sensor_b {
            match board.measure_moisture_hz(plant_id, plant_hal::Sensor::B) {
                Ok(raw) => match map_range_moisture(
                    raw,
                    config.moisture_sensor_min_frequency,
                    config.moisture_sensor_max_frequency,
                ) {
                    Ok(moisture_percent) => MoistureSensorState::MoistureValue {
                        raw_hz: raw,
                        moisture_percent,
                    },
                    Err(err) => MoistureSensorState::SensorError(err),
                },
                Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
                    err.to_string(),
                )),
            }
        } else {
            MoistureSensorState::Disabled
        };

        let previous_pump = board.last_pump_time(plant_id);
        let consecutive_pump_count = board.consecutive_pump_count(plant_id);
        let state = Self {
            sensor_a,
            sensor_b,
            pump: PumpState {
                consecutive_pump_count,
                previous_pump,
            },
        };
        if state.is_err() {
            let _ = board.fault(plant_id, true);
        }
        state
    }

    pub fn pump_in_timeout(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> bool {
        if matches!(plant_conf.mode, PlantWateringMode::OFF) {
            return false;
        }
        self.pump.previous_pump.is_some_and(|last_pump| {
            last_pump
                .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
                .is_some_and(|earliest_next_allowed_pump| {
                    earliest_next_allowed_pump > *current_time
                })
        })
    }

    pub fn is_err(&self) -> bool {
        self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some()
    }

    pub fn plant_moisture(
        &self,
    ) -> (
        Option<f32>,
        (Option<&MoistureSensorError>, Option<&MoistureSensorError>),
    ) {
        match (
            self.sensor_a.moisture_percent(),
            self.sensor_b.moisture_percent(),
        ) {
            (Some(moisture_a), Some(moisture_b)) => {
                (Some((moisture_a + moisture_b) / 2.), (None, None))
            }
            (Some(moisture_percent), _) => (Some(moisture_percent), (None, self.sensor_b.is_err())),
            (_, Some(moisture_percent)) => (Some(moisture_percent), (self.sensor_a.is_err(), None)),
            _ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
        }
    }

    pub fn needs_to_be_watered(
        &self,
        plant_conf: &PlantConfig,
        current_time: &DateTime<Tz>,
    ) -> bool {
        match plant_conf.mode {
            PlantWateringMode::OFF => false,
            PlantWateringMode::TargetMoisture => {
                let (moisture_percent, _) = self.plant_moisture();
                if let Some(moisture_percent) = moisture_percent {
                    if self.pump_in_timeout(plant_conf, current_time) {
                        false
                    } else if moisture_percent < plant_conf.target_moisture {
                        in_time_range(
                            current_time,
                            plant_conf.pump_hour_start,
                            plant_conf.pump_hour_end,
                        )
                    } else {
                        false
                    }
                } else {
                    // in case no moisture can be determined, do not water the plant
                    false
                }
            }
            PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
        }
    }

    pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo {
        PlantInfo {
            sensor_a: &self.sensor_a,
            sensor_b: &self.sensor_b,
            mode: plant_conf.mode,
            do_water: self.needs_to_be_watered(plant_conf, current_time),
            dry: if let Some(moisture_percent) = self.plant_moisture().0 {
                moisture_percent < plant_conf.target_moisture
            } else {
                false
            },
            cooldown: self.pump_in_timeout(plant_conf, current_time),
            out_of_work_hour: in_time_range(
                current_time,
                plant_conf.pump_hour_start,
                plant_conf.pump_hour_end,
            ),
            consecutive_pump_count: self.pump.consecutive_pump_count,
            pump_error: self.pump.is_err(plant_conf),
            last_pump: self
                .pump
                .previous_pump
                .map(|t| t.with_timezone(&current_time.timezone())),
            next_pump: if matches!(
                plant_conf.mode,
                PlantWateringMode::TimerOnly | PlantWateringMode::TargetMoisture
            ) {
                self.pump.previous_pump.and_then(|last_pump| {
                    last_pump
                        .checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
                        .map(|t| t.with_timezone(&current_time.timezone()))
                })
            } else {
                None
            },
        }
    }
}

#[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked
pub struct PlantInfo<'a> {
    /// state of humidity sensor on bank a
    sensor_a: &'a MoistureSensorState,
    /// state of humidity sensor on bank b
    sensor_b: &'a MoistureSensorState,
    /// configured plant watering mode
    mode: PlantWateringMode,
    /// the plant needs to be watered
    do_water: bool,
    /// plant is considered to be dry according to settings
    dry: bool,
    /// plant irrigation cooldown is active
    cooldown: bool,
    /// plant should not be watered at this time of day TODO: does this really belong here? Isn't this a global setting?
    out_of_work_hour: bool,
    /// how often has the pump been watered without reaching target moisture
    consecutive_pump_count: u32,
    pump_error: Option<PumpError>,
    /// last time when the pump was active
    last_pump: Option<DateTime<Tz>>,
    /// next time when pump should activate
    next_pump: Option<DateTime<Tz>>,
}
