#!/usr/bin/env python3
"""
POKER BOT POPULATION GENERATOR v1.0
Reference Implementation from NCFCA Poker Bot

Purpose:
  Generate realistic poker tables/player pools with configurable archetype distributions.
  Each agent has behavioral parameters (vpip, pfr, aggression, tilt_factor, latency).
  Parameters randomized within ranges to create diverse but realistic populations.
  
  This is the REFERENCE PATTERN for Market Population Generator.
  Same structure applies to markets (archetype distributions, parameter randomization).
"""

import random
import numpy as np
from dataclasses import dataclass
from typing import Dict, List, Tuple
from enum import Enum


# ============================================================
# ARCHETYPE PROFILES & RANDOMIZATION
# ============================================================

class PlayerArchetype(str, Enum):
    """Four poker player archetypes"""
    FISH = "Fish"      # Loose, weak, tilts easily
    REG = "Reg"        # Tight, solid, minimal tilt
    SHARK = "Shark"    # Tight, aggressive, adaptive
    GTO = "GTO"        # Balanced, exploitative/unexploitable


# Base profiles: expected values per archetype
BASE_PROFILES = {
    PlayerArchetype.FISH: {
        "vpip": 45,           # Plays 45% of hands preflop
        "pfr": 10,            # Raises pre-flop 10%
        "aggression": 1.0,    # Post-flop aggression (1.0 = loose, 3.5 = tight/aggressive)
        "tilt_multiplier": 2.0,  # How much tilt amplifies poor play
        "tilt_threshold": 0.6,   # How easily tilts (0.6 = easy, 0.05 = hard)
        "latency_variance": 0.1, # Decision time variance (low = predictable)
    },
    
    PlayerArchetype.REG: {
        "vpip": 22,
        "pfr": 18,
        "aggression": 2.5,
        "tilt_multiplier": 1.1,  # Resists tilt better
        "tilt_threshold": 0.1,
        "latency_variance": 0.5,
    },
    
    PlayerArchetype.SHARK: {
        "vpip": 28,
        "pfr": 24,
        "aggression": 3.5,
        "tilt_multiplier": 0.0,  # No tilt
        "tilt_threshold": 0.05,  # Nearly impossible to tilt
        "latency_variance": 1.5,
    },
    
    PlayerArchetype.GTO: {
        "vpip": 25,
        "pfr": 22,
        "aggression": 2.8,
        "tilt_multiplier": 0.0,
        "tilt_threshold": 0.0,
        "latency_variance": 0.0,  # Deterministic
    }
}

# Randomization ranges: create variance within each archetype
RANDOMIZATION_RANGES = {
    PlayerArchetype.FISH: {
        "vpip": (40, 50),
        "pfr": (8, 14),
        "aggression": (0.8, 1.3),
    },
    
    PlayerArchetype.REG: {
        "vpip": (19, 25),
        "pfr": (15, 21),
        "aggression": (2.2, 2.9),
    },
    
    PlayerArchetype.SHARK: {
        "vpip": (25, 32),
        "pfr": (21, 27),
        "aggression": (3.2, 4.0),
    },
    
    PlayerArchetype.GTO: {
        "vpip": (23, 27),
        "pfr": (20, 24),
        "aggression": (2.6, 3.0),
    }
}


# ============================================================
# AGENT DATA STRUCTURE
# ============================================================

@dataclass
class PokerAgent:
    """One agent in the population"""
    agent_id: str
    archetype: PlayerArchetype
    
    # Behavioral parameters
    vpip_base: float          # % of hands played preflop
    pfr_base: float           # % of hands raised preflop
    aggression: float         # Post-flop aggression level
    tilt_multiplier: float    # Tilt effect magnitude
    tilt_threshold: float     # Tilt susceptibility
    latency_variance: float   # Decision time variance
    
    # Runtime state
    tilt_state: float = 0.0        # Current tilt level (0.0 = normal, 1.0 = maximum tilt)
    stack: float = 100.0           # Chips in play
    cumulative_winloss: float = 0.0  # Total winloss this session
    hands_played: int = 0          # Hands completed
    
    def __repr__(self):
        return f"Agent({self.agent_id}, {self.archetype.value}, tilt={self.tilt_state:.2f})"


# ============================================================
# POPULATION GENERATOR
# ============================================================

class PopulationGenerator:
    """
    Generate realistic player populations with configurable archetype distributions.
    
    Usage:
        generator = PopulationGenerator(
            distribution={"Fish": 0.40, "Reg": 0.40, "Shark": 0.15, "GTO": 0.05}
        )
        table = generator.generate_table(seats=6)
        pool = generator.generate_player_pool(size=1000)
    """
    
    def __init__(self, distribution: Dict[str, float] = None):
        """
        Initialize with archetype distribution.
        
        distribution: Dict mapping archetype name (str) to probability
        Example: {"Fish": 0.40, "Reg": 0.40, "Shark": 0.15, "GTO": 0.05}
        
        If None, uses default even distribution.
        """
        if distribution is None:
            distribution = {"Fish": 0.25, "Reg": 0.25, "Shark": 0.25, "GTO": 0.25}
        
        self.distribution = self._normalize_distribution(distribution)
        self.agent_counter = 0
    
    def _normalize_distribution(self, dist: Dict[str, float]) -> Dict[PlayerArchetype, float]:
        """Convert string keys to PlayerArchetype enums and normalize to probabilities"""
        enum_dist = {}
        
        for archetype_name, prob in dist.items():
            # Convert string to enum
            if isinstance(archetype_name, str):
                # Special handling for GTO (not "Gto")
                if archetype_name.upper() == "GTO":
                    archetype = PlayerArchetype.GTO
                else:
                    archetype = PlayerArchetype(archetype_name.capitalize())
            else:
                archetype = archetype_name
            
            enum_dist[archetype] = prob
        
        # Normalize to probabilities
        total = sum(enum_dist.values())
        if total == 0:
            raise ValueError("Distribution probabilities must sum to > 0")
        
        return {arch: prob / total for arch, prob in enum_dist.items()}
    
    def generate_table(self, seats: int = 6, randomize_seats: bool = True) -> List[PokerAgent]:
        """
        Generate one poker table (6-10 players with realistic distribution).
        
        Args:
            seats: Number of players at table
            randomize_seats: Shuffle seating order if True
        
        Returns:
            List of PokerAgent objects
        """
        table = []
        for i in range(seats):
            agent = self._make_agent()
            table.append(agent)
        
        if randomize_seats:
            random.shuffle(table)
        
        return table
    
    def generate_player_pool(self, size: int = 1000) -> List[PokerAgent]:
        """
        Generate a large player pool (e.g., poker site population).
        
        Args:
            size: Number of agents to generate
        
        Returns:
            List of PokerAgent objects
        """
        return [self._make_agent() for _ in range(size)]
    
    def _sample_archetype(self) -> PlayerArchetype:
        """Sample one archetype according to distribution"""
        archetypes = list(self.distribution.keys())
        probabilities = list(self.distribution.values())
        return random.choices(archetypes, weights=probabilities, k=1)[0]
    
    def _make_agent(self) -> PokerAgent:
        """Create one agent with randomized parameters"""
        archetype = self._sample_archetype()
        base_profile = BASE_PROFILES[archetype]
        rand_ranges = RANDOMIZATION_RANGES[archetype]
        
        # Randomize key parameters within archetype-specific ranges.
        # FIX (found and verified 2026-07-30): these three were originally
        # drawn as fully independent uniforms -- no correlation between
        # them at all, which didn't match the actual goal of preserving
        # realistic variable correlation. VPIP is drawn first, then PFR
        # and aggression are blended with VPIP's position within its own
        # range (tighter play pairs with a higher PFR-to-VPIP ratio and
        # higher aggression -- a real, defensible poker pattern). Verified
        # at 20,000-agent scale: VPIP-PFR r=-0.83, VPIP-aggression r=-0.71.
        vpip = random.uniform(rand_ranges["vpip"][0], rand_ranges["vpip"][1])
        vpip_lo, vpip_hi = rand_ranges["vpip"]
        vpip_pos = (vpip - vpip_lo) / (vpip_hi - vpip_lo) if vpip_hi > vpip_lo else 0.5

        pfr_lo, pfr_hi = rand_ranges["pfr"]
        pfr_indep = random.uniform(pfr_lo, pfr_hi)
        pfr_pos = (pfr_indep - pfr_lo) / (pfr_hi - pfr_lo) if pfr_hi > pfr_lo else 0.5
        blended_pfr_pos = 0.6 * (1 - vpip_pos) + 0.4 * pfr_pos
        pfr = pfr_lo + blended_pfr_pos * (pfr_hi - pfr_lo)

        agg_lo, agg_hi = rand_ranges["aggression"]
        agg_indep = random.uniform(agg_lo, agg_hi)
        agg_pos = (agg_indep - agg_lo) / (agg_hi - agg_lo) if agg_hi > agg_lo else 0.5
        blended_agg_pos = 0.5 * (1 - vpip_pos) + 0.5 * agg_pos
        aggression = agg_lo + blended_agg_pos * (agg_hi - agg_lo)
        
        # Tilt/latency from base profile (less randomization)
        tilt_mult = base_profile["tilt_multiplier"] * random.uniform(0.8, 1.2)
        tilt_thresh = base_profile["tilt_threshold"] * random.uniform(0.9, 1.1)
        latency_var = base_profile["latency_variance"] * random.uniform(0.8, 1.2)
        
        agent = PokerAgent(
            agent_id=f"{archetype.value}_{self.agent_counter:05d}",
            archetype=archetype,
            vpip_base=vpip,
            pfr_base=pfr,
            aggression=aggression,
            tilt_multiplier=tilt_mult,
            tilt_threshold=tilt_thresh,
            latency_variance=latency_var,
        )
        
        self.agent_counter += 1
        return agent


# ============================================================
# POPULATION STATISTICS
# ============================================================

class PopulationAnalyzer:
    """Analyze generated population for correctness"""
    
    @staticmethod
    def compute_statistics(population: List[PokerAgent]) -> Dict:
        """
        Compute archetype distribution and parameter statistics.
        
        Returns:
            Dict with counts, means, stds per archetype
        """
        stats = {}
        
        # Group by archetype
        by_archetype = {}
        for agent in population:
            if agent.archetype not in by_archetype:
                by_archetype[agent.archetype] = []
            by_archetype[agent.archetype].append(agent)
        
        # Compute per-archetype statistics
        for archetype in PlayerArchetype:
            agents = by_archetype.get(archetype, [])
            
            if not agents:
                continue
            
            vpip_values = [a.vpip_base for a in agents]
            pfr_values = [a.pfr_base for a in agents]
            agg_values = [a.aggression for a in agents]
            
            stats[archetype.value] = {
                "count": len(agents),
                "percentage": len(agents) / len(population) * 100,
                "vpip": {
                    "mean": float(np.mean(vpip_values)),
                    "std": float(np.std(vpip_values)),
                    "min": float(np.min(vpip_values)),
                    "max": float(np.max(vpip_values)),
                },
                "pfr": {
                    "mean": float(np.mean(pfr_values)),
                    "std": float(np.std(pfr_values)),
                    "min": float(np.min(pfr_values)),
                    "max": float(np.max(pfr_values)),
                },
                "aggression": {
                    "mean": float(np.mean(agg_values)),
                    "std": float(np.std(agg_values)),
                    "min": float(np.min(agg_values)),
                    "max": float(np.max(agg_values)),
                }
            }
        
        return stats
    
    @staticmethod
    def print_population_report(population: List[PokerAgent]):
        """Pretty-print population statistics"""
        stats = PopulationAnalyzer.compute_statistics(population)
        
        print(f"\n{'='*80}")
        print(f"POPULATION STATISTICS ({len(population)} agents)")
        print(f"{'='*80}\n")
        
        for archetype_name in ["Fish", "Reg", "Shark", "GTO"]:
            if archetype_name not in stats:
                continue
            
            s = stats[archetype_name]
            print(f"{archetype_name} ({s['count']} agents, {s['percentage']:.1f}%):")
            print(f"  VPIP:       {s['vpip']['mean']:.1f}% ± {s['vpip']['std']:.1f}% (range: {s['vpip']['min']:.0f}-{s['vpip']['max']:.0f})")
            print(f"  PFR:        {s['pfr']['mean']:.1f}% ± {s['pfr']['std']:.1f}% (range: {s['pfr']['min']:.0f}-{s['pfr']['max']:.0f})")
            print(f"  Aggression: {s['aggression']['mean']:.2f} ± {s['aggression']['std']:.2f} (range: {s['aggression']['min']:.2f}-{s['aggression']['max']:.2f})")
            print()


# ============================================================
# MAIN: EXAMPLE USAGE
# ============================================================

if __name__ == "__main__":
    print("="*80)
    print("POKER BOT POPULATION GENERATOR v1.0")
    print("="*80)
    
    # Example 1: Generate one table
    print("\n[Example 1] Generate one 6-seat table with default distribution")
    print("-"*80)
    
    generator = PopulationGenerator()
    table = generator.generate_table(seats=6)
    
    print(f"Table generated with {len(table)} agents:")
    for agent in table:
        print(f"  {agent}")
    
    # Example 2: Custom distribution (soft games)
    print("\n[Example 2] Generate 50-player pool with custom distribution (soft games)")
    print("-"*80)
    
    soft_game_distribution = {
        "Fish": 0.50,      # 50% fish (juicy games)
        "Reg": 0.35,       # 35% regulars
        "Shark": 0.12,     # 12% sharks
        "GTO": 0.03,       # 3% GTO players
    }
    
    generator = PopulationGenerator(distribution=soft_game_distribution)
    pool = generator.generate_player_pool(size=50)
    
    print(f"Generated {len(pool)} agents with custom distribution:")
    PopulationAnalyzer.print_population_report(pool)
    
    # Example 3: Large scale population (10,000 agents)
    print("\n[Example 3] Generate 10,000-agent population (large poker ecosystem)")
    print("-"*80)
    
    generator = PopulationGenerator(distribution=soft_game_distribution)
    large_pool = generator.generate_player_pool(size=10000)
    
    print(f"Generated {len(large_pool)} agents")
    PopulationAnalyzer.print_population_report(large_pool)
    
    # Example 4: Verify randomization (should see variance within archetypes)
    print("\n[Example 4] Randomization verification")
    print("-"*80)
    
    fish_agents = [a for a in large_pool if a.archetype == PlayerArchetype.FISH]
    print(f"\nFish archetype sample (first 5 of {len(fish_agents)}):")
    for agent in fish_agents[:5]:
        print(f"  {agent.agent_id}: vpip={agent.vpip_base:.1f}%, pfr={agent.pfr_base:.1f}%, agg={agent.aggression:.2f}")
    
    print(f"\nNote: Each Fish agent has randomized parameters within ranges:")
    print(f"      VPIP: 40-50%, PFR: 8-14%, Aggression: 0.8-1.3")
    print(f"      This creates realistic diversity within the archetype.")
    
    print("\n" + "="*80)
    print("POKER BOT POPULATION GENERATOR COMPLETE")
    print("="*80)
