"""
MHA Flow v2.0.8 - Production Flask Application
Complete system with authentication, visualization, and advanced features
"""
import os
import json
import time
import numpy as np
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from werkzeug.utils import secure_filename
from functools import wraps
from datetime import datetime, timedelta

# Load environment variables from .env file
from dotenv import load_dotenv
load_dotenv()

# Import configuration
from config import get_config

# Import SQL models
from models import db, User, OptimizationResult, CustomAlgorithm, ComparisonSession, SystemSettings

# Import database helpers
from db_helpers import (
    get_user_by_id, get_user_by_username, get_user_by_email, count_users,
    save_object, delete_object, rollback, get_optimization_result_by_id,
    get_user_optimization_results, get_user_custom_algorithms,
    count_user_optimizations, count_user_custom_algos, count_user_comparisons,
    get_best_result, count_successful_runs, get_all_completed_results, get_user_id_for_mongodb
)

# Import MHA components
from mha_toolbox.complete_algorithm_registry_v2 import AlgorithmRegistry
from mha_toolbox.custom_algorithm_manager import CustomAlgorithmManager
from mha_toolbox.algorithm_recommender import AlgorithmRecommender
from mha_toolbox.hyperparameter_tuning import HyperparameterTuner

# Import objective functions
from objective_functions.standard_functions import OBJECTIVE_FUNCTIONS

def load_algorithm_class(algorithm_name):
    """Dynamically load algorithm class from file"""
    try:
        # Get algorithm info from registry
        algo_info = registry.get_algorithm(algorithm_name)
        if not algo_info:
            return None
        
        # Get the file name (e.g., 'pso.py' -> 'pso')
        file_name = algo_info.get('file', '').replace('.py', '')
        if not file_name:
            return None
        
        # Determine module path from file name
        # Registry may store paths like 'hybrid/abc_de_hybrid' — normalize to module path
        if '/' in file_name:
            # Convert path separators to module dots: 'hybrid/abc_de_hybrid' -> 'hybrid.abc_de_hybrid'
            module_path = f'mha_toolbox.algorithms.{file_name.replace("/", ".")}'
        else:
            # Try direct path first, fall back to hybrid subfolder
            module_path = f'mha_toolbox.algorithms.{file_name}'
        
        # Dynamic import
        import importlib
        import inspect
        
        try:
            module = importlib.import_module(module_path)
        except ModuleNotFoundError:
            # If direct import fails and it's a hybrid, try hybrid subfolder
            category = algo_info.get('category', '')
            if category == 'Hybrid' or 'hybrid' in file_name.lower():
                module_path = f'mha_toolbox.algorithms.hybrid.{file_name}'
                module = importlib.import_module(module_path)
            else:
                raise
        
        # Get BaseOptimizer class for comparison
        from mha_toolbox.base import BaseOptimizer
        
        # Find classes that inherit from BaseOptimizer (but not BaseOptimizer itself)
        algorithm_classes = []
        for name, obj in inspect.getmembers(module, inspect.isclass):
            if obj != BaseOptimizer and issubclass(obj, BaseOptimizer):
                algorithm_classes.append(obj)
        
        # Return the first valid algorithm class found
        if algorithm_classes:
            return algorithm_classes[0]
        
        return None
        
    except Exception as e:
        return None


def run_algorithm(algorithm_class, objective_func, population_size, max_iterations,
                  dimensions, lower_bound, upper_bound):
    """
    Instantiate and run an algorithm, handling both new-style and legacy patterns.
    
    Returns (result_model_or_tuple, execution_time) where result_model has
    .best_solution_, .best_fitness_, .global_fitness_ attributes.
    """
    import time as _time
    
    # Try standard BaseOptimizer pattern first
    try:
        algorithm = algorithm_class(
            population_size=population_size,
            max_iterations=max_iterations
        )
        algorithm.dimensions_ = dimensions
        algorithm.lower_bound_ = lower_bound
        algorithm.upper_bound_ = upper_bound
        
        start_time = _time.time()
        result_model = algorithm.optimize(objective_function=objective_func)
        execution_time = _time.time() - start_time
        return result_model, execution_time
    except TypeError:
        pass
    
    # Fallback: legacy algorithms that need objective_function/dimensions/bounds in __init__
    try:
        bounds = np.array([[lower_bound, upper_bound]] * dimensions)
        algorithm = algorithm_class(
            objective_function=objective_func,
            dimensions=dimensions,
            bounds=bounds,
            population_size=population_size,
            max_iterations=max_iterations
        )
        start_time = _time.time()
        result = algorithm.optimize()  # Legacy optimize() takes no args
        execution_time = _time.time() - start_time
        
        # Wrap 3-tuple return into a model-like object
        if isinstance(result, tuple):
            from types import SimpleNamespace
            best_sol, best_fit, conv = result[0], result[1], result[2] if len(result) > 2 else []
            result_model = SimpleNamespace(
                best_solution_=np.array(best_sol),
                best_fitness_=float(best_fit),
                global_fitness_=conv if conv else [],
                execution_time_=execution_time
            )
            return result_model, execution_time
        return result, execution_time
    except Exception:
        raise

# Get configuration based on environment
env = os.environ.get('FLASK_ENV', 'development')
config_obj = get_config(env)

app = Flask(__name__)
app.config.from_object(config_obj)

# SQLite database initialization
app.config['SQLALCHEMY_DATABASE_URI'] = config_obj.get_database_uri()
print("✅ Using SQLite Database")

# Initialize SQL database
db.init_app(app)

# Initialize Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message = 'Please log in to access this page.'

# Initialize MHA components
registry = AlgorithmRegistry()
custom_manager = CustomAlgorithmManager()
recommender = AlgorithmRecommender()

# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)


@login_manager.user_loader
def load_user(user_id):
    """Load user by ID"""
    return get_user_by_id(user_id)


def admin_required(f):
    """Decorator for admin-only routes"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.is_admin:
            flash('Admin access required', 'error')
            return redirect(url_for('index'))
        return f(*args, **kwargs)
    return decorated_function


# ============================================================================
# STATIC ROUTES
# ============================================================================

@app.route('/favicon.ico')
def favicon():
    """Serve favicon to prevent 404 errors"""
    return '', 204  # No Content response

# ============================================================================
# AUTHENTICATION ROUTES
# ============================================================================

@app.route('/register', methods=['GET', 'POST'])
def register():
    """User registration"""
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))
    
    if request.method == 'POST':
        data = request.form if request.form else request.json
        username = data.get('username', '').strip()
        email = data.get('email', '').strip()
        password = data.get('password', '')
        
        # Validation
        if not username or not email or not password:
            return jsonify({'error': 'All fields are required'}), 400
        
        if len(password) < 6:
            return jsonify({'error': 'Password must be at least 6 characters'}), 400
        
        # Check if user exists
        if get_user_by_username(username):
            return jsonify({'error': 'Username already exists'}), 400
        
        if get_user_by_email(email):
            return jsonify({'error': 'Email already registered'}), 400
        
        # Create user
        user = User(username=username, email=email)
        user.set_password(password)
        
        # First user becomes admin
        if count_users() == 0:
            user.is_admin = True
        
        save_object(user)
        
        login_user(user)
        flash('Registration successful!', 'success')
        
        if request.is_json:
            return jsonify({'message': 'Registration successful', 'redirect': url_for('dashboard')}), 201
        return redirect(url_for('dashboard'))
    
    return render_template('register.html')


@app.route('/login', methods=['GET', 'POST'])
def login():
    """User login"""
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))
    
    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '')
        remember = request.form.get('remember', type=bool)
        
        user = get_user_by_username(username)
        
        if user and user.check_password(password):
            login_user(user, remember=remember)
            user.update_last_login()
            
            flash(f'Welcome back, {user.username}!', 'success')
            
            next_page = request.args.get('next')
            return redirect(next_page or url_for('dashboard'))
        
        flash('Invalid username or password', 'error')
    
    return render_template('login.html')


@app.route('/api/auth/login', methods=['POST'])
def api_login():
    """API endpoint for login (matches frontend expectations)"""
    try:
        data = request.get_json()
        username = data.get('username', '').strip()
        password = data.get('password', '')
        
        if not username or not password:
            return jsonify({'error': 'Username and password are required'}), 400
        
        user = get_user_by_username(username)
        
        if user and user.check_password(password):
            # Make session permanent before login
            session.permanent = True
            login_user(user, remember=True)
            user.update_last_login()
            
            # Debug: Check session
            
            # Return redirect URL and set delay for cookie
            return jsonify({
                'success': True,
                'message': 'Login successful',
                'username': username,
                'redirect': '/dashboard'
            }), 200
        
        return jsonify({'error': 'Invalid username or password'}), 401
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/auth/register', methods=['POST'])
def api_register():
    """API endpoint for registration"""
    try:
        data = request.get_json()
        username = data.get('username', '').strip()
        email = data.get('email', '').strip()
        password = data.get('password', '')
        
        # Validation
        if not username or not email or not password:
            return jsonify({'error': 'All fields are required'}), 400
        
        if len(username) < 3:
            return jsonify({'error': 'Username must be at least 3 characters'}), 400
        
        if len(password) < 6:
            return jsonify({'error': 'Password must be at least 6 characters'}), 400
        
        # Check if user exists
        if get_user_by_username(username):
            return jsonify({'error': 'Username already exists'}), 409
        
        if get_user_by_email(email):
            return jsonify({'error': 'Email already registered'}), 409
        
        # Create user
        user = User(username=username, email=email, is_active=True)
        user.set_password(password)
        
        save_object(user)
        
        login_user(user)
        
        return jsonify({
            'message': 'Registration successful',
            'user': {
                'username': user.username,
                'email': user.email
            }
        }), 201
        
    except Exception as e:
        rollback()
        return jsonify({'error': 'Registration failed. Please try again.'}), 500


@app.route('/logout')
@login_required
def logout():
    """User logout"""
    logout_user()
    flash('You have been logged out', 'info')
    return redirect(url_for('index'))


# ============================================================================
# MAIN ROUTES
# ============================================================================

@app.route('/')
def index():
    """Landing page"""
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))
    
    return render_template('landing.html')


@app.route('/dashboard')
@login_required
def dashboard():
    """User dashboard"""
    
    # Get user ID (integer)
    user_id = current_user.id
    
    # Get user's recent results
    recent_results = get_user_optimization_results(user_id, limit=5)
    
    # Get user's custom algorithms
    custom_algos = get_user_custom_algorithms(user_id, limit=5)
    
    # Statistics
    total_optimizations = count_user_optimizations(user_id)
    total_custom_algos = count_user_custom_algos(user_id)
    
    # Best result
    best_result = get_best_result(user_id)
    
    # Calculate success rate
    successful_runs = count_successful_runs(user_id)
    
    # Calculate average improvement (if results exist)
    avg_improvement = 0
    if total_optimizations > 0:
        results = get_all_completed_results(user_id)
        if results:
            improvements = []
            for r in results:
                if r.best_fitness and r.best_fitness < 1000:  # Filter outliers
                    improvement = abs((1.0 - r.best_fitness) * 100)
                    improvements.append(min(improvement, 100))
            if improvements:
                avg_improvement = sum(improvements) / len(improvements)
    
    # Algorithm category counts
    try:
        swarm_algos = registry.get_algorithms_by_category('swarm_intelligence')
        evolutionary_algos = registry.get_algorithms_by_category('evolutionary')
        physics_algos = registry.get_algorithms_by_category('physics_based')
        hybrid_algos = registry.get_algorithms_by_category('hybrid')
        
        swarm_count = len(swarm_algos) if swarm_algos else 0
        evolutionary_count = len(evolutionary_algos) if evolutionary_algos else 0
        physics_count = len(physics_algos) if physics_algos else 0
        hybrid_count = len(hybrid_algos) if hybrid_algos else 0
    except:
        # Fallback counts
        swarm_count = 15
        evolutionary_count = 8
        physics_count = 5
        hybrid_count = 3
    
    stats = {
        'total_algorithms': registry.get_total_count(),
        'user_experiments': total_optimizations,
        'successful_runs': successful_runs,
        'avg_improvement': int(avg_improvement),
        'swarm_count': swarm_count,
        'evolutionary_count': evolutionary_count,
        'physics_count': physics_count,
        'hybrid_count': hybrid_count
    }
    
    return render_template('dashboard.html',
                         recent_results=recent_results,
                         custom_algos=custom_algos,
                         total_optimizations=total_optimizations,
                         total_custom_algos=total_custom_algos,
                         best_result=best_result,
                         stats=stats)


@app.route('/profile')
@login_required
def profile():
    """User profile page"""
    user_id = get_user_id_for_mongodb(current_user.id)
    
    # Get stats
    stats = {
        'total_optimizations': count_user_optimizations(user_id),
        'custom_algorithms': count_user_custom_algos(user_id),
        'total_comparisons': count_user_comparisons(user_id)
    }
    
    # Get recent optimization results (last 5)
    recent_results = get_user_optimization_results(user_id, limit=5)
    
    # Get algorithm submissions
    submissions = get_user_custom_algorithms(user_id)
    
    return render_template('profile.html',
                         stats=stats,
                         recent_results=recent_results,
                         submissions=submissions)


@app.route('/profile/update', methods=['POST'])
@login_required
def update_profile():
    """Update user profile"""
    email = request.form.get('email')
    new_password = request.form.get('new_password')
    confirm_password = request.form.get('confirm_password')
    
    try:
        if email and email != current_user.email:
            # Check if email already exists
            existing = get_user_by_email(email)
            if existing and existing.id != current_user.id:
                flash('Email already in use', 'error')
                return redirect(url_for('profile'))
            current_user.email = email
        
        if new_password:
            if new_password != confirm_password:
                flash('Passwords do not match', 'error')
                return redirect(url_for('profile'))
            if len(new_password) < 6:
                flash('Password must be at least 6 characters', 'error')
                return redirect(url_for('profile'))
            current_user.set_password(new_password)
        
        save_object(current_user)
        flash('Profile updated successfully!', 'success')
        
    except Exception as e:
        flash(f'Error updating profile: {str(e)}', 'error')
    
    return redirect(url_for('profile'))


@app.route('/optimize')
@login_required
def optimize_page():
    """Optimization interface"""
    # Get algorithm names only (not full dict objects)
    all_algos = registry.list_algorithms()
    algorithm_names = [algo['code'] if isinstance(algo, dict) else str(algo) for algo in all_algos]
    objective_funcs = list(OBJECTIVE_FUNCTIONS.keys())
    
    return render_template('optimize.html',
                         algorithms=algorithm_names,
                         objective_functions=objective_funcs)


@app.route('/compare')
@login_required
def compare_page():
    """Algorithm comparison interface"""
    # Get algorithm names only
    all_algos = registry.list_algorithms()
    algorithm_names = [algo['code'] if isinstance(algo, dict) else str(algo) for algo in all_algos]
    objective_funcs = list(OBJECTIVE_FUNCTIONS.keys())
    
    return render_template('compare.html',
                         algorithms=algorithm_names,
                         objective_functions=objective_funcs)


@app.route('/upload-algorithm')
@login_required
def upload_algorithm_page():
    """Custom algorithm upload interface"""
    return render_template('upload_algorithm.html')


@app.route('/dataset-upload')
@login_required
def dataset_upload_page():
    """Dataset upload interface"""
    return render_template('dataset_upload.html')


@app.route('/algorithm-recommend')
@login_required
def algorithm_recommend_page():
    """Algorithm recommendation interface"""
    return render_template('algorithm_recommend.html')


@app.route('/levify')
@login_required
def levify_page():
    """Levy Flight transformation interface"""
    return render_template('levify.html')


@app.route('/settings')
@login_required
def settings_page():
    """User settings"""
    return render_template('settings.html')


@app.route('/results')
@login_required
def results():
    """View comprehensive optimization results"""
    return render_template('results.html')


@app.route('/results/<int:result_id>')
@login_required
def view_result(result_id):
    """View detailed optimization result"""
    result = OptimizationResult.query.get_or_404(result_id)
    
    # Check if user owns this result
    if result.user_id != current_user.id and not current_user.is_admin:
        flash('Access denied', 'error')
        return redirect(url_for('dashboard'))
    
    return render_template('result_detail.html', result=result)


@app.route('/history')
@login_required
def history():
    """View optimization history"""
    page = request.args.get('page', 1, type=int)
    per_page = 20
    
    results = OptimizationResult.query.filter_by(user_id=current_user.id)\
        .order_by(OptimizationResult.created_at.desc())\
        .paginate(page=page, per_page=per_page, error_out=False)
    
    return render_template('history.html', results=results)


@app.route('/api/history', methods=['GET'])
@login_required
def api_get_history():
    """Get optimization history for current user"""
    results = OptimizationResult.query.filter_by(user_id=current_user.id)\
        .order_by(OptimizationResult.created_at.desc())\
        .all()
    
    history_data = []
    for result in results:
        history_data.append({
            'id': result.id,
            'algorithm': result.algorithm_name,
            'objective_function': result.objective_function,
            'best_fitness': result.best_fitness,
            'execution_time': result.execution_time,
            'population_size': result.population_size,
            'max_iterations': result.max_iterations,
            'dimensions': result.dimensions,
            'lower_bound': result.lower_bound,
            'upper_bound': result.upper_bound,
            'status': result.status,
            'created_at': result.created_at.isoformat() if result.created_at else None,
            'convergence_curve': result.get_convergence_data(),
            'best_solution': result.get_best_position()
        })
    
    return jsonify({'history': history_data}), 200


@app.route('/api/history/<int:result_id>', methods=['DELETE'])
@login_required
def api_delete_result(result_id):
    """Delete an optimization result"""
    result = get_optimization_result_by_id(result_id)
    if not result:
        return jsonify({'error': 'Result not found'}), 404
    
    # Check if user owns this result
    user_id = current_user.id
    if result.user_id != user_id and not current_user.is_admin:
        return jsonify({'error': 'Access denied'}), 403
    
    delete_object(result)
    
    return jsonify({'message': 'Result deleted successfully'}), 200


@app.route('/api/history/<int:result_id>/rerun', methods=['POST'])
@login_required
def api_rerun_result(result_id):
    """Rerun an optimization with same parameters"""
    original_result = OptimizationResult.query.get_or_404(result_id)
    
    # Check if user owns this result
    if original_result.user_id != current_user.id and not current_user.is_admin:
        return jsonify({'error': 'Access denied'}), 403
    
    # Return the parameters to re-run
    return jsonify({
        'algorithm': original_result.algorithm_name,
        'objective_function': original_result.objective_function,
        'population_size': original_result.population_size,
        'max_iterations': original_result.max_iterations,
        'dimensions': original_result.dimensions,
        'lower_bound': original_result.lower_bound,
        'upper_bound': original_result.upper_bound
    }), 200


# ============================================================================
# API ROUTES
# ============================================================================

@app.route('/api/generate-dataset', methods=['POST'])
@login_required
def api_generate_dataset():
    """Generate random dataset for testing"""
    data = request.json
    
    try:
        from sklearn.datasets import make_classification, make_regression
        import pandas as pd
        import io
        
        n_samples = int(data.get('n_samples', 1000))
        n_features = int(data.get('n_features', 10))
        problem_type = data.get('problem_type', 'classification')
        n_classes = int(data.get('n_classes', 2))
        noise = float(data.get('noise', 0.1))
        
        # Validate inputs
        if n_samples < 10 or n_samples > 100000:
            return jsonify({'error': 'Samples must be between 10 and 100,000'}), 400
        if n_features < 2 or n_features > 1000:
            return jsonify({'error': 'Features must be between 2 and 1,000'}), 400
        
        # Generate dataset
        if problem_type == 'classification':
            X, y = make_classification(
                n_samples=n_samples,
                n_features=n_features,
                n_classes=n_classes,
                n_informative=max(2, n_features // 2),
                n_redundant=min(2, n_features // 4),
                flip_y=noise,
                random_state=42
            )
        else:  # regression
            X, y = make_regression(
                n_samples=n_samples,
                n_features=n_features,
                noise=noise * 10,
                random_state=42
            )
        
        # Create DataFrame
        feature_names = [f'feature_{i+1}' for i in range(n_features)]
        df = pd.DataFrame(X, columns=feature_names)
        df['target'] = y
        
        # Convert to CSV
        csv_buffer = io.StringIO()
        df.to_csv(csv_buffer, index=False)
        csv_data = csv_buffer.getvalue()
        
        return jsonify({
            'csv_data': csv_data,
            'dataset_info': {
                'samples': n_samples,
                'features': n_features,
                'type': problem_type,
                'classes': n_classes if problem_type == 'classification' else 'N/A',
                'size_kb': len(csv_data) / 1024
            },
            'message': f'Generated {n_samples} samples with {n_features} features'
        }), 200
        
    except Exception as e:
        return jsonify({'error': f'Generation failed: {str(e)}'}), 500


@app.route('/api/algorithms', methods=['GET'])
@login_required
def api_get_algorithms():
    """Get list of all available algorithms organized by category"""
    category = request.args.get('category')
    
    if category:
        algorithms = registry.get_algorithms_by_category(category)
        return jsonify({'algorithms': algorithms}), 200
    else:
        # Return all algorithms organized by category
        categories = registry.get_categories()
        algorithms_by_category = {}
        for cat in categories:
            algorithms_by_category[cat] = registry.get_algorithms_by_category(cat)
        
        return jsonify({
            'categories': categories,
            'algorithms_by_category': algorithms_by_category,
            'total': len(registry.algorithms)
        }), 200


@app.route('/api/algorithm/<algorithm_name>', methods=['GET'])
@login_required
def api_get_algorithm_info(algorithm_name):
    """Get detailed algorithm information"""
    info = registry.get_algorithm_info(algorithm_name)
    
    if info:
        return jsonify(info), 200
    return jsonify({'error': 'Algorithm not found'}), 404


@app.route('/api/recommend', methods=['POST'])
@login_required
def api_recommend_algorithm():
    """Recommend algorithms based on dataset analysis or problem characteristics"""
    data = request.json
    
    # Check if dataset is provided for intelligent analysis
    if 'dataset_name' in data:
        dataset_name = data.get('dataset_name')
        task_type = data.get('task_type', 'optimization')  # optimization, feature_selection, hyperparameter
        
        # Load sample dataset for analysis
        try:
            from sklearn import datasets
            if dataset_name == 'breast_cancer':
                dataset = datasets.load_breast_cancer()
                X, y = dataset.data, dataset.target
            elif dataset_name == 'wine':
                dataset = datasets.load_wine()
                X, y = dataset.data, dataset.target
            elif dataset_name == 'iris':
                dataset = datasets.load_iris()
                X, y = dataset.data, dataset.target
            elif dataset_name == 'digits':
                dataset = datasets.load_digits()
                X, y = dataset.data, dataset.target
            elif dataset_name == 'california_housing':
                dataset = datasets.fetch_california_housing()
                X, y = dataset.data, dataset.target
            elif dataset_name == 'diabetes':
                dataset = datasets.load_diabetes()
                X, y = dataset.data, dataset.target
            else:
                return jsonify({'error': 'Unknown dataset'}), 400
            
            # Analyze dataset characteristics
            n_samples, n_features = X.shape
            dimensionality = 'low' if n_features < 10 else 'medium' if n_features < 50 else 'high'
            complexity = 'simple' if n_samples < 500 else 'medium' if n_samples < 2000 else 'complex'
            
            # Get intelligent recommendations based on task type
            if task_type == 'feature_selection':
                # Algorithms good for feature selection
                recommended = ['GWO', 'PSO', 'GA', 'WOA', 'ALO', 'SSA', 'DE', 'HHO', 'JS', 'RSA']
                reason = f'Feature selection for {n_features}-dimensional dataset with {n_samples} samples'
            elif task_type == 'hyperparameter':
                # Algorithms good for hyperparameter tuning
                recommended = ['PSO', 'GA', 'DE', 'GWO', 'WOA', 'BO', 'TPE', 'SA', 'HS', 'ES']
                reason = f'Hyperparameter optimization for ML model'
            else:
                # General optimization based on dataset characteristics
                if dimensionality == 'high':
                    recommended = ['PSO', 'DE', 'GWO', 'GA', 'WOA', 'ALO', 'JS', 'HHO', 'ChOA', 'GTO']
                    reason = f'High-dimensional optimization ({n_features} features)'
                elif complexity == 'complex':
                    recommended = ['GWO', 'WOA', 'ALO', 'PSO', 'SSA', 'HHO', 'JS', 'DE', 'GA', 'RSA']
                    reason = f'Complex optimization problem ({n_samples} samples)'
                else:
                    recommended = ['PSO', 'GWO', 'WOA', 'GA', 'DE', 'ALO', 'SSA', 'HHO', 'JS', 'RSA']
                    reason = f'General optimization ({dimensionality} dimensionality)'
            
            return jsonify({
                'recommendations': recommended[:10],
                'dataset_analysis': {
                    'samples': n_samples,
                    'features': n_features,
                    'dimensionality': dimensionality,
                    'complexity': complexity,
                    'task_type': task_type
                },
                'reason': reason
            }), 200
            
        except Exception as e:
            return jsonify({'error': f'Dataset analysis failed: {str(e)}'}), 500
    
    # Fallback to manual problem characteristics
    problem_type = data.get('problem_type', 'continuous')
    dimensions = data.get('dimensions', 10)
    has_constraints = data.get('has_constraints', False)
    multimodal = data.get('multimodal', True)
    
    recommendations = recommender.recommend(
        problem_type=problem_type,
        dimensions=dimensions,
        has_constraints=has_constraints,
        multimodal=multimodal
    )
    
    return jsonify({'recommendations': recommendations}), 200


@app.route('/api/optimize', methods=['POST'])
@login_required
def api_optimize():
    """Run optimization with multiple algorithms"""
    data = request.json
    
    try:
        # Extract parameters - handle both single algorithm and multiple
        algorithms = data.get('algorithms', [data.get('algorithm')]) if data.get('algorithms') else [data.get('algorithm')]
        if not algorithms or not algorithms[0]:
            return jsonify({'error': 'No algorithms selected'}), 400
            
        objective_func_name = data.get('objective_function')
        population_size = data.get('population_size', 30)
        max_iterations = data.get('max_iterations', 100)
        dimensions = data.get('dimensions', 10)
        lower_bound = data.get('lower_bound', -10.0)
        upper_bound = data.get('upper_bound', 10.0)
        
        # Validate
        if not objective_func_name:
            return jsonify({'error': 'Missing objective function'}), 400
        
        # Get objective function
        if objective_func_name not in OBJECTIVE_FUNCTIONS:
            return jsonify({'error': 'Invalid objective function'}), 400
        
        objective_func = OBJECTIVE_FUNCTIONS[objective_func_name]
        
        # Run all selected algorithms
        results = []
        all_convergences = []
        
        for algorithm_name in algorithms:
            try:
                # Get algorithm class using dynamic loader
                algorithm_class = load_algorithm_class(algorithm_name)
                if not algorithm_class:
                    results.append({'algorithm': algorithm_name, 'error': 'Algorithm class not found'})
                    continue
                
                # Run algorithm (handles both new and legacy patterns)
                result_model, execution_time = run_algorithm(
                    algorithm_class, objective_func,
                    population_size, max_iterations,
                    dimensions, lower_bound, upper_bound
                )
                
                # Extract results from the model
                best_position = result_model.best_solution_
                best_fitness = result_model.best_fitness_
                convergence = result_model.global_fitness_
                
                # Save result to database
                result = OptimizationResult(
                    user_id=current_user.id,
                    algorithm_name=algorithm_name,
                    objective_function=objective_func_name,
                    population_size=population_size,
                    max_iterations=max_iterations,
                    dimensions=dimensions,
                    lower_bound=lower_bound,
                    upper_bound=upper_bound,
                    best_fitness=float(best_fitness),
                    execution_time=execution_time,
                    status='completed'
                )
                result.set_best_position(best_position)
                result.set_convergence_data(convergence)
                
                save_object(result)
                
                # Prepare result object with all necessary data for comprehensive analysis
                results.append({
                    'algorithm': algorithm_name,
                    'result_id': result.id,
                    'best_fitness': float(best_fitness),
                    'mean_fitness': float(best_fitness),  # Single run, so mean = best
                    'std_fitness': 0.0,  # Single run, so std = 0
                    'execution_time': execution_time,
                    'convergence_curve': [float(f) for f in convergence] if convergence else [],
                    'best_solution': [float(p) for p in best_position] if best_position is not None else [],
                    'n_features_selected': int(sum(1 for p in best_position if p >= 0.5)) if best_position is not None else None,
                    'runs': [{
                        'best_fitness': float(best_fitness),
                        'execution_time': execution_time
                    }]  # Single run data for box plots
                })
                
                all_convergences.append({
                    'name': algorithm_name,
                    'data': convergence
                })
                
            except Exception as e:
                import traceback
                traceback.print_exc()
                results.append({'algorithm': algorithm_name, 'error': str(e)})
        
        # Return combined results
        if not results:
            return jsonify({'error': 'No algorithms could be run'}), 500
            
        # Find best result
        successful_results = [r for r in results if 'best_fitness' in r]
        if successful_results:
            best_result = min(successful_results, key=lambda x: x['best_fitness'])
            return jsonify({
                'results': results,
                'best_fitness': best_result['best_fitness'],
                'best_algorithm': best_result['algorithm'],
                'convergence_curve': all_convergences[0]['data'] if all_convergences else [],
                'all_convergences': all_convergences,
                'execution_time': sum(r['execution_time'] for r in successful_results) / len(successful_results),
                'message': f'Optimization completed for {len(successful_results)}/{len(algorithms)} algorithms'
            }), 200
        else:
            return jsonify({'error': 'All algorithms failed', 'results': results}), 500
        
    except Exception as e:
        # Log error and save failed result
        import traceback
        traceback.print_exc()
        
        result = OptimizationResult(
            user_id=current_user.id,
            algorithm_name=str(data.get('algorithms', ['unknown'])),
            objective_function=data.get('objective_function', 'unknown'),
            status='failed',
            error_message=str(e)
        )
        save_object(result)
        
        return jsonify({'error': str(e)}), 500


@app.route('/api/compare', methods=['POST'])
@login_required
def api_compare_algorithms():
    """Compare multiple algorithms"""
    data = request.json
    
    try:
        algorithms = data.get('algorithms', [])
        objective_func_name = data.get('objective_function')
        population_size = data.get('population_size', 30)
        max_iterations = data.get('max_iterations', 100)
        dimensions = data.get('dimensions', 10)
        lower_bound = data.get('lower_bound', -10.0)
        upper_bound = data.get('upper_bound', 10.0)
        
        if not algorithms or len(algorithms) < 2:
            return jsonify({'error': 'At least 2 algorithms required'}), 400
        
        if objective_func_name not in OBJECTIVE_FUNCTIONS:
            return jsonify({'error': 'Invalid objective function'}), 400
        
        objective_func = OBJECTIVE_FUNCTIONS[objective_func_name]
        
        results = []
        for algo_name in algorithms:
            try:
                algorithm_class = load_algorithm_class(algo_name)
                if not algorithm_class:
                    continue
                
                # Run algorithm (handles both new and legacy patterns)
                result_model, execution_time = run_algorithm(
                    algorithm_class, objective_func,
                    population_size, max_iterations,
                    dimensions, lower_bound, upper_bound
                )
                convergence = result_model.global_fitness_ if hasattr(result_model, 'global_fitness_') else []
                # Convert numpy arrays to lists for JSON serialization
                if hasattr(convergence, 'tolist'):
                    convergence = convergence.tolist() if hasattr(convergence, 'tolist') else list(convergence)
                else:
                    convergence = [float(x) if hasattr(x, 'item') else x for x in convergence]
                
                results.append({
                    'algorithm': algo_name,
                    'best_fitness': float(result_model.best_fitness_),
                    'execution_time': float(execution_time),
                    'convergence': convergence
                })
            except Exception as e:
                import traceback
                traceback.print_exc()
                results.append({'algorithm': algo_name, 'error': str(e)})
        
        # Save comparison session
        comparison = ComparisonSession(
            user_id=current_user.id,
            session_name=f"Comparison {datetime.now().strftime('%Y-%m-%d %H:%M')}",
            objective_function=objective_func_name,
            population_size=population_size,
            max_iterations=max_iterations,
            dimensions=dimensions
        )
        comparison.set_algorithms_tested(algorithms)
        comparison.set_results_summary(results)
        
        save_object(comparison)
        
        return jsonify({
            'comparison_id': comparison.id,
            'results': results,
            'message': 'Comparison completed successfully'
        }), 200
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/upload-algorithm', methods=['POST'])
@login_required
def api_upload_algorithm():
    """Upload custom algorithm - submits to GitHub for admin review"""
    try:
        if 'file' not in request.files:
            return jsonify({'error': 'No file provided'}), 400
        
        file = request.files['file']
        if file.filename == '':
            return jsonify({'error': 'No file selected'}), 400
        
        if not file.filename.endswith('.py'):
            return jsonify({'error': 'Only Python files (.py) allowed'}), 400
        
        # Read file content
        file_content = file.read().decode('utf-8')
        filename = secure_filename(file.filename)
        
        # Validate algorithm
        validation = custom_manager.validate_algorithm_code(file_content)
        
        if not validation['valid']:
            return jsonify({
                'error': 'Invalid algorithm',
                'details': validation.get('errors', [])
            }), 400
        
        # Save a local copy
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{current_user.id}_{filename}")
        with open(filepath, 'w') as f:
            f.write(file_content)
        
        # Prepare metadata for GitHub
        metadata = {
            'algorithm_name': validation['class_name'],
            'description': request.form.get('description', ''),
            'category': request.form.get('category', 'Custom'),
            'author': current_user.username,
            'email': current_user.email,
            'syntax_valid': True,
            'structure_valid': True,
            'inherits_base': validation.get('inherits_base', True),
            'parameters': validation.get('parameters', {}),
            'submitted_at': datetime.now().isoformat()
        }
        
        # Try GitHub submission
        github_result = {'success': False}
        github_token = os.environ.get('GITHUB_TOKEN')
        github_repo = os.environ.get('GITHUB_REPO')
        
        if github_token and github_repo:
            try:
                from mha_toolbox.github_integration import GitHubAlgorithmUploader
                uploader = GitHubAlgorithmUploader(token=github_token, repo=github_repo)
                github_result = uploader.submit_algorithm(
                    algorithm_name=validation['class_name'],
                    algorithm_file_path=filepath,
                    algorithm_content=file_content,
                    metadata=metadata,
                    author=current_user.username
                )
            except Exception as gh_error:
                github_result = {'success': False, 'error': str(gh_error)}
        
        # Save to database with pending status
        custom_algo = CustomAlgorithm(
            user_id=current_user.id,
            algorithm_name=validation['class_name'],
            class_name=validation['class_name'],
            filename=filename,
            file_content=file_content,
            description=request.form.get('description', ''),
            is_validated=True,
            validation_message='Validated - awaiting admin approval',
            status='pending',
            github_branch=github_result.get('branch'),
            github_pr_number=github_result.get('pr_number'),
            pr_url=github_result.get('pr_url')
        )
        
        save_object(custom_algo)
        
        if github_result.get('success'):
            return jsonify({
                'message': 'Algorithm submitted for review! A pull request has been created.',
                'algorithm_name': validation['class_name'],
                'algorithm_id': custom_algo.id,
                'pr_url': github_result.get('pr_url'),
                'pr_number': github_result.get('pr_number'),
                'status': 'pending'
            }), 201
        else:
            # GitHub failed but local save succeeded
            return jsonify({
                'message': 'Algorithm saved locally. Admin will review manually.',
                'algorithm_name': validation['class_name'],
                'algorithm_id': custom_algo.id,
                'status': 'pending',
                'github_status': 'GitHub integration not configured or failed'
            }), 201
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


# Admin routes for algorithm approval
@app.route('/admin/algorithms')
@login_required
def admin_algorithms():
    """Admin view for pending algorithm submissions"""
    if not current_user.is_admin:
        flash('Access denied. Admin privileges required.', 'error')
        return redirect(url_for('dashboard'))
    
    pending = CustomAlgorithm.query.filter_by(status='pending').order_by(CustomAlgorithm.upload_date.desc()).all()
    approved = CustomAlgorithm.query.filter_by(status='approved').order_by(CustomAlgorithm.upload_date.desc()).all()
    
    return render_template('admin_algorithms.html', pending=pending, approved=approved)


@app.route('/api/admin/algorithm/<int:algo_id>/approve', methods=['POST'])
@login_required
def admin_approve_algorithm(algo_id):
    """Approve a pending algorithm"""
    if not current_user.is_admin:
        return jsonify({'error': 'Admin privileges required'}), 403
    
    algo = CustomAlgorithm.query.get_or_404(algo_id)
    algo.status = 'approved'
    algo.reviewed_at = datetime.now()
    algo.reviewed_by = current_user.id
    algo.admin_notes = request.json.get('notes', '') if request.json else ''
    
    # Load algorithm into registry
    try:
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{algo.user_id}_{algo.filename}")
        if os.path.exists(filepath):
            custom_manager.load_algorithm_from_file(filepath)
    except Exception as e:
        pass  # Ignore load errors, admin can still approve
    
    save_object(algo)
    
    return jsonify({
        'message': f'Algorithm {algo.algorithm_name} approved!',
        'status': 'approved'
    })


@app.route('/api/admin/algorithm/<int:algo_id>/reject', methods=['POST'])
@login_required
def admin_reject_algorithm(algo_id):
    """Reject a pending algorithm"""
    if not current_user.is_admin:
        return jsonify({'error': 'Admin privileges required'}), 403
    
    algo = CustomAlgorithm.query.get_or_404(algo_id)
    algo.status = 'rejected'
    algo.reviewed_at = datetime.now()
    algo.reviewed_by = current_user.id
    algo.admin_notes = request.json.get('notes', 'Rejected by admin') if request.json else 'Rejected by admin'
    
    save_object(algo)
    
    return jsonify({
        'message': f'Algorithm {algo.algorithm_name} rejected',
        'status': 'rejected'
    })


@app.route('/api/admin/algorithm/<int:algo_id>/code', methods=['GET'])
@login_required
def admin_view_algorithm_code(algo_id):
    """View algorithm code (admin only)"""
    if not current_user.is_admin:
        return jsonify({'error': 'Admin privileges required'}), 403
    
    algo = CustomAlgorithm.query.get_or_404(algo_id)
    return jsonify({
        'algorithm_name': algo.algorithm_name,
        'code': algo.file_content,
        'description': algo.description,
        'author': algo.user.username if hasattr(algo, 'user') and algo.user else 'Unknown'
    })


@app.route('/api/hyperparameter-tune', methods=['POST'])
@login_required
def api_hyperparameter_tune():
    """Hyperparameter tuning"""
    data = request.json
    
    try:
        algorithm_name = data.get('algorithm')
        objective_func_name = data.get('objective_function')
        method = data.get('method', 'grid')  # grid, random, bayesian
        param_ranges = data.get('param_ranges', {})
        
        if not algorithm_name or not objective_func_name:
            return jsonify({'error': 'Missing required parameters'}), 400
        
        objective_func = OBJECTIVE_FUNCTIONS.get(objective_func_name)
        if not objective_func:
            return jsonify({'error': 'Invalid objective function'}), 400
        
        algorithm_class = registry.get_algorithm(algorithm_name)
        if not algorithm_class:
            return jsonify({'error': 'Algorithm not found'}), 404
        
        # Initialize tuner
        tuner = HyperparameterTuner(algorithm_class, objective_func)
        
        # Run tuning
        start_time = time.time()
        best_params, best_score, history = tuner.tune(
            method=method,
            param_ranges=param_ranges,
            dimensions=data.get('dimensions', 10),
            n_iter=data.get('n_iterations', 10)
        )
        execution_time = time.time() - start_time
        
        return jsonify({
            'best_params': best_params,
            'best_score': float(best_score),
            'history': history,
            'execution_time': execution_time,
            'message': 'Hyperparameter tuning completed'
        }), 200
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/statistics', methods=['GET'])
@login_required
def api_statistics():
    """Get user statistics"""
    stats = {
        'total_optimizations': OptimizationResult.query.filter_by(user_id=current_user.id).count(),
        'total_custom_algorithms': CustomAlgorithm.query.filter_by(user_id=current_user.id).count(),
        'total_comparisons': ComparisonSession.query.filter_by(user_id=current_user.id).count(),
        'most_used_algorithm': (lambda row: {'name': row[0], 'count': row[1]} if row else None)(
            db.session.query(
                OptimizationResult.algorithm_name,
                db.func.count(OptimizationResult.id).label('count')
            ).filter_by(user_id=current_user.id)\
             .group_by(OptimizationResult.algorithm_name)\
             .order_by(db.desc('count')).first()
        ),
        'average_execution_time': db.session.query(
            db.func.avg(OptimizationResult.execution_time)
        ).filter_by(user_id=current_user.id).scalar() or 0
    }
    
    return jsonify(stats), 200


# ============================================================================
# HEALTH & UTILITY API ENDPOINTS
# ============================================================================

@app.route('/api/health', methods=['GET'])
def api_health():
    """Health check endpoint for server status"""
    return jsonify({
        'status': 'healthy',
        'version': '2.0.8',
        'server': 'MHA Flow Production',
        'timestamp': datetime.utcnow().isoformat()
    }), 200


@app.route('/api/algorithms/list', methods=['GET'])
@login_required
def api_algorithms_list():
    """Get flat list of all algorithm names (for dropdowns)"""
    try:
        algorithms = sorted(registry.algorithms.keys())
        return jsonify({'algorithms': algorithms}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/algorithms/characteristics/<algorithm_name>', methods=['GET'])
@login_required
def api_algorithm_characteristics(algorithm_name):
    """Get algorithm characteristics for levify system"""
    try:
        from mha_toolbox.levify import LevifyTransformer
        transformer = LevifyTransformer()
        chars = transformer.get_algorithm_characteristics(algorithm_name)
        if chars:
            return jsonify(chars.to_dict()), 200
        # Return default characteristics if not profiled
        return jsonify({
            'name': algorithm_name,
            'exploration_score': 0.5,
            'exploitation_score': 0.5,
            'convergence_speed': 0.5,
            'diversity_maintenance': 0.5,
            'local_optima_avoidance': 0.5,
            'phase': 'balanced',
            'parameter_count': 3,
            'dimension_scalability': 'medium'
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/api/levify/transform', methods=['POST'])
@login_required
def api_levify_transform():
    """Apply levify transformation to an algorithm"""
    try:
        from mha_toolbox.levify import LevifyTransformer
        data = request.json
        algorithm = data.get('algorithm', '')
        transform_type = data.get('type', 'exploration')
        intensity = float(data.get('intensity', 0.5))

        transformer = LevifyTransformer()

        if transform_type == 'exploration':
            result = transformer.levify_to_exploration(algorithm, intensity)
        elif transform_type == 'exploitation':
            result = transformer.levify_to_exploitation(algorithm, intensity)
        else:
            result = transformer.levify_to_exploration(algorithm, intensity)

        if 'error' in result:
            return jsonify({'success': False, 'error': result['error']}), 400

        return jsonify({
            'success': True,
            'transformation': result
        }), 200
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500


@app.route('/api/dataset/upload', methods=['POST'])
@login_required
def api_dataset_upload():
    """Upload and analyze a dataset file"""
    try:
        if 'file' not in request.files:
            return jsonify({'success': False, 'error': 'No file provided'}), 400

        file = request.files['file']
        if file.filename == '':
            return jsonify({'success': False, 'error': 'No file selected'}), 400

        problem_type = request.form.get('problem_type', 'optimization')
        filename = secure_filename(file.filename)

        # Read dataset based on extension
        import pandas as pd
        import io

        if filename.endswith('.csv'):
            df = pd.read_csv(io.BytesIO(file.read()))
        elif filename.endswith(('.xls', '.xlsx')):
            df = pd.read_excel(io.BytesIO(file.read()))
        elif filename.endswith('.json'):
            df = pd.read_json(io.BytesIO(file.read()))
        else:
            return jsonify({'success': False, 'error': 'Unsupported file format. Use CSV, Excel, or JSON.'}), 400

        # Analyze using DatasetIntelligence
        from mha_toolbox.dataset_intelligence import DatasetIntelligence
        intelligence = DatasetIntelligence()

        # Separate features and target (last column as target)
        if df.shape[1] > 1:
            X = df.iloc[:, :-1].values
            y = df.iloc[:, -1].values
        else:
            X = df.values
            y = None

        profile = intelligence.analyze_dataset(X, target=y, dataset_name=filename, problem_type=problem_type)
        report = intelligence.get_intelligence_report(profile)

        # Store dataset info in session for later use
        session['dataset_profile'] = {
            'filename': filename,
            'num_samples': int(profile.num_samples),
            'dimensions': int(profile.dimensions)
        }

        return jsonify({
            'success': True,
            'profile': profile.to_dict(),
            'report': report
        }), 200

    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500


@app.route('/api/dataset/profile', methods=['GET'])
@login_required
def api_dataset_profile():
    """Get the current dataset profile from session"""
    profile = session.get('dataset_profile')
    if not profile:
        return jsonify({'error': 'No dataset loaded'}), 400
    return jsonify(profile), 200


# ============================================================================
# ERROR HANDLERS
# ============================================================================

@app.errorhandler(404)
def not_found(error):
    """404 error handler"""
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Not found'}), 404
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_error(error):
    """500 error handler"""
    db.session.rollback()
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Internal server error'}), 500
    return render_template('500.html'), 500


# ============================================================================
# DATABASE INITIALIZATION
# ============================================================================

@app.cli.command()
def init_db():
    """Initialize database"""
    db.create_all()


@app.cli.command()
def create_admin():
    """Create admin user"""
    username = input("Admin username: ")
    email = input("Admin email: ")
    password = input("Admin password: ")
    
    admin = User(username=username, email=email, is_admin=True)
    admin.set_password(password)
    
    save_object(admin)
    


if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    
    app.run(debug=True, host='0.0.0.0', port=5000)
