from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re
import tomllib
from typing import Literal, TypeAlias, cast


class ActivationTomlError(ValueError):
    """Raised when an activation rules file does not match the supported schema."""


ActivationReferenceKind: TypeAlias = Literal[
    "steam",
    "packageid",
    "local",
]


@dataclass(frozen=True)
class ActivationReference:
    kind: ActivationReferenceKind
    value: str


@dataclass(frozen=True)
class ActivationRule:
    mod: ActivationReference
    when_all_present: tuple[ActivationReference, ...] = ()
    when_all_absent: tuple[ActivationReference, ...] = ()
    when_any_present: tuple[ActivationReference, ...] = ()
    when_any_absent: tuple[ActivationReference, ...] = ()
    reason: str | None = None
    enabled: bool = True


@dataclass(frozen=True)
class ActivationRulesDocument:
    rules: tuple[ActivationRule, ...]
    source_text: str = ""
    source_path: Path | None = None


_STEAM_REFERENCE_RE = re.compile(r"^steam:(?P<value>\d+)$")
_PACKAGE_ID_REFERENCE_RE = re.compile(r"^packageid:(?P<value>\S+)$")
_LOCAL_REFERENCE_RE = re.compile(r"^local:(?P<value>\S+)$")
_RULE_ALLOWED_KEYS = frozenset(
    {
        "mod",
        "when_all_present",
        "when_all_absent",
        "when_any_present",
        "when_any_absent",
        "reason",
        "enabled",
    },
)


def parse_activation_toml(toml_text: str) -> ActivationRulesDocument:
    try:
        document = tomllib.loads(toml_text)
    except tomllib.TOMLDecodeError as exc:
        raise ActivationTomlError(f"invalid activation TOML: {exc}") from exc

    top_level_keys = set(document)
    unsupported_top_level_keys = top_level_keys - {"rules"}
    if unsupported_top_level_keys:
        unexpected_keys = ", ".join(sorted(unsupported_top_level_keys))
        raise ActivationTomlError(f"unsupported top-level keys: {unexpected_keys}")

    raw_rules = document.get("rules")
    if raw_rules is None:
        return ActivationRulesDocument(rules=(), source_text=toml_text)
    if not isinstance(raw_rules, list):
        raise ActivationTomlError("top-level 'rules' must be an array of tables")

    typed_raw_rules = cast(list[object], raw_rules)
    rules = tuple(
        _parse_rule(raw_rule, index=index)
        for index, raw_rule in enumerate(typed_raw_rules, start=1)
    )
    return ActivationRulesDocument(rules=rules, source_text=toml_text)


def load_activation_toml(path: Path) -> ActivationRulesDocument:
    document = parse_activation_toml(path.read_text(encoding="utf-8"))
    return ActivationRulesDocument(
        rules=document.rules,
        source_text=document.source_text,
        source_path=path,
    )


def _parse_rule(raw_rule: object, *, index: int) -> ActivationRule:
    if not isinstance(raw_rule, dict):
        raise ActivationTomlError(f"rule #{index} must be a TOML table")

    typed_rule = cast(dict[str, object], raw_rule)
    unsupported_keys = set(typed_rule) - _RULE_ALLOWED_KEYS
    if unsupported_keys:
        unexpected_keys = ", ".join(sorted(unsupported_keys))
        raise ActivationTomlError(f"rule #{index} has unsupported fields: {unexpected_keys}")

    raw_mod = _require_rule_field(typed_rule, index=index, key="mod")
    if not isinstance(raw_mod, str):
        raise ActivationTomlError(f"rule #{index} field 'mod' must be a string")

    when_all_present = _parse_reference_array(
        typed_rule.get("when_all_present"),
        index=index,
        field_name="when_all_present",
    )
    when_all_absent = _parse_reference_array(
        typed_rule.get("when_all_absent"),
        index=index,
        field_name="when_all_absent",
    )
    when_any_present = _parse_reference_array(
        typed_rule.get("when_any_present"),
        index=index,
        field_name="when_any_present",
    )
    when_any_absent = _parse_reference_array(
        typed_rule.get("when_any_absent"),
        index=index,
        field_name="when_any_absent",
    )
    if not (
        when_all_present
        or when_all_absent
        or when_any_present
        or when_any_absent
    ):
        raise ActivationTomlError(
            f"rule #{index} must include at least one activation condition field",
        )

    raw_reason = typed_rule.get("reason")
    if raw_reason is not None and not isinstance(raw_reason, str):
        raise ActivationTomlError(f"rule #{index} field 'reason' must be a string")
    if raw_reason == "":
        raise ActivationTomlError(f"rule #{index} field 'reason' must not be empty")

    raw_enabled = typed_rule.get("enabled")
    if raw_enabled is None:
        enabled = True
    elif isinstance(raw_enabled, bool):
        enabled = raw_enabled
    else:
        raise ActivationTomlError(f"rule #{index} field 'enabled' must be a boolean")

    return ActivationRule(
        mod=_parse_reference(raw_mod, field_name="mod", index=index),
        when_all_present=when_all_present,
        when_all_absent=when_all_absent,
        when_any_present=when_any_present,
        when_any_absent=when_any_absent,
        reason=raw_reason,
        enabled=enabled,
    )


def _require_rule_field(raw_rule: dict[str, object], *, index: int, key: str) -> object:
    if key not in raw_rule:
        raise ActivationTomlError(f"rule #{index} is missing required field '{key}'")
    return raw_rule[key]


def _parse_reference_array(
    raw_value: object,
    *,
    index: int,
    field_name: str,
) -> tuple[ActivationReference, ...]:
    if raw_value is None:
        return ()
    if not isinstance(raw_value, list):
        raise ActivationTomlError(
            f"rule #{index} field '{field_name}' must be an array of strings",
        )

    references: list[ActivationReference] = []
    for item in cast(list[object], raw_value):
        if not isinstance(item, str):
            raise ActivationTomlError(
                f"rule #{index} field '{field_name}' must be an array of strings",
            )
        references.append(_parse_reference(item, field_name=field_name, index=index))
    return tuple(references)


def _parse_reference(
    raw_reference: str,
    *,
    field_name: str,
    index: int,
) -> ActivationReference:
    steam_match = _STEAM_REFERENCE_RE.fullmatch(raw_reference)
    if steam_match is not None:
        return ActivationReference(kind="steam", value=steam_match.group("value"))

    package_id_match = _PACKAGE_ID_REFERENCE_RE.fullmatch(raw_reference)
    if package_id_match is not None:
        return ActivationReference(
            kind="packageid",
            value=package_id_match.group("value"),
        )

    local_match = _LOCAL_REFERENCE_RE.fullmatch(raw_reference)
    if local_match is not None:
        return ActivationReference(kind="local", value=local_match.group("value"))

    raise ActivationTomlError(
        f"rule #{index} field '{field_name}' has unsupported mod reference: {raw_reference!r}",
    )
