"""
Evolution Engine — Autonomous Parameter Tuning via Genetic Algorithm.

Instead of hardcoded behavioral parameters (scroll probability, boredom decay,
resonance thresholds), this engine EVOLVES them based on real session outcomes.

Inspired by Tesla's real-time neural network weight updates from fleet data:
- Each session is a "generation"
- Session outcomes (follows gained, blocks, duration) determine "fitness"
- Winning parameters are preserved; losing parameters are mutated
- Hard safety bounds prevent the bot from evolving into dangerous territory

All parameters persist in Qdrant, surviving restarts.
"""

import logging
import random
import time
from dataclasses import asdict, dataclass, field
from typing import Any

logger = logging.getLogger(__name__)


# ── Hard Safety Bounds ──
# These are absolute limits that CANNOT be exceeded through evolution.
# Think of them as the "physics" constraints of the system.
SAFETY_BOUNDS = {
    "scroll_correction_probability": (0.05, 0.35),  # Never below 5%, never above 35%
    "boredom_decay_rate": (0.05, 0.5),  # How fast boredom accumulates
    "resonance_threshold": (0.3, 0.9),  # Content quality filter
    "interaction_cooldown_seconds": (1.0, 10.0),  # Min pause between interactions
    "max_follows_per_session": (5, 40),  # Absolute follow cap
    "max_likes_per_session": (10, 80),  # Absolute like cap
    "session_duration_target_minutes": (15, 120),  # Session length target
    "story_view_probability": (0.1, 0.8),  # How often to view stories
}


@dataclass
class Genome:
    """
    The bot's behavioral DNA — a set of evolvable parameters.
    Each parameter has a current value and respects hard safety bounds.
    """

    scroll_correction_probability: float = 0.15
    boredom_decay_rate: float = 0.2
    resonance_threshold: float = 0.7
    interaction_cooldown_seconds: float = 2.5
    max_follows_per_session: int = 15
    max_likes_per_session: int = 30
    session_duration_target_minutes: float = 45.0
    story_view_probability: float = 0.4

    # Metadata
    generation: int = 0
    best_fitness: float = 0.0
    last_updated: float = field(default_factory=time.time)

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict) -> "Genome":
        # Filter out unknown keys for forward-compatibility
        known = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in d.items() if k in known}
        return cls(**filtered)


@dataclass
class SessionResult:
    """
    Outcome metrics from a completed session.
    Used to calculate fitness for the current genome.
    """

    follows_gained: int = 0
    likes_given: int = 0
    stories_viewed: int = 0
    blocks_received: int = 0
    duration_minutes: float = 0.0
    prediction_error_rate: float = 0.0  # From Active Inference
    profiles_scraped: int = 0


class EvolutionEngine:
    """
    Genetic algorithm for behavioral parameter optimization.

    Lifecycle:
    1. Load genome from Qdrant (or use defaults)
    2. Bot uses genome parameters during session
    3. After session, evaluate fitness
    4. If fitness improved → lock genome (preserve winning params)
    5. If fitness decreased → mutate genome (try new params)
    6. Persist genome to Qdrant
    """

    _instance = None

    @classmethod
    def get_instance(cls, username: str = None) -> "EvolutionEngine":
        if cls._instance is None:
            cls._instance = cls(username or "default")
        return cls._instance

    @classmethod
    def reset(cls):
        cls._instance = None

    def __init__(self, username: str):
        self.username = username
        self.genome = Genome()
        self._qdrant_connected = False
        self._load_genome()

    def _load_genome(self):
        """Load persisted genome from Qdrant, or use defaults."""
        try:
            from GramAddict.core.qdrant_memory import QdrantBase

            self._db = QdrantBase("evolution_genomes_v1", vector_size=128)

            if not self._db.is_connected:
                logger.debug("[Evolution] Qdrant not available. Using default genome.")
                return

            self._qdrant_connected = True

            # Try to recall existing genome
            vec = self._db._get_embedding(f"genome_{self.username}")
            if not vec:
                return

            results = self._db.client.query_points(
                collection_name=self._db.collection_name,
                query=vec,
                limit=1,
                score_threshold=0.95,
            ).points

            if results:
                payload = results[0].payload
                genome_data = payload.get("genome", {})
                if genome_data:
                    self.genome = Genome.from_dict(genome_data)
                    logger.info(
                        f"🧬 [Evolution] Loaded genome generation {self.genome.generation} "
                        f"(fitness: {self.genome.best_fitness:.3f})"
                    )
        except Exception as e:
            logger.debug(f"[Evolution] Failed to load genome: {e}")

    def _save_genome(self):
        """Persist genome to Qdrant."""
        if not self._qdrant_connected:
            return

        try:
            vec = self._db._get_embedding(f"genome_{self.username}")
            if not vec:
                return

            self.genome.last_updated = time.time()
            payload = {
                "username": self.username,
                "genome": self.genome.to_dict(),
            }

            self._db.upsert_point(
                f"genome_{self.username}",
                payload,
                vector=vec,
                log_success=f"🧬 [Evolution] Saved genome generation {self.genome.generation}",
            )
        except Exception as e:
            logger.debug(f"[Evolution] Failed to save genome: {e}")

    def compute_fitness(self, result: SessionResult) -> float:
        """
        Computes a fitness score [0.0 - 1.0] from session outcomes.

        Reward:
        - Follows gained (high value)
        - Likes given (medium value)
        - Stories viewed (low value)
        - Longer sessions (moderate value)

        Penalty:
        - Blocks received (SEVERE penalty — 50% fitness reduction per block)
        - High prediction error rate (moderate penalty)
        """
        if result.blocks_received > 0:
            # Blocks are catastrophic — any genome that triggers a block is unfit
            block_penalty = 0.5**result.blocks_received
            logger.warning(
                f"🧬 [Evolution] BLOCK PENALTY: {result.blocks_received} blocks → "
                f"fitness multiplier {block_penalty:.3f}"
            )
        else:
            block_penalty = 1.0

        # Normalize outcomes to [0, 1] range
        follow_score = min(result.follows_gained / 20.0, 1.0)  # Cap at 20
        like_score = min(result.likes_given / 50.0, 1.0)  # Cap at 50
        story_score = min(result.stories_viewed / 20.0, 1.0)  # Cap at 20
        duration_score = min(result.duration_minutes / 60.0, 1.0)  # Cap at 60 min

        # Prediction accuracy bonus
        accuracy_bonus = 1.0 - result.prediction_error_rate

        # Weighted fitness
        raw_fitness = (
            follow_score * 0.35  # Follows are most valuable
            + like_score * 0.20  # Likes are secondary
            + story_score * 0.05  # Stories are minor
            + duration_score * 0.15  # Session stability matters
            + accuracy_bonus * 0.25  # Prediction accuracy = environmental mastery
        )

        fitness = raw_fitness * block_penalty
        fitness = max(0.0, min(1.0, fitness))  # Clamp to [0, 1]

        return round(fitness, 4)

    def evolve(self, result: SessionResult):
        """
        Evaluate session and evolve the genome.

        If fitness improved → lock parameters (exploitation)
        If fitness decreased → mutate parameters (exploration)
        """
        fitness = self.compute_fitness(result)

        logger.info(
            f"🧬 [Evolution] Generation {self.genome.generation} fitness: {fitness:.4f} "
            f"(best: {self.genome.best_fitness:.4f})"
        )

        if fitness >= self.genome.best_fitness:
            # ── Exploitation: Lock winning parameters ──
            logger.info(f"🧬 [Evolution] ✅ Fitness improved! Locking generation {self.genome.generation}.")
            self.genome.best_fitness = fitness
        else:
            # ── Exploration: Mutate parameters ──
            logger.info(f"🧬 [Evolution] 🔀 Fitness regressed. Mutating for generation {self.genome.generation + 1}.")
            self._mutate()

        self.genome.generation += 1
        self._save_genome()

    def _mutate(self, mutation_rate: float = 0.15):
        """
        Mutate genome parameters within safety bounds.

        Each parameter has a `mutation_rate` chance of being modified.
        Mutations are small (±10-20% of current value) to ensure gradual evolution.
        """
        for param_name, (low, high) in SAFETY_BOUNDS.items():
            if random.random() > mutation_rate:
                continue

            current = getattr(self.genome, param_name, None)
            if current is None:
                continue

            # Mutation: ±10-20% of range
            param_range = high - low
            delta = random.uniform(-0.2, 0.2) * param_range

            new_value = current + delta

            # Clamp to safety bounds
            if isinstance(current, int):
                new_value = int(max(low, min(high, round(new_value))))
            else:
                new_value = max(low, min(high, new_value))

            old_value = current
            setattr(self.genome, param_name, new_value)
            logger.debug(f"🧬 [Mutation] {param_name}: {old_value} → {new_value}")

    def get_param(self, name: str, default: Any = None) -> Any:
        """Get a parameter value from the current genome."""
        return getattr(self.genome, name, default)
