//! Demonstrates pre-write hooks generated by `#[modbus_app]`.
//!
//! Covered behaviors:
//! - `on_write_N = ...` for single FC05 / FC06 writes
//! - `on_batch_write = ...` for FC0F / FC10 writes
//! - `notify_via_batch = true` to route a single write through the batch hook
//!
//! Run:
//! `cargo run -p mbus-server --example write_hooks`

use mbus_core::errors::MbusError;
use mbus_core::transport::UnitIdOrSlaveAddr;
use mbus_server::ServerCoilHandler;
use mbus_server::ServerHoldingRegisterHandler;
#[cfg(feature = "traffic")]
use mbus_server::TrafficNotifier;
use mbus_server::{CoilsModel, HoldingRegistersModel, modbus_app};

#[derive(Debug, Default, CoilsModel)]
struct CoilMapDemo {
    #[coil(addr = 0)]
    run_enable: bool,
    #[coil(addr = 1, notify_via_batch = true)]
    alarm_ack: bool,
    #[coil(addr = 2, notify_via_batch = true)]
    maintenance_mode: bool,
}

#[derive(Debug, Default, HoldingRegistersModel)]
struct HoldingMapDemo {
    #[reg(addr = 10, scale = 0.1, unit = "C")]
    temperature_setpoint: u16,
    #[reg(addr = 11, notify_via_batch = true)]
    fan_speed_pct: u16,
}

#[derive(Debug, Default)]
#[modbus_app(
    coils(coils, on_batch_write = on_coil_batch_write, on_write_0 = on_run_enable_write),
    holding_registers(
        registers,
        on_batch_write = on_register_batch_write,
        on_write_10 = on_setpoint_write,
    )
)]
struct HookedApp {
    coils: CoilMapDemo,
    registers: HoldingMapDemo,
    audit_log: Vec<String>,
    compressor_running: bool,
}

#[cfg(feature = "traffic")]
impl TrafficNotifier for HookedApp {}

impl HookedApp {
    fn on_run_enable_write(&mut self, address: u16, old: bool, new: bool) -> Result<(), MbusError> {
        self.audit_log.push(format!(
            "single coil hook: addr={} old={} new={}",
            address, old, new
        ));

        if self.compressor_running && !new {
            self.audit_log
                .push("reject: cannot disable run_enable while compressor is running".into());
            return Err(MbusError::InvalidValue);
        }

        Ok(())
    }

    fn on_coil_batch_write(
        &mut self,
        start: u16,
        qty: u16,
        values: &[u8],
    ) -> Result<(), MbusError> {
        self.audit_log.push(format!(
            "batch coil hook: start={} qty={} packed={:02X?}",
            start, qty, values
        ));
        Ok(())
    }

    fn on_setpoint_write(&mut self, address: u16, old: u16, new: u16) -> Result<(), MbusError> {
        self.audit_log.push(format!(
            "single register hook: addr={} old={} new={}",
            address, old, new
        ));

        if !(180..=300).contains(&new) {
            self.audit_log.push(
                "reject: temperature_setpoint must stay in the 18.0 C ..= 30.0 C window".into(),
            );
            return Err(MbusError::InvalidValue);
        }

        Ok(())
    }

    fn on_register_batch_write(
        &mut self,
        start: u16,
        qty: u16,
        values: &[u16],
    ) -> Result<(), MbusError> {
        self.audit_log.push(format!(
            "batch register hook: start={} qty={} values={:?}",
            start, qty, values
        ));
        Ok(())
    }
}

fn unit_id(v: u8) -> UnitIdOrSlaveAddr {
    UnitIdOrSlaveAddr::try_from(v).expect("valid unit id")
}

fn main() -> Result<(), MbusError> {
    let mut app = HookedApp {
        compressor_running: true,
        ..HookedApp::default()
    };

    app.registers
        .set_temperature_setpoint_scaled(22.5)
        .expect("initial scaled setpoint should fit");
    app.registers.set_fan_speed_pct(30);

    println!("=== Hooked writes example ===");
    println!(
        "initial state: run_enable={} setpoint={:.1} {} fan_speed={} %",
        app.coils.run_enable,
        app.registers.temperature_setpoint_scaled(),
        HoldingMapDemo::temperature_setpoint_unit(),
        app.registers.fan_speed_pct()
    );

    println!();
    println!("1. Single FC05 write uses on_write_0 and can reject");
    let disable_result = app.write_single_coil_request(1, unit_id(1), 0, false);
    println!("   disabling run_enable while compressor is running -> {disable_result:?}");
    println!("   run_enable after rejection -> {}", app.coils.run_enable);

    app.compressor_running = false;
    app.write_single_coil_request(2, unit_id(1), 0, true)?;
    println!(
        "   enabling run_enable after stop -> {}",
        app.coils.run_enable
    );

    println!();
    println!("2. Single writes can be forwarded to the batch hook with notify_via_batch");
    app.write_single_coil_request(3, unit_id(1), 1, true)?;
    app.write_single_register_request(4, unit_id(1), 11, 55)?;
    println!(
        "   alarm_ack={} fan_speed={} %",
        app.coils.alarm_ack,
        app.registers.fan_speed_pct()
    );

    println!();
    println!("3. Real FC0F / FC10 batch writes use on_batch_write before commit");
    app.write_multiple_coils_request(5, unit_id(1), 1, 2, &[0b0000_0011])?;
    app.write_multiple_registers_request(6, unit_id(1), 10, &[240, 65])?;
    println!(
        "   maintenance_mode={} setpoint={:.1} {} fan_speed={} %",
        app.coils.maintenance_mode,
        app.registers.temperature_setpoint_scaled(),
        HoldingMapDemo::temperature_setpoint_unit(),
        app.registers.fan_speed_pct()
    );

    println!();
    println!("4. Single FC06 write uses on_write_10 and can reject");
    let bad_setpoint = app.write_single_register_request(7, unit_id(1), 10, 450);
    println!("   writing 45.0 C -> {bad_setpoint:?}");
    println!(
        "   setpoint after rejection -> {:.1} {}",
        app.registers.temperature_setpoint_scaled(),
        HoldingMapDemo::temperature_setpoint_unit()
    );

    println!();
    println!("Audit log:");
    for entry in &app.audit_log {
        println!("  - {entry}");
    }

    Ok(())
}
