"""Per-device-model quirks for RGB lighting.

Keyed by ``device.modelId``. For normal HID++ devices that is the string
Logitech composes by concatenating every transport PID (btid + btleid + wpid
+ usbid) — one entry covers the model on any transport. For Centurion
headsets it is the firmware-stable model byte (G522 ``"32"``, G325 ``"44"``);
see ``device._get_ids_centurion``.

Two postures, by feature class:

* **Effect parameters that do NOT persist** (zone effects, LED directions) —
  default-ALLOW. Those are validated and low-harm (a wrong value is cosmetic
  and transient). They use blocklists elsewhere, e.g. ``LedDirectionBlocklist``
  in ``hidpp20.py``. Nothing of that kind lives here.

* **NVconfig-saved colors** (0x8071 RGBEffects boot effects, 0x0622 HeadsetRGB
  signature effects) — default-DENY. Those writes persist to non-volatile
  storage, so an unvalidated control can durably misconfigure a device. Every
  field is hidden and every slot suppressed unless the exact model is listed
  here as known-good.

Setting ``SOLAAR_EXPERIMENTAL`` truthy bypasses the allowlists entirely — for
testers / reverse-engineering on devices not yet validated.

Entries are hand-curated; document the observation in a comment.
"""

from __future__ import annotations

import os

_ALL_NVCONFIG_FIELDS = {"color1", "color2", "speed"}


def _experimental() -> bool:
    """True when SOLAAR_EXPERIMENTAL is set truthy — bypasses allowlist masking."""
    return os.environ.get("SOLAAR_EXPERIMENTAL", "").strip().lower() in ("1", "true", "yes", "on")


# Feature 0x8071 RGBEffects, Function 3 NvConfig — persistent boot/shutdown
# effects. Default-DENY allowlist: modelId -> cap_id -> set of color fields
# the firmware is KNOWN to honor. A listed cap shows the setting (its On/Off
# toggle plus the listed color pickers); an empty set shows the toggle only.
# An unlisted cap or unlisted model suppresses the setting entirely.
RGB_EFFECTS_NVCONFIG_ALLOWED: dict[str, dict[int, set[str]]] = {
    # G502 X PLUS — startup (0x0001): color bytes are inert, only the enabled
    # flag is honored, so no color fields are allowed (toggle only). Shutdown
    # (0x0040) is not supported and is suppressed by build()-time probe anyway.
    "4099C0950000": {0x0001: set()},
    # G515 LIGHTSPEED TKL — startup and shutdown both honor both colors.
    "B38940B4C355": {0x0001: {"color1", "color2"}, 0x0040: {"color1", "color2"}},
}

# Feature 0x0622 HeadsetRGB signature effects — persistent startup / shutdown
# / passive effects. Default-DENY allowlist: modelId -> effect_id -> set of
# fields the firmware is KNOWN to honor. An unlisted effect_id or model
# suppresses that signature-effect setting entirely.
HEADSET_SIGNATURE_EFFECTS_ALLOWED: dict[str, dict[int, set[str]]] = {
    # G522 LIGHTSPEED (Centurion model byte 0x32, from DeviceInfo func 0;
    # confirmed against multiple diagnostic logs). Verified against user
    # reports: startup honors only the primary color (secondary unused, speed
    # has no effect); shutdown honors both colors (speed has no effect). The
    # passive slot (effect_id 2) is omitted — its behavior is not understood,
    # so the whole passive setting is suppressed.
    "32": {
        0: {"color1"},
        1: {"color1", "color2"},
    },
}


def rgb_effects_nvconfig_allowed_fields(device, cap_id: int) -> set[str] | None:
    """Color fields to expose for an 0x8071 NvConfig boot effect.

    Returns the allowed field set (possibly empty — On/Off toggle only), or
    ``None`` to suppress the setting entirely.
    """
    if _experimental():
        return set(_ALL_NVCONFIG_FIELDS)
    model_id = getattr(device, "modelId", None) or ""
    return RGB_EFFECTS_NVCONFIG_ALLOWED.get(model_id, {}).get(cap_id)


def headset_signature_allowed_fields(device, effect_id: int) -> set[str] | None:
    """Fields to expose for an 0x0622 signature effect slot.

    Returns the allowed field set, or ``None`` to suppress the slot entirely.
    """
    if _experimental():
        return set(_ALL_NVCONFIG_FIELDS)
    model_id = getattr(device, "modelId", None) or ""
    return HEADSET_SIGNATURE_EFFECTS_ALLOWED.get(model_id, {}).get(effect_id)
