"""Real-Time Transfer (RTT) — SEGGER RTT channel I/O via OpenOCD."""

import logging

from fastmcp import Context
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool

from mcjtag.mixins.helpers import parse_hex, require_session
from mcjtag.models import (
    RTTChannelInfo,
    RTTChannelListResult,
    RTTReadResult,
    RTTSetupResult,
    RTTStatusResult,
    RTTWriteResult,
)
from mcjtag.state import SessionState

log = logging.getLogger(__name__)


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


class RTTMixin(MCPMixin):
    """Tools for SEGGER Real-Time Transfer (RTT) -- high-speed target I/O via shared RAM.

    RTT is a protocol that uses a control block in target RAM to create
    ring-buffer channels between the target firmware and the debugger. It
    provides much higher throughput than semihosting or SWO (typically
    >1 MB/s) with minimal target CPU overhead.

    Requirements: The target firmware must include the SEGGER RTT library
    and initialize RTT before channels can be used. The control block
    contains a magic string ("SEGGER RTT" by default) that OpenOCD
    searches for in RAM.

    Channel conventions:
    - Channel 0 "Terminal": Printf-style logging output (up) and terminal
      input (down). Most firmware only uses this channel.
    - Channel 1+: Application-defined. Often used for binary data, profiling,
      or bidirectional command/response protocols.

    Up-channels carry data FROM the target TO the host (reads).
    Down-channels carry data FROM the host TO the target (writes).

    Typical workflow: rtt_setup -> rtt_start -> rtt_channels -> rtt_read/rtt_write.
    """

    @mcp_tool()
    async def rtt_setup(
        self,
        ctx: Context,
        address: str,
        size: str,
        id_string: str = "SEGGER RTT",
    ) -> RTTSetupResult:
        """Configure the RAM search region for finding the RTT control block.

        OpenOCD scans the specified RAM region for the RTT ID string to
        locate the control block. The control block is placed by the firmware
        linker, usually near the start of RAM. A smaller search region is
        faster but may miss the block if its location varies between builds.

        The firmware must have initialized RTT (called SEGGER_RTT_Init or
        equivalent) before rtt_start can find the control block. If rtt_start
        fails after setup, the firmware may not have reached its RTT init code
        yet -- try halting, stepping past init, then retrying.

        This only configures the search -- call rtt_start afterwards to
        actually scan for the control block and activate channels.

        Args:
            address: Start of the RAM region to search, in hex. Use the base
                     of SRAM (e.g. "0x20000000" for most Cortex-M). Check the
                     linker map file for the exact _SEGGER_RTT symbol address
                     to narrow the search.
            size: Size of the search region in hex (e.g. "0x10000" for 64KB,
                  "0x1000" for 4KB). Larger values take longer to scan but are
                  more likely to find the control block.
            id_string: RTT control block magic string (default "SEGGER RTT").
                       Only change this if the firmware uses a custom ID string.
        """
        state = _get_state(ctx)
        require_session(state)

        addr = parse_hex(address, "RTT search address")
        sz = parse_hex(size, "RTT search size")
        await state.session.rtt.setup(addr, sz, id_string=id_string)
        return RTTSetupResult(
            address=f"0x{addr:08x}",
            size=f"0x{sz:x}",
            id_string=id_string,
            success=True,
        )

    @mcp_tool()
    async def rtt_start(self, ctx: Context) -> RTTStatusResult:
        """Start RTT -- scans RAM for the control block and activates channels.

        Call rtt_setup first to configure the search region. This triggers
        the actual memory scan for the RTT ID string. If successful,
        channels become available for rtt_read and rtt_write.

        If this fails, common causes are:
        - rtt_setup was not called yet
        - The firmware has not initialized RTT (target may need to run first)
        - The search region does not contain the control block
        - The ID string does not match
        """
        state = _get_state(ctx)
        require_session(state)

        await state.session.rtt.start()
        return RTTStatusResult(action="start", success=True)

    @mcp_tool()
    async def rtt_stop(self, ctx: Context) -> RTTStatusResult:
        """Stop RTT communication and release channels.

        After stopping, rtt_read and rtt_write will fail. Call rtt_start
        again to reactivate. Any unread data in up-channel buffers is lost.
        """
        state = _get_state(ctx)
        require_session(state)

        await state.session.rtt.stop()
        return RTTStatusResult(action="stop", success=True)

    @mcp_tool()
    async def rtt_channels(self, ctx: Context) -> RTTChannelListResult:
        """List all RTT channels with their names, buffer sizes, and directions.

        RTT must be started first (call rtt_start). Returns both up-channels
        (target-to-host, used with rtt_read) and down-channels (host-to-target,
        used with rtt_write).

        Each channel has a name (set by firmware), buffer size (in bytes,
        determines how much data can be buffered before overflow), and direction.
        Channel 0 is conventionally named "Terminal" for printf-style logging.

        The buffer size matters: if the target writes faster than the host
        reads, data is lost (ring buffer wraps). Poll rtt_read frequently
        for high-throughput channels, or increase the firmware buffer size.
        """
        state = _get_state(ctx)
        require_session(state)

        channels = await state.session.rtt.channels()
        return RTTChannelListResult(
            channels=[
                RTTChannelInfo(
                    index=ch.index,
                    name=ch.name,
                    size=ch.size,
                    direction=ch.direction,
                )
                for ch in channels
            ],
            count=len(channels),
        )

    @mcp_tool()
    async def rtt_read(self, ctx: Context, channel: int = 0) -> RTTReadResult:
        """Read pending data from an RTT up-channel (target to host).

        Returns whatever text the target firmware has written to this channel
        since the last read. Returns an empty string if no data is pending.
        This is non-blocking -- it returns immediately with available data
        rather than waiting for new data.

        For continuous log monitoring, call rtt_read repeatedly in a loop.
        Data is consumed on read (ring buffer read pointer advances), so
        each call returns new data only.

        Args:
            channel: Up-channel index (default 0). Channel 0 is conventionally
                     the firmware's printf/logging output. Use rtt_channels to
                     discover available channels and their names.
        """
        state = _get_state(ctx)
        require_session(state)

        data = await state.session.rtt.read(channel)
        return RTTReadResult(channel=channel, data=data)

    @mcp_tool()
    async def rtt_write(self, ctx: Context, data: str, channel: int = 0) -> RTTWriteResult:
        """Write a string to an RTT down-channel (host to target).

        Sends text data to the target firmware via the specified down-channel.
        The firmware must be actively reading from this channel for the data
        to be consumed. If the firmware's read buffer is full, data may be
        dropped (depends on the firmware's RTT mode: skip, trim, or block).

        Common uses: sending commands to a firmware CLI on channel 0, or
        sending configuration/control data on application channels (1+).

        Args:
            data: String to send to the target. Sent as raw bytes (UTF-8).
                  Include "\\n" if the firmware expects newline-terminated input.
            channel: Down-channel index (default 0). Must correspond to a
                     down-channel the firmware is reading from. Use rtt_channels
                     to see available channels.
        """
        state = _get_state(ctx)
        require_session(state)

        await state.session.rtt.write(channel, data)
        return RTTWriteResult(channel=channel, length=len(data), success=True)
