"""
Custom Algorithm Manager
========================

This module handles uploading, validating, and integrating custom MHA algorithms.

Features:
- Parse uploaded Python files
- Identify MHA algorithms automatically  
- Deep automatic variable/parameter mapping (even single-letter vars like A, B, C)
- Validate algorithm structure
- Integrate into local system for immediate personal use
- Submit to GitHub branch for admin review
- Contributor credit tracking for released versions

Author: MHA Flow Development Team
License: MIT
"""

import ast
import inspect
import importlib.util
import os
import json
import re
import textwrap
from typing import Dict, List, Tuple, Optional, Any
from pathlib import Path
from datetime import datetime
import numpy as np

# Import GitHub integration
try:
    from mha_toolbox.github_integration import GitHubAlgorithmUploader
    GITHUB_AVAILABLE = True
except ImportError:
    GITHUB_AVAILABLE = False


class AlgorithmParser:
    """Parse and analyze uploaded Python files to extract algorithm information"""
    
    def __init__(self):
        self.required_methods = ['optimize', '__init__']
        self.optional_methods = ['update', 'evaluate', 'initialize_population']
    
    def parse_file(self, file_path: str) -> Dict[str, Any]:
        """
        Parse a Python file and extract algorithm information.
        
        Parameters
        ----------
        file_path : str
            Path to the Python file
            
        Returns
        -------
        info : dict
            Dictionary containing:
            - classes: List of class definitions
            - functions: List of function definitions
            - imports: List of imports
            - is_valid_mha: Whether it's a valid MHA algorithm
        """
        with open(file_path, 'r', encoding='utf-8') as f:
            source_code = f.read()
        
        try:
            tree = ast.parse(source_code)
        except SyntaxError as e:
            return {
                'error': f"Syntax error in file: {e}",
                'is_valid_mha': False
            }
        
        classes = []
        functions = []
        imports = []
        
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                class_info = self._analyze_class(node)
                classes.append(class_info)
            elif isinstance(node, ast.FunctionDef):
                func_info = self._analyze_function(node)
                functions.append(func_info)
            elif isinstance(node, (ast.Import, ast.ImportFrom)):
                imports.append(self._get_import_info(node))
        
        # Determine if it's a valid MHA
        is_valid_mha, main_class = self._is_valid_mha(classes)
        
        return {
            'classes': classes,
            'functions': functions,
            'imports': imports,
            'is_valid_mha': is_valid_mha,
            'main_class': main_class,
            'source_code': source_code
        }
    
    def _analyze_class(self, node: ast.ClassDef) -> Dict:
        """Analyze a class definition"""
        methods = []
        for item in node.body:
            if isinstance(item, ast.FunctionDef):
                method_info = self._analyze_function(item)
                methods.append(method_info)
        
        # Get base classes
        bases = [self._get_name(base) for base in node.bases]
        
        return {
            'name': node.name,
            'methods': methods,
            'bases': bases,
            'lineno': node.lineno
        }
    
    def _analyze_function(self, node: ast.FunctionDef) -> Dict:
        """Analyze a function definition"""
        # Extract parameters
        params = []
        for arg in node.args.args:
            param_name = arg.arg
            # Try to get type annotation if available
            param_type = None
            if arg.annotation:
                param_type = ast.unparse(arg.annotation) if hasattr(ast, 'unparse') else None
            params.append({
                'name': param_name,
                'type': param_type
            })
        
        # Get return annotation
        return_type = None
        if node.returns:
            return_type = ast.unparse(node.returns) if hasattr(ast, 'unparse') else None
        
        # Extract docstring
        docstring = ast.get_docstring(node)
        
        return {
            'name': node.name,
            'params': params,
            'return_type': return_type,
            'docstring': docstring,
            'lineno': node.lineno
        }
    
    def _get_import_info(self, node) -> str:
        """Get import information"""
        if isinstance(node, ast.Import):
            return ', '.join([alias.name for alias in node.names])
        elif isinstance(node, ast.ImportFrom):
            module = node.module or ''
            names = ', '.join([alias.name for alias in node.names])
            return f"from {module} import {names}"
        return ''
    
    def _get_name(self, node) -> str:
        """Get name from AST node"""
        if isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Attribute):
            return f"{self._get_name(node.value)}.{node.attr}"
        return str(node)
    
    def _is_valid_mha(self, classes: List[Dict]) -> Tuple[bool, Optional[Dict]]:
        """
        Determine if the file contains a valid MHA algorithm.
        
        A valid MHA must:
        1. Have at least one class
        2. Have an 'optimize' method
        3. Have an '__init__' method with parameters
        """
        for cls in classes:
            method_names = [m['name'] for m in cls['methods']]
            
            # Check for required methods (optimize or _optimize)
            has_optimize = 'optimize' in method_names or '_optimize' in method_names
            has_init = '__init__' in method_names
            
            if has_optimize and has_init:
                # Get optimize method details
                opt_name = 'optimize' if 'optimize' in method_names else '_optimize'
                optimize_method = next(m for m in cls['methods'] if m['name'] == opt_name)
                
                # Check if optimize has expected parameters
                param_names = [p['name'] for p in optimize_method['params']]
                
                # Common MHA optimize parameters (expanded for custom algorithms)
                mha_indicators = ['objective_func', 'fitness_func', 'bounds', 
                                'dimensions', 'problem', 'n_dims',
                                'objective_function', 'func', 'f', 'fobj',
                                'X', 'y', 'kwargs']
                
                if any(indicator in param_names for indicator in mha_indicators):
                    return True, cls
                
                # Also valid if it just has __init__ + optimize with any params
                # (since custom algorithms may use non-standard names)
                if len(param_names) > 1:  # More than just 'self'
                    return True, cls
        
        return False, None


class ParameterMapper:
    """
    Deep automatic variable/parameter mapping system.
    
    Maps arbitrary custom parameter names (including single-letter variables like
    A, B, C, N, D) to the standard MHA system parameters using:
    - Exact name matching
    - Semantic similarity (abbreviation detection)
    - Context-based inference (analyzing how variables are used in code)
    - Type/usage pattern detection (loop counters = iterations, array sizes = population)
    """
    
    def __init__(self):
        # Standard parameters with all known variants
        self.standard_params = {
            'population_size': {
                'aliases': ['pop_size', 'n_agents', 'n_particles', 'swarm_size', 
                           'colony_size', 'n_individuals', 'NOP', 'npop', 'n_pop',
                           'num_particles', 'num_agents', 'pop', 'N', 'n', 'np_size',
                           'search_agents', 'num_search_agents', 'nsa', 'sol_per_pop',
                           'popsize', 'mu', 'n_whales', 'wolves', 'hawks'],
                'patterns': [r'pop', r'swarm', r'colony', r'agents?', r'particles?',
                            r'individuals?', r'search.?agents?', r'wolves', r'whales'],
                'usage_hints': ['range(', 'for i in range(', 'np.random', 'zeros(', 'ones('],
                'typical_range': (5, 500),
                'description': 'Number of search agents in the population'
            },
            'max_iterations': {
                'aliases': ['max_iter', 'iterations', 'n_iterations', 'max_gen',
                           'generations', 'epochs', 'MAX_ITER', 'T', 't_max',
                           'max_epochs', 'num_iterations', 'n_iter', 'iters',
                           'max_cycles', 'cycles', 'num_gen', 'ngen', 'max_t',
                           'run_time', 'max_FEs', 'max_evals'],
                'patterns': [r'iter', r'gen', r'epoch', r'cycle', r'max.?t\b'],
                'usage_hints': ['for t in range(', 'for iter', 'while', 'convergence'],
                'typical_range': (10, 10000),
                'description': 'Maximum number of iterations/generations'
            },
            'dimensions': {
                'aliases': ['dim', 'n_dims', 'n_dimensions', 'D', 'd', 'problem_size',
                           'ndim', 'num_dims', 'n_var', 'n_variables', 'nvars',
                           'dimension', 'size', 'n_features'],
                'patterns': [r'dim', r'n_var', r'features?', r'variables?'],
                'usage_hints': ['shape[1]', 'X.shape', 'len(bounds)'],
                'typical_range': (1, 10000),
                'description': 'Number of dimensions/variables'
            },
            'lower_bound': {
                'aliases': ['lb', 'lower', 'min_bound', 'x_min', 'lo', 'bound_low',
                           'min_val', 'lower_limit', 'l_bound', 'low'],
                'patterns': [r'lower', r'\blb\b', r'min.?bound', r'x.?min'],
                'usage_hints': ['bound[0]', 'bounds[:, 0]'],
                'typical_range': None,
                'description': 'Lower bound of search space'
            },
            'upper_bound': {
                'aliases': ['ub', 'upper', 'max_bound', 'x_max', 'hi', 'bound_high',
                           'max_val', 'upper_limit', 'u_bound', 'high'],
                'patterns': [r'upper', r'\bub\b', r'max.?bound', r'x.?max'],
                'usage_hints': ['bound[1]', 'bounds[:, 1]'],
                'typical_range': None,
                'description': 'Upper bound of search space'
            },
            'objective_function': {
                'aliases': ['fitness_func', 'cost_func', 'eval_func', 'objective',
                           'fitness_function', 'obj_func', 'f', 'func', 'fn',
                           'cost_function', 'evaluate', 'fit_func', 'fobj'],
                'patterns': [r'fitness', r'objective', r'cost', r'eval', r'\bfobj\b'],
                'usage_hints': ['(solution)', '(position)', '(x)'],
                'typical_range': None,
                'description': 'Objective/fitness function to optimize'
            }
        }
        
        # Single-letter variable inference rules (context-dependent)
        self.single_letter_rules = {
            'N': ['population_size', 'dimensions'],          # Most commonly population
            'n': ['population_size', 'dimensions'],
            'M': ['max_iterations', 'population_size'],
            'm': ['max_iterations', 'population_size'],
            'T': ['max_iterations'],                          # Time/iterations
            't': ['max_iterations'],
            'D': ['dimensions'],
            'd': ['dimensions'],
            'P': ['population_size'],
            'p': ['population_size'],
            'G': ['max_iterations'],                          # Generations
            'g': ['max_iterations'],
            'K': ['population_size', 'max_iterations'],
            'L': ['lower_bound'],
            'U': ['upper_bound'],
            'f': ['objective_function'],
            'F': ['objective_function'],
        }

    def suggest_mapping(self, param_name: str) -> Optional[str]:
        """Suggest a standard parameter name for a custom parameter."""
        param_lower = param_name.lower().strip()
        
        # 1. Exact alias match
        for standard, info in self.standard_params.items():
            if param_lower in [v.lower() for v in info['aliases']]:
                return standard
        
        # 2. Regex pattern match
        for standard, info in self.standard_params.items():
            for pattern in info['patterns']:
                if re.search(pattern, param_lower, re.IGNORECASE):
                    return standard
        
        # 3. Single-letter inference
        if len(param_name) == 1 and param_name in self.single_letter_rules:
            candidates = self.single_letter_rules[param_name]
            return candidates[0]  # Return most likely
        
        return None

    def deep_analyze_code(self, source_code: str, detected_params: List[str]) -> Dict[str, Any]:
        """
        Deep analysis of source code to infer parameter meanings from usage context.
        
        Analyzes how variables are actually used in the code to determine their
        semantic meaning, even for single-letter or ambiguous parameter names.
        
        Parameters
        ----------
        source_code : str
            Full source code of the algorithm
        detected_params : list
            List of detected parameter/variable names from __init__
            
        Returns
        -------
        analysis : dict
            Deep mapping analysis with confidence scores
        """
        try:
            tree = ast.parse(source_code)
        except SyntaxError:
            return self.create_mapping_prompt(detected_params)
        
        # Analyze how each parameter is used
        usage_analysis = {}
        for param in detected_params:
            if param == 'self':
                continue
            usages = self._find_variable_usages(tree, param, source_code)
            usage_analysis[param] = usages
        
        # Build mappings with confidence
        mappings = {}
        unmapped = []
        assigned_standards = set()  # Track which standard params are already assigned
        
        # First pass: direct name matches (high confidence)
        for param in detected_params:
            if param == 'self':
                continue
            suggestion = self.suggest_mapping(param)
            if suggestion and suggestion not in assigned_standards:
                mappings[param] = {
                    'suggested': suggestion,
                    'confidence': 'high',
                    'reason': 'Name match',
                    'confirmed': False
                }
                assigned_standards.add(suggestion)
        
        # Second pass: context-based inference for unmapped params
        remaining_params = [p for p in detected_params if p != 'self' and p not in mappings]
        
        for param in remaining_params:
            if param in usage_analysis:
                inferred = self._infer_from_usage(param, usage_analysis[param], assigned_standards)
                if inferred:
                    mappings[param] = {
                        'suggested': inferred['standard_param'],
                        'confidence': inferred['confidence'],
                        'reason': inferred['reason'],
                        'confirmed': False
                    }
                    assigned_standards.add(inferred['standard_param'])
                    continue
            
            # Third pass: positional inference for __init__ params
            param_index = detected_params.index(param)
            positional = self._infer_from_position(param, param_index, assigned_standards)
            if positional:
                mappings[param] = {
                    'suggested': positional,
                    'confidence': 'low',
                    'reason': f'Positional inference (param #{param_index + 1})',
                    'confirmed': False
                }
                assigned_standards.add(positional)
                continue
            
            unmapped.append(param)
        
        return {
            'mapped': mappings,
            'unmapped': unmapped,
            'requires_user_input': len(unmapped) > 0,
            'usage_analysis': {k: str(v)[:200] for k, v in usage_analysis.items()}
        }

    def _infer_from_position(self, param_name: str, position: int, 
                            assigned: set) -> Optional[str]:
        """Infer standard param from position in __init__ signature."""
        # Common convention: __init__(self, pop_size, max_iter, dims, ...)
        positional_order = ['population_size', 'max_iterations', 'dimensions',
                           'lower_bound', 'upper_bound']
        
        for std in positional_order:
            if std not in assigned:
                # For first param: likely population_size
                if position == 0 and std == 'population_size':
                    return std
                # For second param: likely max_iterations
                if position == 1 and std == 'max_iterations':
                    return std
                # For third param: likely dimensions
                if position == 2 and std == 'dimensions':
                    return std
        
        return None

    def _find_variable_usages(self, tree: ast.AST, var_name: str, source_code: str) -> List[Dict]:
        """Find all usages of a variable in the AST and classify them."""
        usages = []
        
        for node in ast.walk(tree):
            # Check if var is used in a range() call → likely iteration count or pop size
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name) and node.func.id == 'range':
                    for arg in node.args:
                        if self._node_references_var(arg, var_name):
                            usages.append({'type': 'range_arg', 'context': 'loop_bound'})
                
                # Check if var is used as array size in np.random/np.zeros
                if isinstance(node.func, ast.Attribute):
                    func_name = getattr(node.func, 'attr', '')
                    if func_name in ('random', 'uniform', 'zeros', 'ones', 'empty', 'rand'):
                        for i, arg in enumerate(node.args):
                            if self._node_references_var(arg, var_name):
                                if i == 0:
                                    usages.append({'type': 'array_rows', 'context': 'population_size'})
                                elif i == 1:
                                    usages.append({'type': 'array_cols', 'context': 'dimensions'})
            
            # Check if var is used in comparison (< ub, > lb pattern)
            if isinstance(node, ast.Compare):
                for comparator in node.comparators:
                    if self._node_references_var(comparator, var_name):
                        usages.append({'type': 'comparison', 'context': 'bound'})
            
            # Check if var is called as a function → objective function
            if isinstance(node, ast.Call):
                if self._node_references_var(node.func, var_name):
                    usages.append({'type': 'called_as_function', 'context': 'objective_function'})
        
        return usages

    def _node_references_var(self, node: ast.AST, var_name: str) -> bool:
        """Check if an AST node references a specific variable."""
        if isinstance(node, ast.Name) and node.id == var_name:
            return True
        if isinstance(node, ast.Attribute):
            # Check self.var_name
            if isinstance(node.value, ast.Name) and node.value.id == 'self' and node.attr == var_name:
                return True
        return False

    def _infer_from_usage(self, param_name: str, usages: List[Dict], 
                         assigned: set = None) -> Optional[Dict]:
        """Infer parameter meaning from how it's used in code."""
        if assigned is None:
            assigned = set()
        
        # Count evidence for each standard param
        evidence = {}
        for usage in usages:
            ctx = usage['context']
            if ctx == 'loop_bound':
                evidence.setdefault('max_iterations', 0)
                evidence['max_iterations'] += 1
            elif ctx == 'population_size':
                evidence.setdefault('population_size', 0)
                evidence['population_size'] += 1
            elif ctx == 'dimensions':
                evidence.setdefault('dimensions', 0)
                evidence['dimensions'] += 1
            elif ctx == 'bound':
                evidence.setdefault('lower_bound', 0)
                evidence['lower_bound'] += 0.5
                evidence.setdefault('upper_bound', 0)
                evidence['upper_bound'] += 0.5
            elif ctx == 'objective_function':
                evidence.setdefault('objective_function', 0)
                evidence['objective_function'] += 2  # Strong signal
        
        if not evidence:
            return None
        
        # Filter out already-assigned standard params
        available = {k: v for k, v in evidence.items() if k not in assigned}
        if not available:
            return None
        
        # Pick the highest evidence among available
        best_match = max(available, key=available.get)
        score = available[best_match]
        
        confidence = 'high' if score >= 2 else 'medium' if score >= 1 else 'low'
        usage_types = [u['context'] for u in usages]
        
        return {
            'standard_param': best_match,
            'confidence': confidence,
            'reason': f'Inferred from code usage pattern ({", ".join(usage_types[:3])})'
        }

    def create_mapping_prompt(self, detected_params: List[str]) -> Dict[str, Any]:
        """
        Create a mapping prompt for user to confirm parameter mappings.
        
        Parameters
        ----------
        detected_params : list
            List of detected parameter names
            
        Returns
        -------
        mapping_prompt : dict
            Dictionary with suggested mappings for UI display
        """
        mappings = {}
        unmapped = []
        
        for param in detected_params:
            if param == 'self':
                continue
            suggestion = self.suggest_mapping(param)
            if suggestion:
                mappings[param] = {
                    'suggested': suggestion,
                    'confidence': 'high',
                    'confirmed': False
                }
            else:
                unmapped.append(param)
        
        return {
            'mapped': mappings,
            'unmapped': unmapped,
            'requires_user_input': len(unmapped) > 0
        }

    def generate_wrapper_code(self, class_name: str, parameter_mapping: Dict[str, str],
                             original_code: str, contributor_info: Dict) -> str:
        """
        Generate a wrapper class that adapts the custom algorithm to BaseOptimizer.
        
        This creates an adapter that:
        - Translates standard parameters to custom variable names
        - Wraps the optimize method to match BaseOptimizer interface
        - Preserves the original algorithm logic unchanged
        
        Parameters
        ----------
        class_name : str
            Original class name
        parameter_mapping : dict
            Mapping of {custom_param: standard_param}
        original_code : str
            The original algorithm source code
        contributor_info : dict
            Contributor credits
            
        Returns
        -------
        wrapper_code : str
            Python source code for the wrapped algorithm
        """
        wrapper_name = f"{class_name}Wrapped"
        
        # Build parameter translation in __init__
        init_translations = []
        reverse_map = {}  # standard -> custom
        custom_hyperparams = []
        
        for custom_param, mapping_info in parameter_mapping.items():
            if isinstance(mapping_info, dict):
                standard = mapping_info.get('suggested', mapping_info.get('standard'))
            else:
                standard = mapping_info
            
            if standard and standard != 'algorithm_specific':
                reverse_map[standard] = custom_param
                init_translations.append(
                    f"        # Map: {standard} -> {custom_param} (original)")
            else:
                custom_hyperparams.append(custom_param)
        
        # Build the wrapper
        credit_line = f"Contributed by: {contributor_info.get('author', 'Unknown')}"
        credit_email = contributor_info.get('email', '')
        credit_date = contributor_info.get('date', datetime.now().isoformat())
        
        # Extra kwargs for custom hyperparameters
        extra_kwargs_str = ""
        extra_attrs_str = ""
        for hp in custom_hyperparams:
            extra_kwargs_str += f", {hp}=None"
            extra_attrs_str += f"        self.{hp}_ = {hp}\n"
        
        wrapper_code = f'''"""
Wrapped Custom Algorithm: {class_name}
{credit_line}
Email: {credit_email}
Date: {credit_date}

Auto-generated wrapper that adapts the custom algorithm to BaseOptimizer interface.
Original algorithm parameters are automatically mapped to standard MHA parameters.
"""

from mha_toolbox.base import BaseOptimizer
import numpy as np
import importlib.util
import os

# ============================================================
# ORIGINAL ALGORITHM CODE (preserved unchanged)
# ============================================================
{original_code}

# ============================================================  
# WRAPPED ADAPTER CLASS
# ============================================================
class {wrapper_name}(BaseOptimizer):
    """
    {class_name} - Wrapped for MHA Flow compatibility.
    
    {credit_line}
    
    Parameter Mapping:
{chr(10).join(f"        {std} -> {cust}" for std, cust in reverse_map.items())}
    """
    
    CONTRIBUTOR = {{
        "author": "{contributor_info.get('author', 'Unknown')}",
        "email": "{credit_email}",
        "date": "{credit_date}",
        "original_class": "{class_name}"
    }}
    
    def __init__(self, population_size=30, max_iterations=100{extra_kwargs_str}, **kwargs):
        super().__init__(population_size=population_size, 
                        max_iterations=max_iterations, **kwargs)
        self.algorithm_name_ = "{class_name}"
{extra_attrs_str}
        # Create the original algorithm instance with mapped parameters
        original_kwargs = {{}}
'''
        # Add parameter mapping logic
        for standard, custom in reverse_map.items():
            if standard == 'population_size':
                wrapper_code += f"        original_kwargs['{custom}'] = self.population_size_\n"
            elif standard == 'max_iterations':
                wrapper_code += f"        original_kwargs['{custom}'] = self.max_iterations_\n"
            elif standard == 'dimensions':
                wrapper_code += f"        original_kwargs['{custom}'] = self.dimensions_\n"
            elif standard == 'lower_bound':
                wrapper_code += f"        original_kwargs['{custom}'] = self.lower_bound_\n"
            elif standard == 'upper_bound':
                wrapper_code += f"        original_kwargs['{custom}'] = self.upper_bound_\n"
        
        # Add custom hyperparams
        for hp in custom_hyperparams:
            wrapper_code += f"        if self.{hp}_ is not None:\n"
            wrapper_code += f"            original_kwargs['{hp}'] = self.{hp}_\n"
        
        wrapper_code += f'''
        # Pass remaining kwargs (skip standard MHA params already handled by BaseOptimizer)
        _STANDARD_PARAMS = {{'population_size', 'max_iterations', 'dimensions',
                            'lower_bound', 'upper_bound', 'verbose', 'mode'}}
        for k, v in kwargs.items():
            if k not in original_kwargs and k not in _STANDARD_PARAMS:
                original_kwargs[k] = v
        
        try:
            self._original_algo = {class_name}(**original_kwargs)
        except TypeError:
            # Fallback: try with only recognized params
            self._original_algo = {class_name}(**{{k: v for k, v in original_kwargs.items() 
                                                    if v is not None}})

    def _parse_result(self, result):
        """Normalize algorithm result to 5-tuple."""
        if isinstance(result, tuple) and len(result) >= 5:
            return result
        elif isinstance(result, tuple) and len(result) >= 2:
            best_sol, best_fit = result[0], result[1]
            gf = result[2] if len(result) > 2 else [best_fit]
            lf = result[3] if len(result) > 3 else None  
            lp = result[4] if len(result) > 4 else None
            return best_sol, best_fit, gf, lf, lp
        elif hasattr(result, \'best_solution_\'):
            return (result.best_solution_, result.best_fitness_, 
                    result.global_fitness_, 
                    getattr(result, \'local_fitness_\', None),
                    getattr(result, \'local_positions_\', None))
        else:
            raise TypeError(f"Unexpected result type: {{type(result)}}")

    def _optimize(self, objective_function, X=None, y=None, **kwargs):
        """Run the original algorithm\'s optimization with mapped parameters."""
        algo = self._original_algo
        
        # Update bounds/dims on the original if they were set later
        for attr in [\'population_size\', \'max_iterations\', \'dimensions\',
                     \'lower_bound\', \'upper_bound\']:
            std_val = getattr(self, attr + \'_\', None)
            if std_val is not None:
'''
        # Map standard attrs back to custom names  
        for standard, custom in reverse_map.items():
            wrapper_code += f"                if attr == '{standard}':\n"
            wrapper_code += f"                    try:\n"
            wrapper_code += f"                        setattr(algo, '{custom}', std_val)\n"
            wrapper_code += f"                    except AttributeError:\n"
            wrapper_code += f"                        pass\n"
        
        wrapper_code += f'''
        # Try different calling conventions (most permissive first)
        signatures = [
            lambda: algo.optimize(objective_function),
            lambda: algo.optimize(objective_function, X=X, y=y, **kwargs),
            lambda: algo.optimize(objective_function,
                                  bounds=(self.lower_bound_, self.upper_bound_),
                                  dimension=self.dimensions_),
            lambda: algo.optimize(objective_function, **kwargs),
        ]
        
        last_err = None
        for sig in signatures:
            try:
                result = sig()
                return self._parse_result(result)
            except TypeError as e:
                last_err = e
                continue
        raise TypeError(f"Could not call original algorithm optimize(): {{last_err}}")
'''
        return wrapper_code


class AlgorithmValidator:
    """Validate uploaded algorithms for correctness and compatibility"""
    
    def __init__(self):
        self.validation_rules = {
            'has_class': 'Algorithm must be defined as a class',
            'has_optimize': 'Must have an optimize() method',
            'has_init': 'Must have __init__() method',
            'returns_result': 'optimize() must return results',
            'handles_bounds': 'Must handle search space bounds',
            'numpy_compatible': 'Must work with numpy arrays'
        }
    
    def validate(self, algorithm_info: Dict) -> Dict[str, Any]:
        """
        Validate an algorithm's structure and compatibility.
        
        Parameters
        ----------
        algorithm_info : dict
            Parsed algorithm information
            
        Returns
        -------
        validation_result : dict
            Validation results with pass/fail for each rule
        """
        results = {}
        
        if not algorithm_info.get('is_valid_mha', False):
            return {
                'valid': False,
                'error': 'Not recognized as a valid MHA algorithm',
                'details': results
            }
        
        main_class = algorithm_info.get('main_class')
        if not main_class:
            return {
                'valid': False,
                'error': 'No main algorithm class found',
                'details': results
            }
        
        # Check each validation rule
        results['has_class'] = main_class is not None
        
        method_names = [m['name'] for m in main_class['methods']]
        results['has_optimize'] = 'optimize' in method_names or '_optimize' in method_names
        results['has_init'] = '__init__' in method_names
        
        # Check optimize method signature
        if results['has_optimize']:
            opt_name = 'optimize' if 'optimize' in method_names else '_optimize'
            optimize_method = next(m for m in main_class['methods'] if m['name'] == opt_name)
            param_names = [p['name'] for p in optimize_method['params']]
            results['handles_bounds'] = any(
                b in param_names for b in ['bounds', 'lb_ub', 'limits', 'kwargs',
                                           'lower_bound', 'upper_bound']
            )
            
            # Check if returns something - be lenient for custom algorithms
            results['returns_result'] = True  # Assume true; validated at runtime
        else:
            results['handles_bounds'] = False
            results['returns_result'] = False
        
        # Check for numpy usage (lenient - many valid algorithms import it differently)
        imports = algorithm_info.get('imports', [])
        results['numpy_compatible'] = (
            any('numpy' in imp or 'np' in imp for imp in imports) or
            'numpy' in algorithm_info.get('source_code', '') or
            'np.' in algorithm_info.get('source_code', '')
        )
        
        # Core requirements: class + init + optimize
        core_passed = results['has_class'] and results['has_init'] and results['has_optimize']
        
        return {
            'valid': core_passed,
            'details': results,
            'warnings': self._generate_warnings(results),
            'main_class_name': main_class['name']
        }
    
    def _generate_warnings(self, results: Dict[str, bool]) -> List[str]:
        """Generate warning messages for failed validations"""
        warnings = []
        for rule, passed in results.items():
            if not passed and rule in self.validation_rules:
                warnings.append(self.validation_rules[rule])
        return warnings


class AlgorithmIntegrator:
    """
    Integrate validated custom algorithms into the system.
    
    Supports two modes:
    - Local-only: Algorithm available immediately for the uploader on their system
    - Published: Algorithm available to all users after admin approval in a new release
    """
    
    def __init__(self, custom_algorithms_dir: str = None):
        if custom_algorithms_dir is None:
            # Use the algorithms/custom dir relative to this module
            base = Path(__file__).parent
            custom_algorithms_dir = str(base / "algorithms" / "custom")
        self.custom_dir = Path(custom_algorithms_dir)
        self.custom_dir.mkdir(parents=True, exist_ok=True)
        self.registry_file = self.custom_dir / "custom_registry.json"
        
        # Local-only registry (per-user algorithms not yet published)
        self.local_registry_file = self.custom_dir / "local_registry.json"
        
        # Initialize registries
        if not self.registry_file.exists():
            self._init_registry()
        if not self.local_registry_file.exists():
            self._init_local_registry()
    
    def _init_registry(self):
        """Initialize empty published registry"""
        registry = {
            'algorithms': {},
            'metadata': {
                'created': datetime.now().isoformat(),
                'version': '1.0.0'
            }
        }
        with open(self.registry_file, 'w') as f:
            json.dump(registry, f, indent=2)
    
    def _init_local_registry(self):
        """Initialize empty local (personal) registry"""
        registry = {
            'algorithms': {},
            'metadata': {
                'created': datetime.now().isoformat(),
                'version': '1.0.0',
                'description': 'Local-only custom algorithms. These are available only on this system until approved for release.'
            }
        }
        with open(self.local_registry_file, 'w') as f:
            json.dump(registry, f, indent=2)
    
    def add_algorithm_local(self,
                           source_code: str,
                           algorithm_info: Dict,
                           parameter_mapping: Dict,
                           wrapper_code: str,
                           contributor_info: Dict,
                           category: str = 'custom',
                           metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Add a custom algorithm for local-only personal use.
        
        The algorithm is saved locally and registered so the uploader can use it
        immediately on their system. Others cannot access it until it's approved
        and included in a release.
        
        Parameters
        ----------
        source_code : str
            Original algorithm source code
        algorithm_info : dict
            Parsed algorithm information
        parameter_mapping : dict
            Parameter mappings
        wrapper_code : str
            Generated wrapper code
        contributor_info : dict
            Author/contributor information
        category : str
            Algorithm category
        metadata : dict, optional
            Additional metadata
            
        Returns
        -------
        result : dict
            Integration result with algorithm_id and file path
        """
        class_name = algorithm_info.get('main_class', {}).get('name', 'Unknown')
        if isinstance(class_name, dict):
            class_name = class_name.get('name', 'Unknown')
        
        author = contributor_info.get('author', 'unknown')
        algo_id = f"custom_{class_name.lower()}_{author.lower()}"
        
        # Save the wrapper file (contains both original + adapted code)
        dest_file = self.custom_dir / f"{algo_id}.py"
        
        try:
            with open(dest_file, 'w', encoding='utf-8') as f:
                f.write(wrapper_code)
            
            # Update local registry
            registry = self._load_local_registry()
            registry['algorithms'][algo_id] = {
                'class_name': class_name,
                'wrapper_class': f"{class_name}Wrapped",
                'file': str(dest_file.name),
                'category': category,
                'parameter_mapping': parameter_mapping,
                'contributor': contributor_info,
                'metadata': metadata or {},
                'status': 'local_only',
                'enabled': True,
                'added_at': datetime.now().isoformat(),
                'usage_count': 0
            }
            self._save_local_registry(registry)
            
            return {
                'success': True,
                'algorithm_id': algo_id,
                'file_path': str(dest_file),
                'wrapper_class': f"{class_name}Wrapped",
                'message': f'Algorithm {class_name} integrated locally. You can use it now!',
                'usage_example': f"from mha_toolbox.algorithms.custom.{algo_id} import {class_name}Wrapped\noptimizer = {class_name}Wrapped(population_size=30, max_iterations=100)\nresult = optimizer.optimize(objective_function=your_func)"
            }
            
        except Exception as e:
            return {
                'success': False,
                'error': str(e),
                'message': f'Failed to integrate algorithm locally: {e}'
            }
    
    def add_algorithm(self,
                     source_file: str,
                     algorithm_info: Dict,
                     parameter_mapping: Dict,
                     category: str = 'custom',
                     metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Add a custom algorithm to the system.
        
        Parameters
        ----------
        source_file : str
            Path to source Python file
        algorithm_info : dict
            Parsed algorithm information
        parameter_mapping : dict
            Parameter mappings
        category : str
            Algorithm category
        metadata : dict, optional
            Additional metadata
            
        Returns
        -------
        result : dict
            Integration result
        """
        # Generate unique algorithm ID
        class_name = algorithm_info['main_class']['name']
        algo_id = f"custom_{class_name.lower()}"
        
        # Copy file to custom directory
        dest_file = self.custom_dir / f"{algo_id}.py"
        
        try:
            # Read source
            with open(source_file, 'r', encoding='utf-8') as f:
                source_code = f.read()
            
            # Add header comment
            header = f'''"""
Custom Algorithm: {class_name}
Uploaded: {metadata.get('upload_date', 'Unknown')}
Author: {metadata.get('author', 'Unknown')}
Category: {category}

This is a user-contributed algorithm integrated into MHA Flow.
"""

'''
            # Write to destination
            with open(dest_file, 'w', encoding='utf-8') as f:
                f.write(header + source_code)
            
            # Update registry
            registry = self._load_registry()
            registry['algorithms'][algo_id] = {
                'class_name': class_name,
                'file': str(dest_file.name),
                'category': category,
                'parameter_mapping': parameter_mapping,
                'metadata': metadata or {},
                'enabled': True
            }
            self._save_registry(registry)
            
            return {
                'success': True,
                'algorithm_id': algo_id,
                'file_path': str(dest_file),
                'message': f'Successfully integrated {class_name}'
            }
            
        except Exception as e:
            return {
                'success': False,
                'error': str(e),
                'message': f'Failed to integrate algorithm: {e}'
            }
    
    def load_custom_algorithm(self, algo_id: str):
        """
        Dynamically load a custom algorithm.
        
        Parameters
        ----------
        algo_id : str
            Algorithm ID
            
        Returns
        -------
        algorithm_class : class
            Loaded algorithm class
        """
        registry = self._load_registry()
        
        if algo_id not in registry['algorithms']:
            raise ValueError(f"Algorithm {algo_id} not found in registry")
        
        algo_info = registry['algorithms'][algo_id]
        file_path = self.custom_dir / algo_info['file']
        
        # Load module dynamically
        spec = importlib.util.spec_from_file_location(algo_id, file_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        
        # Get the class
        class_name = algo_info['class_name']
        algorithm_class = getattr(module, class_name)
        
        return algorithm_class
    
    def list_custom_algorithms(self) -> List[Dict]:
        """List all registered custom algorithms"""
        registry = self._load_registry()
        return [
            {
                'id': algo_id,
                **algo_info
            }
            for algo_id, algo_info in registry['algorithms'].items()
            if algo_info.get('enabled', True)
        ]
    
    def remove_algorithm(self, algo_id: str) -> bool:
        """Remove a custom algorithm"""
        registry = self._load_registry()
        
        if algo_id in registry['algorithms']:
            # Delete file
            algo_info = registry['algorithms'][algo_id]
            file_path = self.custom_dir / algo_info['file']
            if file_path.exists():
                file_path.unlink()
            
            # Remove from registry
            del registry['algorithms'][algo_id]
            self._save_registry(registry)
            return True
        
        return False
    
    def _load_registry(self) -> Dict:
        """Load published registry from file"""
        with open(self.registry_file, 'r') as f:
            return json.load(f)
    
    def _save_registry(self, registry: Dict):
        """Save published registry to file"""
        with open(self.registry_file, 'w') as f:
            json.dump(registry, f, indent=2)
    
    def _load_local_registry(self) -> Dict:
        """Load local-only registry from file"""
        if not self.local_registry_file.exists():
            self._init_local_registry()
        with open(self.local_registry_file, 'r') as f:
            return json.load(f)
    
    def _save_local_registry(self, registry: Dict):
        """Save local-only registry to file"""
        with open(self.local_registry_file, 'w') as f:
            json.dump(registry, f, indent=2)
    
    def load_local_algorithm(self, algo_id: str):
        """
        Dynamically load a local-only custom algorithm.
        
        Parameters
        ----------
        algo_id : str
            Algorithm ID
            
        Returns
        -------
        algorithm_class : class
            Loaded wrapped algorithm class
        """
        registry = self._load_local_registry()
        
        if algo_id not in registry['algorithms']:
            raise ValueError(f"Local algorithm {algo_id} not found in registry")
        
        algo_info = registry['algorithms'][algo_id]
        file_path = self.custom_dir / algo_info['file']
        
        # Load module dynamically
        spec = importlib.util.spec_from_file_location(algo_id, file_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        
        # Get the wrapped class
        wrapper_class_name = algo_info.get('wrapper_class', algo_info['class_name'])
        algorithm_class = getattr(module, wrapper_class_name)
        
        # Track usage
        algo_info['usage_count'] = algo_info.get('usage_count', 0) + 1
        algo_info['last_used'] = datetime.now().isoformat()
        self._save_local_registry(registry)
        
        return algorithm_class
    
    def list_local_algorithms(self) -> List[Dict]:
        """List all local-only custom algorithms for the current user."""
        registry = self._load_local_registry()
        return [
            {
                'id': algo_id,
                **algo_info
            }
            for algo_id, algo_info in registry['algorithms'].items()
            if algo_info.get('enabled', True)
        ]
    
    def list_all_algorithms(self) -> List[Dict]:
        """List both published and local custom algorithms."""
        published = self.list_custom_algorithms()
        local = self.list_local_algorithms()
        
        # Mark source
        for algo in published:
            algo['source'] = 'published'
        for algo in local:
            algo['source'] = 'local'
        
        return published + local
    
    def get_contributor_credits(self) -> List[Dict]:
        """Get all contributor credits from both registries."""
        credits = []
        
        for registry_file in [self.registry_file, self.local_registry_file]:
            if registry_file.exists():
                with open(registry_file, 'r') as f:
                    registry = json.load(f)
                for algo_id, algo_info in registry['algorithms'].items():
                    contributor = algo_info.get('contributor', algo_info.get('metadata', {}))
                    credits.append({
                        'algorithm_id': algo_id,
                        'algorithm_name': algo_info.get('class_name', algo_id),
                        'author': contributor.get('author', 'Unknown'),
                        'email': contributor.get('email', ''),
                        'date': contributor.get('date', algo_info.get('added_at', '')),
                        'status': algo_info.get('status', 'unknown'),
                        'category': algo_info.get('category', 'custom')
                    })
        
        return credits


class CustomAlgorithmManager:
    """
    Main manager orchestrating all custom algorithm operations.
    
    Complete workflow:
    1. User uploads algorithm file
    2. System parses and validates the algorithm
    3. Deep variable mapping auto-maps custom params to standard ones
    4. Algorithm is wrapped and saved locally for immediate personal use
    5. Algorithm is submitted to GitHub branch for admin review
    6. Contributor credit is tracked throughout
    7. On approval + new release, algorithm becomes available to all with credit
    """
    
    def __init__(self):
        self.parser = AlgorithmParser()
        self.mapper = ParameterMapper()
        self.validator = AlgorithmValidator()
        self.integrator = AlgorithmIntegrator()
        
        # GitHub integration (optional)
        self.use_github = GITHUB_AVAILABLE
        self.github_uploader = None
        if self.use_github:
            try:
                token = os.environ.get('GITHUB_TOKEN')
                repo = os.environ.get('GITHUB_REPO')
                if token:
                    self.github_uploader = GitHubAlgorithmUploader(token=token, repo=repo)
            except Exception:
                self.use_github = False
    
    def validate_algorithm_code(self, code_content: str) -> Dict[str, Any]:
        """
        Validate algorithm code from a string (e.g., uploaded file content).
        
        Parameters
        ----------
        code_content : str
            Python source code of the algorithm
            
        Returns
        -------
        result : dict
            Validation result with 'valid', 'class_name', 'errors', etc.
        """
        import ast
        import re
        
        result = {
            'valid': False,
            'class_name': None,
            'errors': [],
            'inherits_base': False,
            'parameters': {}
        }
        
        # Check syntax
        try:
            tree = ast.parse(code_content)
        except SyntaxError as e:
            result['errors'].append(f"Syntax error: {e}")
            return result
        
        # Find classes
        classes = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
        if not classes:
            result['errors'].append("No class definition found in the file")
            return result
        
        # Find the main algorithm class (prefer one that inherits from BaseOptimizer)
        main_class = None
        for cls in classes:
            base_names = []
            for base in cls.bases:
                if isinstance(base, ast.Name):
                    base_names.append(base.id)
                elif isinstance(base, ast.Attribute):
                    base_names.append(base.attr)
            
            if any('Optimizer' in b or 'Base' in b for b in base_names):
                main_class = cls
                result['inherits_base'] = True
                break
        
        if main_class is None:
            # Use the first class
            main_class = classes[0]
        
        result['class_name'] = main_class.name
        
        # Check for required methods
        method_names = [node.name for node in ast.walk(main_class) if isinstance(node, ast.FunctionDef)]
        
        has_init = '__init__' in method_names
        has_optimize = 'optimize' in method_names or '_optimize' in method_names
        
        if not has_init:
            result['errors'].append("Missing __init__ method")
        
        if not has_optimize:
            result['errors'].append("Missing optimize or _optimize method")
        
        # Extract parameters from __init__
        for node in ast.walk(main_class):
            if isinstance(node, ast.FunctionDef) and node.name == '__init__':
                for arg in node.args.args:
                    if arg.arg != 'self':
                        result['parameters'][arg.arg] = 'any'
                break
        
        if not result['errors']:
            result['valid'] = True
        
        return result

    def process_upload(self, file_path: str, metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Complete workflow to process an uploaded algorithm file.
        
        This is the NEW unified workflow that:
        1. Parses and validates the algorithm
        2. Deep-analyzes variables and auto-maps them
        3. Generates a wrapper for BaseOptimizer compatibility
        4. Saves locally for immediate personal use
        5. Returns mapping info for user confirmation
        
        Parameters
        ----------
        file_path : str
            Path to uploaded Python file
        metadata : dict, optional
            Additional metadata (author, description, etc.)
            
        Returns
        -------
        result : dict
            Processing result with status, mappings, and next steps
        """
        result = {
            'stage': 'parsing',
            'success': False
        }
        
        # Stage 1: Parse file
        try:
            algorithm_info = self.parser.parse_file(file_path)
            result['parse_info'] = {
                'classes': [c['name'] for c in algorithm_info.get('classes', [])],
                'is_valid_mha': algorithm_info.get('is_valid_mha', False),
                'main_class': algorithm_info.get('main_class', {}).get('name') if algorithm_info.get('main_class') else None
            }
            
            if 'error' in algorithm_info:
                result['error'] = algorithm_info['error']
                return result
                
        except Exception as e:
            result['error'] = f"Parsing failed: {e}"
            return result
        
        result['stage'] = 'validation'
        
        # Stage 2: Validate
        try:
            validation = self.validator.validate(algorithm_info)
            result['validation'] = validation
            
            if not validation['valid']:
                result['error'] = validation.get('error', 'Validation failed')
                result['warnings'] = validation.get('warnings', [])
                return result
                
        except Exception as e:
            result['error'] = f"Validation failed: {e}"
            return result
        
        result['stage'] = 'parameter_mapping'
        
        # Stage 3: Deep parameter mapping with code analysis
        try:
            source_code = algorithm_info.get('source_code', '')
            main_class = algorithm_info['main_class']
            init_method = next(
                (m for m in main_class['methods'] if m['name'] == '__init__'),
                None
            )
            
            if init_method:
                param_names = [p['name'] for p in init_method['params'] if p['name'] != 'self']
            else:
                param_names = []
            
            # Use deep analysis for better mapping
            mapping_result = self.mapper.deep_analyze_code(source_code, param_names)
            result['parameter_mapping'] = mapping_result
            
        except Exception as e:
            result['error'] = f"Parameter mapping failed: {e}"
            return result
        
        result['stage'] = 'ready_for_integration'
        result['success'] = True
        result['message'] = 'Algorithm validated and ready for integration'
        result['class_name'] = main_class['name']
        
        return result
    
    def complete_upload(self,
                       file_path: str,
                       parameter_mapping: Dict,
                       contributor_info: Dict,
                       category: str = 'custom',
                       description: str = '',
                       metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Complete the upload: generate wrapper, save locally, submit to GitHub.
        
        This should be called AFTER process_upload() succeeds and the user
        confirms or adjusts the parameter mapping.
        
        Parameters
        ----------
        file_path : str
            Path to uploaded Python file
        parameter_mapping : dict
            Confirmed parameter mapping {custom_param: standard_param}
        contributor_info : dict
            Author info: {author, email, date}
        category : str
            Algorithm category
        description : str
            Algorithm description
        metadata : dict, optional
            Additional metadata
            
        Returns
        -------
        result : dict
            Complete result with local integration + GitHub submission status
        """
        result = {
            'local_integration': None,
            'github_submission': None,
            'success': False
        }
        
        # Read source code
        with open(file_path, 'r', encoding='utf-8') as f:
            source_code = f.read()
        
        # Parse to get algorithm info
        algorithm_info = self.parser.parse_file(file_path)
        class_name = algorithm_info.get('main_class', {}).get('name', 'CustomAlgorithm')
        
        # Ensure contributor info has required fields
        contributor_info.setdefault('date', datetime.now().isoformat())
        
        # Step 1: Generate wrapper code with variable mapping
        try:
            wrapper_code = self.mapper.generate_wrapper_code(
                class_name=class_name,
                parameter_mapping=parameter_mapping,
                original_code=source_code,
                contributor_info=contributor_info
            )
        except Exception as e:
            result['error'] = f"Wrapper generation failed: {e}"
            return result
        
        # Step 2: Save locally for immediate personal use
        try:
            local_result = self.integrator.add_algorithm_local(
                source_code=source_code,
                algorithm_info=algorithm_info,
                parameter_mapping=parameter_mapping,
                wrapper_code=wrapper_code,
                contributor_info=contributor_info,
                category=category,
                metadata={
                    'description': description,
                    **(metadata or {})
                }
            )
            result['local_integration'] = local_result
        except Exception as e:
            result['local_integration'] = {
                'success': False,
                'error': f"Local integration failed: {e}"
            }
        
        # Step 3: Submit to GitHub for admin review
        if self.use_github and self.github_uploader:
            try:
                github_result = self.github_uploader.submit_algorithm(
                    algorithm_name=class_name,
                    algorithm_file_path=file_path,
                    algorithm_content=source_code,
                    metadata={
                        'algorithm_name': class_name,
                        'description': description,
                        'category': category,
                        'author': contributor_info.get('author', 'Unknown'),
                        'email': contributor_info.get('email', ''),
                        'parameter_mapping': parameter_mapping,
                        'contributor_credit': contributor_info,
                        'submitted_at': datetime.now().isoformat(),
                        **(metadata or {})
                    },
                    author=contributor_info.get('author', 'Unknown')
                )
                result['github_submission'] = github_result
            except Exception as e:
                result['github_submission'] = {
                    'success': False,
                    'error': f"GitHub submission failed: {e}"
                }
        else:
            result['github_submission'] = {
                'success': False,
                'message': 'GitHub integration not configured. Algorithm saved locally only.'
            }
        
        # Overall success if local integration worked
        result['success'] = (
            result.get('local_integration', {}).get('success', False)
        )
        
        if result['success']:
            result['message'] = (
                f'Algorithm {class_name} is now available locally on your system! '
                f'It has been submitted for review and will be available to all users '
                f'in the next release with full credit to {contributor_info.get("author", "you")}.'
            )
            result['algorithm_id'] = result['local_integration'].get('algorithm_id')
            result['usage_example'] = result['local_integration'].get('usage_example')
        
        return result
    
    def integrate_algorithm(self,
                          file_path: str,
                          parameter_mapping: Dict,
                          category: str = 'custom',
                          metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Integrate a validated algorithm into the system.
        
        DEPRECATED: Use complete_upload() instead for the full workflow.
        """
        algorithm_info = self.parser.parse_file(file_path)
        
        return self.integrator.add_algorithm(
            file_path,
            algorithm_info,
            parameter_mapping,
            category,
            metadata
        )
    
    def submit_to_github(self,
                        file_path: str,
                        algorithm_name: str,
                        author: str,
                        description: str,
                        category: str = 'custom',
                        metadata: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Submit custom algorithm to GitHub for admin review.
        """
        if not self.use_github or not self.github_uploader:
            return {
                'success': False,
                'error': 'GitHub integration not available. Configure GITHUB_TOKEN and GITHUB_REPO.',
                'fallback': 'Algorithm saved locally. Contact administrator for review.'
            }
        
        # Read algorithm file content
        with open(file_path, 'r', encoding='utf-8') as f:
            algorithm_content = f.read()
        
        # Prepare metadata with credit
        full_metadata = {
            'algorithm_name': algorithm_name,
            'description': description,
            'category': category,
            'contributor_credit': {
                'author': author,
                'submitted_at': datetime.now().isoformat()
            },
            **(metadata or {})
        }
        
        try:
            return self.github_uploader.submit_algorithm(
                algorithm_name=algorithm_name,
                algorithm_file_path=file_path,
                algorithm_content=algorithm_content,
                metadata=full_metadata,
                author=author
            )
        except Exception as e:
            return {
                'success': False,
                'error': f'GitHub submission failed: {str(e)}'
            }
    
    def check_submission_status(self, pr_number: int) -> Dict[str, Any]:
        """Check the status of a submitted algorithm."""
        if not self.use_github or not self.github_uploader:
            return {'success': False, 'error': 'GitHub integration not available'}
        return self.github_uploader.get_submission_status(pr_number)
    
    def load_local_algorithm(self, algo_id: str):
        """Load a local custom algorithm for use."""
        return self.integrator.load_local_algorithm(algo_id)
    
    def list_custom_algorithms(self) -> List[Dict]:
        """List all registered custom algorithms (published)."""
        return self.integrator.list_custom_algorithms()
    
    def list_local_algorithms(self) -> List[Dict]:
        """List all local-only custom algorithms."""
        return self.integrator.list_local_algorithms()
    
    def list_all_custom_algorithms(self) -> List[Dict]:
        """List both published and local custom algorithms."""
        return self.integrator.list_all_algorithms()
    
    def remove_algorithm(self, algo_id: str) -> bool:
        """Remove a custom algorithm"""
        return self.integrator.remove_algorithm(algo_id)
    
    def get_algorithm(self, algo_id: str) -> Optional[Dict]:
        """Get information about a specific custom algorithm"""
        # Check published first, then local
        registry = self.integrator._load_registry()
        algo = registry['algorithms'].get(algo_id)
        if algo:
            return algo
        
        local_registry = self.integrator._load_local_registry()
        return local_registry['algorithms'].get(algo_id)
    
    def get_contributor_credits(self) -> List[Dict]:
        """Get all contributor credits."""
        return self.integrator.get_contributor_credits()


# Example usage and testing
if __name__ == "__main__":
    print("=" * 70)
    print("Custom Algorithm Manager - Test Suite")
    print("=" * 70)
    
    manager = CustomAlgorithmManager()
    
    print("\n[Test] Algorithm parser and validator ready")
    print(f"Custom algorithms directory: {manager.integrator.custom_dir}")
    print(f"Published registry: {manager.integrator.registry_file}")
    print(f"Local registry: {manager.integrator.local_registry_file}")
    
    # List any existing custom algorithms
    custom_algos = manager.list_all_custom_algorithms()
    print(f"\nTotal custom algorithms: {len(custom_algos)}")
    for algo in custom_algos:
        print(f"  - {algo['id']}: {algo['class_name']} [{algo.get('category', 'custom')}] "
              f"({algo.get('source', 'unknown')})")
    
    # Show contributor credits
    credits = manager.get_contributor_credits()
    if credits:
        print(f"\nContributor Credits:")
        for credit in credits:
            print(f"  - {credit['algorithm_name']} by {credit['author']} ({credit['status']})")
    
    print("\n" + "=" * 70)
