"""NMEA wire protocol: checksum, framing, validation, and ACK parsing.

Implements the NMEA 0183 framing used by Quectel LC29H modules.
Checksum algorithm extracted from QGNSS.exe (FUN_14024abd0).

Protocol constants from QNMEA.dll decompilation:
  MIN_NMEA_LEN=12, MAX_NMEA_LEN=82
  Start='$' (0x24), End='*' (0x2a), Separator=',' (0x2c), CRLF='\\r\\n'
"""

NMEA_START = "$"
NMEA_END = "*"
NMEA_SEP = ","
NMEA_CRLF = "\r\n"

MIN_NMEA_LEN = 12
MAX_NMEA_LEN = 82

# ACK result codes from $PAIR001 response
ACK_SUCCESS = 0
ACK_FAIL = 1
ACK_NOT_SUPPORTED = 2
ACK_INVALID_PARAM = 3
ACK_TIMEOUT = 4


def compute_checksum(sentence: str) -> str:
    """Compute NMEA checksum: XOR all bytes between '$' and '*'.

    Args:
        sentence: NMEA sentence body (without $ prefix and *XX suffix).
                  e.g. "PAIR050,1000" or "GPGGA,123519,..."

    Returns:
        Two-character uppercase hex checksum, e.g. "2F".
    """
    checksum = 0
    for char in sentence:
        checksum ^= ord(char)
    return f"{checksum:02X}"


def frame_command(body: str) -> bytes:
    """Frame an NMEA command for transmission.

    Takes the sentence body (e.g. "PAIR050,1000"), wraps it with
    $...*XX\\r\\n framing, and returns bytes ready for serial write.

    This matches QGNSS.exe's FUN_14024abd0 which appends *XX\\r\\n.
    """
    cs = compute_checksum(body)
    return f"{NMEA_START}{body}{NMEA_END}{cs}{NMEA_CRLF}".encode("ascii")


def validate_checksum(sentence: str) -> bool:
    """Validate the checksum of a received NMEA sentence.

    Args:
        sentence: Full NMEA sentence including $ and *XX (with optional \\r\\n).

    Returns:
        True if the checksum is valid.
    """
    sentence = sentence.strip()
    if not sentence.startswith(NMEA_START) or NMEA_END not in sentence:
        return False

    body = sentence[1 : sentence.rindex(NMEA_END)]
    expected = sentence[sentence.rindex(NMEA_END) + 1 :]
    return compute_checksum(body).upper() == expected.upper()


def parse_ack(line: str) -> tuple[str, int]:
    """Parse a $PAIR001 ACK/NACK response.

    Format: $PAIR001,<CmdID>,<Result>*CS

    Args:
        line: Full NMEA sentence.

    Returns:
        Tuple of (command_id, result_code).
        Result codes: 0=success, 1=fail, 2=not supported,
                     3=invalid param, 4=timeout.

    Raises:
        ValueError: If the line is not a valid PAIR001 response.
    """
    line = line.strip()
    if not validate_checksum(line):
        raise ValueError(f"Invalid checksum: {line}")

    body = line[1 : line.rindex(NMEA_END)]
    parts = body.split(NMEA_SEP)

    if len(parts) < 3 or parts[0] != "PAIR001":
        raise ValueError(f"Not a PAIR001 response: {line}")

    return parts[1], int(parts[2])


def parse_response(line: str) -> tuple[str, list[str]]:
    """Parse any NMEA/PAIR/PQTM response into sentence ID and fields.

    Args:
        line: Full NMEA sentence (e.g. "$PAIR021,LC29HDANR...*XX").

    Returns:
        Tuple of (sentence_id, [field1, field2, ...]).
        sentence_id includes the full prefix (e.g. "PAIR021", "GPGGA", "PQTMVERNO").

    Raises:
        ValueError: If the line fails checksum validation.
    """
    line = line.strip()
    if not validate_checksum(line):
        raise ValueError(f"Invalid checksum: {line}")

    body = line[1 : line.rindex(NMEA_END)]
    parts = body.split(NMEA_SEP)
    return parts[0], parts[1:]
