"""ISO channel naming convention parser.

Decomposes channel codes per SAE J211-1 / ISO 13499 into structured fields,
enabling automatic grouping, human-readable labeling, and criteria matching.

Two code formats are supported:

**16-character ISO 13499 (with dummy type)**::

    11HEAD0000H3ACXP
    ^^    ^^^^  ^^^^
    |  |    |  | ||`- sense (P/N)
    |  |    |  | |`-- direction (X/Y/Z/R/0)
    |  |    |  | `--- measurement (AC/FO/MO/DS/DC/VE/AN...)
    |  |    |  `----- dummy type (H3/P3/PC/S2/WS...)
    |  |    `-------- fine location (0000/UP00/LE00...)
    |  `------------- main location (HEAD/NECK/CHST...)
    `---------------- test object (11/12/13/10/B0/D0...)

**14-character simplified (no dummy type)**::

    11HEAD0000ACXA
    ^^    ^^^^  ^^
    |  |    |  ||`- sense (A/P/N)
    |  |    |  |`-- direction (X/Y/Z/R)
    |  |    |  `--- measurement (AC/FO/MO...)
    |  |    `------- fine location
    |  `------------ main location
    `--------------- test object
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Self

from impakt.channel.lookup import (
    DIRECTIONS,
    FINE_LOCATIONS,
    MAIN_LOCATIONS,
    MEASUREMENTS,
    SENSE_CODES,
    TEST_OBJECTS,
)

# Known dummy/sensor type codes at positions 11-12 of 16-char codes
DUMMY_TYPE_CODES: set[str] = {
    "H3",  # Hybrid III
    "H2",  # Hybrid II
    "P3",  # P-series (3-year-old child)
    "P6",  # P-series (6-year-old child)
    "PC",  # Pedestrian child headform
    "PA",  # Pedestrian adult headform
    "S2",  # SID-IIs
    "ES",  # ES-2re
    "WS",  # WorldSID
    "TH",  # THOR
    "Q0",  # Q-series child dummy
    "Q1",  # Q1/Q1.5
    "Q3",  # Q3
    "Q6",  # Q6
    "BF",  # BioFidel
    "00",  # Generic/structural (often barrier or vehicle channels)
}


def _has_embedded_dummy(code: str) -> bool:
    """Detect if positions 11-12 contain a dummy type code.

    If positions 11-12 are a known dummy code AND positions 13-14 are a
    known measurement code, the code uses the 16-char format.
    """
    if len(code) < 16:
        return False
    candidate_dummy = code[10:12]
    candidate_meas = code[12:14]
    return candidate_dummy in DUMMY_TYPE_CODES and candidate_meas in MEASUREMENTS


@dataclass(frozen=True, slots=True)
class ChannelCode:
    """Parsed ISO channel naming code.

    Handles both 16-char ISO 13499 codes (with embedded dummy type) and
    14-char simplified codes.

    Attributes:
        raw: Original unparsed string.
        test_object: Positions 1-2 (e.g., '11' = driver).
        main_location: Positions 3-6 (e.g., 'HEAD').
        fine_location: Positions 7-10 (e.g., '0000').
        dummy_type: Positions 11-12 in 16-char codes (e.g., 'H3'). Empty for 14-char.
        measurement: The measurement type (e.g., 'AC', 'FO', 'MO').
        direction: Axis character (X/Y/Z/R/0).
        sense: Sign convention character (P/N/A).
    """

    raw: str
    test_object: str = ""
    main_location: str = ""
    fine_location: str = ""
    dummy_type: str = ""
    measurement: str = ""
    direction: str = ""
    sense: str = ""
    filter_class: str = ""
    _valid: bool = field(default=False, repr=False)

    @classmethod
    def parse(cls, raw: str) -> Self:
        """Parse a raw channel code string.

        Auto-detects whether the code is 16-char (with dummy type) or
        14-char (without). Tolerant of variations.
        """
        code = raw.strip().upper()

        if len(code) < 14:
            return cls(raw=raw, _valid=False)

        test_object = code[0:2]
        main_location = code[2:6]
        fine_location = code[6:10]

        if _has_embedded_dummy(code):
            # 16-character format: pos 11-12 = dummy, 13-14 = meas, 15 = dir, 16 = sense
            dummy_type = code[10:12]
            measurement = code[12:14]
            direction = code[14:15] if len(code) > 14 else ""
            sense = code[15:16] if len(code) > 15 else ""
            filter_class = ""
        else:
            # 14-character format: pos 11-12 = meas, 13 = dir, 14 = sense
            dummy_type = ""
            measurement = code[10:12]
            direction = code[12:13]
            sense = code[13:14] if len(code) > 13 else ""
            filter_class = code[14:16] if len(code) > 14 else ""

        return cls(
            raw=raw,
            test_object=test_object,
            main_location=main_location,
            fine_location=fine_location,
            dummy_type=dummy_type,
            measurement=measurement,
            direction=direction,
            sense=sense,
            filter_class=filter_class,
            _valid=True,
        )

    @property
    def is_valid(self) -> bool:
        return self._valid

    def group_key(self) -> str:
        """Key for grouping X/Y/Z component channels.

        Two channels belong to the same group if they share everything
        except the direction field. This enables automatic resultant
        computation.
        """
        if not self._valid:
            return self.raw
        return (
            f"{self.test_object}{self.main_location}"
            f"{self.fine_location}{self.dummy_type}{self.measurement}"
            f"_{self.sense}"
        )

    def is_component(self) -> bool:
        """Whether this is an X, Y, or Z component (not a resultant)."""
        return self.direction in ("X", "Y", "Z")

    def is_resultant(self) -> bool:
        """Whether this is a pre-computed resultant channel."""
        return self.direction == "R"

    def axis(self) -> str | None:
        """Return the axis letter, or None if resultant or unknown."""
        if self.direction in ("X", "Y", "Z"):
            return self.direction
        return None

    # ----- Human-readable descriptions -----

    @property
    def test_object_label(self) -> str:
        return TEST_OBJECTS.get(self.test_object, f"Object {self.test_object}")

    @property
    def location_label(self) -> str:
        main = MAIN_LOCATIONS.get(self.main_location, self.main_location)
        fine = FINE_LOCATIONS.get(self.fine_location, "")
        if fine and fine != "Center of Gravity / Primary":
            return f"{main} ({fine})"
        return main

    @property
    def dummy_type_label(self) -> str:
        labels = {
            "H3": "Hybrid III",
            "H2": "Hybrid II",
            "P3": "P3 Child",
            "P6": "P6 Child",
            "PC": "Ped. Child",
            "PA": "Ped. Adult",
            "S2": "SID-IIs",
            "ES": "ES-2re",
            "WS": "WorldSID",
            "TH": "THOR",
            "Q1": "Q1.5",
            "Q3": "Q3",
            "Q6": "Q6",
        }
        return labels.get(self.dummy_type, self.dummy_type)

    @property
    def measurement_label(self) -> str:
        info = MEASUREMENTS.get(self.measurement)
        if info:
            return info[0]
        return self.measurement

    @property
    def measurement_unit(self) -> str:
        info = MEASUREMENTS.get(self.measurement)
        if info:
            return info[1]
        return ""

    @property
    def direction_label(self) -> str:
        return DIRECTIONS.get(self.direction, self.direction)

    @property
    def sense_label(self) -> str:
        return SENSE_CODES.get(self.sense, self.sense)

    @property
    def description(self) -> str:
        """Full human-readable description.

        Example: ``Driver Head (Hybrid III) — Acceleration X (Longitudinal)``
        """
        parts = [self.test_object_label, self.location_label]
        if self.dummy_type:
            parts.append(f"({self.dummy_type_label})")
        parts.extend(["—", self.measurement_label, self.direction_label])
        return " ".join(parts)

    @property
    def short_label(self) -> str:
        """Short label suitable for plot legends.

        Example: ``Head Accel X``
        """
        loc = MAIN_LOCATIONS.get(self.main_location, self.main_location)
        meas = self.measurement_label
        abbrev = {
            "Acceleration": "Accel",
            "Displacement": "Disp",
            "Deflection": "Defl",
            "Velocity": "Vel",
            "Angular Velocity": "Ang Vel",
            "Angular Acceleration": "Ang Accel",
        }
        meas = abbrev.get(meas, meas)
        return f"{loc} {meas} {self.direction}"

    def matches(self, pattern: str) -> bool:
        """Check if this channel code matches a glob-like pattern.

        Supports ``*`` (any characters) and ``{X,Y,Z}`` set notation.
        Matches against the raw code string.
        """
        regex = pattern.replace(".", r"\.")
        regex = re.sub(
            r"\{([^}]+)\}",
            lambda m: "(" + "|".join(re.escape(x) for x in m.group(1).split(",")) + ")",
            regex,
        )
        regex = regex.replace("*", ".*")
        regex = f"^{regex}$"
        return bool(re.match(regex, self.raw, re.IGNORECASE))


def parse_channel_code(raw: str) -> ChannelCode:
    """Convenience function to parse a channel code string."""
    return ChannelCode.parse(raw)
