"""Pure deterministic policies for season/competition orchestration.

This module intentionally has no Django imports so workflow planning and
calendar evidence can be replayed without booting the ORM.
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import date, timedelta
import hashlib
import json
import math

ORCHESTRATION_POLICY_KEY = "ftg.season.competition_orchestration.v1"
ORCHESTRATION_POLICY_VERSION = "2.0.0"
ORCHESTRATION_SCHEMA_VERSION = 1
CALENDAR_POLICY_KEY = "ftg.season.calendar_windows.v1"
CALENDAR_POLICY_VERSION = "1.0.0"

STEP_KEYS = (
    "PRECHECK",
    "FINALIZE_SOURCE",
    "COMPLETE_SOURCE",
    "MATERIALIZE_PLAYERS",
    "APPLY_MOVEMENT",
    "GENERATE_LEAGUES",
    "CREATE_COMPETITIONS",
    "QUALIFY_COMPETITIONS",
    "DRAW_COMPETITIONS",
    "BUILD_CALENDAR",
    "MARK_UPCOMING",
)


def payload_sha256(payload: dict) -> str:
    encoded = json.dumps(
        payload,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
        default=str,
    ).encode("utf-8")
    return hashlib.sha256(encoded).hexdigest()


def deterministic_seed(*parts: object) -> int:
    raw = ":".join(str(part) for part in parts).encode("utf-8")
    value = int.from_bytes(hashlib.sha256(raw).digest()[:8], "big", signed=False)
    return (value % ((2**63) - 1)) + 1


@dataclass(frozen=True, slots=True)
class CalendarWindow:
    phase: str
    sequence: int
    scheduled_date: date
    metadata: dict

    def to_payload(self) -> dict:
        return {
            "phase": self.phase,
            "sequence": self.sequence,
            "scheduled_date": self.scheduled_date.isoformat(),
            "metadata": self.metadata,
        }


class SeasonCalendarPolicyV1:
    """Date-level windows only; local kickoff time remains a Match concern."""

    policy_key = CALENDAR_POLICY_KEY
    policy_version = CALENDAR_POLICY_VERSION
    league_round_gap_days = 7
    federation_round_gap_days = 14
    continental_start_offset_days = 21
    regional_cup_start_offset_days = 7

    @classmethod
    def league_windows(cls, *, start_date: date, end_date: date, round_count: int = 38,
                       round_gap_days=None, break_start=None, break_end=None):
        gap = cls.league_round_gap_days if round_gap_days is None else int(round_gap_days)
        if not 3 <= gap <= 30 or (break_start is None) != (break_end is None):
            raise ValueError("Invalid league round spacing or midseason break.")
        windows = []
        current = start_date
        for sequence in range(1, int(round_count) + 1):
            if break_start is not None:
                if sequence <= int(round_count) // 2 and current >= break_start:
                    raise ValueError("First half of league must finish before the midseason window.")
                if sequence > int(round_count) // 2:
                    current = max(current, break_end)
            windows.append(CalendarWindow(
                phase="LEAGUE_ROUND",
                sequence=sequence,
                scheduled_date=current,
                metadata={"round_number": sequence},
            ))
            current += timedelta(days=gap)
        windows = tuple(windows)
        cls._ensure_within_season(windows=windows, end_date=end_date)
        return windows

    @classmethod
    def regional_cup_windows(cls, *, start_date: date, end_date: date, bracket_size: int):
        rounds = cls._knockout_round_count(bracket_size)
        first_date = start_date + timedelta(days=cls.regional_cup_start_offset_days)
        windows = tuple(
            CalendarWindow(
                phase="KNOCKOUT_ROUND",
                sequence=sequence,
                scheduled_date=first_date + timedelta(days=(sequence - 1) * cls.federation_round_gap_days),
                metadata={"round_number": sequence, "bracket_size": int(bracket_size)},
            )
            for sequence in range(1, rounds + 1)
        )
        cls._ensure_within_season(windows=windows, end_date=end_date)
        return windows

    @classmethod
    def group_stage_knockout_windows(
        cls,
        *,
        start_date: date,
        end_date: date,
        group_count: int,
        offset_days: int = 0,
    ):
        group_count = int(group_count)
        if group_count < 2 or group_count & (group_count - 1):
            raise ValueError("Group count must be a power of two and at least two.")
        first_date = start_date + timedelta(days=cls.continental_start_offset_days + int(offset_days))
        windows = []
        for sequence in range(1, 7):
            windows.append(
                CalendarWindow(
                    phase="GROUP_MATCHDAY",
                    sequence=sequence,
                    scheduled_date=first_date + timedelta(days=(sequence - 1) * cls.federation_round_gap_days),
                    metadata={"matchday": sequence, "group_count": group_count},
                )
            )
        qualifier_count = group_count * 2
        knockout_rounds = int(math.log2(qualifier_count))
        knockout_start = windows[-1].scheduled_date + timedelta(days=cls.federation_round_gap_days)
        for sequence in range(1, knockout_rounds + 1):
            windows.append(
                CalendarWindow(
                    phase="KNOCKOUT_ROUND",
                    sequence=sequence,
                    scheduled_date=knockout_start + timedelta(days=(sequence - 1) * cls.federation_round_gap_days),
                    metadata={
                        "round_number": sequence,
                        "qualifier_count": qualifier_count,
                    },
                )
            )
        windows = tuple(windows)
        cls._ensure_within_season(windows=windows, end_date=end_date)
        return windows

    @staticmethod
    def _knockout_round_count(bracket_size: int) -> int:
        value = int(bracket_size)
        if value < 2 or value & (value - 1):
            raise ValueError("Knockout bracket size must be a power of two and at least two.")
        return int(math.log2(value))

    @staticmethod
    def _ensure_within_season(*, windows, end_date):
        if windows and windows[-1].scheduled_date > end_date:
            raise ValueError("Calendar policy would schedule a competition window after the Season end date.")
