"""Shared helpers for mcjtag mixin tools."""

import logging
import os
import struct

from openocd.errors import OpenOCDError

from mcjtag.state import SessionState

log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Hex parsing
# ---------------------------------------------------------------------------


def parse_hex(value: str, name: str = "address") -> int:
    """Parse a hex string into an integer with a clear error message.

    Accepts formats like "0x08000000", "08000000", "0X20000000".
    Strips whitespace before parsing.
    """
    cleaned = value.strip()
    try:
        result = int(cleaned, 16)
    except (ValueError, TypeError) as exc:
        raise OpenOCDError(
            f"Invalid hex {name}: {value!r}. Expected a hex value like '0x08000000' or '20000000'."
        ) from exc
    # I-3: Reject negative values — nonsensical for addresses/register values
    if result < 0:
        raise OpenOCDError(
            f"Negative {name}: {value!r}. Addresses and values must be non-negative."
        )
    return result


# ---------------------------------------------------------------------------
# Session guard
# ---------------------------------------------------------------------------


def require_session(state: SessionState) -> None:
    """Raise if no OpenOCD session is active."""
    if not state.session:
        raise OpenOCDError("Not connected to OpenOCD. Use connect or start_openocd first.")


# ---------------------------------------------------------------------------
# Safe write ranges (C2)
# ---------------------------------------------------------------------------

# Default: only allow writes to typical Cortex-M SRAM regions.
# Override with MCJTAG_SAFE_WRITE_RANGES="0x20000000-0x20100000,0x10000000-0x10010000"
# Set to "none" for unrestricted writes.
_DEFAULT_SAFE_WRITE_RANGES = [
    (0x20000000, 0x20100000),  # Main SRAM (up to 1 MB)
]


def get_safe_write_ranges() -> list[tuple[int, int]]:
    """Parse MCJTAG_SAFE_WRITE_RANGES env var or return defaults."""
    raw = os.environ.get("MCJTAG_SAFE_WRITE_RANGES")
    if raw is None:
        return _DEFAULT_SAFE_WRITE_RANGES
    if raw.strip().lower() == "none":
        return []  # Unrestricted mode — explicit opt-in
    ranges: list[tuple[int, int]] = []
    for pair in raw.split(","):
        pair = pair.strip()
        if not pair:
            continue
        if "-" not in pair:
            log.warning("MCJTAG_SAFE_WRITE_RANGES: skipping entry %r (no '-' separator)", pair)
            continue
        parts = pair.split("-", 1)
        try:
            start, end = int(parts[0], 16), int(parts[1], 16)
        except ValueError:
            log.warning("MCJTAG_SAFE_WRITE_RANGES: skipping entry %r (invalid hex)", pair)
            continue
        if start >= end:
            log.warning(
                "MCJTAG_SAFE_WRITE_RANGES: skipping reversed range 0x%08x-0x%08x",
                start,
                end,
            )
            continue
        ranges.append((start, end))
    return ranges


def check_write_address(addr: int, byte_size: int) -> None:
    """Raise if the write region falls outside safe ranges.

    Safe ranges default to SRAM only. Set MCJTAG_SAFE_WRITE_RANGES=none
    to disable this check for advanced use.
    """
    safe_ranges = get_safe_write_ranges()
    if not safe_ranges:
        return  # Unrestricted mode
    end_addr = addr + byte_size
    for start, end in safe_ranges:
        if start <= addr and end_addr <= end:
            return
    range_strs = [f"0x{s:08x}-0x{e:08x}" for s, e in safe_ranges]
    raise OpenOCDError(
        f"Write to 0x{addr:08x} ({byte_size} bytes) is outside safe write ranges: "
        f"{', '.join(range_strs)}. "
        f"Set MCJTAG_SAFE_WRITE_RANGES=none to disable this check."
    )


# ---------------------------------------------------------------------------
# Hexdump formatting (H1 — build from already-read values, no re-read)
# ---------------------------------------------------------------------------

_HEXDUMP_BYTES_PER_LINE = 16


def values_to_bytes(values: list[int], width: int) -> bytes:
    """Convert a list of integer values to little-endian bytes."""
    fmt_map = {8: "<B", 16: "<H", 32: "<I", 64: "<Q"}
    fmt = fmt_map.get(width)
    if fmt is None:
        raise OpenOCDError(f"Unsupported width: {width}")
    return b"".join(struct.pack(fmt, v) for v in values)


def format_hexdump(addr: int, data: bytes) -> str:
    """Format bytes as a hex + ASCII dump (16 bytes per line).

    Output format::

        08000000: 00 50 00 20 A1 01 00 08  AB 01 00 08 AD 01 00 08  |.P. ............|
    """
    lines: list[str] = []
    for offset in range(0, len(data), _HEXDUMP_BYTES_PER_LINE):
        chunk = data[offset : offset + _HEXDUMP_BYTES_PER_LINE]
        line_addr = addr + offset

        hex_parts: list[str] = []
        for i, b in enumerate(chunk):
            hex_parts.append(f"{b:02X}")
            if i == 7:
                hex_parts.append("")
        hex_str = " ".join(hex_parts).ljust(49)

        ascii_str = "".join(chr(b) if 0x20 <= b < 0x7F else "." for b in chunk)
        lines.append(f"{line_addr:08X}: {hex_str} |{ascii_str}|")

    return "\n".join(lines)
