"""
Security Manager Component

Handles ESP security features including eFuse management, flash encryption
status, and security auditing. Operations shell out to esptool/espefuse
as async subprocesses.
"""

import asyncio
import logging
import re
from typing import Any

from fastmcp import Context, FastMCP

from ..config import ESPToolServerConfig

logger = logging.getLogger(__name__)


class SecurityManager:
    """ESP security features management"""

    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.

        Returns:
            dict with "success", "output", and optionally "error"
        """
        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 _espefuse_cmd(self, port: str, args: list[str]) -> list[str]:
        """Build an espefuse command list."""
        return ["espefuse", "--port", port, *args]

    def _esptool_cmd(self, port: str, args: list[str]) -> list[str]:
        """Build an esptool command list."""
        return [self.config.esptool_path, "--port", port, *args]

    def _register_tools(self) -> None:
        """Register security management tools"""

        @self.app.tool("esp_security_audit")
        async def security_audit(
            context: Context,
            port: str | None = None,
        ) -> dict[str, Any]:
            """Perform comprehensive security audit of an ESP device.

            Connects to the device and gathers security-relevant information:
            chip identity, flash encryption status, secure boot state, and
            eFuse summary. Returns a structured report suitable for evaluating
            the device's security posture.

            Requires a connected device (physical or QEMU via socket:// URI).

            Args:
                port: Serial port or socket:// URI (required)
            """
            return await self._security_audit_impl(context, port)

        @self.app.tool("esp_enable_flash_encryption")
        async def enable_flash_encryption(
            context: Context,
            port: str | None = None,
            key_file: str | None = None,
        ) -> dict[str, Any]:
            """Enable flash encryption with optional key.

            Checks current flash encryption status via eFuse summary. If
            encryption is already enabled, reports the current state. Actual
            eFuse burning for flash encryption requires esp_efuse_burn with
            specific eFuse names (FLASH_CRYPT_CNT, etc.) — this tool provides
            guidance and status checking.

            WARNING: Flash encryption is a one-way operation on real hardware.
            Test thoroughly on QEMU first.

            Args:
                port: Serial port or socket:// URI (required)
                key_file: Path to encryption key file (for reference only)
            """
            return await self._flash_encryption_impl(context, port, key_file)

        @self.app.tool("esp_efuse_read")
        async def read_efuse(
            context: Context,
            port: str | None = None,
            efuse_name: str | None = None,
        ) -> dict[str, Any]:
            """Read eFuse values from an ESP device.

            Without efuse_name: returns full human-readable eFuse summary
            (espefuse summary). With efuse_name: returns that specific eFuse's
            value parsed from the summary.

            eFuses are one-time-programmable bits that control chip security,
            MAC address, calibration data, and more. Reading is non-destructive.

            Args:
                port: Serial port or socket:// URI (required)
                efuse_name: Specific eFuse to read (e.g. "MAC", "FLASH_CRYPT_CNT").
                           If omitted, returns full summary.
            """
            return await self._efuse_read_impl(context, port, efuse_name)

        @self.app.tool("esp_efuse_burn")
        async def burn_efuse(
            context: Context,
            efuse_name: str,
            value: str,
            port: str | None = None,
        ) -> dict[str, Any]:
            """Burn eFuse (DANGEROUS - requires confirmation).

            Permanently programs an eFuse bit field on the ESP device. This
            operation is IRREVERSIBLE on real hardware — burned bits cannot be
            reset. Safe to test on QEMU virtual devices (eFuses reset when
            instance is recreated).

            Common eFuses: FLASH_CRYPT_CNT, ABS_DONE_0, JTAG_DISABLE,
            DISABLE_DL_ENCRYPT, DISABLE_DL_DECRYPT.

            Uses --do-not-confirm flag since confirmation is handled at the
            MCP client level.

            Args:
                efuse_name: Name of the eFuse to burn (e.g. "JTAG_DISABLE")
                value: Value to burn (e.g. "1", "0x1")
                port: Serial port or socket:// URI (required)
            """
            return await self._efuse_burn_impl(context, efuse_name, value, port)

    async def _security_audit_impl(
        self,
        context: Context,
        port: str | None,
    ) -> dict[str, Any]:
        """Gather security-relevant info from the device."""

        if not port:
            return {"success": False, "error": "Port is required for security audit"}

        report: dict[str, Any] = {"port": port}

        # 1. Get chip info and security info from esptool
        security_result = await self._run_cmd(
            self._esptool_cmd(port, ["get-security-info"]),
            timeout=15.0,
        )

        if security_result["success"]:
            report["security_info"] = self._parse_security_info(security_result["output"])
        else:
            # get-security-info may not be supported on all chips
            report["security_info"] = {"note": security_result["error"]}

        # 2. Get chip ID
        chip_result = await self._run_cmd(
            self._esptool_cmd(port, ["chip-id"]),
            timeout=15.0,
        )
        if chip_result["success"]:
            chip_match = re.search(r"Chip ID:\s*(0x[0-9a-fA-F]+)", chip_result["output"])
            if chip_match:
                report["chip_id"] = chip_match.group(1)

        # 3. Get eFuse summary for security-relevant fuses
        efuse_result = await self._run_cmd(
            self._espefuse_cmd(port, ["summary"]),
            timeout=15.0,
        )

        if efuse_result["success"]:
            parsed = self._parse_efuse_summary(efuse_result["output"])
            report["efuse_summary"] = parsed

            # Extract security-relevant fields
            security_fuses = {}
            security_names = [
                "FLASH_CRYPT_CNT", "ABS_DONE_0", "ABS_DONE_1",
                "JTAG_DISABLE", "DISABLE_DL_ENCRYPT", "DISABLE_DL_DECRYPT",
                "DISABLE_DL_CACHE", "FLASH_CRYPT_CONFIG",
            ]
            for name in security_names:
                if name in parsed:
                    security_fuses[name] = parsed[name]

            report["security_fuses"] = security_fuses

            # Determine security posture
            flash_encrypted = security_fuses.get("FLASH_CRYPT_CNT", "0") not in ("0", "0x0", "= 0")
            secure_boot = security_fuses.get("ABS_DONE_0", "0") not in ("0", "0x0", "= 0")
            jtag_disabled = security_fuses.get("JTAG_DISABLE", "0") not in ("0", "0x0", "= 0")

            report["posture"] = {
                "flash_encryption": "enabled" if flash_encrypted else "disabled",
                "secure_boot": "enabled" if secure_boot else "disabled",
                "jtag": "disabled" if jtag_disabled else "enabled (vulnerable)",
            }
        else:
            report["efuse_error"] = efuse_result["error"]

        report["success"] = True
        return report

    async def _flash_encryption_impl(
        self,
        context: Context,
        port: str | None,
        key_file: str | None,
    ) -> dict[str, Any]:
        """Check flash encryption status and provide guidance."""

        if not port:
            return {"success": False, "error": "Port is required"}

        # Read the encryption-relevant eFuses
        efuse_result = await self._run_cmd(
            self._espefuse_cmd(port, ["summary"]),
            timeout=15.0,
        )

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

        parsed = self._parse_efuse_summary(efuse_result["output"])

        flash_crypt_cnt = parsed.get("FLASH_CRYPT_CNT", "unknown")
        flash_crypt_config = parsed.get("FLASH_CRYPT_CONFIG", "unknown")

        # Determine current state
        is_encrypted = flash_crypt_cnt not in ("0", "0x0", "= 0", "unknown")

        result: dict[str, Any] = {
            "success": True,
            "port": port,
            "flash_encryption_enabled": is_encrypted,
            "FLASH_CRYPT_CNT": flash_crypt_cnt,
            "FLASH_CRYPT_CONFIG": flash_crypt_config,
        }

        if is_encrypted:
            result["message"] = "Flash encryption is already enabled on this device."
        else:
            result["message"] = (
                "Flash encryption is NOT enabled. To enable, you need to: "
                "1) Generate or provide an encryption key, "
                "2) Burn FLASH_CRYPT_CNT and FLASH_CRYPT_CONFIG eFuses, "
                "3) Flash encrypted firmware. "
                "WARNING: This is irreversible on real hardware. Test on QEMU first."
            )
            if key_file:
                result["key_file"] = key_file

        return result

    async def _efuse_read_impl(
        self,
        context: Context,
        port: str | None,
        efuse_name: str | None,
    ) -> dict[str, Any]:
        """Read eFuse values via espefuse summary."""

        if not port:
            return {"success": False, "error": "Port is required for eFuse read"}

        result = await self._run_cmd(
            self._espefuse_cmd(port, ["summary"]),
            timeout=15.0,
        )

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

        parsed = self._parse_efuse_summary(result["output"])

        if efuse_name:
            # Return specific eFuse
            if efuse_name in parsed:
                return {
                    "success": True,
                    "port": port,
                    "efuse_name": efuse_name,
                    "value": parsed[efuse_name],
                }
            else:
                # Try case-insensitive match
                for key, val in parsed.items():
                    if key.upper() == efuse_name.upper():
                        return {
                            "success": True,
                            "port": port,
                            "efuse_name": key,
                            "value": val,
                        }
                return {
                    "success": False,
                    "error": f"eFuse '{efuse_name}' not found",
                    "available_efuses": list(parsed.keys()),
                    "port": port,
                }

        # Return full summary
        return {
            "success": True,
            "port": port,
            "efuses": parsed,
            "raw_output": result["output"][:2000],
        }

    async def _efuse_burn_impl(
        self,
        context: Context,
        efuse_name: str,
        value: str,
        port: str | None,
    ) -> dict[str, Any]:
        """Burn an eFuse value via espefuse burn-efuse."""

        if not port:
            return {"success": False, "error": "Port is required for eFuse burn"}

        # Read before to record the change
        before = await self._run_cmd(
            self._espefuse_cmd(port, ["summary"]),
            timeout=15.0,
        )
        before_parsed = self._parse_efuse_summary(before.get("output", "")) if before["success"] else {}
        before_value = before_parsed.get(efuse_name, "unknown")

        # Burn the eFuse (--do-not-confirm since MCP client handles confirmation)
        result = await self._run_cmd(
            self._espefuse_cmd(port, ["--do-not-confirm", "burn-efuse", efuse_name, value]),
            timeout=30.0,
        )

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

        # Read after to confirm
        after = await self._run_cmd(
            self._espefuse_cmd(port, ["summary"]),
            timeout=15.0,
        )
        after_parsed = self._parse_efuse_summary(after.get("output", "")) if after["success"] else {}
        after_value = after_parsed.get(efuse_name, "unknown")

        return {
            "success": True,
            "port": port,
            "efuse_name": efuse_name,
            "value_requested": value,
            "value_before": before_value,
            "value_after": after_value,
            "warning": "eFuse burn is IRREVERSIBLE on real hardware",
        }

    def _parse_efuse_summary(self, output: str) -> dict[str, str]:
        """Parse espefuse summary output into a dict of name -> value.

        espefuse v5.x summary lines look like:
            ADC_VREF (BLOCK0)  True ADC reference voltage  = 1100 R/W (0b00000)
            MAC (BLOCK0)       Factory MAC address          = ab:cd:ef:01:02:03 R/W (0x...)
        The value sits between '= ' and the R/W permission field.
        """
        efuses: dict[str, str] = {}
        for line in output.splitlines():
            # Match: NAME (BLOCKn) <description> = <value> R/W|R/-|-/- (<hex>)
            match = re.match(
                r"\s*(\w+)\s+\(BLOCK\d+\)\s+.*?=\s+(.+?)\s+[RW/-]+/[RW/-]+\s",
                line,
            )
            if match:
                name = match.group(1).strip()
                value = match.group(2).strip()
                efuses[name] = value
        return efuses

    def _parse_security_info(self, output: str) -> dict[str, str]:
        """Parse esptool get-security-info output."""
        info: dict[str, str] = {}
        for line in output.splitlines():
            if ":" in line:
                key, _, val = line.partition(":")
                key = key.strip()
                val = val.strip()
                if key and val:
                    info[key] = val
        return info

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