"""ForkScore weight set + defaults.

Each weight maps to a term in the ForkScore formula. Defaults are
opinionated for Phase 1a single-validator deployments; v8 paper will
calibrate per-network.

Weight tuning notes:

- α = β = γ (3 batteries weighted equally by default). If your
  deployment has stronger needs in one axis, bump that one.
- δ (SelfModelCalibrationGain) modest by default — the SelfModel
  surface is still maturing. Bump once Phase 1b SelfModel claim
  histories are real.
- ε (AuditCompleteness) modest — the chain is already enforced; this
  weight surfaces missing audit rows as a soft signal, not a hard
  reject.
- ζ (ValidatorDiversity) ZERO in single-validator mode. v8 multi-
  validator deployments must set it positive or the protocol degrades
  to "first-mover wins."
- η (RegressionPenalty) heavier than improvement weights so the
  scorer is biased toward refusing regressions.
- θ (CapitalCostPenalty) modest — penalize cost increases without
  letting them dominate. v8 paper's "patch-the-planet" stewardship
  rule will raise this for capital-sensitive deployments.
- ι (SecurityRiskPenalty) ZERO in Phase 1a — no security-bench
  fixtures yet. Reserved.
- κ (ComplexityPenalty) ZERO in Phase 1a. Reserved.
- λ (MemoryInvalidationPenalty) modest — penalize forks that
  invalidate large memory regions; not a hard reject.

Non-weight field:

- ``delta_aggregator`` (ticket #000047) — *categorical*, not a
  numeric weight. Picks how ``fork_score._delta_5{s,t,f}`` collapse
  the per-sub-battery Δ-rate vector into one term: ``"mean"``
  (default — broad, balanced, noise-robust, the choice that matches
  ``docs/bench-maxing.md``'s per-rate 5-pp floor), ``"max"`` (the
  largest single-sub gain, floored at 0 — rewards specialization),
  or ``"sum"`` (total improvement — rewards breadth *and* magnitude,
  but can over-reward many sub-noise-floor gains). The per-sub
  hard-regression flags (`HARD_REGRESSION_FLOOR`) are computed
  independently of the aggregator, so a single-sub regression still
  forces REJECT under any choice. Default stays ``"mean"`` until a
  below-ceiling bench baseline (#000046) settles the choice
  empirically; changing the default bumps
  ``fork_score.ESTIMATOR_VERSION``.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass


#: Allowed values for :attr:`WeightSet.delta_aggregator` (ticket #000047).
DELTA_AGGREGATORS = ("mean", "max", "sum")


@dataclass(frozen=True)
class WeightSet:
    """ForkScore weights. Numeric weights are non-negative (sign lives in
    the formula); ``delta_aggregator`` is a categorical knob, not a weight."""

    alpha: float = 1.0    # Δ5S
    beta: float = 1.0     # Δ5T
    gamma: float = 1.0    # Δ5F
    delta: float = 0.5    # SelfModelCalibrationGain
    epsilon: float = 0.3  # AuditCompleteness
    zeta: float = 0.0     # ValidatorDiversity (off in single-validator)
    eta: float = 2.0      # RegressionPenalty
    theta: float = 0.5    # CapitalCostPenalty
    iota: float = 0.0     # SecurityRiskPenalty (reserved)
    kappa: float = 0.0    # ComplexityPenalty (reserved)
    lambda_: float = 0.5  # MemoryInvalidationPenalty (lambda is reserved word)
    delta_aggregator: str = "mean"  # #000047 — ∈ DELTA_AGGREGATORS; not a weight

    def __post_init__(self) -> None:
        if self.delta_aggregator not in DELTA_AGGREGATORS:
            raise ValueError(
                f"delta_aggregator must be one of {DELTA_AGGREGATORS}, "
                f"got {self.delta_aggregator!r}"
            )

    def as_dict(self) -> dict:
        return asdict(self)


DEFAULT_WEIGHTS = WeightSet()


def from_dict(data: dict) -> WeightSet:
    """Build a WeightSet from a dict (e.g., parsed JSON / YAML).

    Accepts both Greek-letter keys (``alpha``, ``beta``, …) and the
    Python-safe ``lambda_`` for the memory-invalidation weight. The
    ``delta_aggregator`` key (if present) is taken verbatim as a string
    (validated by :meth:`WeightSet.__post_init__`). Missing keys fall
    through to :data:`DEFAULT_WEIGHTS`.
    """
    base = DEFAULT_WEIGHTS.as_dict()
    for key, value in data.items():
        if key == "lambda":
            base["lambda_"] = float(value)
        elif key == "delta_aggregator":
            base["delta_aggregator"] = str(value)
        elif key in base:
            base[key] = float(value)
    return WeightSet(**base)
