# SPDX-License-Identifier: Apache-2.0
"""Profiles (SPEC §8): one recorder's expression of the protocol — parameters and localisation."""
from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from pathlib import Path

_DUR = re.compile(r"^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$")
REQUIRED = ("id", "default_level", "locales", "retention")
EXTENSIONS = ("side_conversations", "claiming")


def parse_duration(s) -> float:
    """ISO 8601 duration → seconds (Y = 365 d, M = 30 d). A bare number is taken as seconds."""
    if isinstance(s, (int, float)) and not isinstance(s, bool):
        return float(s)
    m = _DUR.match(s) if isinstance(s, str) else None
    if not m or s in ("P", "PT"):
        raise ValueError(f"not an ISO 8601 duration: {s!r}")
    y, mo, w, d, h, mi, se = (float(g) if g else 0.0 for g in m.groups())
    return (((y * 365 + mo * 30 + w * 7 + d) * 24 + h) * 60 + mi) * 60 + se


def _level(x, name: str) -> int:
    if not isinstance(x, int) or isinstance(x, bool) or not 0 <= x <= 5:
        raise ValueError(f"{name} must be an integer level 0–5")
    return x


@dataclass(frozen=True)
class Profile:
    id: str
    default_level: int
    locales: tuple[str, ...]
    retention: tuple[tuple[float, int], ...]
    max_default_level: int = 4
    max_voice_level: int = 4
    invocation: tuple[str, ...] = ()
    objection_window: float = 30.0
    session_gap: float = 600.0
    attribution_threshold: float = 0.8
    max_participants: int = 10
    room_raise: str = "invocation"             # "invocation" | "disabled"
    extensions: tuple[str, ...] = ()
    reply_window: float = 6.0                  # side_conversations extension
    replies_for_group: int = 2                 # side_conversations extension
    extra: dict = field(default_factory=dict, compare=False)

    def __post_init__(self) -> None:
        for n in ("default_level", "max_default_level", "max_voice_level"):
            _level(getattr(self, n), n)
        if not self.default_level <= self.max_default_level <= self.max_voice_level:
            raise ValueError("need default_level ≤ max_default_level ≤ max_voice_level")
        if not self.locales:
            raise ValueError("at least one locale")
        if not 0 <= self.attribution_threshold <= 1:
            raise ValueError("attribution_threshold must be in 0–1")
        if not isinstance(self.max_participants, int) or self.max_participants < 1:
            raise ValueError("max_participants must be ≥ 1")
        if self.room_raise not in ("invocation", "disabled"):
            raise ValueError("room_raise must be 'invocation' or 'disabled'")
        bad = [e for e in self.extensions if e not in EXTENSIONS and not re.search(r"[.:]", e)]   # others need a URI / reverse-domain name
        if bad:
            raise ValueError(f"unknown extensions {bad}")
        if not self.objection_window > 0:
            raise ValueError("objection_window must be > 0")
        if not self.session_gap > self.objection_window:
            raise ValueError("session_gap must be longer than objection_window")
        last_until, last_level = 0.0, 6
        for until, level in self.retention:
            _level(level, "retention max_level")
            if until <= last_until or level >= last_level:
                raise ValueError("retention steps must have increasing ages and decreasing levels")
            last_until, last_level = until, level

    @classmethod
    def from_dict(cls, d: dict) -> "Profile":
        missing = [k for k in REQUIRED if k not in d]
        if missing:
            raise ValueError(f"profile is missing {missing}")
        known = {f for f in cls.__dataclass_fields__ if f != "extra"}
        kw = {k: v for k, v in d.items() if k in known}
        for k in ("objection_window", "session_gap", "reply_window"):
            if k in kw:
                kw[k] = parse_duration(kw[k])
        kw["retention"] = tuple((parse_duration(s["until"]), s["max_level"]) for s in d["retention"])
        for k in ("invocation", "locales", "extensions"):
            if k in kw:
                kw[k] = tuple(kw[k])
        return cls(**kw, extra={k: v for k, v in d.items() if k not in known})

    @classmethod
    def load(cls, path: str | Path) -> "Profile":
        return cls.from_dict(json.loads(Path(path).read_text()))
