"""Breakpoint and watchpoint management."""

import logging
from typing import Literal

from fastmcp import Context
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from openocd.errors import OpenOCDError

from mcjtag.mixins.helpers import parse_hex, require_session
from mcjtag.models import (
    BreakpointActionResult,
    BreakpointInfo,
    BreakpointListResult,
    WatchpointActionResult,
    WatchpointInfo,
    WatchpointListResult,
)
from mcjtag.state import SessionState

log = logging.getLogger(__name__)


def _get_state(ctx: Context) -> SessionState:
    return ctx.request_context.lifespan_context


class BreakpointMixin(MCPMixin):
    """Tools for setting, removing, and listing breakpoints and watchpoints."""

    @mcp_tool()
    async def breakpoint_add(
        self,
        ctx: Context,
        address: str,
        length: int = 2,
        hw: bool = False,
    ) -> BreakpointActionResult:
        """Set a breakpoint at an instruction address.

        Software breakpoints patch the instruction in memory (unlimited count).
        Hardware breakpoints use on-chip comparators (limited — typically 2-6
        on Cortex-M). Use hw=True when setting breakpoints in flash, since
        flash cannot be patched in place.

        Args:
            address: Instruction address in hex (e.g. "0x08001234").
            length: Breakpoint length in bytes — 2 for Thumb, 4 for ARM.
            hw: Request a hardware breakpoint.
        """
        state = _get_state(ctx)
        require_session(state)

        addr = parse_hex(address, "breakpoint address")
        if addr % 2 != 0:
            raise OpenOCDError(
                f"Breakpoint address 0x{addr:08x} is not 2-byte aligned. "
                f"ARM Thumb instructions require even addresses."
            )
        if length not in (2, 4):
            raise OpenOCDError(f"Breakpoint length must be 2 (Thumb) or 4 (ARM), got {length}")

        await state.session.breakpoints.add(addr, length=length, hw=hw)
        return BreakpointActionResult(
            address=f"0x{addr:08x}",
            action="add",
            success=True,
        )

    @mcp_tool()
    async def breakpoint_remove(self, ctx: Context, address: str) -> BreakpointActionResult:
        """Remove a breakpoint at the given address.

        Args:
            address: Address of the breakpoint to remove, in hex.
        """
        state = _get_state(ctx)
        require_session(state)

        addr = parse_hex(address, "breakpoint address")
        await state.session.breakpoints.remove(addr)
        return BreakpointActionResult(
            address=f"0x{addr:08x}",
            action="remove",
            success=True,
        )

    @mcp_tool()
    async def breakpoint_list(self, ctx: Context) -> BreakpointListResult:
        """List all active breakpoints.

        Returns each breakpoint's address, type (hw/sw), length, and
        enabled status.
        """
        state = _get_state(ctx)
        require_session(state)

        bps = await state.session.breakpoints.list()
        return BreakpointListResult(
            breakpoints=[
                BreakpointInfo(
                    number=bp.number,
                    type=bp.type,
                    address=f"0x{bp.address:08x}",
                    length=bp.length,
                    enabled=bp.enabled,
                )
                for bp in bps
            ],
            count=len(bps),
        )

    @mcp_tool()
    async def watchpoint_add(
        self,
        ctx: Context,
        address: str,
        length: int,
        access: Literal["r", "w", "rw"] = "rw",
    ) -> WatchpointActionResult:
        """Set a data watchpoint to trigger on memory access.

        The target halts when the watched address range is accessed.
        Watchpoints use hardware comparators (typically 2-4 on Cortex-M).

        Args:
            address: Memory address to watch, in hex (e.g. "0x20001000").
            length: Number of bytes to watch (must be power of 2 on most targets).
            access: Access type — "r" for read, "w" for write, "rw" for either.
        """
        state = _get_state(ctx)
        require_session(state)

        addr = parse_hex(address, "watchpoint address")
        if length <= 0:
            raise OpenOCDError(f"Watchpoint length must be positive, got {length}")

        await state.session.breakpoints.add_watchpoint(addr, length, access=access)
        return WatchpointActionResult(
            address=f"0x{addr:08x}",
            action="add",
            success=True,
        )

    @mcp_tool()
    async def watchpoint_remove(self, ctx: Context, address: str) -> WatchpointActionResult:
        """Remove a watchpoint at the given address.

        Args:
            address: Address of the watchpoint to remove, in hex.
        """
        state = _get_state(ctx)
        require_session(state)

        addr = parse_hex(address, "watchpoint address")
        await state.session.breakpoints.remove_watchpoint(addr)
        return WatchpointActionResult(
            address=f"0x{addr:08x}",
            action="remove",
            success=True,
        )

    @mcp_tool()
    async def watchpoint_list(self, ctx: Context) -> WatchpointListResult:
        """List all active watchpoints.

        Returns each watchpoint's address, length, and access type (r/w/rw).
        """
        state = _get_state(ctx)
        require_session(state)

        wps = await state.session.breakpoints.list_watchpoints()
        return WatchpointListResult(
            watchpoints=[
                WatchpointInfo(
                    number=wp.number,
                    address=f"0x{wp.address:08x}",
                    length=wp.length,
                    access=wp.access,
                )
                for wp in wps
            ],
            count=len(wps),
        )
