from __future__ import annotations

from dataclasses import dataclass, field
from hashlib import sha256
from typing import Any


@dataclass(slots=True)
class RuntimeVariantCandidate:
    id: str
    variant_key: str
    traffic_weight: int
    content_config: dict[str, Any] | list[Any] | None = None


@dataclass(slots=True)
class AssignmentDecision:
    experiment_id: str
    experiment_key: str
    variant_id: str
    variant_key: str
    bucket: int
    reason: str = "weighted_assignment"


@dataclass(slots=True)
class RuntimeExperimentContext:
    experiment_id: str
    experiment_key: str
    status: str
    site_key: str | None = None
    assignment_salt: str | None = None
    release_id: str | None = None
    release_version: int | None = None
    payload: dict[str, Any] | list[Any] | None = None
    variants: list[RuntimeVariantCandidate] = field(default_factory=list)


def build_bucket(seed: str) -> int:
    """Create a stable 0-9999 bucket from a visitor+experiment seed."""

    digest = sha256(seed.encode("utf-8")).hexdigest()
    return int(digest[:8], 16) % 10000


def choose_variant(
    *,
    experiment_id: str,
    experiment_key: str,
    visitor_id: str,
    site_key: str | None = None,
    assignment_salt: str | None = None,
    variants: list[RuntimeVariantCandidate],
) -> AssignmentDecision | None:
    """Deterministically assign a visitor based on weighted variants.

    This first version intentionally lives in pure Python with no storage
    dependency so we can review the bucketing rule before wiring persistence.
    """

    active_variants = [variant for variant in variants if variant.traffic_weight > 0]
    if not active_variants:
        return None

    total_weight = sum(variant.traffic_weight for variant in active_variants)
    if total_weight <= 0:
        return None

    seed = f"{site_key or 'global'}:{assignment_salt or experiment_key}:{visitor_id}:{experiment_key}"
    bucket = build_bucket(seed)
    scaled_bucket = bucket % total_weight

    running_total = 0
    for variant in active_variants:
        running_total += variant.traffic_weight
        if scaled_bucket < running_total:
            return AssignmentDecision(
                experiment_id=experiment_id,
                experiment_key=experiment_key,
                variant_id=variant.id,
                variant_key=variant.variant_key,
                bucket=bucket,
            )

    chosen = active_variants[-1]
    return AssignmentDecision(
        experiment_id=experiment_id,
        experiment_key=experiment_key,
        variant_id=chosen.id,
        variant_key=chosen.variant_key,
        bucket=bucket,
        reason="weighted_assignment_fallback",
    )
