"""
Firmware Builder Component

Provides firmware binary conversion and analysis using esptool's
elf2image and image-info commands.
"""

import asyncio
import logging
import re
from pathlib import Path
from typing import Any

from fastmcp import Context, FastMCP

from ..config import ESPToolServerConfig

logger = logging.getLogger(__name__)


class FirmwareBuilder:
    """ESP firmware building and compilation"""

    def __init__(self, app: FastMCP, config: ESPToolServerConfig) -> None:
        self.app = app
        self.config = config
        self._register_tools()

    async def _run_cmd(
        self,
        cmd: list[str],
        timeout: float = 30.0,
    ) -> dict[str, Any]:
        """Run a CLI command as an async subprocess."""
        proc = None
        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
            output = (stdout or b"").decode() + (stderr or b"").decode()

            if proc.returncode != 0:
                return {"success": False, "error": output.strip()[:500]}

            return {"success": True, "output": output}

        except asyncio.TimeoutError:
            if proc and proc.returncode is None:
                proc.kill()
                await proc.wait()
            return {"success": False, "error": f"Timeout after {timeout}s"}
        except FileNotFoundError:
            return {"success": False, "error": f"Command not found: {cmd[0]}"}
        except Exception as e:
            if proc and proc.returncode is None:
                proc.kill()
                await proc.wait()
            return {"success": False, "error": str(e)}

    def _register_tools(self) -> None:
        """Register firmware building tools"""

        @self.app.tool("esp_elf_to_binary")
        async def elf_to_binary(
            context: Context, elf_path: str, output_path: str | None = None
        ) -> dict[str, Any]:
            """Convert ELF file to flashable binary"""
            return await self._elf_to_binary_impl(context, elf_path, output_path)

        @self.app.tool("esp_elf_to_ram_binary")
        async def elf_to_ram_binary(
            context: Context,
            elf_path: str,
            output_path: str | None = None,
            chip: str = "auto",
        ) -> dict[str, Any]:
            """Convert ELF file to RAM-loadable binary for use with esp_load_ram.

            Creates a binary with RAM segments (IRAM/DRAM) placed first, suitable
            for loading directly into device RAM without touching flash. Perfect
            for rapid development iteration.

            Workflow:
            1. Build your project with ESP-IDF (disable secure boot/signed images)
            2. Use this tool to convert the ELF to a RAM binary
            3. Use esp_load_ram to load and execute on device

            Requirements:
            - ELF must NOT have embedded SHA256 digest at reserved offset
            - Disable CONFIG_SECURE_BOOT and CONFIG_SECURE_SIGNED_APPS in sdkconfig
            - Some PlatformIO builds may have incompatible settings

            Note: Features requiring flash (OTA, NVS, SPIFFS) won't work from RAM.

            Args:
                elf_path: Path to the ELF file from your build
                output_path: Output binary path (default: <elf_name>-ram.bin)
                chip: Target chip type (auto, esp32, esp32s3, etc.)
            """
            return await self._elf_to_ram_binary_impl(context, elf_path, output_path, chip)

        @self.app.tool("esp_firmware_analyze")
        async def analyze_firmware(context: Context, firmware_path: str) -> dict[str, Any]:
            """Analyze firmware binary structure"""
            return await self._firmware_analyze_impl(context, firmware_path)

    async def _elf_to_binary_impl(
        self,
        context: Context,
        elf_path: str,
        output_path: str | None,
    ) -> dict[str, Any]:
        """Convert ELF to flashable binary via esptool elf2image."""

        elf = Path(elf_path)
        if not elf.exists():
            return {"success": False, "error": f"ELF file not found: {elf_path}"}

        cmd = [self.config.esptool_path, "--chip", "auto", "elf2image"]

        if output_path:
            cmd.extend(["--output", output_path])

        cmd.append(elf_path)

        result = await self._run_cmd(cmd, timeout=30.0)

        if not result["success"]:
            return {"success": False, "error": result["error"], "elf_path": elf_path}

        # Determine the output file path
        if output_path:
            out = Path(output_path)
        else:
            # esptool elf2image default: <input>-<chip>.bin or <input>.bin
            # Look for likely output files
            out = elf.with_suffix(".bin")
            if not out.exists():
                # Try common patterns
                for candidate in elf.parent.glob(f"{elf.stem}*.bin"):
                    out = candidate
                    break

        response: dict[str, Any] = {
            "success": True,
            "elf_path": elf_path,
            "esptool_output": result["output"][:1000],
        }

        if out.exists():
            response["output_path"] = str(out)
            response["output_size_bytes"] = out.stat().st_size

        return response

    async def _elf_to_ram_binary_impl(
        self,
        context: Context,
        elf_path: str,
        output_path: str | None,
        chip: str,
    ) -> dict[str, Any]:
        """Convert ELF to RAM-loadable binary via esptool elf2image --ram-only-header."""

        elf = Path(elf_path)
        if not elf.exists():
            return {"success": False, "error": f"ELF file not found: {elf_path}"}

        # Determine output path
        if output_path:
            out = Path(output_path)
        else:
            out = elf.with_name(f"{elf.stem}-ram.bin")

        cmd = [
            self.config.esptool_path,
            "--chip", chip,
            "elf2image",
            "--ram-only-header",
            "--output", str(out),
            elf_path,
        ]

        result = await self._run_cmd(cmd, timeout=30.0)

        if not result["success"]:
            return {
                "success": False,
                "error": result["error"],
                "elf_path": elf_path,
            }

        response: dict[str, Any] = {
            "success": True,
            "elf_path": elf_path,
            "output_path": str(out),
            "chip": chip,
            "ram_optimized": True,
        }

        if out.exists():
            response["output_size_bytes"] = out.stat().st_size

        # Add usage hint
        response["usage_hint"] = (
            f"Load to device with: esp_load_ram(binary_path='{out}', port='<your-port>')"
        )

        return response

    async def _firmware_analyze_impl(
        self,
        context: Context,
        firmware_path: str,
    ) -> dict[str, Any]:
        """Analyze firmware binary via esptool image-info."""

        fw = Path(firmware_path)
        if not fw.exists():
            return {"success": False, "error": f"Firmware file not found: {firmware_path}"}

        # image-info --version 2 gives extended output
        result = await self._run_cmd(
            [self.config.esptool_path, "image-info", "--version", "2", firmware_path],
            timeout=15.0,
        )

        if not result["success"]:
            return {"success": False, "error": result["error"], "firmware_path": firmware_path}

        output = result["output"]
        info = self._parse_image_info(output)

        return {
            "success": True,
            "firmware_path": firmware_path,
            "file_size_bytes": fw.stat().st_size,
            **info,
            "raw_output": output[:2000],
        }

    def _parse_image_info(self, output: str) -> dict[str, Any]:
        """Parse esptool image-info output into structured data."""
        info: dict[str, Any] = {}

        # Extract key fields using regex
        patterns = {
            "entry_point": r"Entry point:\s*(0x[0-9a-fA-F]+)",
            "chip": r"Chip:\s*(\S+)",
            "flash_mode": r"Flash mode:\s*(\S+)",
            "flash_size": r"Flash size:\s*(\S+)",
            "flash_freq": r"Flash freq:\s*(\S+)",
        }

        for key, pattern in patterns.items():
            match = re.search(pattern, output)
            if match:
                info[key] = match.group(1)

        # Parse segments
        segments = []
        # Pattern: Segment N: len 0xNNNNN load 0xNNNNNNNN ...
        for match in re.finditer(
            r"Segment\s+(\d+):\s+len\s+(0x[0-9a-fA-F]+)\s+load\s+(0x[0-9a-fA-F]+)",
            output,
        ):
            segments.append({
                "index": int(match.group(1)),
                "length": match.group(2),
                "load_address": match.group(3),
            })

        if segments:
            info["segments"] = segments
            info["segment_count"] = len(segments)

        # Check for validation status
        if "valid" in output.lower():
            valid_match = re.search(r"Validation\s+Hash:\s*(\S+)", output, re.IGNORECASE)
            if valid_match:
                info["validation_hash"] = valid_match.group(1)

        return info

    async def health_check(self) -> dict[str, Any]:
        """Component health check"""
        return {"status": "healthy", "note": "Firmware builder ready"}
