"""Drift model for PROTOCOL - hidden parameter changes over time.

Drift causes parameters to change at scheduled turns, requiring agents
to detect and adapt to these changes.

Key concepts:
- Drift count varies by profile (DEV: 1-2, STANDARD: 3-5, HARD: 4-6)
- Drift times have minimum spacing of 8 turns
- Each drift changes 1-2 parameters (1 with probability 2/3)
- Delta values are sampled from profile-specific sets
"""

from __future__ import annotations

from dataclasses import dataclass
from enum import Enum

from purefn.games.protocol.prng import DomainPRNG
from purefn.games.protocol.profiles import Profile
from purefn.games.protocol.templates import TemplateType

# Minimum spacing between drift events
DRIFT_MIN_SPACING = 8

# Minimum and maximum turn offsets for drift
DRIFT_T_MIN = 10
DRIFT_T_MAX_OFFSET = 5  # turn_limit - this value


@dataclass
class DriftConfig:
    """Configuration for drift events per profile."""

    count_min: int
    count_max: int
    deltas_k: tuple[int, ...]
    deltas_a: tuple[int, ...]
    deltas_b: tuple[int, ...]
    deltas_r: tuple[int, ...]
    deltas_s: tuple[int, ...]


# Delta sets per profile
DRIFT_CONFIGS: dict[Profile, DriftConfig] = {
    Profile.TINY: DriftConfig(
        count_min=0,
        count_max=1,
        deltas_k=(0x01,),
        deltas_a=(0x02,),
        deltas_b=(0x01,),
        deltas_r=(1,),
        deltas_s=(1,),
    ),
    Profile.DEV: DriftConfig(
        count_min=1,
        count_max=2,
        deltas_k=(0x01, 0x03, 0x0F),
        deltas_a=(0x02, 0x06),
        deltas_b=(0x01, 0x03, 0x07, 0x0F),
        deltas_r=(1,),
        deltas_s=(1,),
    ),
    Profile.STANDARD: DriftConfig(
        count_min=3,
        count_max=5,
        deltas_k=(0x01, 0x03, 0x0F, 0x33, 0x55),
        deltas_a=(0x02, 0x06, 0x0E, 0x1E),
        deltas_b=(0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F),
        deltas_r=(1, 3),
        deltas_s=(1, 2, 3),
    ),
    Profile.HARD: DriftConfig(
        count_min=4,
        count_max=6,
        deltas_k=(0x01, 0x03, 0x0F, 0x33, 0x55, 0xAA),
        deltas_a=(0x02, 0x06, 0x0E, 0x1E, 0x3E, 0x7E),
        deltas_b=(0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F),
        deltas_r=(1, 3, 5),
        deltas_s=(1, 2, 3, 5),
    ),
}


class DriftParam(Enum):
    """Parameters that can drift."""

    K = "K"
    A = "A"
    B = "B"
    R = "R"
    S = "S"


# Eligible parameters per template
TEMPLATE_PARAMS: dict[TemplateType, tuple[DriftParam, ...]] = {
    TemplateType.T1: (DriftParam.K, DriftParam.A, DriftParam.B, DriftParam.R),
    TemplateType.T2: (DriftParam.K, DriftParam.A, DriftParam.B, DriftParam.R),
    TemplateType.T3: (DriftParam.K, DriftParam.B, DriftParam.R, DriftParam.S),
}


@dataclass
class DriftEvent:
    """A single drift event."""

    turn: int
    params_changed: list[DriftParam]
    deltas: dict[DriftParam, int]


@dataclass
class DriftSchedule:
    """Complete drift schedule for a game."""

    events: list[DriftEvent]


def sample_drift_count(prng: DomainPRNG, profile: Profile) -> int:
    """Sample the number of drift events for this game.

    Args:
        prng: Domain PRNG instance
        profile: Game profile

    Returns:
        Number of drift events
    """
    config = DRIFT_CONFIGS[profile]
    return prng.rand_int("drift:schedule", config.count_min, config.count_max)


def _compute_drift_range(turn_limit: int) -> tuple[int, int]:
    """Compute valid drift time range."""
    return DRIFT_T_MIN, turn_limit - DRIFT_T_MAX_OFFSET


def _adjust_drift_count(count: int, t_min: int, t_max: int) -> int:
    """Adjust drift count to fit within available range."""
    if t_max < t_min:
        return 0
    min_range_needed = (count - 1) * DRIFT_MIN_SPACING
    if t_max - t_min < min_range_needed:
        return max(0, (t_max - t_min) // DRIFT_MIN_SPACING + 1)
    return count


def _is_valid_drift_time(t: int, existing: list[int]) -> bool:
    """Check if time maintains minimum spacing with existing times."""
    return all(abs(t - et) >= DRIFT_MIN_SPACING for et in existing)


def sample_drift_times(
    prng: DomainPRNG,
    count: int,
    turn_limit: int,
    max_attempts: int = 1000,
) -> list[int]:
    """Sample drift event times with minimum spacing constraint.

    Uses rejection sampling to ensure minimum spacing between events.

    Args:
        prng: Domain PRNG instance
        count: Number of drift events to schedule
        turn_limit: Maximum turn number
        max_attempts: Maximum sampling attempts before giving up

    Returns:
        Sorted list of drift turn numbers
    """
    if count == 0:
        return []

    t_min, t_max = _compute_drift_range(turn_limit)
    count = _adjust_drift_count(count, t_min, t_max)
    if count == 0:
        return []

    times: list[int] = []
    attempts = 0

    while len(times) < count and attempts < max_attempts:
        t = prng.rand_int("drift:schedule", t_min, t_max)
        attempts += 1

        if _is_valid_drift_time(t, times):
            times.append(t)

    times.sort()
    return times


def sample_param_count(prng: DomainPRNG, event_index: int) -> int:
    """Sample number of parameters to change (1 or 2).

    1 parameter with probability 2/3, 2 with probability 1/3.

    Args:
        prng: Domain PRNG instance
        event_index: Index of this drift event

    Returns:
        1 or 2
    """
    # Use 3 values: 0, 1 -> 1 param, 2 -> 2 params
    val = prng.rand_int(f"drift:event:{event_index}:count", 0, 2)
    return 1 if val < 2 else 2


def sample_params_to_change(
    prng: DomainPRNG,
    event_index: int,
    template_type: TemplateType,
    param_count: int,
) -> list[DriftParam]:
    """Sample which parameters to change.

    Args:
        prng: Domain PRNG instance
        event_index: Index of this drift event
        template_type: Template type (determines eligible params)
        param_count: Number of parameters to change (1 or 2)

    Returns:
        List of parameters to change
    """
    eligible = list(TEMPLATE_PARAMS[template_type])
    result = []

    for i in range(param_count):
        if not eligible:
            break
        idx = prng.rand_int(f"drift:event:{event_index}:param:{i}", 0, len(eligible) - 1)
        param = eligible.pop(idx)
        result.append(param)

    return result


def sample_delta(
    prng: DomainPRNG,
    event_index: int,
    param: DriftParam,
    profile: Profile,
) -> int:
    """Sample delta value for a parameter.

    Args:
        prng: Domain PRNG instance
        event_index: Index of this drift event
        param: Parameter being changed
        profile: Game profile (determines delta set)

    Returns:
        Delta value to apply
    """
    config = DRIFT_CONFIGS[profile]

    delta_set: tuple[int, ...]
    if param == DriftParam.K:
        delta_set = config.deltas_k
    elif param == DriftParam.A:
        delta_set = config.deltas_a
    elif param == DriftParam.B:
        delta_set = config.deltas_b
    elif param == DriftParam.R:
        delta_set = config.deltas_r
    else:  # DriftParam.S
        delta_set = config.deltas_s

    idx = prng.rand_int(f"drift:event:{event_index}:{param.value}", 0, len(delta_set) - 1)
    return delta_set[idx]


def generate_drift_schedule(
    prng: DomainPRNG,
    profile: Profile,
    template_type: TemplateType,
    turn_limit: int,
) -> DriftSchedule:
    """Generate complete drift schedule for a game.

    Args:
        prng: Domain PRNG instance
        profile: Game profile
        template_type: Template type
        turn_limit: Maximum turn number

    Returns:
        Complete drift schedule
    """
    count = sample_drift_count(prng, profile)
    times = sample_drift_times(prng, count, turn_limit)

    events = []
    for i, turn in enumerate(times):
        param_count = sample_param_count(prng, i)
        params = sample_params_to_change(prng, i, template_type, param_count)
        deltas = {p: sample_delta(prng, i, p, profile) for p in params}

        events.append(DriftEvent(turn=turn, params_changed=params, deltas=deltas))

    return DriftSchedule(events=events)


def _apply_drift_k(current: int, delta: int) -> int:
    """Apply drift operation to K parameter.

    K := K XOR deltaK

    Args:
        current: Current K value
        delta: Delta to apply

    Returns:
        New K value
    """
    return current ^ delta


def _apply_drift_a(current: int, delta: int) -> int:
    """Apply drift operation to A parameter.

    A := (A XOR deltaA) | 0x01 (keep odd)

    Args:
        current: Current A value
        delta: Delta to apply

    Returns:
        New A value (always odd)
    """
    return (current ^ delta) | 0x01


def _apply_drift_b(current: int, delta: int) -> int:
    """Apply drift operation to B parameter.

    B := (B + deltaB) mod 256

    Args:
        current: Current B value
        delta: Delta to apply

    Returns:
        New B value
    """
    return (current + delta) & 0xFF


def _apply_drift_r(current: int, delta: int) -> int:
    """Apply drift operation to R parameter.

    R := (R + deltaR) mod 8

    Args:
        current: Current R value
        delta: Delta to apply

    Returns:
        New R value (0-7)
    """
    return (current + delta) % 8


def _apply_drift_s(current: int, delta: int) -> int:
    """Apply drift operation to S parameter.

    S := (S + deltaS) mod 8

    Args:
        current: Current S value
        delta: Delta to apply

    Returns:
        New S value (0-7)
    """
    return (current + delta) % 8


def apply_drift_operation(param: DriftParam, current: int, delta: int) -> int:
    """Apply drift operation to a parameter.

    Args:
        param: Parameter being changed
        current: Current value
        delta: Delta to apply

    Returns:
        New value after drift
    """
    if param == DriftParam.K:
        return _apply_drift_k(current, delta)
    elif param == DriftParam.A:
        return _apply_drift_a(current, delta)
    elif param == DriftParam.B:
        return _apply_drift_b(current, delta)
    elif param == DriftParam.R:
        return _apply_drift_r(current, delta)
    else:  # DriftParam.S
        return _apply_drift_s(current, delta)


def apply_drift_event(
    params: dict[str, int],
    event: DriftEvent,
) -> dict[str, dict[str, int]]:
    """Apply a drift event to current parameters.

    Modifies params dict in-place.

    Args:
        params: Current parameter values (modified in-place)
        event: Drift event to apply

    Returns:
        Dict with 'old' and 'new' values for changed params
    """
    changes: dict[str, dict[str, int]] = {"old": {}, "new": {}}

    for param in event.params_changed:
        param_name = param.value
        delta = event.deltas[param]
        old_value = params.get(param_name, 0)
        new_value = apply_drift_operation(param, old_value, delta)

        changes["old"][param_name] = old_value
        changes["new"][param_name] = new_value
        params[param_name] = new_value

    return changes


def get_pending_drift_events(
    schedule: DriftSchedule,
    last_applied_turn: int,
    current_turn: int,
) -> list[DriftEvent]:
    """Get drift events that should be applied.

    Returns events with turn > last_applied_turn and turn <= current_turn.

    Args:
        schedule: Complete drift schedule
        last_applied_turn: Last turn where drift was applied (or 0 if none)
        current_turn: Current turn number

    Returns:
        List of events to apply (in turn order)
    """
    return [event for event in schedule.events if last_applied_turn < event.turn <= current_turn]
