import os
import json
import csv
import math
import hashlib
from collections import deque, defaultdict, Counter
import numpy as np
from scipy import stats, signal
from typing import Optional, Dict, List
import warnings
warnings.filterwarnings('ignore')

try:
    from sklearn.neural_network import MLPRegressor
    from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, IsolationForest
    from sklearn.preprocessing import StandardScaler, RobustScaler
    import pywt
    ADVANCED_ML = True
except ImportError:
    ADVANCED_ML = False

try:
    import torch
    import torch.nn as nn
    import torch.optim as optim
    CUDA_AVAILABLE = torch.cuda.is_available()
    DEVICE = torch.device("cuda" if CUDA_AVAILABLE else "cpu")
except ImportError:
    CUDA_AVAILABLE = False
    DEVICE = None
    class MockNN:
        class Module: pass
    nn = MockNN

FEATURE_DIMENSION = 67
ML_MIN_TRAINING_SIZE = 50

class SimpleKalmanFilter:
    def __init__(self, process_variance=1e-5, measurement_variance=0.1):
        self.process_variance = process_variance
        self.measurement_variance = measurement_variance
        self.posteri_estimate = 50.0
        self.posteri_error_estimate = 1.0

    def update(self, measurement):
        priori_estimate = self.posteri_estimate
        priori_error_estimate = self.posteri_error_estimate + self.process_variance
        blending_factor = priori_error_estimate / (priori_error_estimate + self.measurement_variance)
        self.posteri_estimate = priori_estimate + blending_factor * (measurement - priori_estimate)
        self.posteri_error_estimate = (1 - blending_factor) * priori_error_estimate
        return self.posteri_estimate
    
    def get_noise_level(self):
        return self.posteri_error_estimate

class RegimeDetector:
    def __init__(self, window_size=50):
        self.window_size = window_size
        self.regime_history = deque(maxlen=100)
        
    def calculate_hurst(self, ts):
        if len(ts) < 20: return 0.5
        lags = range(2, min(20, len(ts)//2))
        tau = [np.std(np.subtract(ts[lag:], ts[:-lag])) for lag in lags]
        if len(tau) > 0:
            try: return np.polyfit(np.log(lags), np.log(tau), 1)[0] * 2.0
            except: return 0.5
        return 0.5

    def detect_micro_regime(self, data):
        if len(data) < 10: return "NORMAL", 0.0
        recent = np.array(data)[-10:]
        volatility = np.std(recent)
        if volatility > 40: return "CHAOS", 1.0
        slope, _, _, _, _ = stats.linregress(np.arange(len(recent)), recent)
        if abs(slope) > 5.0: return "FLASH_TREND", abs(slope) / 10.0
        return "NORMAL", 0.0

    def detect(self, data):
        if len(data) < self.window_size:
            return "RANDOM", 0.5
        recent_data = np.array(data)[-self.window_size:]
        hurst = self.calculate_hurst(recent_data)
        acf_1 = np.corrcoef(recent_data[:-1], recent_data[1:])[0, 1]
        volatility = np.std(recent_data)
        
        regime = "RANDOM"
        strength = 0.5
        if hurst > 0.6 and acf_1 > 0.1:
            regime, strength = "TRENDING", min(1.0, (hurst - 0.5) * 2 + acf_1)
        elif hurst < 0.4 and acf_1 < -0.1:
            regime, strength = "MEAN_REVERTING", min(1.0, (0.5 - hurst) * 2 + abs(acf_1))
        elif volatility < 15:
            regime, strength = "LOW_VOLATILITY", 1.0 - (volatility / 15.0)
        elif volatility > 35:
            regime, strength = "HIGH_VOLATILITY", (volatility - 35.0) / 15.0
            
        self.regime_history.append(regime)
        return regime, strength

class PatternMasterEngine:
    """
    Advanced Multi-Gram Markov Chain logic (V4).
    Tracks 3, 5, and 10-length sequences for deeper anomalies.
    """
    def __init__(self):
        self.ns = [3, 5, 8]
        self.memory = {n: {} for n in self.ns}
        self.history = []

    def get_key(self, seq):
        return "".join(['1' if x > 50.5 else '0' for x in seq])

    def update(self, result):
        for n in self.ns:
            if len(self.history) >= n:
                prev = self.history[-n:]
                key = self.get_key(prev)
                if key not in self.memory[n]: self.memory[n][key] = [0, 0]
                idx = 1 if result > 50.5 else 0
                self.memory[n][key][idx] += 1
        self.history.append(result)

    def predict(self):
        """Returns Multi-Gram (Probability_Over_50.5, Confidence)"""
        weights, probs = [], []
        
        for n in self.ns:
            if len(self.history) < n: continue
            
            curr = self.history[-n:]
            key = self.get_key(curr)
            
            if key not in self.memory[n]: continue
            
            under, over = self.memory[n][key]
            total = under + over
            
            if total < 2: continue
            
            probs.append(over / total)
            
            # Deeper patterns (higher n) get exponentially more weight if they have samples
            weights.append((total / 10.0) * (n ** 2))
            
        if not probs: return 50.0, 0.0
        
        prob_over = np.average(probs, weights=weights)
        confidence = min(1.0, sum(weights) / 50.0)
        
        # Convert back to raw 0-100 prediction space
        pred_val = 50.5 + (prob_over - 0.5) * 100
        return pred_val, confidence

import hmac
import hashlib

class ServerSeedProfiler:
    """
    Deterministically calculates the exact outcome of a roll
    without relying on statistics or machine learning if the Server Seed is known.
    """
    def __init__(self):
        self.known_seeds = {}
        
    def add_known_seed(self, server_seed_hash, unhashed_server_seed):
        self.known_seeds[server_seed_hash] = unhashed_server_seed
        
    def verify_outcome(self, server_seed, client_seed, nonce):
        msg = f"{client_seed}:{nonce}:0"
        hash_digest = hmac.new(
            key=server_seed.encode('utf-8'),
            msg=msg.encode('utf-8'),
            digestmod=hashlib.sha256
        ).hexdigest()
        
        val = int(hash_digest[:8], 16)
        # Apply Stake rounding math
        if val < (4294967296 / 10001) * 10000:
             return (val % 10000) / 100.0
        else:
             # Very rare edge case, requires next chunk of hash
             try:
                 val2 = int(hash_digest[8:16], 16)
                 return (val2 % 10000) / 100.0
             except:
                 return 50.0

class AdvancedFeatureEngineer:
    def __init__(self):
        pass

    def extract_wavelet_features(self, data, wavelet='db4', level=3):
        if len(data) < 2**level: return np.zeros(level * 4)
        coeffs = pywt.wavedec(data, wavelet, level=level)
        features = []
        for coeff in coeffs:
            features.extend([np.mean(coeff), np.std(coeff), np.max(np.abs(coeff)), stats.skew(coeff) if len(coeff) > 3 else 0])
        return np.array(features)

    def extract_statistical_moments(self, data, windows=[5, 10, 20, 50]):
        features = []
        for window in windows:
            if len(data) >= window:
                wd = data[-window:]
                mean_wd = np.mean(wd)
                std_wd = np.std(wd) + 1e-9
                momentum = wd[-1] - wd[0]
                
                # Deviation from expected 50.5 Stake mean
                z_score = (mean_wd - 50.5) / (std_wd / np.sqrt(window))
                prob_dev = stats.norm.cdf(z_score)

                features.extend([
                    mean_wd, std_wd, stats.skew(wd), stats.kurtosis(wd), 
                    np.median(wd), np.percentile(wd, 25), np.percentile(wd, 75), 
                    momentum, z_score, prob_dev
                ])
            else:
                features.extend([50, 29, 0, 0, 50, 37.5, 62.5, 0, 0, 0.5])
        return np.array(features)

    def extract_identifier_features(self, identifier, nonce=0):
        if not identifier: return np.zeros(15)
        ords = [ord(c) for c in identifier]
        features = [
            np.mean(ords), np.std(ords), np.sum(ords), ords[0], ords[-1],
            sum(1 for c in identifier if c.isdigit()), sum(1 for c in identifier if c.isupper()),
            int(hashlib.md5(identifier.encode()).hexdigest(), 16) % 100, len(set(identifier)),
            1 if 'x' in identifier else 0, nonce, nonce % 10, nonce % 100, np.log1p(nonce), 1 if nonce == 0 else 0
        ]
        return np.array(features)
        
    def extract_all_features(self, data, current_identifier=None, current_nonce=0):
        if len(data) < 20: return np.zeros(FEATURE_DIMENSION)
        # 12 features
        w_feats = self.extract_wavelet_features(data)
        # 40 features
        s_feats = self.extract_statistical_moments(data)
        # 15 features
        i_feats = self.extract_identifier_features(current_identifier, current_nonce)
        
        combined = np.concatenate([w_feats, s_feats, i_feats])
        
        # Fallback length padding/truncating just in case
        if len(combined) < FEATURE_DIMENSION:
            combined = np.pad(combined, (0, FEATURE_DIMENSION - len(combined)))
        elif len(combined) > FEATURE_DIMENSION:
            combined = combined[:FEATURE_DIMENSION]
            
        return combined

from .online_learning import OnlineLearningTracker
from .meta_learning import MetaLearningOptimizer
from .advanced_dl import TransformerPredictor, HAS_TORCH
from .anomaly import RNGAnomalyDetector
from .miner import BackgroundSeedMiner

class GroundbreakingPredictor:
    def __init__(self):
        self.history = deque(maxlen=1000)
        self.identifier_history = deque(maxlen=1000)
        self.nonce_history = deque(maxlen=1000)
        
        self.pattern_engine = PatternMasterEngine()
        
        self.feature_engineer = AdvancedFeatureEngineer()
        self.regime_detector = RegimeDetector()
        self.kalman_filter = SimpleKalmanFilter()
        
        # Exact Deterministic Mathematics
        self.seed_profiler = ServerSeedProfiler()
        self.miner = BackgroundSeedMiner()
        
        # Anomaly Detection
        self.anomaly_detector = RNGAnomalyDetector()
        
        # Deep Learning Transformer
        self.transformer = TransformerPredictor() if HAS_TORCH else None
        
        # Meta Learning (Self Evolving)
        self.meta_learner = MetaLearningOptimizer()
        
        # True Online Learning (River)
        self.online_tracker = OnlineLearningTracker(FEATURE_DIMENSION)
        
        self.models = {}
        self.model_scores = defaultdict(lambda: 50.0)
        self.model_confidences = defaultdict(lambda: 0.5)
        self.accuracy_history = deque(maxlen=100)
        
        # Caching features to eliminate O(N^2) extraction penalties
        self.X_history = deque(maxlen=2000)
        self.y_history = deque(maxlen=2000)
        
        self.shadow_model = None
        self.shadow_trained = False
        
        if ADVANCED_ML:
            self._init_ml_models()

        self.predictions = deque(maxlen=100)
        self.total_preds = 0
        self.correct_preds = 0
        self.denoised_history = deque(maxlen=1000)
        
        if ADVANCED_ML:
            self._init_ml_models()

        self.predictions = deque(maxlen=100)
        self.total_preds = 0
        self.correct_preds = 0
        self.denoised_history = deque(maxlen=1000)

    def _init_ml_models(self):
        try:
            self.models['deep_nn'] = MLPRegressor(hidden_layer_sizes=(128, 64), max_iter=100, random_state=42)
            self.models['random_forest'] = RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42)
            self.shadow_model = GradientBoostingRegressor(n_estimators=30, learning_rate=0.05, max_depth=3, random_state=42)
            self.scalers = {name: StandardScaler() for name in self.models.keys()}
            self.shadow_scaler = StandardScaler()
            self.models_trained = {name: False for name in self.models.keys()}
        except Exception:
            pass

    def train_ml_models(self):
        # Only train on the most recent 600 items for speed
        data = np.array(self.history)[-600:]
        X = np.array(self.X_history)[-600:]
        y = np.array(self.y_history)[-600:]
        
        # Train Deep Learning Transformer First
        if self.transformer and not self.transformer.is_trained:
             self.transformer.train(data.tolist())
        
        for name, model in self.models.items():
            try:
                X_scaled = self.scalers[name].fit_transform(X)
                model.fit(X_scaled, y)
                self.models_trained[name] = True
                split = int(len(X) * 0.8)
                if split > 0:
                    y_pred = model.predict(X_scaled[split:])
                    mae = np.mean(np.abs(y_pred - y[split:]))
                    # Stricter scoring, rapid dropoff for high MAE
                    self.model_scores[name] = max(0, min(100, 100 * (1 - (mae / 40)**2)))
            except:
                pass

    def add(self, result, identifier=None, nonce=0):
        # Update streaming models instantly
        if len(self.history) >= 20:
             current_id = self.identifier_history[-1] if self.identifier_history else None
             current_nonce = self.nonce_history[-1] if self.nonce_history else 0
             features = self.feature_engineer.extract_all_features(np.array(self.history), current_id, current_nonce)
             self.online_tracker.update(features, result)
             self.X_history.append(features)
             self.y_history.append(result)
             
        self.denoised_history.append(self.kalman_filter.update(result))
        if len(self.predictions) > 0:
            last_pred = self.predictions[-1]
            won = (last_pred >= 50 and result > 50.5) or (last_pred < 50 and result < 49.5)
            self.total_preds += 1
            if won: self.correct_preds += 1
            
        # Update Anomaly Detector
        self.anomaly_detector.add(result)
        
        # Evolution processing
        if len(self.predictions) > 0:
            last_pred = self.predictions[-1]
            won = (last_pred >= 50 and result > 50.5) or (last_pred < 50 and result < 49.5)
            self.total_preds += 1
            if won: self.correct_preds += 1
            
            acc = self.correct_preds / self.total_preds
            self.accuracy_history.append(acc)
            if len(self.accuracy_history) > 10 and len(self.accuracy_history) % 10 == 0:
                 self.meta_learner.update_weights([acc] * len(self.meta_learner.strategies))
                 self.meta_learner.evolve()
            
        self.history.append(result)
        self.pattern_engine.update(result)
        self.identifier_history.append(identifier if identifier else "")
        self.nonce_history.append(nonce)
        
        if len(self.history) >= ML_MIN_TRAINING_SIZE and len(self.history) % 150 == 0:
            self.train_ml_models()

    def predict(self, current_identifier=None, current_nonce=0, unhashed_server_seed=None, server_seed_hash=None):
        if unhashed_server_seed and current_identifier and len(unhashed_server_seed) == 64:
             # If we explicitly have the unhashed seed, use it
             self.seed_profiler.add_known_seed(current_identifier, unhashed_server_seed)
             
        # Check if the background miner found the seed
        cracked = self.miner.get_cracked_seed()
        if cracked and current_identifier:
             self.seed_profiler.add_known_seed(current_identifier, cracked)
             
        # Deterministic Interception - If we know the Server Seed, bypass ML entirely
        if current_identifier and current_identifier in self.seed_profiler.known_seeds:
             exact_seed = self.seed_profiler.known_seeds[current_identifier]
             # Predict the exact nonce
             exact_roll = self.seed_profiler.verify_outcome(exact_seed, current_identifier, current_nonce)
             return {
                'prediction': float(exact_roll),
                'confidence': 1.0, # 100% Guaranteed mathematically
                'pattern_prob': 1.0 if exact_roll > 50.5 else 0.0,
                'pattern_conf': 1.0,
                'accuracy': 1.0,
                'micro_regime': 'DETERMINISTIC',
                'rng_integrity': 'NORMAL'
             }
             
        # If we don't know the seed, but have the hash, fire a background mining job
        if server_seed_hash and len(self.history) >= 10:
             self.miner.farm_collision_async(
                 client_seed=current_identifier, 
                 target_outcomes=list(self.history)[-10:], 
                 start_nonce=max(1, current_nonce - 10), 
                 public_hash=server_seed_hash,
                 known_prefix=unhashed_server_seed if unhashed_server_seed else None
             )
             
        if len(self.history) < 20: return None

        data = np.array(self.history)
        all_predictions = {}
        
        regime, regime_strength = self.regime_detector.detect(data)
        
        # Adaptive Mean Reversion / Momentum Fallback
        mean_dev = np.mean(data[-20:]) - 50
        hurst = self.regime_detector.calculate_hurst(data[-50:])
        
        if hurst > 0.55:
             # Momentum tracking
             all_predictions['trend_momentum'] = 50 + (mean_dev * 1.5)
        else:
             # Mean Reversion
             all_predictions['mean_rev_short'] = 50 - (mean_dev * 1.5)
        
        features = self.feature_engineer.extract_all_features(data, current_identifier, current_nonce).reshape(1, -1)
        features_1d = features[0]
        
        # 1. Fetch Online River Predictors
        online_preds = self.online_tracker.predict(features_1d)
        for p_name, p_data in online_preds.items():
             all_predictions[p_name] = p_data["prediction"]
             self.model_scores[p_name] = p_data["score"]
        
        # 2. Fetch Deep Batch Predictors
        if ADVANCED_ML:
            for name, model in self.models.items():
                if self.models_trained.get(name, False):
                    try:
                        X_scaled = self.scalers[name].transform(features)
                        all_predictions[f'ml_{name}'] = np.clip(model.predict(X_scaled)[0], 0, 100)
                    except: pass
                    
        # 3. Fetch Deep Sequence Transformer
        if self.transformer and self.transformer.is_trained:
             t_pred = self.transformer.predict(data.tolist())
             if t_pred:
                 all_predictions['transformer_dl'] = t_pred
                 self.model_scores['transformer_dl'] = 75.0 # High base trust for Transformer
                    
        if not all_predictions: return None
        
        # Anomaly Check 
        anomaly_state, anomaly_score = self.anomaly_detector.check_anomaly()
        
        weights, values = [], []
        best_strategy = self.meta_learner.get_best_strategy()
        
        for name, pred in all_predictions.items():
            base_score = self.model_scores.get(name, 50.0)
            base_score *= best_strategy['momentum_weight'] # Apply meta-learned momentum
            
            # Substantially sharpen weights (exponential curve)
            weight = np.exp(np.clip(base_score, 0, 100) / 10.0)
            weights.append(weight)
            values.append(pred)
            
        # V3.7 Hybrid Ensemble Logic
        pat_prob, pat_conf = self.pattern_engine.predict()
        pat_pred = 50.5 + (pat_prob - 0.5) * 100
        
        # Calculate ML prediction
        if weights and values:
             ml_pred = np.average(values, weights=weights)
        else:
             ml_pred = 50.0
             
        # Apply threshold adjustment from meta-learner to ML prediction only
        ml_pred += best_strategy['threshold_adj'] * 10 
        
        # Final Ensemble Mix - Trust Pattern Engine over ML if confident
        if pat_conf > 0.0:
            final_pred = (pat_pred * pat_conf) + (ml_pred * (1 - pat_conf))
        else:
            final_pred = ml_pred
            
        # Adjust confidence metrics based on source
        if pat_conf > 0.0:
            ensemble_conf = max(pat_conf, 0.45)
        else:
            # Derive ML confidence from variance
            ensemble_conf = 1.0 / (1.0 + np.average((np.array(values) - ml_pred)**2, weights=weights) / 100.0)
        
        # Penalize confidence if anomalous state detected
        if anomaly_state == -1:
             ensemble_conf *= 0.5 
        
        self.predictions.append(final_pred)
        
        return {
            'prediction': float(final_pred),
            'confidence': float(np.clip(ensemble_conf, 0.3, 0.95)),
            'pattern_prob': float(pat_prob),
            'pattern_conf': float(pat_conf),
            'accuracy': self.correct_preds / self.total_preds if self.total_preds > 0 else 0.0,
            'micro_regime': self.regime_detector.detect_micro_regime(data)[0],
            'rng_integrity': 'ANOMALY' if anomaly_state == -1 else 'NORMAL'
        }
