# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-must-an-instance-bound-rounding-residual-so-party-shares-sum-to-the-total.md
# Remnant Warrant:    https://corpusfetch.com/c/how-must-an-instance-bound-rounding-residual-so-party-shares-sum-to-the-total.warrant.json
# Markdown SHA-256:   45345cd628e6387e8750f08f053d04a1b96880c8283c22e767b930f03433a3dc
# Code AST SHA-256:   bdd6e9ce25f049a161a39fea0595307334842023ba726ee30d39ca7630958f0a
# Verified Classes:   RoundingResidualError, RoundingGate
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class RoundingResidualError(RuntimeError):
    """Raised when the sum of rounded party shares does not equal the rounded total."""


@dataclass(frozen=True)
class RoundingGate:
    rounded_total: float
    rounded_shares: tuple
    residual_handled: bool  # either allocated to a party or held in explicit account

    def verify(self, epsilon: float = 1e-9) -> bool:
        if not self.rounded_shares:
            raise RoundingResidualError("no shares to allocate")
        share_sum = sum(self.rounded_shares)
        residual = round(self.rounded_total - share_sum, 10)
        if abs(residual) <= epsilon:
            return True
        if self.residual_handled:
            return True
        raise RoundingResidualError(
            f"shares sum to {share_sum} but total is {self.rounded_total}: "
            f"residual {residual} unallocated"
        )


def dispatch_allocation_mode(gate: RoundingGate) -> str:
    try:
        gate.verify()
    except RoundingResidualError:
        return "largest-remainder-or-residual-account"
    return "allocate-shares"
