from __future__ import annotations

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


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


OrderingRuleType: TypeAlias = Literal["before", "after"]
OrderingReferenceKind: TypeAlias = Literal[
    "steam",
    "packageid",
    "local",
]


@dataclass(frozen=True)
class OrderingReference:
    kind: OrderingReferenceKind
    value: str


@dataclass(frozen=True)
class OrderingRule:
    rule_type: OrderingRuleType
    mod: OrderingReference
    target: OrderingReference
    reason: str | None = None
    enabled: bool = True


@dataclass(frozen=True)
class OrderingRulesDocument:
    rules: tuple[OrderingRule, ...]
    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({"type", "mod", "target", "reason", "enabled"})


def parse_ordering_toml(toml_text: str) -> OrderingRulesDocument:
    try:
        document = tomllib.loads(toml_text)
    except tomllib.TOMLDecodeError as exc:
        raise OrderingTomlError(f"invalid ordering 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 OrderingTomlError(f"unsupported top-level keys: {unexpected_keys}")

    raw_rules = document.get("rules")
    if raw_rules is None:
        return OrderingRulesDocument(rules=(), source_text=toml_text)
    if not isinstance(raw_rules, list):
        raise OrderingTomlError("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 OrderingRulesDocument(rules=rules, source_text=toml_text)


def load_ordering_toml(path: Path) -> OrderingRulesDocument:
    document = parse_ordering_toml(path.read_text(encoding="utf-8"))
    return OrderingRulesDocument(
        rules=document.rules,
        source_text=document.source_text,
        source_path=path,
    )


def render_ordering_rule(rule: OrderingRule) -> str:
    lines = [
        "[[rules]]",
        f'type = "{rule.rule_type}"',
        f"mod = {_quote_toml_string(_format_reference(rule.mod))}",
        f"target = {_quote_toml_string(_format_reference(rule.target))}",
    ]
    if rule.reason is not None:
        lines.append(f"reason = {_quote_toml_string(rule.reason)}")
    if not rule.enabled:
        lines.append("enabled = false")
    return "\n".join(lines) + "\n"


def append_ordering_rule(path: Path, rule: OrderingRule) -> None:
    existing_text = ""
    if path.is_file():
        existing_text = path.read_text(encoding="utf-8")
        _ = parse_ordering_toml(existing_text)

    appended_rule = render_ordering_rule(rule)
    if existing_text == "":
        updated_text = appended_rule
    else:
        separator = ""
        if not existing_text.endswith("\n"):
            separator += "\n"
        if existing_text.strip() != "" and not existing_text.endswith("\n\n"):
            separator += "\n"
        updated_text = f"{existing_text}{separator}{appended_rule}"

    _ = parse_ordering_toml(updated_text)
    path.parent.mkdir(parents=True, exist_ok=True)
    _ = path.write_text(updated_text, encoding="utf-8", newline="\n")


def parse_ordering_reference(raw_reference: str) -> OrderingReference:
    return _parse_reference(raw_reference, field_name="reference", index=1)


def _parse_rule(raw_rule: object, *, index: int) -> OrderingRule:
    if not isinstance(raw_rule, dict):
        raise OrderingTomlError(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 OrderingTomlError(f"rule #{index} has unsupported fields: {unexpected_keys}")

    raw_type = _require_rule_field(typed_rule, index=index, key="type")
    if not isinstance(raw_type, str):
        raise OrderingTomlError(f"rule #{index} field 'type' must be a string")
    if raw_type not in {"before", "after"}:
        raise OrderingTomlError(
            f"rule #{index} field 'type' must be 'before' or 'after'",
        )

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

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

    raw_reason = typed_rule.get("reason")
    if raw_reason is not None and not isinstance(raw_reason, str):
        raise OrderingTomlError(f"rule #{index} field 'reason' must be a string")
    if raw_reason == "":
        raise OrderingTomlError(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 OrderingTomlError(f"rule #{index} field 'enabled' must be a boolean")

    return OrderingRule(
        rule_type=cast(OrderingRuleType, raw_type),
        mod=_parse_reference(raw_mod, field_name="mod", index=index),
        target=_parse_reference(raw_target, field_name="target", index=index),
        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 OrderingTomlError(f"rule #{index} is missing required field '{key}'")
    return raw_rule[key]


def _parse_reference(
    raw_reference: str,
    *,
    field_name: str,
    index: int,
) -> OrderingReference:
    steam_match = _STEAM_REFERENCE_RE.fullmatch(raw_reference)
    if steam_match is not None:
        return OrderingReference(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 OrderingReference(
            kind="packageid",
            value=package_id_match.group("value"),
        )

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

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


def _format_reference(reference: OrderingReference) -> str:
    return f"{reference.kind}:{reference.value}"


def _quote_toml_string(value: str) -> str:
    escaped_value = value.replace("\\", "\\\\").replace('"', '\\"')
    return f'"{escaped_value}"'
