import numpy as np
import pandas as pd
import os
import gc
import warnings
from pathlib import Path
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import torch
import torch.nn as nn
from datetime import datetime

warnings.filterwarnings('ignore')

class CloudMLEOptimizer:
    def __init__(self):
        print("ARIEL SPACE CHALLENGE - MLE-STAR CLOUD EDITION")
        print(f"Started at: {datetime.now()}")
        self.detect_environment()
        
    def detect_environment(self):
        print("\nDETECTING KAGGLE CLOUD ENVIRONMENT:")
        
        # Check GPU availability
        self.gpu_available = torch.cuda.is_available()
        if self.gpu_available:
            gpu_name = torch.cuda.get_device_name(0)
            gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
            print(f"GPU: {gpu_name} ({gpu_memory:.1f}GB)")
        else:
            print("GPU: Not available (using CPU)")
        
        # Check CPU and memory
        import psutil
        cpu_count = psutil.cpu_count()
        memory_gb = psutil.virtual_memory().total / (1024**3)
        available_gb = psutil.virtual_memory().available / (1024**3)
        
        print(f"CPU: {cpu_count} cores")
        print(f"RAM: {memory_gb:.1f}GB total, {available_gb:.1f}GB available")
        print(f"Environment: Kaggle Cloud (Optimized for large dataset)")
        
        return {
            'gpu': self.gpu_available,
            'cpu_cores': cpu_count,
            'ram_gb': memory_gb,
            'available_gb': available_gb
        }

class AdvancedSpaceDataProcessor:
    def __init__(self):
        self.scalers = {}
        self.feature_names = []
        
    def load_data_efficiently(self, competition_path="/kaggle/input/ariel-data-challenge-2025"):
        print("\nLOADING ARIEL SPACE TELESCOPE DATA:")
        
        try:
            # Load training data
            train_path = f"{competition_path}/train.parquet"
            if os.path.exists(train_path):
                print("Loading training data (parquet format)...")
                train_df = pd.read_parquet(train_path)
            else:
                train_csv = f"{competition_path}/train.csv"
                print("Loading training data (CSV format)...")
                train_df = pd.read_csv(train_csv)
            
            print(f"Training data loaded: {train_df.shape[0]:,} rows, {train_df.shape[1]} columns")
            
            # Load test data
            test_path = f"{competition_path}/test.parquet"
            if os.path.exists(test_path):
                test_df = pd.read_parquet(test_path)
            else:
                test_df = pd.read_csv(f"{competition_path}/test.csv")
            
            print(f"Test data loaded: {test_df.shape[0]:,} rows, {test_df.shape[1]} columns")
            
            return train_df, test_df
            
        except Exception as e:
            print(f"Error loading data: {e}")
            return self.create_sample_data()
    
    def create_sample_data(self):
        print("Creating sample data for testing...")
        
        n_samples = 1000
        n_features = 20
        
        np.random.seed(42)
        X = np.random.normal(0, 1, (n_samples, n_features))
        y = np.sum(X[:, :5], axis=1) + 0.1 * np.random.normal(0, 1, n_samples)
        
        feature_names = [f'feature_{i}' for i in range(n_features)]
        train_df = pd.DataFrame(X, columns=feature_names)
        train_df['target'] = y
        
        X_test = np.random.normal(0, 1, (200, n_features))
        test_df = pd.DataFrame(X_test, columns=feature_names)
        test_df['id'] = range(len(test_df))
        
        print(f"Sample data created: {len(train_df)} training, {len(test_df)} test samples")
        return train_df, test_df
    
    def advanced_feature_engineering(self, df):
        print("\nADVANCED FEATURE ENGINEERING:")
        
        df_processed = df.copy()
        
        # Get numeric columns only
        numeric_cols = df_processed.select_dtypes(include=[np.number]).columns.tolist()
        
        # Remove target and id columns from feature engineering
        feature_cols = [col for col in numeric_cols if col not in ['target', 'id']]
        
        if len(feature_cols) > 1:
            # Add statistical features
            df_processed['mean_all'] = df_processed[feature_cols].mean(axis=1)
            df_processed['std_all'] = df_processed[feature_cols].std(axis=1)
            df_processed['max_all'] = df_processed[feature_cols].max(axis=1)
            df_processed['min_all'] = df_processed[feature_cols].min(axis=1)
            
            print(f"Added 4 statistical features")
        
        print(f"Feature engineering complete: {len(df.columns)} -> {len(df_processed.columns)} features")
        
        return df_processed

class GPUOptimizedEnsemble:
    def __init__(self, use_gpu=True):
        self.use_gpu = use_gpu and torch.cuda.is_available()
        self.models = {}
        self.scalers = {}
        self.device = torch.device('cuda' if self.use_gpu else 'cpu')
        print(f"Ensemble initialized on: {self.device}")
    
    def create_neural_network(self, input_size):
        class SpaceNet(nn.Module):
            def __init__(self, input_size):
                super().__init__()
                self.layers = nn.Sequential(
                    nn.Linear(input_size, 512),
                    nn.ReLU(),
                    nn.Dropout(0.3),
                    nn.Linear(512, 256),
                    nn.ReLU(),
                    nn.Dropout(0.3),
                    nn.Linear(256, 128),
                    nn.ReLU(),
                    nn.Dropout(0.2),
                    nn.Linear(128, 1)
                )
            
            def forward(self, x):
                return self.layers(x)
        
        return SpaceNet(input_size).to(self.device)
    
    def train_ensemble(self, X_train, y_train, X_val=None, y_val=None):
        print("\nTRAINING ENSEMBLE MODELS:")
        
        # Prepare data
        if X_val is None:
            X_train, X_val, y_train, y_val = train_test_split(
                X_train, y_train, test_size=0.2, random_state=42
            )
        
        # Scale data
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)
        X_val_scaled = scaler.transform(X_val)
        self.scalers['standard'] = scaler
        
        # 1. Random Forest
        print("Training Random Forest...")
        rf_model = RandomForestRegressor(
            n_estimators=200,
            max_depth=15,
            random_state=42,
            n_jobs=-1
        )
        rf_model.fit(X_train_scaled, y_train)
        rf_pred = rf_model.predict(X_val_scaled)
        rf_score = mean_squared_error(y_val, rf_pred, squared=False)
        self.models['random_forest'] = rf_model
        print(f"Random Forest RMSE: {rf_score:.6f}")
        
        # 2. Gradient Boosting
        print("Training Gradient Boosting...")
        gb_model = GradientBoostingRegressor(
            n_estimators=200,
            max_depth=8,
            learning_rate=0.1,
            random_state=42
        )
        gb_model.fit(X_train_scaled, y_train)
        gb_pred = gb_model.predict(X_val_scaled)
        gb_score = mean_squared_error(y_val, gb_pred, squared=False)
        self.models['gradient_boost'] = gb_model
        print(f"Gradient Boosting RMSE: {gb_score:.6f}")
        
        # 3. Neural Network
        if self.use_gpu:
            print("Training GPU-Optimized Neural Network...")
            neural_net = self.create_neural_network(X_train_scaled.shape[1])
            
            X_train_tensor = torch.FloatTensor(X_train_scaled).to(self.device)
            y_train_tensor = torch.FloatTensor(y_train.values if hasattr(y_train, 'values') else y_train).to(self.device)
            
            criterion = nn.MSELoss()
            optimizer = torch.optim.Adam(neural_net.parameters(), lr=0.001)
            
            neural_net.train()
            best_loss = float('inf')
            
            for epoch in range(50):
                optimizer.zero_grad()
                outputs = neural_net(X_train_tensor).squeeze()
                loss = criterion(outputs, y_train_tensor)
                loss.backward()
                optimizer.step()
                
                if epoch % 10 == 0:
                    print(f"Epoch {epoch}: Loss = {loss.item():.6f}")
                
                if loss.item() < best_loss:
                    best_loss = loss.item()
            
            self.models['neural_network'] = neural_net
            print(f"Neural Network final loss: {best_loss:.6f}")
        
        else:
            print("Training CPU Neural Network...")
            mlp_model = MLPRegressor(
                hidden_layer_sizes=(256, 128),
                max_iter=200,
                random_state=42
            )
            mlp_model.fit(X_train_scaled, y_train)
            mlp_pred = mlp_model.predict(X_val_scaled)
            mlp_score = mean_squared_error(y_val, mlp_pred, squared=False)
            self.models['neural_network'] = mlp_model
            print(f"MLP Network RMSE: {mlp_score:.6f}")
        
        print(f"\nENSEMBLE TRAINING COMPLETE - {len(self.models)} models trained")
    
    def predict(self, X_test):
        print("\nGENERATING ENSEMBLE PREDICTIONS:")
        
        X_test_scaled = self.scalers['standard'].transform(X_test)
        
        predictions = {}
        weights = {'random_forest': 0.3, 'gradient_boost': 0.4, 'neural_network': 0.3}
        
        for name, model in self.models.items():
            if name == 'neural_network' and self.use_gpu:
                model.eval()
                with torch.no_grad():
                    X_tensor = torch.FloatTensor(X_test_scaled).to(self.device)
                    pred = model(X_tensor).squeeze().cpu().numpy()
            else:
                pred = model.predict(X_test_scaled)
            
            predictions[name] = pred
        
        # Weighted ensemble prediction
        final_predictions = np.zeros(len(X_test))
        for name, pred in predictions.items():
            final_predictions += pred * weights[name]
        
        print(f"Ensemble predictions generated for {len(X_test):,} samples")
        
        return final_predictions

def main():
    print("="*80)
    print("ARIEL SPACE CHALLENGE 2025 - AUTOMATED EXECUTION")
    print("="*80)
    
    # Initialize components
    cloud_optimizer = CloudMLEOptimizer()
    processor = AdvancedSpaceDataProcessor()
    
    # Load data
    print("\n" + "="*60)
    print("DATA LOADING PHASE")
    print("="*60)
    
    train_df, test_df = processor.load_data_efficiently()
    
    # Feature engineering
    print("\n" + "="*60)
    print("FEATURE ENGINEERING PHASE")
    print("="*60)
    
    train_processed = processor.advanced_feature_engineering(train_df)
    test_processed = processor.advanced_feature_engineering(test_df)
    
    # Prepare features and target
    target_col = 'target'
    id_col = 'id'
    
    feature_cols = [col for col in train_processed.columns if col not in [target_col, id_col]]
    
    X_train = train_processed[feature_cols]
    y_train = train_processed[target_col] if target_col in train_processed.columns else train_processed.iloc[:, -1]
    X_test = test_processed[feature_cols] if feature_cols else test_processed.drop(columns=[col for col in [id_col] if col in test_processed.columns])
    
    print(f"Final feature set: {len(feature_cols)} features")
    print(f"Training samples: {len(X_train):,}")
    print(f"Test samples: {len(X_test):,}")
    
    # Model training
    print("\n" + "="*60)
    print("MODEL TRAINING PHASE")
    print("="*60)
    
    ensemble = GPUOptimizedEnsemble(use_gpu=True)
    ensemble.train_ensemble(X_train, y_train)
    
    # Generate predictions
    print("\n" + "="*60)
    print("PREDICTION PHASE")
    print("="*60)
    
    predictions = ensemble.predict(X_test)
    
    # Create submission
    print("\n" + "="*60)
    print("SUBMISSION CREATION")
    print("="*60)
    
    if id_col in test_df.columns:
        submission_df = pd.DataFrame({
            'id': test_df[id_col],
            'target': predictions
        })
    else:
        submission_df = pd.DataFrame({
            'id': range(len(predictions)),
            'target': predictions
        })
    
    # Save submission
    submission_df.to_csv('submission.csv', index=False)
    
    print(f"Submission created: {len(submission_df):,} predictions")
    print(f"Prediction stats:")
    print(f"   Mean: {predictions.mean():.6f}")
    print(f"   Std:  {predictions.std():.6f}")
    print(f"   Min:  {predictions.min():.6f}")
    print(f"   Max:  {predictions.max():.6f}")
    
    # Save training log
    with open('training_log.txt', 'w') as f:
        f.write(f"ARIEL SPACE CHALLENGE 2025 - TRAINING LOG\n")
        f.write(f"Completed at: {datetime.now()}\n")
        f.write(f"Training samples: {len(X_train):,}\n")
        f.write(f"Test samples: {len(X_test):,}\n")
        f.write(f"Features: {len(feature_cols)}\n")
        f.write(f"Models trained: {len(ensemble.models)}\n")
        f.write(f"GPU used: {ensemble.use_gpu}\n")
        f.write(f"Prediction mean: {predictions.mean():.6f}\n")
        f.write(f"Prediction std: {predictions.std():.6f}\n")
    
    print("\n" + "="*80)
    print("ARIEL SPACE CHALLENGE AUTOMATION COMPLETE!")
    print(f"Files created: submission.csv, training_log.txt")
    print(f"Completed at: {datetime.now()}")
    print("Ready for competition submission!")
    print("="*80)
    
    return submission_df

if __name__ == "__main__":
    submission = main()
    
    print(f"\nFINAL SUBMISSION READY:")
    print(f"   Rows: {len(submission):,}")
    print(f"   Columns: {list(submission.columns)}")
    print(f"   File: submission.csv")
    
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
    
    print("Cloud execution complete - all files saved!")
