//! Hardware board info tool — returns chip name, architecture, memory map for Telegram/agent.
//!
//! Use when user asks "what board do I have?", "board info", "connected hardware", etc.
//! Uses probe-rs for Nucleo when available; otherwise static datasheet info.

use super::traits::{Tool, ToolResult};
use async_trait::async_trait;
use serde_json::json;

/// Static board info (datasheets). Used when probe-rs is unavailable.
const BOARD_INFO: &[(&str, &str, &str)] = &[
    (
        "nucleo-f401re",
        "STM32F401RET6",
        "ARM Cortex-M4, 84 MHz. Flash: 512 KB, RAM: 128 KB. User LED on PA5 (pin 13).",
    ),
    (
        "nucleo-f411re",
        "STM32F411RET6",
        "ARM Cortex-M4, 100 MHz. Flash: 512 KB, RAM: 128 KB. User LED on PA5 (pin 13).",
    ),
    (
        "arduino-uno",
        "ATmega328P",
        "8-bit AVR, 16 MHz. Flash: 16 KB, SRAM: 2 KB. Built-in LED on pin 13.",
    ),
    (
        "arduino-uno-q",
        "STM32U585 + Qualcomm",
        "Dual-core: STM32 (MCU) + Linux (aarch64). GPIO via Bridge app on port 9999.",
    ),
    (
        "esp32",
        "ESP32",
        "Dual-core Xtensa LX6, 240 MHz. Flash: 4 MB typical. Built-in LED on GPIO 2.",
    ),
    (
        "rpi-gpio",
        "Raspberry Pi",
        "ARM Linux. Native GPIO via sysfs/rppal. No fixed LED pin.",
    ),
];

/// Tool: return full board info (chip, architecture, memory map) for agent/Telegram.
pub struct HardwareBoardInfoTool {
    boards: Vec<String>,
}

impl HardwareBoardInfoTool {
    pub fn new(boards: Vec<String>) -> Self {
        Self { boards }
    }

    fn static_info_for_board(&self, board: &str) -> Option<String> {
        BOARD_INFO
            .iter()
            .find(|(b, _, _)| *b == board)
            .map(|(_, chip, desc)| {
                format!(
                    "**Board:** {}\n**Chip:** {}\n**Description:** {}",
                    board, chip, desc
                )
            })
    }
}

#[async_trait]
impl Tool for HardwareBoardInfoTool {
    fn name(&self) -> &str {
        "hardware_board_info"
    }

    fn description(&self) -> &str {
        "Return full board info (chip, architecture, memory map) for connected hardware. Use when: user asks for 'board info', 'what board do I have', 'connected hardware', 'chip info', 'what hardware', or 'memory map'."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "board": {
                    "type": "string",
                    "description": "Optional board name (e.g. nucleo-f401re). If omitted, returns info for first configured board."
                }
            }
        })
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
        if self.boards.is_empty() {
            return Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(
                    "No peripherals configured. Add boards to config.toml [peripherals.boards]."
                        .into(),
                ),
                structured: None,
            });
        }

        let board = args
            .get("board")
            .and_then(|v| v.as_str())
            .map(String::from)
            .unwrap_or_else(|| self.boards[0].clone());

        if !self.boards.iter().any(|known| known == &board) {
            return Ok(ToolResult {
                success: false,
                output: String::new(),
                error: Some(format!(
                    "Board '{board}' is not configured. Configured boards: {}",
                    self.boards.join(", ")
                )),
                structured: None,
            });
        }

        let mut output = String::new();

        #[cfg(feature = "probe")]
        if board == "nucleo-f401re" || board == "nucleo-f411re" {
            let chip = if board == "nucleo-f411re" {
                "STM32F411RETx"
            } else {
                "STM32F401RETx"
            };
            match probe_board_info(chip) {
                Ok(info) => {
                    return Ok(ToolResult {
                        success: true,
                        output: info,
                        error: None,
                        structured: None,
                    });
                }
                Err(e) => {
                    use std::fmt::Write;
                    let _ = write!(
                        output,
                        "probe-rs attach failed: {e}. Using static info.\n\n"
                    );
                }
            }
        }

        if let Some(info) = self.static_info_for_board(&board) {
            output.push_str(&info);
            if let Some(mem) = memory_map_static(&board) {
                use std::fmt::Write;
                let _ = write!(output, "\n\n**Memory map:**\n{mem}");
            }
        } else {
            use std::fmt::Write;
            let _ = write!(
                output,
                "Board '{board}' configured. No static info available."
            );
        }

        Ok(ToolResult {
            success: true,
            output,
            error: None,
            structured: None,
        })
    }
}

#[cfg(feature = "probe")]
fn probe_board_info(chip: &str) -> anyhow::Result<String> {
    use probe_rs::config::MemoryRegion;
    use probe_rs::{Session, SessionConfig};
    use std::fmt::Write;

    let session = Session::auto_attach(chip, SessionConfig::default())
        .map_err(|e| anyhow::anyhow!("{}", e))?;
    let target = session.target();
    let arch = session.architecture();

    let mut out = format!(
        "**Board:** {}\n**Chip:** {}\n**Architecture:** {:?}\n\n**Memory map:**\n",
        chip, target.name, arch
    );
    for region in &target.memory_map {
        match region {
            MemoryRegion::Ram(ram) => {
                let (start, end) = (ram.range.start, ram.range.end);
                let _ = writeln!(
                    out,
                    "RAM: 0x{:08X} - 0x{:08X} ({} KB)",
                    start,
                    end,
                    (end - start) / 1024
                );
            }
            MemoryRegion::Nvm(flash) => {
                let (start, end) = (flash.range.start, flash.range.end);
                let _ = writeln!(
                    out,
                    "Flash: 0x{:08X} - 0x{:08X} ({} KB)",
                    start,
                    end,
                    (end - start) / 1024
                );
            }
            MemoryRegion::Generic(_) => {}
        }
    }
    out.push_str("\n(Info read via USB/SWD — no firmware on target needed.)");
    Ok(out)
}

fn memory_map_static(board: &str) -> Option<&'static str> {
    match board {
        "nucleo-f401re" | "nucleo-f411re" => Some(
            "Flash: 0x0800_0000 - 0x0807_FFFF (512 KB)\nRAM: 0x2000_0000 - 0x2001_FFFF (128 KB)",
        ),
        "arduino-uno" => Some("Flash: 16 KB, SRAM: 2 KB, EEPROM: 1 KB"),
        "esp32" => Some("Flash: 4 MB, IRAM/DRAM per ESP-IDF layout"),
        _ => None,
    }
}

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

    #[test]
    fn static_info_for_known_board_contains_chip_and_description() {
        let tool = HardwareBoardInfoTool::new(vec!["nucleo-f401re".to_string()]);

        let info = tool
            .static_info_for_board("nucleo-f401re")
            .expect("known board should return static info");

        assert!(info.contains("**Board:** nucleo-f401re"));
        assert!(info.contains("**Chip:** STM32F401RET6"));
    }

    #[test]
    fn static_info_for_unknown_board_returns_none() {
        let tool = HardwareBoardInfoTool::new(vec!["custom".to_string()]);
        assert!(tool.static_info_for_board("custom").is_none());
    }

    #[test]
    fn memory_map_static_has_entries_for_supported_boards() {
        assert!(memory_map_static("nucleo-f401re").is_some());
        assert!(memory_map_static("arduino-uno").is_some());
        assert!(memory_map_static("unknown").is_none());
    }

    #[tokio::test]
    async fn execute_fails_when_boards_configuration_is_empty() {
        let tool = HardwareBoardInfoTool::new(Vec::new());

        let result = tool
            .execute(json!({}))
            .await
            .expect("execute should return ToolResult");

        assert!(!result.success);
        assert!(result
            .error
            .as_deref()
            .unwrap_or_default()
            .contains("No peripherals configured"));
    }

    #[tokio::test]
    async fn execute_returns_static_info_for_known_board() {
        let tool = HardwareBoardInfoTool::new(vec!["nucleo-f401re".to_string()]);

        let result = tool
            .execute(json!({}))
            .await
            .expect("execute should return ToolResult");

        assert!(result.success);
        assert!(result.output.contains("**Board:** nucleo-f401re"));
        assert!(result.output.contains("**Memory map:**"));
    }

    #[tokio::test]
    async fn execute_uses_requested_board_argument_when_provided() {
        let tool = HardwareBoardInfoTool::new(vec![
            "nucleo-f401re".to_string(),
            "arduino-uno".to_string(),
        ]);

        let result = tool
            .execute(json!({ "board": "arduino-uno" }))
            .await
            .expect("execute should return ToolResult");

        assert!(result.success);
        assert!(result.output.contains("**Board:** arduino-uno"));
        assert!(result.output.contains("ATmega328P"));
    }

    #[tokio::test]
    async fn execute_reports_unknown_board_without_failing() {
        let tool = HardwareBoardInfoTool::new(vec!["custom-board".to_string()]);

        let result = tool
            .execute(json!({}))
            .await
            .expect("execute should return ToolResult");

        assert!(result.success);
        assert!(result.output.contains("No static info available"));
    }
}
