#!/usr/bin/env python3
"""
Deep Learning Engine mit LSTM für Lotto-Vorhersagen
====================================================

Implementiert LSTM-basierte Modelle zur Vorhersage von Lotto-Zahlen
basierend auf historischen Sequenzen und Features.

Features:
- LSTM-Netzwerk für zeitliche Sequenzen
- Sequence-to-Probability Mapping
- Feature Engineering Integration
- Model Persistence & Caching
- Hybrid mit RandomForest
"""

import numpy as np
import pandas as pd
import os
import json
import pickle
from datetime import datetime
from typing import Dict, List, Tuple, Optional
import warnings
warnings.filterwarnings('ignore')

# TensorFlow/Keras Imports
try:
    import tensorflow as tf
    from tensorflow import keras
    from tensorflow.keras.models import Sequential, load_model
    from tensorflow.keras.layers import LSTM, Dense, Dropout, Bidirectional, BatchNormalization
    from tensorflow.keras.optimizers import Adam
    from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
    from tensorflow.keras.regularizers import l2
    TENSORFLOW_AVAILABLE = True
except ImportError:
    TENSORFLOW_AVAILABLE = False
    print("⚠️  TensorFlow not available. Install with: pip install tensorflow")


class DeepLearningEngine:
    """
    LSTM-basierter Deep Learning Engine für Lotto-Vorhersagen.

    Architecture:
    - Input: Sequence of historical draws + features
    - LSTM layers: Learn temporal patterns
    - Dense layers: Map to probability distribution
    - Output: Probability for each number
    """

    def __init__(
        self,
        num_numbers: int = 49,
        sequence_length: int = 20,
        cache_dir: str = None,
        fast_mode: bool = True
    ):
        """
        Args:
            num_numbers: Maximum number (49 for Lotto, 50 for Eurojackpot)
            sequence_length: How many past draws to consider
            cache_dir: Directory for model persistence
            fast_mode: Use faster training (fewer epochs)
        """
        self.num_numbers = num_numbers
        self.sequence_length = sequence_length
        self.cache_dir = cache_dir
        self.fast_mode = fast_mode

        # Model components
        self.model = None
        self.is_trained = False
        self.training_history = {}

        # Configuration
        self.config = {
            'lstm_units_1': 128,
            'lstm_units_2': 64,
            'dense_units': 128,
            'dropout_rate': 0.3,
            'learning_rate': 0.001,
            'batch_size': 32,
            'epochs': 30 if fast_mode else 100,
            'validation_split': 0.2
        }

        if cache_dir:
            os.makedirs(cache_dir, exist_ok=True)
            self.model_path = os.path.join(cache_dir, f'lstm_model_{num_numbers}.h5')
            self.config_path = os.path.join(cache_dir, f'lstm_config_{num_numbers}.json')
        else:
            self.model_path = None
            self.config_path = None

        print(f"🧠 Deep Learning Engine initialized")
        print(f"   Numbers: 1-{num_numbers}")
        print(f"   Sequence Length: {sequence_length}")
        print(f"   Fast Mode: {fast_mode}")

    def _build_model(self, num_features: int) -> Sequential:
        """
        Builds LSTM architecture.

        Architecture:
        Input (sequence_length, num_features)
          ↓
        Bidirectional LSTM(128) + Dropout(0.3)
          ↓
        Bidirectional LSTM(64) + Dropout(0.3)
          ↓
        Dense(128, relu) + BatchNorm + Dropout(0.3)
          ↓
        Dense(num_numbers, sigmoid)
        """
        model = Sequential([
            # First Bidirectional LSTM layer
            Bidirectional(
                LSTM(
                    self.config['lstm_units_1'],
                    return_sequences=True,
                    kernel_regularizer=l2(0.01)
                ),
                input_shape=(self.sequence_length, num_features)
            ),
            Dropout(self.config['dropout_rate']),
            BatchNormalization(),

            # Second Bidirectional LSTM layer
            Bidirectional(
                LSTM(
                    self.config['lstm_units_2'],
                    return_sequences=False,
                    kernel_regularizer=l2(0.01)
                )
            ),
            Dropout(self.config['dropout_rate']),
            BatchNormalization(),

            # Dense layers
            Dense(
                self.config['dense_units'],
                activation='relu',
                kernel_regularizer=l2(0.01)
            ),
            BatchNormalization(),
            Dropout(self.config['dropout_rate']),

            # Output layer - probability for each number
            Dense(self.num_numbers, activation='sigmoid')
        ])

        # Compile
        model.compile(
            optimizer=Adam(learning_rate=self.config['learning_rate']),
            loss='binary_crossentropy',
            metrics=['accuracy', 'AUC']
        )

        return model

    def _prepare_sequences(
        self,
        df: pd.DataFrame,
        features_df: pd.DataFrame
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Prepares sequences for LSTM training.

        Args:
            df: Historical draws (with columns Z1-Z6)
            features_df: Engineered features

        Returns:
            X: (num_samples, sequence_length, num_features)
            y: (num_samples, num_numbers) - binary matrix
        """
        print(f"   Preparing sequences (length={self.sequence_length})...")

        # Ensure data is sorted by date
        if 'datum' in df.columns:
            df = df.sort_values('datum').reset_index(drop=True)

        # Extract number columns
        num_cols = [col for col in df.columns if col.startswith('Z')]

        # Normalize features to [0, 1]
        features_normalized = features_df.copy()
        for col in features_normalized.columns:
            min_val = features_normalized[col].min()
            max_val = features_normalized[col].max()
            if max_val > min_val:
                features_normalized[col] = (features_normalized[col] - min_val) / (max_val - min_val)
            else:
                features_normalized[col] = 0.5

        X_sequences = []
        y_targets = []

        # Create sequences
        for i in range(self.sequence_length, len(df)):
            # Get sequence of features
            sequence = features_normalized.iloc[i - self.sequence_length:i].values
            X_sequences.append(sequence)

            # Target: next draw as binary vector
            target = np.zeros(self.num_numbers)
            next_draw = df.iloc[i][num_cols].values
            for num in next_draw:
                if 1 <= num <= self.num_numbers:
                    target[int(num) - 1] = 1
            y_targets.append(target)

        X = np.array(X_sequences)
        y = np.array(y_targets)

        print(f"   ✅ Created {len(X)} sequences")
        print(f"   Shape: X={X.shape}, y={y.shape}")

        return X, y

    def train(
        self,
        df: pd.DataFrame,
        features_df: pd.DataFrame,
        force_retrain: bool = False
    ) -> bool:
        """
        Trains LSTM model on historical data.

        Args:
            df: Historical draws
            features_df: Engineered features
            force_retrain: Retrain even if cached model exists

        Returns:
            Success status
        """
        if not TENSORFLOW_AVAILABLE:
            print("❌ TensorFlow not available")
            return False

        # Check for cached model
        if not force_retrain and self.model_path and os.path.exists(self.model_path):
            print("   📦 Loading cached LSTM model...")
            try:
                self.model = load_model(self.model_path)
                self.is_trained = True

                # Load config
                if os.path.exists(self.config_path):
                    with open(self.config_path, 'r') as f:
                        self.training_history = json.load(f)

                print(f"   ✅ Loaded cached model")
                return True
            except Exception as e:
                print(f"   ⚠️  Failed to load cached model: {e}")
                print("   🔄 Training new model...")

        print(f"\n🧠 TRAINING DEEP LEARNING MODEL (LSTM)")
        print("=" * 70)

        # Prepare data
        X, y = self._prepare_sequences(df, features_df)

        if len(X) < 100:
            print("   ⚠️  Not enough data for training (need >100 sequences)")
            return False

        # Build model
        print(f"   Building LSTM architecture...")
        num_features = X.shape[2]
        self.model = self._build_model(num_features)

        # Show summary
        print(f"\n   📊 Model Summary:")
        total_params = self.model.count_params()
        print(f"   Total parameters: {total_params:,}")

        # Callbacks
        callbacks = [
            EarlyStopping(
                monitor='val_loss',
                patience=10,
                restore_best_weights=True,
                verbose=0
            ),
            ReduceLROnPlateau(
                monitor='val_loss',
                factor=0.5,
                patience=5,
                verbose=0
            )
        ]

        # Train
        print(f"\n   🚀 Training for {self.config['epochs']} epochs...")
        print(f"   Batch size: {self.config['batch_size']}")
        print(f"   Validation split: {self.config['validation_split']:.1%}")

        try:
            history = self.model.fit(
                X, y,
                batch_size=self.config['batch_size'],
                epochs=self.config['epochs'],
                validation_split=self.config['validation_split'],
                callbacks=callbacks,
                verbose=1
            )

            # Store training history
            self.training_history = {
                'trained_at': datetime.now().isoformat(),
                'num_samples': len(X),
                'num_features': num_features,
                'final_loss': float(history.history['loss'][-1]),
                'final_val_loss': float(history.history['val_loss'][-1]),
                'final_accuracy': float(history.history['accuracy'][-1]),
                'final_val_accuracy': float(history.history['val_accuracy'][-1]),
                'epochs_trained': len(history.history['loss'])
            }

            self.is_trained = True

            # Save model
            if self.model_path:
                print(f"\n   💾 Saving model to cache...")
                self.model.save(self.model_path)

                with open(self.config_path, 'w') as f:
                    json.dump(self.training_history, f, indent=2)

                print(f"   ✅ Model saved")

            # Print results
            print(f"\n   ✅ TRAINING COMPLETED")
            print(f"   Final Loss: {self.training_history['final_loss']:.4f}")
            print(f"   Final Val Loss: {self.training_history['final_val_loss']:.4f}")
            print(f"   Final Accuracy: {self.training_history['final_accuracy']:.4f}")
            print(f"   Epochs: {self.training_history['epochs_trained']}")

            return True

        except Exception as e:
            print(f"\n   ❌ Training failed: {e}")
            import traceback
            traceback.print_exc()
            return False

    def predict(
        self,
        recent_df: pd.DataFrame,
        recent_features: pd.DataFrame
    ) -> Dict[int, float]:
        """
        Predicts probabilities for each number.

        Args:
            recent_df: Recent draws (at least sequence_length)
            recent_features: Recent features

        Returns:
            {number: probability} for numbers 1-num_numbers
        """
        if not self.is_trained or self.model is None:
            print("⚠️  Model not trained, returning uniform distribution")
            return {i: 0.5 for i in range(1, self.num_numbers + 1)}

        # Prepare last sequence
        if len(recent_df) < self.sequence_length:
            print(f"⚠️  Not enough recent data (need {self.sequence_length}, got {len(recent_df)})")
            return {i: 0.5 for i in range(1, self.num_numbers + 1)}

        # Get last sequence
        recent_features_normalized = recent_features.copy()
        for col in recent_features_normalized.columns:
            min_val = recent_features_normalized[col].min()
            max_val = recent_features_normalized[col].max()
            if max_val > min_val:
                recent_features_normalized[col] = (recent_features_normalized[col] - min_val) / (max_val - min_val)
            else:
                recent_features_normalized[col] = 0.5

        sequence = recent_features_normalized.iloc[-self.sequence_length:].values
        X = np.array([sequence])  # Shape: (1, sequence_length, num_features)

        # Predict
        predictions = self.model.predict(X, verbose=0)[0]  # Shape: (num_numbers,)

        # Convert to dictionary
        result = {i + 1: float(predictions[i]) for i in range(self.num_numbers)}

        return result

    def get_model_info(self) -> Dict:
        """Returns model information."""
        return {
            'is_trained': self.is_trained,
            'tensorflow_available': TENSORFLOW_AVAILABLE,
            'num_numbers': self.num_numbers,
            'sequence_length': self.sequence_length,
            'config': self.config,
            'training_history': self.training_history,
            'model_exists': self.model is not None
        }


class HybridDeepLearningPredictor:
    """
    Kombiniert RandomForest + LSTM für robustere Vorhersagen.
    """

    def __init__(
        self,
        dl_engine: DeepLearningEngine,
        rf_weight: float = 0.4,
        dl_weight: float = 0.6
    ):
        """
        Args:
            dl_engine: Deep Learning Engine
            rf_weight: Weight for RandomForest predictions
            dl_weight: Weight for Deep Learning predictions
        """
        self.dl_engine = dl_engine
        self.rf_weight = rf_weight
        self.dl_weight = dl_weight

        print(f"🔀 Hybrid Predictor: RF={rf_weight:.1%} + DL={dl_weight:.1%}")

    def predict(
        self,
        rf_predictions: Dict[int, float],
        recent_df: pd.DataFrame,
        recent_features: pd.DataFrame
    ) -> Dict[int, float]:
        """
        Combines RandomForest and Deep Learning predictions.

        Args:
            rf_predictions: Predictions from RandomForest
            recent_df: Recent draws for DL
            recent_features: Recent features for DL

        Returns:
            Combined predictions
        """
        # Get DL predictions
        dl_predictions = self.dl_engine.predict(recent_df, recent_features)

        # Combine
        combined = {}
        for num in range(1, self.dl_engine.num_numbers + 1):
            rf_score = rf_predictions.get(num, 0.5)
            dl_score = dl_predictions.get(num, 0.5)

            combined[num] = (
                self.rf_weight * rf_score +
                self.dl_weight * dl_score
            )

        # Normalize to [0, 1]
        min_score = min(combined.values())
        max_score = max(combined.values())
        if max_score > min_score:
            combined = {
                num: (score - min_score) / (max_score - min_score)
                for num, score in combined.items()
            }

        return combined
