"""Decode calibration-relevant Wheeltec G90 logs from a V2 capture.

GNSS-owned measurement time is preserved for every record. Host receive time
only identifies the chunk that completed the line and must not be substituted
for the measurement timestamp.
"""

from __future__ import annotations

import bisect
import math
from dataclasses import dataclass

from .capture_format_v2 import CaptureFile, RawChunk, iter_contiguous_segments


def nmea_checksum_valid(line: str) -> bool:
    star = line.rfind("*")
    if star < 0:
        return False
    try:
        expected = int(line[star + 1 : star + 3], 16)
    except ValueError:
        return False
    value = 0
    for char in line[1:star]:
        value ^= ord(char)
    return value == expected


def unicore_checksum_valid(line: str) -> bool:
    """Validate the CRC32 suffix used by Unicore hash-prefixed logs."""

    star = line.rfind("*")
    if star < 0:
        return False
    try:
        expected = int(line[star + 1 : star + 9], 16)
    except ValueError:
        return False
    crc = 0
    for value in line[1:star].encode("ascii", "replace"):
        crc ^= value
        for _ in range(8):
            crc = (crc >> 1) ^ (0xEDB88320 if crc & 1 else 0)
    return (crc & 0xFFFFFFFF) == expected


def g90_checksum_valid(line: str) -> bool:
    if line.startswith("$"):
        return nmea_checksum_valid(line)
    if line.startswith("#"):
        return unicore_checksum_valid(line)
    return False


def _safe_float(value: str):
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _safe_int(value: str):
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def parse_nmea_latlon(value: str, hemisphere: str):
    raw = _safe_float(value)
    if raw is None:
        return None
    degrees = math.floor(raw / 100.0)
    result = degrees + (raw - degrees * 100.0) / 60.0
    if hemisphere.upper() in ("S", "W"):
        result = -result
    return result


def parse_gga(line: str) -> dict:
    fields = line[: line.rfind("*")].split(",")
    if len(fields) < 10:
        raise ValueError("GGA has too few fields")
    return {
        "type": "GGA",
        "position_time_utc": fields[1],
        "lat_deg": parse_nmea_latlon(fields[2], fields[3]),
        "lon_deg": parse_nmea_latlon(fields[4], fields[5]),
        "fix_quality": _safe_int(fields[6]),
        "satellites": _safe_int(fields[7]),
        "hdop": _safe_float(fields[8]),
        "altitude_m": _safe_float(fields[9]),
    }


def parse_gnhpr(line: str) -> dict:
    fields = line[: line.rfind("*")].split(",")
    if len(fields) < 7:
        raise ValueError("GNHPR has too few fields")
    quality = _safe_int(fields[5])
    return {
        "type": "GNHPR",
        "position_time_utc": fields[1],
        "heading_deg": _safe_float(fields[2]),
        "pitch_deg": _safe_float(fields[3]),
        "roll_deg": _safe_float(fields[4]),
        "heading_quality": quality,
        "heading_satellites": _safe_int(fields[6]),
        "heading_age_s": _safe_float(fields[7]) if len(fields) > 7 else None,
        "heading_station_id": fields[8] if len(fields) > 8 else None,
        "heading_valid": quality == 4,
    }


def _split_unicore(line: str) -> tuple[list[str], list[str]]:
    before_checksum = line[: line.rfind("*")]
    header, payload = before_checksum.split(";", 1)
    return header[1:].split(","), payload.split(",")


def _parse_unicore_header(fields: list[str]) -> dict:
    if len(fields) < 9:
        raise ValueError("Unicore ASCII header is incomplete")
    return {
        "gnss_week": _safe_int(fields[4]),
        "gnss_tow_ms": _safe_int(fields[5]),
        "leap_seconds": _safe_int(fields[8]),
    }


def parse_bestnava(line: str) -> dict:
    """Parse BESTNAVA position and Doppler-velocity fields."""

    header, fields = _split_unicore(line)
    if len(fields) < 30:
        raise ValueError("BESTNAVA has too few fields")
    result = {
        "type": "BESTNAVA",
        **_parse_unicore_header(header),
        "position_status": fields[0],
        "position_type": fields[1],
        "lat_deg": _safe_float(fields[2]),
        "lon_deg": _safe_float(fields[3]),
        "altitude_m": _safe_float(fields[4]),
        "undulation_m": _safe_float(fields[5]),
        "lat_std_m": _safe_float(fields[7]),
        "lon_std_m": _safe_float(fields[8]),
        "altitude_std_m": _safe_float(fields[9]),
        "station_id": fields[10].strip('"'),
        "differential_age_s": _safe_float(fields[11]),
        "solution_age_s": _safe_float(fields[12]),
        "satellites": _safe_int(fields[13]),
        "solution_satellites": _safe_int(fields[14]),
        "velocity_status": fields[21],
        "velocity_type": fields[22],
        "velocity_latency_s": _safe_float(fields[23]),
        "velocity_age_s": _safe_float(fields[24]),
        "horizontal_speed_m_s": _safe_float(fields[25]),
        "track_ground_deg": _safe_float(fields[26]),
        "vertical_speed_m_s": _safe_float(fields[27]),
        "vertical_speed_std_m_s": _safe_float(fields[28]),
        "horizontal_speed_std_m_s": _safe_float(fields[29]),
    }
    speed = result["horizontal_speed_m_s"]
    track = result["track_ground_deg"]
    if speed is not None and track is not None:
        angle = math.radians(track)
        result["velocity_east_m_s"] = speed * math.sin(angle)
        result["velocity_north_m_s"] = speed * math.cos(angle)
    else:
        result["velocity_east_m_s"] = None
        result["velocity_north_m_s"] = None
    result["position_fixed"] = (
        result["position_status"] == "SOL_COMPUTED"
        and result["position_type"] == "NARROW_INT"
    )
    result["doppler_velocity_valid"] = (
        result["velocity_status"] == "SOL_COMPUTED"
        and result["velocity_type"] == "DOPPLER_VELOCITY"
    )
    return result


def parse_pvtslna(line: str) -> dict:
    """Parse PVTSLNA as a quality-rich fallback/diagnostic record."""

    header, fields = _split_unicore(line)
    if len(fields) < 34:
        raise ValueError("PVTSLNA has too few fields")
    speed_north = _safe_float(fields[17])
    speed_east = _safe_float(fields[18])
    return {
        "type": "PVTSLNA",
        **_parse_unicore_header(header),
        "position_type": fields[0],
        "altitude_m": _safe_float(fields[1]),
        "lat_deg": _safe_float(fields[2]),
        "lon_deg": _safe_float(fields[3]),
        "altitude_std_m": _safe_float(fields[4]),
        "lat_std_m": _safe_float(fields[5]),
        "lon_std_m": _safe_float(fields[6]),
        "differential_age_s": _safe_float(fields[7]),
        "psr_position_type": fields[8],
        "undulation_m": _safe_float(fields[12]),
        "satellites": _safe_int(fields[13]),
        "solution_satellites": _safe_int(fields[14]),
        "velocity_north_m_s": speed_north,
        "velocity_east_m_s": speed_east,
        "horizontal_speed_m_s": (
            None if speed_north is None or speed_east is None
            else math.hypot(speed_north, speed_east)
        ),
        "vertical_speed_m_s": _safe_float(fields[19]),
        "heading_type": fields[20],
        "baseline_length_m": _safe_float(fields[21]),
        "heading_deg": _safe_float(fields[22]),
        "pitch_deg": _safe_float(fields[23]),
        "heading_satellites": _safe_int(fields[24]),
        "heading_solution_satellites": _safe_int(fields[25]),
        "gdop": _safe_float(fields[28]),
        "pdop": _safe_float(fields[29]),
        "hdop": _safe_float(fields[30]),
        "htdop": _safe_float(fields[31]),
        "tdop": _safe_float(fields[32]),
        "position_fixed": fields[0] == "NARROW_INT",
    }


def _chunk_starts(chunks: list[RawChunk]) -> list[int]:
    starts = []
    cursor = 0
    for chunk in chunks:
        starts.append(cursor)
        cursor += len(chunk.raw)
    return starts


def _host_ticks_for_span(chunks: list[RawChunk], starts: list[int], end: int) -> int:
    end_index = max(0, min(len(chunks) - 1, bisect.bisect_left(starts, end) - 1))
    return chunks[end_index].receive_utc_ticks


@dataclass(frozen=True)
class RtkSentence:
    sentence_type: str
    receive_utc_ticks: int
    checksum_valid: bool
    fields: dict
    raw_line: str


def iter_g90_sentences(capture: CaptureFile) -> list[RtkSentence]:
    """Parse native asynchronous GGA/GNHPR/BESTNAVA/PVTSLNA records."""

    rows: list[RtkSentence] = []
    for _segment_id, chunks in iter_contiguous_segments(capture.chunks):
        stream = b"".join(chunk.raw for chunk in chunks)
        starts = _chunk_starts(chunks)
        cursor = 0
        while cursor < len(stream):
            newline = stream.find(b"\n", cursor)
            if newline < 0:
                break
            end = newline + 1
            raw_line = stream[cursor:end].rstrip(b"\r\n")
            cursor = end
            if not raw_line:
                continue
            line = raw_line.decode("ascii", "replace")
            parser = None
            if line.startswith("$GNGGA") or line.startswith("$GPGGA"):
                parser = parse_gga
            elif line.startswith("$GNHPR"):
                parser = parse_gnhpr
            elif line.startswith("#BESTNAVA"):
                parser = parse_bestnava
            elif line.startswith("#PVTSLNA"):
                parser = parse_pvtslna
            if parser is None:
                continue
            ticks = _host_ticks_for_span(chunks, starts, end)
            try:
                fields = parser(line)
            except ValueError:
                continue
            rows.append(
                RtkSentence(
                    sentence_type=str(fields["type"]),
                    receive_utc_ticks=int(ticks),
                    checksum_valid=g90_checksum_valid(line),
                    fields=fields,
                    raw_line=line,
                )
            )
    rows.sort(key=lambda row: row.receive_utc_ticks)
    return rows
