"""FastMCP server for CP210x device customization."""

import subprocess

from fastmcp import Context, FastMCP
from fastmcp.server.elicitation import AcceptedElicitation

from .bindings import (
    ENHANCED_FXN_FLAGS,
    FLUSH_FLAGS_CP2104,
    FLUSH_FLAGS_CP2105,
    PARTS_CP2102N,
    PARTS_WITH_DEVICE_MODE,
    PARTS_WITH_DUAL_PORT_CONFIG,
    PARTS_WITH_FLUSH_CONFIG,
    PARTS_WITH_PORT_CONFIG,
    PARTS_WITH_QUAD_PORT_CONFIG,
    PORT_PIN_FLAGS,
    CP210xDevice,
    CP210xError,
    CP210xLibrary,
    decode_bitmask,
    encode_bitmask,
)

mcp = FastMCP(
    "cp210x",
    instructions="CP210x USB-UART bridge customization - read/write product strings, serial numbers, and device configuration",
)

_lib: CP210xLibrary | None = None


def get_lib() -> CP210xLibrary:
    global _lib
    if _lib is None:
        _lib = CP210xLibrary()
    return _lib


async def confirm_write(ctx: Context, message: str) -> bool:
    """Normal-tier confirmation: falls back to proceeding if elicitation unavailable."""
    try:
        result = await ctx.elicit(message, ["Confirm", "Cancel"])
        return isinstance(result, AcceptedElicitation) and result.data == "Confirm"
    except Exception:
        return True


async def strict_confirm(ctx: Context, message: str) -> dict | None:
    """Strict-tier confirmation: returns error dict if elicitation unavailable."""
    if not ctx:
        return {
            "error": "This operation requires interactive confirmation",
            "message": "Too dangerous to proceed without explicit user consent.",
        }
    try:
        result = await ctx.elicit(message, ["Confirm", "Cancel"])
        confirmed = isinstance(result, AcceptedElicitation) and result.data == "Confirm"
    except Exception:
        return {
            "error": "This operation requires elicitation support",
            "message": "Your MCP client does not support elicitation. "
            "This operation is too dangerous to proceed without explicit confirmation.",
        }
    if not confirmed:
        return {"cancelled": True, "message": "Operation cancelled by user"}
    return None


# ===========================================================================
# Read-only tools (no confirmation needed)
# ===========================================================================

@mcp.tool()
def list_devices() -> list[dict]:
    """List all connected CP210x devices.

    Returns a list of devices with their index, description, and serial number.
    Use the index to reference a specific device in other tools.
    """
    lib = get_lib()
    num_devices = lib.get_num_devices()

    devices = []
    for i in range(num_devices):
        try:
            desc = lib.get_product_string(i, flag=1)
            serial = lib.get_product_string(i, flag=0)
            devices.append({
                "index": i,
                "description": desc,
                "serial_number": serial,
            })
        except CP210xError as e:
            devices.append({"index": i, "error": str(e)})

    return devices


@mcp.tool()
def get_device_info(device_index: int = 0) -> dict:
    """Get detailed information about a CP210x device.

    Args:
        device_index: Zero-based index of the device (default: 0 for first device)

    Returns full device details including part number, VID/PID, strings, and power settings.
    """
    lib = get_lib()

    with CP210xDevice(lib, device_index) as dev:
        part_code, part_name = dev.part_number
        info = {
            "part_number": part_name,
            "part_code": f"0x{part_code:02X}",
            "vid": f"0x{dev.vid:04X}",
            "pid": f"0x{dev.pid:04X}",
            "manufacturer": dev.manufacturer,
            "product": dev.product,
            "serial_number": dev.serial_number,
            "device_version": dev.device_version,
            "max_power_ma": dev.max_power_ma,
            "self_powered": dev.self_powered,
            "is_locked": dev.is_locked,
        }

        # Conditionally add part-specific fields
        fw = dev.firmware_version
        if fw is not None:
            info["firmware_version"] = fw

        flush = dev.flush_buffer_config
        if flush is not None:
            # Decode to named flags based on part
            if part_code in PARTS_WITH_FLUSH_CONFIG:
                flag_map = FLUSH_FLAGS_CP2105 if part_code == 0x05 else FLUSH_FLAGS_CP2104
                info["flush_buffer_config"] = decode_bitmask(flush, flag_map)
            else:
                info["flush_buffer_config_raw"] = f"0x{flush:04X}"

        mode = dev.device_mode
        if mode is not None:
            info["device_mode"] = {"eci": mode[0], "sci": mode[1]}

        return info


@mcp.tool()
def get_firmware_version(device_index: int = 0) -> dict:
    """Get firmware version (CP2102N only).

    Args:
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        fw = dev.firmware_version
        if fw is None:
            part_code, part_name = dev.part_number
            return {"error": f"Firmware version not available on {part_name}",
                    "note": "Only CP2102N variants support this"}
        return {"firmware_version": fw}


@mcp.tool()
def get_flush_buffer_config(device_index: int = 0) -> dict:
    """Get flush buffer configuration (CP2104/CP2105/CP2108).

    Args:
        device_index: Zero-based device index (default: 0)

    Returns named flags showing which buffers are flushed on open/close.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        part_code, part_name = dev.part_number
        if part_code not in PARTS_WITH_FLUSH_CONFIG:
            return {"error": f"Flush buffer config not available on {part_name}",
                    "supported": "CP2104, CP2105, CP2108"}

        raw = lib.get_flush_buffer_config(dev.handle)
        flag_map = FLUSH_FLAGS_CP2105 if part_code == 0x05 else FLUSH_FLAGS_CP2104
        return {
            "raw": f"0x{raw:04X}",
            "flags": decode_bitmask(raw, flag_map),
        }


@mcp.tool()
def get_device_mode(device_index: int = 0) -> dict:
    """Get device mode (CP2105 only).

    Args:
        device_index: Zero-based device index (default: 0)

    Returns ECI and SCI interface modes.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        part_code, part_name = dev.part_number
        if part_code not in PARTS_WITH_DEVICE_MODE:
            return {"error": f"Device mode not available on {part_name}",
                    "supported": "CP2105"}

        eci, sci = lib.get_device_mode(dev.handle)
        return {"eci_mode": eci, "sci_mode": sci}


@mcp.tool()
def get_interface_string(interface_number: int, device_index: int = 0) -> dict:
    """Get USB interface string (CP2105/CP2108 only).

    Args:
        interface_number: Interface index (0 or 1 for CP2105, 0-3 for CP2108)
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        try:
            value = dev.get_interface_string(interface_number)
            return {"interface": interface_number, "value": value}
        except CP210xError as e:
            return {"error": str(e)}


@mcp.tool()
def get_baud_rate_config(device_index: int = 0) -> dict:
    """Get the baud rate alias configuration table (32 entries).

    Args:
        device_index: Zero-based device index (default: 0)

    Returns the full 32-entry baud rate alias table showing how standard
    baud rates map to timer register values.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        configs = lib.get_baud_rate_config(dev.handle)
        return {"entries": configs}


@mcp.tool()
def get_port_config(device_index: int = 0) -> dict:
    """Get GPIO port configuration (auto-detects CP2103/4/5/8).

    Args:
        device_index: Zero-based device index (default: 0)

    Returns pin modes, latch values, and enhanced function settings.
    Auto-dispatches to the correct struct based on part number.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        part_code, part_name = dev.part_number

        if part_code in PARTS_WITH_PORT_CONFIG:
            raw = lib.get_port_config(dev.handle)
            return {
                "type": "single",
                "part": part_name,
                "mode": decode_bitmask(raw["mode"], PORT_PIN_FLAGS),
                "reset_latch": decode_bitmask(raw["reset_latch"], PORT_PIN_FLAGS),
                "suspend_latch": decode_bitmask(raw["suspend_latch"], PORT_PIN_FLAGS),
                "enhanced_fxn": decode_bitmask(raw["enhanced_fxn"], ENHANCED_FXN_FLAGS),
                "raw": raw,
            }
        elif part_code in PARTS_WITH_DUAL_PORT_CONFIG:
            raw = lib.get_dual_port_config(dev.handle)
            return {"type": "dual", "part": part_name, **raw}
        elif part_code in PARTS_WITH_QUAD_PORT_CONFIG:
            raw = lib.get_quad_port_config(dev.handle)
            return {"type": "quad", "part": part_name, **raw}
        else:
            return {"error": f"Port config not available on {part_name}",
                    "supported": "CP2103, CP2104, CP2105, CP2108"}


@mcp.tool()
def get_raw_config(device_index: int = 0, size: int = 512) -> dict:
    """Read raw EPROM configuration blob.

    Args:
        device_index: Zero-based device index (default: 0)
        size: Number of bytes to read (default: 512)

    Returns hex-encoded config data.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        data = lib.get_config(dev.handle, size)
        return {"hex": data.hex(), "size": len(data)}


@mcp.tool()
def create_hex_file(filename: str, device_index: int = 0) -> dict:
    """Dump device config to Intel HEX file.

    Args:
        filename: Output file path for the .hex file
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        lib.create_hex_file(dev.handle, filename)
        return {"success": True, "filename": filename}


# ===========================================================================
# Normal-tier write tools (elicit → fallback to proceed)
# ===========================================================================

@mcp.tool()
async def set_product_string(product: str, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the USB product string (device name shown to host).

    Args:
        product: New product string (max 126 characters)
        device_index: Zero-based device index (default: 0)

    Returns the updated device info. Device may need to be re-plugged for
    changes to appear on the USB host.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_product = dev.product
        new_value = product[:126]
        if ctx and not await confirm_write(
            ctx,
            f"Write product string to CP210x OTP EPROM?\n\n"
            f"  Old: {old_product}\n  New: {new_value}\n\n"
            f"OTP writes are limited — this cannot be undone easily.",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.product = product
        return {"success": True, "old_value": old_product, "new_value": new_value,
                "note": "Re-plug device for changes to take effect on USB host"}


@mcp.tool()
async def set_manufacturer_string(manufacturer: str, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the USB manufacturer string.

    Args:
        manufacturer: New manufacturer string (max 45 characters)
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_mfr = dev.manufacturer
        new_value = manufacturer[:45]
        if ctx and not await confirm_write(
            ctx,
            f"Write manufacturer string to CP210x OTP EPROM?\n\n"
            f"  Old: {old_mfr}\n  New: {new_value}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.manufacturer = manufacturer
        return {"success": True, "old_value": old_mfr, "new_value": new_value,
                "note": "Re-plug device for changes to take effect on USB host"}


@mcp.tool()
async def set_serial_number(serial_number: str, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the USB serial number.

    Args:
        serial_number: New serial number (max 63 characters)
        device_index: Zero-based device index (default: 0)

    Note: Changing serial number may affect udev rules that match on serial.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_serial = dev.serial_number
        new_value = serial_number[:63]
        if ctx and not await confirm_write(
            ctx,
            f"Write serial number to CP210x OTP EPROM?\n\n"
            f"  Old: {old_serial}\n  New: {new_value}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.serial_number = serial_number
        return {"success": True, "old_value": old_serial, "new_value": new_value,
                "note": "Re-plug device for changes to take effect on USB host"}


@mcp.tool()
async def set_max_power(max_power_ma: int, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the maximum USB power draw in milliamps.

    Args:
        max_power_ma: Max power in mA (0-500, will be rounded to nearest 2mA)
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    if max_power_ma < 0 or max_power_ma > 500:
        return {"error": "max_power_ma must be 0-500"}
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_power = dev.max_power_ma
        actual_value = (max_power_ma // 2) * 2
        if ctx and not await confirm_write(
            ctx,
            f"Write max power to CP210x OTP EPROM?\n\n"
            f"  Old: {old_power} mA\n  New: {actual_value} mA",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.max_power_ma = max_power_ma
        return {"success": True, "old_value_ma": old_power, "new_value_ma": actual_value}


@mcp.tool()
async def set_self_powered(self_powered: bool, device_index: int = 0, ctx: Context = None) -> dict:
    """Set whether device reports as self-powered or bus-powered.

    Args:
        self_powered: True for self-powered, False for bus-powered
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_value = dev.self_powered
        mode = "self-powered" if self_powered else "bus-powered"
        if ctx and not await confirm_write(
            ctx,
            f"Write power mode to CP210x OTP EPROM?\n\n"
            f"  Old: {'self-powered' if old_value else 'bus-powered'}\n  New: {mode}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.self_powered = self_powered
        return {"success": True, "old_value": old_value, "new_value": self_powered}


@mcp.tool()
async def set_device_version(version: int, device_index: int = 0, ctx: Context = None) -> dict:
    """Set device version (bcdDevice field).

    Args:
        version: BCD version number (e.g., 0x0100 for 1.00)
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_version = dev.device_version
        if ctx and not await confirm_write(
            ctx,
            f"Write device version to CP210x OTP EPROM?\n\n"
            f"  Old: {old_version}\n  New: 0x{version:04X}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.device_version = version
        new_ver = f"{(version >> 8) & 0xFF}.{version & 0xFF:02d}"
        return {"success": True, "old_value": old_version, "new_value": new_ver}


@mcp.tool()
async def set_interface_string(
    interface_number: int, value: str, device_index: int = 0, ctx: Context = None,
) -> dict:
    """Set USB interface string (CP2105/CP2108 only).

    Args:
        interface_number: Interface index (0 or 1 for CP2105, 0-3 for CP2108)
        value: New interface string
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        try:
            old_value = dev.get_interface_string(interface_number)
        except CP210xError as e:
            return {"error": str(e)}
        if ctx and not await confirm_write(
            ctx,
            f"Write interface {interface_number} string to CP210x OTP EPROM?\n\n"
            f"  Old: {old_value}\n  New: {value}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        dev.set_interface_string(interface_number, value)
        return {"success": True, "interface": interface_number,
                "old_value": old_value, "new_value": value}


@mcp.tool()
async def set_flush_buffer_config(
    flags: dict, device_index: int = 0, ctx: Context = None,
) -> dict:
    """Set flush buffer configuration (CP2104/CP2105/CP2108).

    Args:
        flags: Dict of flag_name -> bool. Flag names depend on part:
               CP2104: open_tx, open_rx, close_tx, close_rx
               CP2105: open_tx_sci, open_rx_sci, close_tx_sci, close_rx_sci,
                       open_tx_eci, open_rx_eci, close_tx_eci, close_rx_eci
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        part_code, part_name = dev.part_number
        if part_code not in PARTS_WITH_FLUSH_CONFIG:
            return {"error": f"Flush buffer config not available on {part_name}"}
        flag_map = FLUSH_FLAGS_CP2105 if part_code == 0x05 else FLUSH_FLAGS_CP2104
        new_value = encode_bitmask(flags, flag_map)
        old_raw = lib.get_flush_buffer_config(dev.handle)
        if ctx and not await confirm_write(
            ctx,
            f"Write flush buffer config to CP210x?\n\n"
            f"  Old: 0x{old_raw:04X}\n  New: 0x{new_value:04X}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        lib.set_flush_buffer_config(dev.handle, new_value)
        return {"success": True, "old_raw": f"0x{old_raw:04X}",
                "new_raw": f"0x{new_value:04X}", "flags": flags}


@mcp.tool()
async def set_device_mode(
    eci_mode: int, sci_mode: int, device_index: int = 0, ctx: Context = None,
) -> dict:
    """Set device mode (CP2105 only).

    Args:
        eci_mode: Enhanced interface mode byte
        sci_mode: Standard interface mode byte
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        part_code, part_name = dev.part_number
        if part_code not in PARTS_WITH_DEVICE_MODE:
            return {"error": f"Device mode not available on {part_name}"}
        old_eci, old_sci = lib.get_device_mode(dev.handle)
        if ctx and not await confirm_write(
            ctx,
            f"Write device mode to CP210x?\n\n"
            f"  Old ECI: 0x{old_eci:02X}, SCI: 0x{old_sci:02X}\n"
            f"  New ECI: 0x{eci_mode:02X}, SCI: 0x{sci_mode:02X}",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        lib.set_device_mode(dev.handle, eci_mode, sci_mode)
        return {"success": True,
                "old": {"eci": old_eci, "sci": old_sci},
                "new": {"eci": eci_mode, "sci": sci_mode}}


@mcp.tool()
async def set_baud_rate_config(
    entries: list[dict], device_index: int = 0, ctx: Context = None,
) -> dict:
    """Set the full 32-entry baud rate alias table.

    Args:
        entries: List of 32 dicts, each with baud_gen, timer0_reload, prescaler, baud_rate
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    if len(entries) != 32:
        return {"error": f"Expected 32 baud config entries, got {len(entries)}"}
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        if ctx and not await confirm_write(
            ctx,
            "Write complete baud rate table (32 entries) to CP210x OTP EPROM?",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        lib.set_baud_rate_config(dev.handle, entries)
        return {"success": True, "entries_written": 32}


@mcp.tool()
async def set_baud_rate_alias(
    index: int, baud_rate: int, baud_gen: int, timer0_reload: int, prescaler: int,
    device_index: int = 0, ctx: Context = None,
) -> dict:
    """Modify a single baud rate alias entry (read-modify-write).

    Args:
        index: Entry index (0-31)
        baud_rate: Target baud rate
        baud_gen: Baud rate generator register value
        timer0_reload: Timer0 reload register value
        prescaler: Prescaler value
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    if not 0 <= index < 32:
        return {"error": "Index must be 0-31"}
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        current = lib.get_baud_rate_config(dev.handle)
        old_entry = current[index].copy()
        current[index] = {
            "index": index,
            "baud_gen": baud_gen,
            "timer0_reload": timer0_reload,
            "prescaler": prescaler,
            "baud_rate": baud_rate,
        }
        if ctx and not await confirm_write(
            ctx,
            f"Modify baud rate alias #{index}?\n\n"
            f"  Old: {old_entry['baud_rate']} baud\n"
            f"  New: {baud_rate} baud",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}
        lib.set_baud_rate_config(dev.handle, current)
        return {"success": True, "index": index,
                "old_entry": old_entry, "new_baud_rate": baud_rate}


@mcp.tool()
async def set_port_config(
    config: dict, device_index: int = 0, ctx: Context = None,
) -> dict:
    """Set GPIO port configuration (auto-detects CP2103/4/5/8).

    Args:
        config: Port configuration dict. Structure depends on part type:
                Single (CP2103/4): {mode, reset_latch, suspend_latch, enhanced_fxn}
                  - Values can be raw integers or named-flag dicts
                Dual (CP2105): {mode, reset_latch, suspend_latch,
                                enhanced_fxn_eci, enhanced_fxn_sci, enhanced_fxn_device}
                Quad (CP2108): Full quad port config dict (see get_port_config output)
        device_index: Zero-based device index (default: 0)
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        part_code, part_name = dev.part_number

        if ctx and not await confirm_write(
            ctx,
            f"Write port configuration to {part_name} OTP EPROM?",
        ):
            return {"cancelled": True, "message": "Write cancelled by user"}

        if part_code in PARTS_WITH_PORT_CONFIG:
            # Allow either raw ints or named-flag dicts
            mode = config.get("mode", 0)
            if isinstance(mode, dict):
                mode = encode_bitmask(mode, PORT_PIN_FLAGS)
            reset = config.get("reset_latch", 0)
            if isinstance(reset, dict):
                reset = encode_bitmask(reset, PORT_PIN_FLAGS)
            suspend = config.get("suspend_latch", 0)
            if isinstance(suspend, dict):
                suspend = encode_bitmask(suspend, PORT_PIN_FLAGS)
            ef = config.get("enhanced_fxn", 0)
            if isinstance(ef, dict):
                ef = encode_bitmask(ef, ENHANCED_FXN_FLAGS)
            lib.set_port_config(dev.handle, mode, reset, suspend, ef)
            return {"success": True, "type": "single", "part": part_name}

        elif part_code in PARTS_WITH_DUAL_PORT_CONFIG:
            lib.set_dual_port_config(
                dev.handle,
                config["mode"], config["reset_latch"], config["suspend_latch"],
                config["enhanced_fxn_eci"], config["enhanced_fxn_sci"],
                config["enhanced_fxn_device"],
            )
            return {"success": True, "type": "dual", "part": part_name}

        elif part_code in PARTS_WITH_QUAD_PORT_CONFIG:
            lib.set_quad_port_config(dev.handle, config)
            return {"success": True, "type": "quad", "part": part_name}

        else:
            return {"error": f"Port config not available on {part_name}"}


@mcp.tool()
def reset_device(device_index: int = 0) -> dict:
    """Reset the CP210x device (USB disconnect/reconnect).

    Args:
        device_index: Zero-based device index (default: 0)

    This triggers a USB re-enumeration, which will make any programmed
    changes visible to the USB host.
    """
    lib = get_lib()
    with CP210xDevice(lib, device_index) as dev:
        dev.reset()
    return {"success": True, "note": "Device has been reset. It may take a moment to re-enumerate."}


@mcp.tool()
async def setup_udev_rule(
    device_index: int = 0,
    symlink_name: str | None = None,
    ctx: Context = None,
) -> dict:
    """Create a udev rule for stable /dev/ symlink for a CP210x device.

    Generates a rule that matches the device's product string and creates
    a persistent symlink (e.g., /dev/rylr998-0D27) that survives re-plugs
    and port reordering.

    Args:
        device_index: Zero-based device index (default: 0)
        symlink_name: Custom symlink name. If not provided, auto-generates
                     from the product string (e.g., "RYLR998 ...0D27" -> "rylr998-0D27")

    Requires sudo for rule installation.
    """
    lib = get_lib()

    with CP210xDevice(lib, device_index) as dev:
        product = dev.product
        vid = dev.vid
        pid = dev.pid

    if not product or product == "CP2102 USB to UART Bridge Controller":
        return {
            "error": "Device has default product string",
            "message": "Customize the product string first to create a unique match rule.",
        }

    if not symlink_name:
        parts = product.split()
        if len(parts) >= 2:
            prefix = parts[0].lower()
            suffix = parts[-1][-4:]
            symlink_name = f"{prefix}-{suffix}"
        else:
            symlink_name = product.lower().replace(" ", "-")[:32]

    match_suffix = product[-4:]
    rule = (
        f'SUBSYSTEM=="tty", ATTRS{{idVendor}}=="{vid:04x}", '
        f'ATTRS{{idProduct}}=="{pid:04x}", '
        f'ATTRS{{product}}=="*{match_suffix}", '
        f'SYMLINK+="{symlink_name}"'
    )

    rules_path = f"/usr/lib/udev/rules.d/99-cp210x-{symlink_name}.rules"
    rule_content = (
        f"# Auto-generated by mcp210x-uart for: {product}\n"
        f"# Creates /dev/{symlink_name} symlink\n"
        f"{rule}\n"
    )

    install_msg = (
        f"Install udev rule for stable device symlink?\n\n"
        f"  Device:  {product}\n"
        f"  Symlink: /dev/{symlink_name}\n"
        f"  File:    {rules_path}\n\n"
        f"Requires sudo to install."
    )

    if ctx and not await confirm_write(ctx, install_msg):
        return {"cancelled": True, "rule": rule_content,
                "message": "Rule not installed. You can install it manually."}

    try:
        result = subprocess.run(
            ["sudo", "tee", rules_path],
            input=rule_content, capture_output=True, text=True, timeout=30,
        )
        if result.returncode != 0:
            return {"error": f"Failed to install rule: {result.stderr.strip()}",
                    "rule": rule_content, "message": "Install manually with: sudo tee " + rules_path}
        subprocess.run(["sudo", "udevadm", "control", "--reload-rules"],
                       capture_output=True, timeout=10)
        subprocess.run(["sudo", "udevadm", "trigger"],
                       capture_output=True, timeout=10)
    except subprocess.TimeoutExpired:
        return {"error": "Sudo timed out — may need interactive password",
                "rule": rule_content, "message": "Install manually with: sudo tee " + rules_path}

    return {"success": True, "symlink": f"/dev/{symlink_name}",
            "rules_file": rules_path, "rule": rule_content,
            "note": "Symlink will appear after device re-plug or udevadm trigger"}


# ===========================================================================
# Strict-tier write tools (hard-refuse without elicitation)
# ===========================================================================

@mcp.tool()
async def set_vid(vid: int, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the USB Vendor ID (DANGEROUS — can break driver matching).

    Args:
        vid: New USB Vendor ID (16-bit, e.g., 0x10C4 for Silicon Labs)
        device_index: Zero-based device index (default: 0)

    WARNING: Changing VID can prevent the device from being recognized by
    standard CP210x drivers. Only use if you have custom drivers.
    """
    lib = get_lib()
    err = await strict_confirm(
        ctx,
        f"Write USB Vendor ID to CP210x OTP EPROM?\n\n"
        f"  New VID: 0x{vid:04X}\n\n"
        f"WARNING: Changing VID can prevent the device from being recognized\n"
        f"by standard CP210x drivers. This is PERMANENT.",
    )
    if err:
        return err
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_vid = dev.vid
        lib.set_vid(dev.handle, vid)
        return {"success": True, "old_vid": f"0x{old_vid:04X}", "new_vid": f"0x{vid:04X}"}


@mcp.tool()
async def set_pid(pid: int, device_index: int = 0, ctx: Context = None) -> dict:
    """Set the USB Product ID (DANGEROUS — can break driver matching).

    Args:
        pid: New USB Product ID (16-bit, e.g., 0xEA60)
        device_index: Zero-based device index (default: 0)

    WARNING: Changing PID can prevent the device from being recognized by
    standard CP210x drivers.
    """
    lib = get_lib()
    err = await strict_confirm(
        ctx,
        f"Write USB Product ID to CP210x OTP EPROM?\n\n"
        f"  New PID: 0x{pid:04X}\n\n"
        f"WARNING: Changing PID can prevent the device from being recognized\n"
        f"by standard CP210x drivers. This is PERMANENT.",
    )
    if err:
        return err
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        old_pid = dev.pid
        lib.set_pid(dev.handle, pid)
        return {"success": True, "old_pid": f"0x{old_pid:04X}", "new_pid": f"0x{pid:04X}"}


@mcp.tool()
async def set_raw_config(hex_data: str, device_index: int = 0, ctx: Context = None) -> dict:
    """Write raw configuration blob to device EPROM (DANGEROUS).

    Args:
        hex_data: Hex-encoded config data
        device_index: Zero-based device index (default: 0)

    WARNING: Writing invalid config data can brick the device.
    Use get_raw_config to read current config first.
    """
    lib = get_lib()
    try:
        data = bytes.fromhex(hex_data)
    except ValueError:
        return {"error": "Invalid hex string"}
    err = await strict_confirm(
        ctx,
        f"Write {len(data)} bytes of raw config to CP210x EPROM?\n\n"
        f"WARNING: Writing invalid config data can brick the device.\n"
        f"This is PERMANENT and cannot be undone.",
    )
    if err:
        return err
    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is locked and cannot be modified"}
        lib.set_config(dev.handle, data)
        return {"success": True, "bytes_written": len(data)}


@mcp.tool()
async def update_firmware(device_index: int = 0, ctx: Context = None) -> dict:
    """Update device firmware (CP2102N only, DANGEROUS).

    Args:
        device_index: Zero-based device index (default: 0)

    WARNING: Firmware update failure can brick the device.
    """
    lib = get_lib()
    err = await strict_confirm(
        ctx,
        "Update CP2102N firmware?\n\n"
        "WARNING: Firmware update failure can BRICK the device.\n"
        "Ensure stable USB connection and power during update.",
    )
    if err:
        return err
    with CP210xDevice(lib, device_index) as dev:
        part_code, part_name = dev.part_number
        if part_code not in PARTS_CP2102N:
            return {"error": f"Firmware update not available on {part_name}",
                    "supported": "CP2102N variants only"}
        lib.update_firmware(dev.handle)
        return {"success": True, "note": "Firmware updated. Device may need reset."}


@mcp.tool()
async def lock_device(device_index: int = 0, ctx: Context = None) -> dict:
    """PERMANENTLY lock the device to prevent further customization.

    Args:
        device_index: Zero-based device index (default: 0)

    WARNING: This is PERMANENT and IRREVERSIBLE! Once locked, the device's
    configuration cannot be changed. The device will still function normally,
    but strings, power settings, etc. cannot be modified.
    """
    lib = get_lib()

    with CP210xDevice(lib, device_index) as dev:
        if dev.is_locked:
            return {"error": "Device is already locked"}

        part_code, part_name = dev.part_number
        err = await strict_confirm(
            ctx,
            f"PERMANENTLY lock this {part_name}?\n\n"
            f"  Product: {dev.product}\n"
            f"  Serial:  {dev.serial_number}\n\n"
            f"THIS CANNOT BE UNDONE. The device configuration\n"
            f"will be frozen forever.",
        )
        if err:
            return err

        lib.lock_device(dev.handle)
        return {"success": True,
                "warning": "Device is now PERMANENTLY locked. Configuration cannot be changed."}


def main():
    """Entry point for the MCP server."""
    mcp.run()


if __name__ == "__main__":
    main()
