"""
oyun_teorisi.types — Core data types for the Game Theory Lens (Lens #5 of 7).

Implements a game-theoretic / strategic interaction instrument for the
multi-lens analytical apparatus defined in AGENTS.md §4.1.

  Lens: OyunTeorisi | Faculty: Nefs | Domain: Game theory // strategic interaction

Sorts implemented (mapping to AGENTS.md Appendix C):
  - NefsMakam       — S_Makam: 4 ascending stations of the nafs
  - Strategy        — A strategic choice available to an agent
  - Player          — An agent at a particular Nefs station
  - PayoffEntry     — A payoff value for a specific (station, strategy pair)
  - GameOutcome     — Result of a strategy profile
  - GameModel       — Complete game specification with station-dependent payoffs

Governing axioms enforced at construction:
  AX62/N-5: Nefs Station Ascent — payoff function transforms with station
            (Emmare → Levvame → Mulhime → Mutmainne)
  AX63:     Tesanüd/İnfirâd asymmetry — affirmations compose super-additively,
            denials remain isolated
  AX52:     Multiplicative gate — zero in any dimension = system collapse
  T6/KV₄:  Convergence bound — all scores in [0, 1)
  AX57:     Transparency — every output must disclose its epistemic status

KV₇ compliance: This module imports ONLY from the standard library.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Tuple


# ---------------------------------------------------------------------------
# Score clamping — T6/KV₄: strict upper bound < 1.0
# ---------------------------------------------------------------------------

def clamp_score(value: float) -> float:
    """Clamp a score to [0, 0.9999].

    Per T6 (Convergence Bound) and KV₄ (Ne Ayn Ne Gayr):
    no instrument achieves convergence = 1.0.
    """
    return min(max(value, 0.0), 0.9999)


# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------

class NefsMakam(Enum):
    """4 ascending stations of the Nefs per AX62/N-5.

    Each station has a qualitatively different payoff structure:
      EMMARE     — Commanding soul: worldly payoffs dominate
      LEVVAME    — Self-reproaching: mixed payoffs, beginning to question
      MULHIME    — Inspired: alignment payoffs start dominating
      MUTMAINNE  — Tranquil: alignment-with-truth payoffs dominate

    The game is inherently *dynamic*: the same strategic interaction yields
    different payoffs depending on the player's current station.  Per N-5,
    this is formally analogous to a dynamic game with evolving utility
    functions — an open area requiring dynamic game models.
    """
    EMMARE = 1
    LEVVAME = 2
    MULHIME = 3
    MUTMAINNE = 4


class StrategyType(Enum):
    """Classification of strategic choices.

    WORLDLY    — Nafsanî / dunya-oriented action
    ALIGNMENT  — Truth / ruhanî-oriented action
    MIXED      — Components of both
    """
    WORLDLY = "worldly"
    ALIGNMENT = "alignment"
    MIXED = "mixed"


class CompositionMode(Enum):
    """How findings compose per AX63 (Tesanüd/İnfirâd asymmetry).

    TESANUD     — Super-additive: affirmative findings compound
    INFIRAD     — Isolated: negative findings don't compose
    INDEPENDENT — Default: no claimed composition direction
    """
    TESANUD = "tesanud"
    INFIRAD = "infirad"
    INDEPENDENT = "independent"


# Ascending station order per AX62
MAKAM_ORDER: List[NefsMakam] = [
    NefsMakam.EMMARE,
    NefsMakam.LEVVAME,
    NefsMakam.MULHIME,
    NefsMakam.MUTMAINNE,
]


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class Strategy:
    """A strategic choice available to a player.

    Attributes:
        name:          Unique identifier for the strategy.
        strategy_type: WORLDLY, ALIGNMENT, or MIXED.
        description:   Optional human-readable description.
    """
    name: str
    strategy_type: StrategyType
    description: str = ""

    def __post_init__(self) -> None:
        if not self.name or not self.name.strip():
            raise ValueError("Strategy name cannot be empty")
        if not isinstance(self.strategy_type, StrategyType):
            raise ValueError(
                f"strategy_type must be StrategyType, got {type(self.strategy_type)}"
            )


@dataclass(frozen=True)
class Player:
    """An agent at a particular Nefs station.

    Per AX62/N-5 the player's station (makam) determines their payoff
    function —  at lower stations worldly payoffs dominate, at higher
    stations alignment-with-truth dominates.

    Attributes:
        name:  Unique identifier for the player.
        makam: Current Nefs station (NefsMakam).
    """
    name: str
    makam: NefsMakam

    def __post_init__(self) -> None:
        if not self.name or not self.name.strip():
            raise ValueError("Player name cannot be empty")
        if not isinstance(self.makam, NefsMakam):
            raise ValueError(f"makam must be NefsMakam, got {type(self.makam)}")


@dataclass(frozen=True)
class PayoffEntry:
    """A single payoff value for a specific strategic interaction at a station.

    Per AX62, the same strategy pair yields different payoffs at different
    stations.  This dataclass captures one cell of that station-dependent
    payoff matrix.

    Attributes:
        player_name:       Which player receives the payoff.
        makam:             Station at which the payoff applies.
        my_strategy:       Strategy name chosen by the player.
        opponent_strategy: Strategy name of the opponent.
        payoff:            Numeric payoff value.
    """
    player_name: str
    makam: NefsMakam
    my_strategy: str
    opponent_strategy: str
    payoff: float

    def __post_init__(self) -> None:
        if not self.player_name or not self.player_name.strip():
            raise ValueError("player_name cannot be empty")
        if not isinstance(self.makam, NefsMakam):
            raise ValueError(f"makam must be NefsMakam, got {type(self.makam)}")
        if not isinstance(self.payoff, (int, float)):
            raise ValueError("Payoff must be numeric")


@dataclass(frozen=True)
class GameOutcome:
    """Result of a specific strategy profile.

    Captures who played what, the resulting payoffs, and whether this
    constitutes a Nash equilibrium.

    Attributes:
        player_names: Tuple of player identifiers.
        strategies:   Tuple of strategy names (one per player).
        payoffs:      Tuple of payoff values (one per player).
        is_nash:      Whether this profile is a Nash equilibrium.
    """
    player_names: tuple
    strategies: tuple
    payoffs: tuple
    is_nash: bool = False

    def __post_init__(self) -> None:
        if len(self.player_names) != len(self.strategies):
            raise ValueError(
                "player_names and strategies must have the same length"
            )
        if len(self.player_names) != len(self.payoffs):
            raise ValueError(
                "player_names and payoffs must have the same length"
            )
        if len(self.player_names) < 2:
            raise ValueError("A game outcome requires at least 2 players")


# ---------------------------------------------------------------------------
# GameModel — mutable aggregate root
# ---------------------------------------------------------------------------

class GameModel:
    """Complete game specification with station-dependent payoffs.

    Core model for the OyunTeorisi lens.  Captures:
      - Players at different Nefs stations (AX62)
      - Strategies available to each player
      - Payoff functions that transform with station ascent (N-5)
      - Game outcomes including Nash equilibria
      - Composition mode for multi-finding aggregation (AX63)

    The game is inherently *dynamic*: the same strategic interaction yields
    different payoffs depending on the player's current station.  At EMMARE
    selfish/worldly strategies dominate; at MUTMAINNE alignment-with-truth
    strategies emerge as dominant.
    """

    def __init__(self, name: str = "") -> None:
        self.name = name
        self._players: Dict[str, Player] = {}
        self._strategies: Dict[str, List[Strategy]] = {}
        self._payoffs: List[PayoffEntry] = []
        self._outcomes: List[GameOutcome] = []
        self._composition_mode: CompositionMode = CompositionMode.INDEPENDENT

    # ---- Player management ------------------------------------------------

    def add_player(self, player: Player) -> None:
        """Register a player in the game."""
        if not isinstance(player, Player):
            raise TypeError(f"Expected Player, got {type(player)}")
        self._players[player.name] = player
        if player.name not in self._strategies:
            self._strategies[player.name] = []

    @property
    def players(self) -> List[Player]:
        """All registered players."""
        return list(self._players.values())

    def get_player(self, name: str) -> Optional[Player]:
        """Look up a player by name.  Returns ``None`` if not found."""
        return self._players.get(name)

    # ---- Strategy management ----------------------------------------------

    def add_strategy(self, player_name: str, strategy: Strategy) -> None:
        """Add a strategy for a registered player (duplicates ignored)."""
        if player_name not in self._players:
            raise KeyError(f"Player '{player_name}' not registered")
        if not isinstance(strategy, Strategy):
            raise TypeError(f"Expected Strategy, got {type(strategy)}")
        if player_name not in self._strategies:
            self._strategies[player_name] = []
        existing = {s.name for s in self._strategies[player_name]}
        if strategy.name not in existing:
            self._strategies[player_name].append(strategy)

    def get_strategies(self, player_name: str) -> List[Strategy]:
        """Get all strategies registered for a player."""
        return list(self._strategies.get(player_name, []))

    # ---- Payoff management ------------------------------------------------

    def add_payoff(self, entry: PayoffEntry) -> None:
        """Register a payoff entry for a registered player."""
        if not isinstance(entry, PayoffEntry):
            raise TypeError(f"Expected PayoffEntry, got {type(entry)}")
        if entry.player_name not in self._players:
            raise KeyError(f"Player '{entry.player_name}' not registered")
        self._payoffs.append(entry)

    @property
    def payoffs(self) -> List[PayoffEntry]:
        """All registered payoff entries."""
        return list(self._payoffs)

    def get_payoff(
        self,
        player_name: str,
        makam: NefsMakam,
        my_strategy: str,
        opponent_strategy: str,
    ) -> Optional[float]:
        """Look up a specific payoff value.

        Returns the payoff for *player_name* at *makam* when they play
        *my_strategy* against *opponent_strategy*.  Returns ``None`` if
        no matching entry exists.
        """
        for entry in self._payoffs:
            if (
                entry.player_name == player_name
                and entry.makam == makam
                and entry.my_strategy == my_strategy
                and entry.opponent_strategy == opponent_strategy
            ):
                return entry.payoff
        return None

    def get_payoffs_at_station(
        self, player_name: str, makam: NefsMakam
    ) -> List[PayoffEntry]:
        """Get all payoff entries for a player at a specific station."""
        return [
            e for e in self._payoffs
            if e.player_name == player_name and e.makam == makam
        ]

    # ---- AX62: Station-dependent payoff transformation --------------------

    def payoff_transforms_with_station(
        self,
        player_name: str,
        my_strategy: str,
        opponent_strategy: str,
    ) -> bool:
        """Check whether the payoff for a strategy pair varies across stations.

        Returns ``True`` if payoffs differ for at least two distinct stations,
        demonstrating AX62-compliant station-dependent transformation.
        """
        values: set = set()
        for makam in NefsMakam:
            p = self.get_payoff(player_name, makam, my_strategy, opponent_strategy)
            if p is not None:
                values.add(p)
        return len(values) > 1

    def station_ascent_ratio(self, player_name: str) -> float:
        """Measure alignment shift across the Nefs station ascent.

        For each (worldly, alignment, opponent) triple, checks whether
        worldly dominates at EMMARE and alignment dominates at MUTMAINNE.
        Returns the fraction of triples satisfying this AX62 pattern
        (a value in [0, 1]).
        """
        strategies = self.get_strategies(player_name)
        if not strategies:
            return 0.0

        worldly = [s for s in strategies if s.strategy_type == StrategyType.WORLDLY]
        alignment = [s for s in strategies if s.strategy_type == StrategyType.ALIGNMENT]
        if not worldly or not alignment:
            return 0.0

        opponent_strats: set = set()
        for p in self._players.values():
            if p.name != player_name:
                for s in self.get_strategies(p.name):
                    opponent_strats.add(s.name)
        if not opponent_strats:
            return 0.0

        shifts = 0
        total = 0
        for w in worldly:
            for a in alignment:
                for opp in opponent_strats:
                    low_w = self.get_payoff(player_name, NefsMakam.EMMARE, w.name, opp)
                    low_a = self.get_payoff(player_name, NefsMakam.EMMARE, a.name, opp)
                    high_w = self.get_payoff(player_name, NefsMakam.MUTMAINNE, w.name, opp)
                    high_a = self.get_payoff(player_name, NefsMakam.MUTMAINNE, a.name, opp)
                    if all(v is not None for v in (low_w, low_a, high_w, high_a)):
                        total += 1
                        # AX62 pattern: worldly ≥ alignment at low station,
                        #                alignment ≥ worldly at high station
                        if low_w >= low_a and high_a >= high_w:
                            shifts += 1

        return shifts / total if total else 0.0

    # ---- Nash equilibria --------------------------------------------------

    def find_nash_equilibria(self) -> List[GameOutcome]:
        """Return all outcomes marked as Nash equilibria."""
        return [o for o in self._outcomes if o.is_nash]

    # ---- Outcome management -----------------------------------------------

    def add_outcome(self, outcome: GameOutcome) -> None:
        """Register a game outcome."""
        if not isinstance(outcome, GameOutcome):
            raise TypeError(f"Expected GameOutcome, got {type(outcome)}")
        self._outcomes.append(outcome)

    @property
    def outcomes(self) -> List[GameOutcome]:
        """All registered game outcomes."""
        return list(self._outcomes)

    # ---- AX63: Composition mode -------------------------------------------

    @property
    def composition_mode(self) -> CompositionMode:
        """Current evidence composition mode per AX63."""
        return self._composition_mode

    @composition_mode.setter
    def composition_mode(self, mode: CompositionMode) -> None:
        if not isinstance(mode, CompositionMode):
            raise TypeError(f"Expected CompositionMode, got {type(mode)}")
        self._composition_mode = mode

    def tesanud_strength(self) -> float:
        """Compute super-additive strength of affirmative findings.

        Per AX63, independent affirmations compose super-additively
        (tesanüd), while denials remain isolated (infirâd).

        Returns a value in [0, 1) where higher = stronger tesanüd.
        """
        nash_count = len(self.find_nash_equilibria())
        total = len(self._outcomes)

        if total == 0:
            return 0.0

        if self._composition_mode == CompositionMode.INFIRAD:
            # Denials don't compose — return minimal value
            return clamp_score(0.1)

        if self._composition_mode == CompositionMode.TESANUD:
            if nash_count == 0:
                return 0.0
            # Super-additive: base ratio + logarithmic boost
            base = nash_count / total
            boost = math.log1p(nash_count) / math.log1p(total)
            raw = (base + boost) / 2
            return clamp_score(raw)

        # INDEPENDENT — simple ratio
        if nash_count == 0:
            return 0.0
        return clamp_score(nash_count / total)

    # ---- Utility ----------------------------------------------------------

    def count_stations_covered(self) -> int:
        """Count how many distinct Nefs stations have payoff entries."""
        return len({e.makam for e in self._payoffs})

    # ---- Serialisation (AX57 transparency) --------------------------------

    def to_dict(self) -> dict:
        """Serialise the game model to a JSON-friendly dict."""
        return {
            "name": self.name,
            "players": [
                {"name": p.name, "makam": p.makam.name}
                for p in self.players
            ],
            "strategies": {
                pname: [
                    {
                        "name": s.name,
                        "type": s.strategy_type.value,
                        "description": s.description,
                    }
                    for s in strats
                ]
                for pname, strats in self._strategies.items()
            },
            "payoffs": [
                {
                    "player": e.player_name,
                    "makam": e.makam.name,
                    "my_strategy": e.my_strategy,
                    "opponent_strategy": e.opponent_strategy,
                    "payoff": e.payoff,
                }
                for e in self._payoffs
            ],
            "outcomes": [
                {
                    "players": list(o.player_names),
                    "strategies": list(o.strategies),
                    "payoffs": list(o.payoffs),
                    "is_nash": o.is_nash,
                }
                for o in self._outcomes
            ],
            "composition_mode": self._composition_mode.value,
        }
