"""Pure versioned player rating materialization and aging policy."""
from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from decimal import Decimal, ROUND_HALF_UP
import hashlib
import json

from players.constants import ALL_ATTRIBUTES, POSITION_WEIGHTS
from .constants import TRAINABLE_PLAYER_ATTRIBUTES

MATERIALIZATION_POLICY_KEY = "ftg.player.rating_materialization.v1"
MATERIALIZATION_POLICY_VERSION = "1.0.0"
AGING_POLICY_KEY = "ftg.player.aging_decline.v1"
AGING_POLICY_VERSION = "1.0.0"
_Q6 = Decimal("0.000001")


def payload_sha256(payload: dict) -> str:
    return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str).encode()).hexdigest()


def age_on(date_of_birth: date, on_date: date) -> int:
    return on_date.year - date_of_birth.year - ((on_date.month, on_date.day) < (date_of_birth.month, date_of_birth.day))


def calculate_overall(*, position: str, ratings: dict[str, int]) -> int:
    weights = POSITION_WEIGHTS.get(position, {})
    total = Decimal("0")
    weight_total = Decimal("0")
    for attribute in ALL_ATTRIBUTES:
        weight = Decimal(str(weights.get(attribute, 1.0)))
        total += Decimal(int(ratings[attribute])) * weight
        weight_total += weight
    if not weight_total:
        return 1
    return max(1, min(100, int((total / weight_total).quantize(Decimal("1"), rounding=ROUND_HALF_UP))))


@dataclass(frozen=True, slots=True)
class AttributeChange:
    attribute_name: str
    rating_before: int
    rating_after: int
    credit_available_before: Decimal
    credit_consumed: Decimal
    credit_available_after: Decimal

    @property
    def delta(self) -> int:
        return self.rating_after - self.rating_before


@dataclass(frozen=True, slots=True)
class MaterializationResult:
    age_years: int
    overall_before: int
    overall_after: int
    potential: int
    changes: tuple[AttributeChange, ...]
    total_credit_consumed: Decimal
    ratings_after: dict[str, int]


class PlayerRatingMaterializationPolicyV1:
    key = MATERIALIZATION_POLICY_KEY
    version = MATERIALIZATION_POLICY_VERSION

    @staticmethod
    def credit_cost(*, rating: int, age_years: int) -> Decimal:
        rating = int(rating)
        if rating < 60:
            base = Decimal("8")
        elif rating < 70:
            base = Decimal("10")
        elif rating < 80:
            base = Decimal("12")
        elif rating < 90:
            base = Decimal("16")
        elif rating < 95:
            base = Decimal("22")
        else:
            base = Decimal("30")
        if age_years <= 18:
            multiplier = Decimal("0.85")
        elif age_years <= 23:
            multiplier = Decimal("0.95")
        elif age_years <= 29:
            multiplier = Decimal("1.00")
        else:
            multiplier = Decimal("1.25")
        return (base * multiplier).quantize(_Q6)

    @classmethod
    def materialize(cls, *, position: str, ratings: dict[str, int], potential: int, date_of_birth: date,
                    observed_on: date, available_credits: dict[str, Decimal], age_years: int | None = None) -> MaterializationResult:
        age = age_on(date_of_birth, observed_on) if age_years is None else age_years
        working = {name: int(ratings[name]) for name in ALL_ATTRIBUTES}
        overall_before = calculate_overall(position=position, ratings=working)
        if overall_before > int(potential):
            raise ValueError("overall cannot exceed potential before materialization")
        changes = []
        total_consumed = Decimal("0")
        # After 34, accumulated development is retained but no longer materialized as positive growth.
        if age > 34:
            return MaterializationResult(age, overall_before, overall_before, int(potential), tuple(), Decimal("0.000000"), working)

        for attribute in TRAINABLE_PLAYER_ATTRIBUTES:
            available = Decimal(available_credits.get(attribute, Decimal("0"))).quantize(_Q6)
            before = working[attribute]
            current = before
            consumed = Decimal("0")
            while current < 100:
                cost = cls.credit_cost(rating=current, age_years=age)
                if available - consumed < cost:
                    break
                candidate = dict(working)
                candidate[attribute] = current + 1
                candidate_overall = calculate_overall(position=position, ratings=candidate)
                if candidate_overall > int(potential):
                    break
                current += 1
                working[attribute] = current
                consumed += cost
            if consumed > 0:
                changes.append(AttributeChange(
                    attribute, before, current, available, consumed.quantize(_Q6), (available - consumed).quantize(_Q6)
                ))
                total_consumed += consumed
        overall_after = calculate_overall(position=position, ratings=working)
        return MaterializationResult(
            age, overall_before, overall_after, int(potential), tuple(changes), total_consumed.quantize(_Q6), working
        )


@dataclass(frozen=True, slots=True)
class AgingResult:
    age_years: int
    overall_before: int
    overall_after: int
    base_decline_points: int
    applied_decline_points: int
    training_protection: Decimal
    retirement_eligible: bool
    attribute_deltas: dict[str, int]
    ratings_after: dict[str, int]


class PlayerAgingPolicyV1:
    key = AGING_POLICY_KEY
    version = AGING_POLICY_VERSION
    _vulnerability = {
        "pace": Decimal("1.50"), "acceleration": Decimal("1.45"), "agility": Decimal("1.25"),
        "stamina": Decimal("1.15"), "jumping": Decimal("1.00"), "strength": Decimal("0.85"),
        "balance": Decimal("0.80"), "natural_fitness": Decimal("0.45"), "dribbling": Decimal("0.35"),
        "ball_control": Decimal("0.25"), "finishing": Decimal("0.22"), "shooting": Decimal("0.22"),
        "tackling": Decimal("0.20"), "marking": Decimal("0.18"), "crossing": Decimal("0.18"),
        "passing": Decimal("0.12"), "vision": Decimal("0.08"), "positioning": Decimal("0.05"),
        "decision_making": Decimal("0.04"), "composure": Decimal("0.04"), "anticipation": Decimal("0.05"),
        "off_ball": Decimal("0.08"), "concentration": Decimal("0.06"), "work_rate": Decimal("0.12"),
        "teamwork": Decimal("0.04"), "heading": Decimal("0.15"), "first_touch": Decimal("0.15"),
        "long_shots": Decimal("0.15"), "set_pieces": Decimal("0.05"), "aggression": Decimal("0.05"),
        "gk_diving": Decimal("0.75"), "gk_reflexes": Decimal("1.10"), "gk_one_on_one": Decimal("0.55"),
        "gk_handling": Decimal("0.25"), "gk_positioning": Decimal("0.08"), "gk_kicking": Decimal("0.18"),
    }

    @staticmethod
    def base_decline(age_years: int) -> int:
        if age_years <= 34:
            return 0
        if age_years <= 36:
            return 2
        if age_years <= 38:
            return 4
        if age_years == 39:
            return 6
        return 8

    @classmethod
    def apply(cls, *, position: str, ratings: dict[str, int], date_of_birth: date, observed_on: date,
              season_training_by_attribute: dict[str, Decimal], player_seed: str, age_years: int | None = None) -> AgingResult:
        age = age_on(date_of_birth, observed_on) if age_years is None else age_years
        working = {name: int(ratings[name]) for name in ALL_ATTRIBUTES}
        overall_before = calculate_overall(position=position, ratings=working)
        base = cls.base_decline(age)
        total_training = sum((Decimal(v) for v in season_training_by_attribute.values()), Decimal("0"))
        training_protection = min(Decimal("0.70"), (total_training / Decimal("60")) * Decimal("0.70"))
        fitness_bonus = Decimal(working.get("natural_fitness", 50)) / Decimal("100") * Decimal("0.15")
        protection = min(Decimal("0.80"), training_protection + fitness_bonus).quantize(Decimal("0.0001"))
        applied = 0 if base == 0 else max(1, int((Decimal(base) * (Decimal("1") - protection)).quantize(Decimal("1"), rounding=ROUND_HALF_UP)))
        deltas = {name: 0 for name in TRAINABLE_PLAYER_ATTRIBUTES}

        def score(attr: str) -> tuple[Decimal, str]:
            trained = Decimal(season_training_by_attribute.get(attr, Decimal("0")))
            targeted = min(Decimal("0.70"), trained / Decimal("12") * Decimal("0.70"))
            vulnerability = cls._vulnerability.get(attr, Decimal("0.10")) * (Decimal("1") - targeted)
            salt = int.from_bytes(hashlib.sha256(f"{player_seed}|{attr}".encode()).digest()[:2], "big")
            return (vulnerability * Decimal(working[attr]) + Decimal(salt % 100) / Decimal("10000"), attr)

        for _ in range(applied):
            eligible = [a for a in TRAINABLE_PLAYER_ATTRIBUTES if working[a] > 1]
            if not eligible:
                break
            attr = max(eligible, key=score)
            working[attr] -= 1
            deltas[attr] -= 1
        deltas = {k: v for k, v in deltas.items() if v}
        overall_after = calculate_overall(position=position, ratings=working)
        return AgingResult(age, overall_before, overall_after, base, sum(-v for v in deltas.values()), protection,
                           age >= 40, deltas, working)
