"""Observation channels for PROTOCOL labs.

Labs provide compressed observations (1-4 bits), not full bytes:
- Parity: parity(v & mask) - 1 bit
- Parity pair: [parity(v & mask1), parity(v & mask2)] - 2 bits
- Hi4: (v >> 4) & 0xF - 4 bits
- Lo4: v & 0xF - 4 bits
- Popcount: popcount(v & mask) - ~3 bits

This prevents trivial "query and copy" strategies.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any


def parity(value: int) -> int:
    """Compute parity (XOR of all bits).

    Args:
        value: Integer value

    Returns:
        0 or 1
    """
    p = 0
    while value:
        p ^= value & 1
        value >>= 1
    return p


def popcount(value: int) -> int:
    """Count number of set bits.

    Args:
        value: Integer value

    Returns:
        Number of 1 bits
    """
    return bin(value).count("1")


def hi4(value: int) -> int:
    """Extract upper nibble (4 bits).

    Args:
        value: Byte value (0-255)

    Returns:
        Upper nibble (0-15)
    """
    return (value >> 4) & 0xF


def lo4(value: int) -> int:
    """Extract lower nibble (4 bits).

    Args:
        value: Byte value (0-255)

    Returns:
        Lower nibble (0-15)
    """
    return value & 0xF


@dataclass
class ChannelConfig:
    """Configuration for an observation channel."""

    kind: str
    masks: list[int] | None = None  # For parity/popcount channels


def observe_parity(value: int, mask: int) -> dict[str, Any]:
    """Observe parity of masked value.

    Args:
        value: Byte value to observe
        mask: Mask to apply before parity

    Returns:
        Observation result with kind and value
    """
    result = parity(value & mask)
    return {"kind": "parity", "mask": f"0x{mask:02X}", "value": result}


def observe_parity_pair(value: int, mask1: int, mask2: int) -> dict[str, Any]:
    """Observe two parity values.

    Args:
        value: Byte value to observe
        mask1: First mask
        mask2: Second mask

    Returns:
        Observation result with kind and values
    """
    p1 = parity(value & mask1)
    p2 = parity(value & mask2)
    return {
        "kind": "parity_pair",
        "masks": [f"0x{mask1:02X}", f"0x{mask2:02X}"],
        "values": [p1, p2],
    }


def observe_hi4(value: int) -> dict[str, Any]:
    """Observe upper nibble.

    Args:
        value: Byte value to observe

    Returns:
        Observation result with kind and value
    """
    return {"kind": "hi4", "value": hi4(value)}


def observe_lo4(value: int) -> dict[str, Any]:
    """Observe lower nibble.

    Args:
        value: Byte value to observe

    Returns:
        Observation result with kind and value
    """
    return {"kind": "lo4", "value": lo4(value)}


def observe_popcount(value: int, mask: int) -> dict[str, Any]:
    """Observe popcount of masked value.

    Args:
        value: Byte value to observe
        mask: Mask to apply before popcount

    Returns:
        Observation result with kind and value
    """
    result = popcount(value & mask)
    return {"kind": "popcount", "mask": f"0x{mask:02X}", "value": result}


def apply_channel(value: int, channel: dict[str, Any]) -> dict[str, Any]:
    """Apply a channel configuration to observe a value.

    Args:
        value: Byte value to observe
        channel: Channel configuration dict with 'kind' and optional 'masks'

    Returns:
        Observation result

    Raises:
        ValueError: If unknown channel kind
    """
    kind = channel.get("kind", "")

    if kind == "parity":
        masks = channel.get("masks", [0xFF])
        return observe_parity(value, masks[0] if masks else 0xFF)

    elif kind == "parity_pair":
        masks = channel.get("masks", [0x55, 0xAA])
        mask1 = masks[0] if len(masks) > 0 else 0x55
        mask2 = masks[1] if len(masks) > 1 else 0xAA
        return observe_parity_pair(value, mask1, mask2)

    elif kind == "hi4":
        return observe_hi4(value)

    elif kind == "lo4":
        return observe_lo4(value)

    elif kind == "popcount":
        masks = channel.get("masks", [0xFF])
        return observe_popcount(value, masks[0] if masks else 0xFF)

    else:
        raise ValueError(f"Unknown channel kind: {kind}")


def decode_mask(mask_str: str) -> int:
    """Decode a hex mask string to integer.

    Args:
        mask_str: Hex string like "0x55"

    Returns:
        Integer mask value
    """
    if mask_str.startswith("0x") or mask_str.startswith("0X"):
        return int(mask_str[2:], 16)
    return int(mask_str, 16)


def apply_channel_from_station(value: int, station: dict[str, Any]) -> dict[str, Any]:
    """Apply channel from station configuration.

    Args:
        value: Byte value to observe
        station: Station dict with 'channel' configuration

    Returns:
        Observation result
    """
    channel = station.get("channel", {"kind": "hi4"})

    # Convert hex mask strings to integers if needed
    if "masks" in channel:
        masks = channel["masks"]
        if masks and isinstance(masks[0], str):
            channel = dict(channel)
            channel["masks"] = [decode_mask(m) for m in masks]

    return apply_channel(value, channel)
