from __future__ import annotations

import time
from typing import Dict

from ..errors import AdapterError
from ..protocols import AUTO, BusProtocol
from .adapter import Adapter, Output
from .command_bundle import AsyncHandlerWithWriter


class ELM327:
    static_commands: Dict[str, str] = {
        "ATAL": Output.ok,
        "ATAT": Output.ok,
        "ATNL": Output.ok,
    }

    dynamic_commands: Dict[str, AsyncHandlerWithWriter] = {
        "AT@1": lambda _s, adapter, writer: writer(adapter.elm_at1_descriptor),
        "AT@2": lambda _s, adapter, writer: writer(adapter.elm_at2_name),
        "ATAT0": lambda _s, adapter, writer: _set_attr(adapter, "adaptive_timing", False, writer),
        "ATAT1": lambda _s, adapter, writer: _set_attr(adapter, "adaptive_timing", True, writer),
        "ATIGN": lambda _s, adapter, writer: writer(adapter.elm_atign_state),
        "ATI": lambda _s, adapter, writer: writer(adapter.elm_ati_version),
        "ATV0": lambda _s, adapter, writer: _set_attr(adapter, "variable_data_length", False, writer),
        "ATV1": lambda _s, adapter, writer: _set_attr(adapter, "variable_data_length", True, writer),
        "ATCAF0": lambda _s, adapter, writer: _set_attr(adapter, "can_auto_format", False, writer),
        "ATCAF1": lambda _s, adapter, writer: _set_attr(adapter, "can_auto_format", True, writer),
        "ATCFC0": lambda _s, adapter, writer: _set_attr(adapter, "auto_segmentation_rx", False, writer),
        "ATCFC1": lambda _s, adapter, writer: _set_attr(adapter, "auto_segmentation_rx", True, writer),
        "ATDP": lambda _s, adapter, writer: writer(adapter.selected_protocol.description),
        "ATDPN": lambda _s, adapter, writer: writer(_describe_selected_protocol(adapter)),
        "ATD": lambda _s, adapter, writer: _defaults(adapter, writer),
        "ATE0": lambda _s, adapter, writer: _set_attr(adapter, "echo", False, writer),
        "ATE1": lambda _s, adapter, writer: _set_attr(adapter, "echo", True, writer),
        "ATH0": lambda _s, adapter, writer: _set_attr(adapter, "print_headers", False, writer),
        "ATH1": lambda _s, adapter, writer: _set_attr(adapter, "print_headers", True, writer),
        "ATL0": lambda _s, adapter, writer: _set_attr(adapter, "linefeed", False, writer),
        "ATL1": lambda _s, adapter, writer: _set_attr(adapter, "linefeed", True, writer),
        "ATMA": lambda _s, _adapter, _writer: (_ for _ in ()).throw(AdapterError.start_monitor_mode()),
        "ATAR": lambda _s, _adapter, writer: writer(Output.ok),
        "ATPC": lambda _s, _adapter, writer: writer(Output.ok),
        "ATR0": lambda _s, adapter, writer: _set_attr(adapter, "responses_enabled", False, writer),
        "ATR1": lambda _s, adapter, writer: _set_attr(adapter, "responses_enabled", True, writer),
        "ATRV": lambda _s, adapter, writer: writer(adapter.elm_atrv_voltage),
        "ATS0": lambda _s, adapter, writer: _set_attr(adapter, "spaces", False, writer),
        "ATS1": lambda _s, adapter, writer: _set_attr(adapter, "spaces", True, writer),
        "ATWS": lambda _s, adapter, writer: _reset_adapter(adapter, writer, with_delay=False),
        "ATZ": lambda _s, adapter, writer: _reset_adapter(adapter, writer, with_delay=True),
    }

    prefix_commands: Dict[str, AsyncHandlerWithWriter] = {
        "ATCM": lambda s, adapter, writer: _set_mask_or_filter(s, adapter, writer, target="mask"),
        "ATCF": lambda s, adapter, writer: _set_mask_or_filter(s, adapter, writer, target="filter"),
        "ATCRA": lambda s, adapter, writer: _set_can_reply_address(s, adapter, writer),
        "ATCEA": lambda s, adapter, writer: _set_extended_address(s, adapter, writer, target="request"),
        "AT+CEA": lambda s, adapter, writer: _set_extended_address(s, adapter, writer, target="request"),
        "ATCER": lambda s, adapter, writer: _set_extended_address(s, adapter, writer, target="reply"),
        "AT+CER": lambda s, adapter, writer: _set_extended_address(s, adapter, writer, target="reply"),
        "ATSH": lambda s, adapter, writer: _set_request_header(s, adapter, writer),
        "ATSP": lambda s, adapter, writer: _set_protocol(s, adapter, writer),
        "ATST": lambda s, adapter, writer: _set_timeout(s, adapter, writer),
        "ATTP": lambda s, adapter, writer: _set_protocol_timeout(s, adapter, writer),
    }


def _set_attr(adapter: Adapter, field: str, value: bool, writer) -> None:
    setattr(adapter, field, value)
    writer(Output.ok)


def _reset_adapter(adapter: Adapter, writer, with_delay: bool) -> None:
    adapter.reset()
    if with_delay:
        time.sleep(1.3)
    adapter.write_line_ending()
    adapter.write_line_ending()
    writer(adapter.elm_ati_version)


def _defaults(adapter: Adapter, writer) -> None:
    adapter.reset()
    writer(Output.ok)


def _set_mask_or_filter(value: str, adapter: Adapter, writer, target: str) -> None:
    length = len(value)
    if length not in (3, 8):
        raise AdapterError.invalid_command()
    try:
        parsed = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    if target == "mask":
        adapter.can_reply_mask = parsed
    else:
        adapter.can_reply_filter = parsed
    writer(Output.ok)


def _set_can_reply_address(value: str, adapter: Adapter, writer) -> None:
    length = len(value)
    if length == 0:
        # Reset to "accept all" — don't apply protocol-specific filtering
        adapter.can_reply_mask = 0
        adapter.can_reply_filter = 0
        writer(Output.ok)
        return
    if length not in (3, 8):
        raise AdapterError.invalid_command()
    try:
        parsed = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    if length == 3:
        adapter.can_reply_mask = 0xFFF
        adapter.can_reply_filter = parsed
    else:
        adapter.can_reply_mask = 0xFFFFFFFF
        adapter.can_reply_filter = parsed
    writer(Output.ok)


def _set_extended_address(value: str, adapter: Adapter, writer, target: str) -> None:
    if value == "":
        if target == "request":
            adapter.request_header_extension = None
        else:
            adapter.reply_header_extension = None
        writer(Output.ok)
        return
    if len(value) != 2:
        raise AdapterError.invalid_command()
    try:
        byte = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    if target == "request":
        adapter.request_header_extension = byte
    else:
        adapter.reply_header_extension = byte
    writer(Output.ok)


def _set_request_header(value: str, adapter: Adapter, writer) -> None:
    length = len(value)
    if length not in (3, 6, 8):
        raise AdapterError.invalid_command()
    if length == 6 and value.startswith("000"):
        # Some tools zero-pad 11-bit CAN headers to 6 hex digits (e.g. ATSH0007DF).
        value = value[-3:]
    try:
        header = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    adapter.request_header = header
    writer(Output.ok)


def _set_protocol(value: str, adapter: Adapter, writer) -> None:
    value = value.strip().upper()
    if not value:
        raise AdapterError.invalid_command()
    if value == "00":
        adapter.selected_protocol = AUTO
        adapter.auto_protocol_start = None
        writer(Output.ok)
        return
    if len(value) == 1:
        if value == "0":
            adapter.selected_protocol = AUTO
            adapter.auto_protocol_start = None
            writer(Output.ok)
            return
        proto = BusProtocol.from_raw(value)
        if proto == BusProtocol.from_raw("?"):
            raise AdapterError.invalid_command()
        adapter.selected_protocol = proto
        adapter.auto_protocol_start = None
        writer(Output.ok)
        return
    if len(value) == 2 and "A" in value:
        if value.count("A") != 1:
            raise AdapterError.invalid_command()
        start = value.replace("A", "")
        if len(start) != 1:
            raise AdapterError.invalid_command()
        if start == "0":
            adapter.selected_protocol = AUTO
            adapter.auto_protocol_start = None
            writer(Output.ok)
            return
        proto = BusProtocol.from_raw(start)
        if proto == BusProtocol.from_raw("?"):
            raise AdapterError.invalid_command()
        adapter.selected_protocol = AUTO
        adapter.auto_protocol_start = proto
        writer(Output.ok)
        return
    if len(value) == 2:
        if value[0] == "0":
            proto_value = value[1]
        elif value[1] == "0":
            proto_value = value[0]
        else:
            raise AdapterError.invalid_command()
        proto = BusProtocol.from_raw(proto_value)
        if proto == BusProtocol.from_raw("?"):
            raise AdapterError.invalid_command()
        adapter.selected_protocol = proto
        adapter.auto_protocol_start = None
        writer(Output.ok)
        return
    raise AdapterError.invalid_command()


def _describe_selected_protocol(adapter: Adapter) -> str:
    if adapter.selected_protocol == AUTO:
        if adapter.auto_protocol_start is not None:
            return f"A{adapter.auto_protocol_start.raw_value}"
        return "A0"
    return adapter.selected_protocol.raw_value


def _set_timeout(value: str, adapter: Adapter, writer) -> None:
    if len(value) != 2:
        raise AdapterError.invalid_command()
    try:
        raw = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    adapter.bus_timeout = 200 + int(raw * 800.0 / 255.0)
    writer(Output.ok)


def _set_protocol_timeout(value: str, adapter: Adapter, writer) -> None:
    value = value.strip().upper()
    if value.startswith("A") and len(value) == 2:
        adapter.selected_protocol = AUTO
        writer(Output.ok)
        return
    try:
        proto_value = int(value, 16)
    except ValueError:
        raise AdapterError.invalid_command()
    if proto_value < 6 or proto_value > 9:
        raise AdapterError.invalid_command()
    proto = BusProtocol.from_raw(value)
    adapter.selected_protocol = proto
    writer(Output.ok)
