import numpy as np
import random
import json
import os

STRATEGY_CACHE = "meta_strategies.json"
STRATEGY_MUTATION_RATE = 0.1
STRATEGY_POOL_SIZE = 10

class MetaLearningOptimizer:
    """Learns which prediction strategies work best and adapts (Self-Evolving AI)"""

    def __init__(self, n_strategies=STRATEGY_POOL_SIZE):
        self.strategies = []
        self.strategy_scores = []
        self.strategy_weights = []

        # Try to load existing strategies from disk
        if not self.load_strategies():
            # Initialize random strategy pool
            for _ in range(n_strategies):
                self.strategies.append({
                    'momentum_weight': np.random.uniform(0.5, 1.5),
                    'ensemble_method': np.random.choice(['weighted', 'voting', 'softmax']),
                    'learning_rate': np.random.uniform(0.001, 0.1),
                    'threshold_adj': np.random.uniform(-0.1, 0.1)
                })
                self.strategy_scores.append(0.5)
                self.strategy_weights.append(1.0 / n_strategies)

    def load_strategies(self):
        if os.path.exists(STRATEGY_CACHE):
            try:
                with open(STRATEGY_CACHE, 'r') as f:
                    data = json.load(f)
                    self.strategies = data['strategies']
                    self.strategy_scores = data['scores']
                    self.strategy_weights = data['weights']
                return True
            except:
                return False
        return False

    def save_strategies(self):
        try:
            with open(STRATEGY_CACHE, 'w') as f:
                json.dump({
                    'strategies': self.strategies,
                    'scores': self.strategy_scores,
                    'weights': self.strategy_weights
                }, f)
        except:
            pass

    def evaluate_strategy(self, strategy, recent_performance):
        """Evaluate strategy performance proxy"""
        return recent_performance * strategy['momentum_weight']

    def update_weights(self, performances):
        """Update strategy weights based on performance using Softmax"""
        exp_perf = np.exp(np.array(performances) / 0.1)
        self.strategy_weights = list(exp_perf / np.sum(exp_perf))

    def mutate_strategy(self, strategy):
        """Genetic algorithm-style mutation"""
        mutated = strategy.copy()

        if np.random.rand() < STRATEGY_MUTATION_RATE:
            mutated['momentum_weight'] *= np.random.uniform(0.9, 1.1)

        if np.random.rand() < STRATEGY_MUTATION_RATE:
            mutated['ensemble_method'] = np.random.choice(['weighted', 'voting', 'softmax'])

        if np.random.rand() < STRATEGY_MUTATION_RATE:
            mutated['learning_rate'] = np.clip(mutated['learning_rate'] * np.random.uniform(0.5, 1.5), 0.0001, 0.5)

        if np.random.rand() < STRATEGY_MUTATION_RATE:
            mutated['threshold_adj'] = np.clip(mutated['threshold_adj'] + np.random.randn() * 0.05, -0.2, 0.2)

        return mutated

    def get_best_strategy(self):
        """Returns the current best parameters"""
        best_idx = np.argmax(self.strategy_scores)
        return self.strategies[best_idx]

    def evolve(self):
        """Evolutionary step: keep best, mutate, introduce diversity"""
        sorted_indices = np.argsort(self.strategy_scores)[::-1]
        n_keep = len(self.strategies) // 2

        # Keep top performers
        new_strategies = [self.strategies[i] for i in sorted_indices[:n_keep]]

        # Mutate to replace bottom performers
        for i in range(n_keep):
            new_strategies.append(self.mutate_strategy(new_strategies[i]))

        self.strategies = new_strategies
        self.strategy_scores = [self.strategy_scores[i] for i in sorted_indices[:n_keep]] + [0.5] * n_keep
        
        # Save to disk for persistent evolution across sessions
        self.save_strategies()
