//! DreamStack Hub — ESP-NOW + UDP Signal Sender
//!
//! Hub-side transport for Panel IR:
//!   - UDP: push full IR JSON to panels on port 9200
//!   - ESP-NOW (via serial bridge): send binary signal frames (<1ms)
//!
//! # Architecture
//!
//! Since Linux doesn't have native ESP-NOW support, we use an
//! ESP32-C3 USB dongle as a serial bridge:
//!
//! ```text
//! ┌──────────┐  UART/USB  ┌──────────┐  ESP-NOW  ┌──────────┐
//! │  ds-hub   │ ─────────► │ C3 bridge │ ────────► │  Panel   │
//! │  (Rust)   │ ◄───────── │  (dongle) │ ◄──────── │ (ESP32)  │
//! └──────────┘             └──────────┘            └──────────┘
//! ```
//!
//! For testing without hardware, the hub falls back to UDP-only mode,
//! which works with the browser previewer over WebSocket.

use std::net::UdpSocket;
use std::io;
use std::time::Duration;
use std::fs;
use std::path::Path;

/// DreamStack binary frame types (must match ds_espnow.h)
pub mod frame {
    pub const DS_NOW_SIG: u8 = 0x20;
    pub const DS_NOW_SIG_BATCH: u8 = 0x21;
    pub const DS_NOW_TOUCH: u8 = 0x30;
    pub const DS_NOW_ACTION: u8 = 0x31;
    pub const DS_NOW_PING: u8 = 0xFE;
    pub const DS_NOW_PONG: u8 = 0xFD;

    pub const DS_UDP_IR_PUSH: u8 = 0x40;
    pub const DS_UDP_IR_FRAG: u8 = 0x41;

    pub const DS_MAGIC: [u8; 2] = [0xD5, 0x7A];
    pub const DS_UDP_PORT: u16 = 9200;
}

/// A signal update ready to send.
#[derive(Debug, Clone)]
pub struct SignalUpdate {
    pub id: u16,
    pub value: i32,
}

/// Hub transport — sends signals and IR to panels.
pub struct DsHub {
    udp: UdpSocket,
    panel_addr: String,
    seq: u8,
    // serial: Option<Box<dyn SerialPort>>,  // For ESP32-C3 bridge (future)
}

impl DsHub {
    /// Create a new hub targeting a panel at the given address.
    ///
    /// ```ignore
    /// let hub = DsHub::new("192.168.1.100")?;
    /// hub.push_ir(&ir_json)?;
    /// hub.send_signal(0, 75)?;
    /// ```
    pub fn new(panel_ip: &str) -> io::Result<Self> {
        let udp = UdpSocket::bind("0.0.0.0:0")?;
        udp.set_broadcast(true)?;
        Ok(DsHub {
            udp,
            panel_addr: format!("{}:{}", panel_ip, frame::DS_UDP_PORT),
            seq: 0,
            // serial: None,
        })
    }

    /// Create a hub that broadcasts to all panels on the network.
    pub fn broadcast() -> io::Result<Self> {
        let udp = UdpSocket::bind("0.0.0.0:0")?;
        udp.set_broadcast(true)?;
        Ok(DsHub {
            udp,
            panel_addr: format!("255.255.255.255:{}", frame::DS_UDP_PORT),
            seq: 0,
        })
    }

    /// Push IR JSON to the panel via UDP.
    /// Fragments if > 1400 bytes (UDP MTU safety margin).
    pub fn push_ir(&mut self, ir_json: &str) -> io::Result<()> {
        let data = ir_json.as_bytes();

        if data.len() <= 1400 {
            // Single packet
            let mut buf = Vec::with_capacity(4 + data.len());
            buf.extend_from_slice(&frame::DS_MAGIC);
            buf.push(frame::DS_UDP_IR_PUSH);
            buf.push(0); // reserved
            buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
            buf.extend_from_slice(data);
            self.udp.send_to(&buf, &self.panel_addr)?;
        } else {
            // Fragment
            let frag_size = 1400;
            let total = (data.len() + frag_size - 1) / frag_size;
            let seq = self.next_seq();

            for i in 0..total {
                let start = i * frag_size;
                let end = std::cmp::min(start + frag_size, data.len());
                let chunk = &data[start..end];

                let mut buf = Vec::with_capacity(6 + chunk.len());
                buf.extend_from_slice(&frame::DS_MAGIC);
                buf.push(frame::DS_UDP_IR_FRAG);
                buf.push(i as u8);       // frag_id
                buf.push(total as u8);   // frag_total
                buf.push(seq);           // group seq
                buf.extend_from_slice(chunk);
                self.udp.send_to(&buf, &self.panel_addr)?;

                // Small delay between fragments to avoid overwhelming receiver
                std::thread::sleep(Duration::from_millis(2));
            }
        }

        Ok(())
    }

    /// Send a single signal update via UDP (fallback, ~2ms).
    /// For sub-1ms, use ESP-NOW via serial bridge.
    pub fn send_signal(&mut self, id: u16, value: i32) -> io::Result<()> {
        let buf: [u8; 7] = [
            frame::DS_NOW_SIG,
            (id & 0xFF) as u8,
            ((id >> 8) & 0xFF) as u8,
            (value & 0xFF) as u8,
            ((value >> 8) & 0xFF) as u8,
            ((value >> 16) & 0xFF) as u8,
            ((value >> 24) & 0xFF) as u8,
        ];
        self.udp.send_to(&buf, &self.panel_addr)?;
        Ok(())
    }

    /// Send a batch of signal updates in one UDP packet.
    pub fn send_signal_batch(&mut self, signals: &[SignalUpdate]) -> io::Result<()> {
        let mut buf = Vec::with_capacity(3 + signals.len() * 6);
        buf.push(frame::DS_NOW_SIG_BATCH);
        buf.push(signals.len() as u8);
        buf.push(self.next_seq());

        for sig in signals {
            buf.extend_from_slice(&sig.id.to_le_bytes());
            buf.extend_from_slice(&sig.value.to_le_bytes());
        }

        self.udp.send_to(&buf, &self.panel_addr)?;
        Ok(())
    }

    /// Send a heartbeat ping.
    pub fn send_ping(&mut self) -> io::Result<()> {
        let buf = [frame::DS_NOW_PING, self.next_seq()];
        self.udp.send_to(&buf, &self.panel_addr)?;
        Ok(())
    }

    /// Listen for panel responses (pong, action events).
    /// Blocks for up to `timeout` duration.
    pub fn recv_event(&self, timeout: Duration) -> io::Result<Option<PanelEvent>> {
        self.udp.set_read_timeout(Some(timeout))?;
        let mut buf = [0u8; 256];
        match self.udp.recv_from(&mut buf) {
            Ok((len, _addr)) => {
                if len < 1 { return Ok(None); }
                match buf[0] {
                    frame::DS_NOW_PONG => Ok(Some(PanelEvent::Pong(buf[1]))),
                    frame::DS_NOW_ACTION if len >= 4 => Ok(Some(PanelEvent::Action {
                        node_id: buf[1],
                        action: buf[2],
                    })),
                    frame::DS_NOW_TOUCH if len >= 8 => {
                        let x = u16::from_le_bytes([buf[4], buf[5]]);
                        let y = u16::from_le_bytes([buf[6], buf[7]]);
                        Ok(Some(PanelEvent::Touch {
                            node_id: buf[1],
                            event: buf[2],
                            x,
                            y,
                        }))
                    }
                    _ => Ok(None),
                }
            }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
            Err(e) => Err(e),
        }
    }

    /// Load IR JSON from a file and push to panel.
    pub fn push_ir_file(&mut self, path: &Path) -> io::Result<()> {
        let json = fs::read_to_string(path)?;
        self.push_ir(&json)
    }

    fn next_seq(&mut self) -> u8 {
        let s = self.seq;
        self.seq = self.seq.wrapping_add(1);
        s
    }
}

/// Events received from a panel.
#[derive(Debug)]
pub enum PanelEvent {
    Pong(u8),
    Action { node_id: u8, action: u8 },
    Touch { node_id: u8, event: u8, x: u16, y: u16 },
}

// ─── Tests ───

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn frame_constants() {
        assert_eq!(frame::DS_MAGIC, [0xD5, 0x7A]);
        assert_eq!(frame::DS_UDP_PORT, 9200);
        assert_eq!(frame::DS_NOW_SIG, 0x20);
        assert_eq!(frame::DS_NOW_SIG_BATCH, 0x21);
        assert_eq!(frame::DS_NOW_TOUCH, 0x30);
        assert_eq!(frame::DS_NOW_ACTION, 0x31);
        assert_eq!(frame::DS_NOW_PING, 0xFE);
        assert_eq!(frame::DS_NOW_PONG, 0xFD);
        assert_eq!(frame::DS_UDP_IR_PUSH, 0x40);
        assert_eq!(frame::DS_UDP_IR_FRAG, 0x41);
    }

    #[test]
    fn signal_update_encoding() {
        // Verify signal encoding matches the wire format:
        // [type(1)] [id_lo(1)] [id_hi(1)] [val(4 LE)]
        let id: u16 = 0x0102;
        let value: i32 = 0x04030201;
        let buf: [u8; 7] = [
            frame::DS_NOW_SIG,
            (id & 0xFF) as u8,
            ((id >> 8) & 0xFF) as u8,
            (value & 0xFF) as u8,
            ((value >> 8) & 0xFF) as u8,
            ((value >> 16) & 0xFF) as u8,
            ((value >> 24) & 0xFF) as u8,
        ];
        assert_eq!(buf[0], 0x20);     // SIG type
        assert_eq!(buf[1], 0x02);     // id lo
        assert_eq!(buf[2], 0x01);     // id hi
        assert_eq!(buf[3], 0x01);     // val byte 0
        assert_eq!(buf[6], 0x04);     // val byte 3
    }

    #[test]
    fn signal_batch_header() {
        // Batch format: [type(1)] [count(1)] [seq(1)] [signals: count × 6]
        let signals = vec![
            SignalUpdate { id: 0, value: 100 },
            SignalUpdate { id: 1, value: 200 },
            SignalUpdate { id: 2, value: -50 },
        ];
        let mut buf = Vec::with_capacity(3 + signals.len() * 6);
        buf.push(frame::DS_NOW_SIG_BATCH);
        buf.push(signals.len() as u8);
        buf.push(0); // seq

        for sig in &signals {
            buf.extend_from_slice(&sig.id.to_le_bytes());
            buf.extend_from_slice(&sig.value.to_le_bytes());
        }

        assert_eq!(buf[0], 0x21);            // BATCH type
        assert_eq!(buf[1], 3);               // count
        assert_eq!(buf.len(), 3 + 3 * 6);   // header + 3 signals × 6 bytes
    }

    #[test]
    fn ir_push_below_mtu() {
        // IR payloads < 1400 bytes should use DS_UDP_IR_PUSH (single packet)
        let ir_json = r#"{"type":"text","value":"hello"}"#;
        assert!(ir_json.len() < 1400);

        let data = ir_json.as_bytes();
        let mut buf = Vec::with_capacity(6 + data.len());
        buf.extend_from_slice(&frame::DS_MAGIC);
        buf.push(frame::DS_UDP_IR_PUSH);
        buf.push(0);
        buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
        buf.extend_from_slice(data);

        assert_eq!(buf[0], 0xD5); // magic[0]
        assert_eq!(buf[1], 0x7A); // magic[1]
        assert_eq!(buf[2], 0x40); // IR_PUSH
    }

    #[test]
    fn panel_event_debug() {
        let pong = PanelEvent::Pong(42);
        let debug = format!("{:?}", pong);
        assert!(debug.contains("Pong"));
        assert!(debug.contains("42"));

        let action = PanelEvent::Action { node_id: 5, action: 1 };
        let debug = format!("{:?}", action);
        assert!(debug.contains("Action"));

        let touch = PanelEvent::Touch { node_id: 1, event: 0, x: 320, y: 240 };
        let debug = format!("{:?}", touch);
        assert!(debug.contains("Touch"));
        assert!(debug.contains("320"));
    }
}

