"""
Lambda Feature - AWS Lambda Function Support

This module provides support for analyzing Python code that uses iicy.LambdaInfraConfig
constructs and generating the corresponding CloudFormation resources using the
generic feature factory system. Includes automatic code packaging with dependency management.
"""

import os
import tempfile
import zipfile
import subprocess
import boto3
from typing import Dict, Any, List, Optional
from pathlib import Path
from ..infra.infra_feature import InfraFeature
from ...models.cloudformation import (
    Resource,
    LambdaFunctionProperties,
    LambdaPermissionProperties,
    LambdaEventSourceMappingProperties,
    IAMRoleProperties,
    SNSSubscriptionProperties,
    CloudFormationTemplate
)


class LambdaFeature(InfraFeature):
    """Feature for generating AWS Lambda function resources with automatic packaging."""
    
    CLOUDFORMATION_TYPE = 'AWS::Lambda::Function'


    def discover(self, context):
        """Discover Lambda configurations using multiple discovery methods"""
        from .lambda_config import LambdaInfraConfig
        instances = []
        
        # Use standard Pydantic instantiation parsing
        instances.extend(self._discover_instances(context, LambdaInfraConfig))
        
        # Use standard decorator parsing
        instances.extend(self._parse_decorator_pattern(context.file_path))
        
        return instances



    def generate(self, context) -> Dict[str, CloudFormationTemplate]:
        """Generate CloudFormation resources for Lambda functions with triggers."""
        
        # Get discovered configs for this feature
        feature_name = self.__class__.__name__
        # Get all config instances for this feature (regardless of type name)
        instances = []
        for config_type_name, config_list in context.feature_configs.get(feature_name, {}).items():
            instances.extend(config_list)

        stacks = {}

        for config in instances:
            stack_name = getattr(config, 'stack', 'IICYStack')

            if stack_name not in stacks:
                stacks[stack_name] = CloudFormationTemplate(
                    description=f"CloudFormation template for stack {stack_name}",
                    resources={},
                    outputs={}
                )

            # Generate all resources (Lambda function, IAM role, triggers)
            resources = self._generate_lambda_resources(config)
            for resource_id, resource in resources.items():
                stacks[stack_name].resources[resource_id] = resource

            # Generate outputs using Pydantic models
            outputs = self.generate_outputs(config, f"{config.logical_id}Function")
            stacks[stack_name].outputs.update(outputs)

        return stacks

    def pre_deploy(self, context, session: boto3.Session = None) -> None:
        """
        Pre-deployment step to package and upload Lambda code to S3.
        This method uses a CloudFormation-native approach:
        1. Creates S3 bucket if it doesn't exist
        2. Packages and uploads Lambda code
        3. CloudFormation will manage the S3 bucket lifecycle
        """
        # Get discovered configs for this feature
        feature_name = self.__class__.__name__
        instances = []
        for config_type_name, config_list in context.feature_configs.get(feature_name, {}).items():
            instances.extend(config_list)

        if not instances:
            return

        # Group configs by stack
        stacks = {}
        for config in instances:
            stack_name = getattr(config, 'stack', 'IICYStack')
            if stack_name not in stacks:
                stacks[stack_name] = []
            stacks[stack_name].append(config)

        # Process each stack
        for stack_name, configs in stacks.items():
            bucket_name = f"{stack_name.lower()}-deployment-bucket"
            
            # Ensure S3 bucket exists for upload
            self._ensure_s3_bucket_for_upload(bucket_name, session)
            
            # Package and upload Lambda code for each config
            for config in configs:
                try:
                    print(f"📦 Packaging Lambda function: {config.function_name}")
                    s3_key = self.package_and_upload_lambda(config, bucket_name, session)
                    print(f"✅ Lambda function {config.function_name} packaged and uploaded to S3")
                except Exception as e:
                    print(f"❌ Failed to package Lambda function {config.function_name}: {e}")
                    raise

    def _ensure_s3_bucket_for_upload(self, bucket_name: str, session: boto3.Session = None) -> None:
        """
        Ensure S3 bucket exists for Lambda code upload.
        Creates the bucket if it doesn't exist. This bucket will be managed
        by CloudFormation after deployment.
        """
        if session is None:
            session = boto3.Session()
        
        s3_client = session.client('s3')
        
        try:
            # Check if bucket exists
            s3_client.head_bucket(Bucket=bucket_name)
            print(f"📦 S3 bucket {bucket_name} already exists")
        except Exception as e:
            # Check if it's a bucket not found error (404 or NoSuchBucket)
            if (hasattr(e, 'response') and 
                e.response.get('Error', {}).get('Code') in ['404', 'NoSuchBucket']):
                # Bucket doesn't exist, create it
                try:
                    print(f"📦 Creating S3 bucket: {bucket_name}")
                    s3_client.create_bucket(Bucket=bucket_name)
                    print(f"✅ S3 bucket {bucket_name} created successfully")
                except Exception as e:
                    print(f"❌ Failed to create S3 bucket {bucket_name}: {e}")
                    raise
            else:
                print(f"❌ Error checking S3 bucket {bucket_name}: {e}")
                raise


    

    def _parse_decorator_pattern(self, file_path: Path) -> List[Any]:
        """Parse @handler decorators"""
        return self._find_decorated_functions(str(file_path))


    def _find_decorated_functions(self, file_path: str) -> List:
        """Find functions decorated with @handler using static AST analysis."""
        import ast
        from pathlib import Path
        
        configs = []
        
        try:
            # Read and parse the file
            with open(file_path, 'r') as f:
                source = f.read()
            
            tree = ast.parse(source)
            
            # Find all function definitions
            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef):
                    # Check if function has decorators
                    for decorator in node.decorator_list:
                        if isinstance(decorator, ast.Call):
                            # Check if it's a handler decorator
                            if (isinstance(decorator.func, ast.Name) and 
                                decorator.func.id == 'handler'):
                                
                                # Extract configuration from AST without executing
                                try:
                                    config = self._extract_handler_config_from_ast(
                                        decorator, node, file_path
                                    )
                                    if config:
                                        # Store the original function in the config for callability
                                        config._original_function = lambda *args, **kwargs: None  # Placeholder
                                        configs.append(config)
                                        
                                except Exception as e:
                                    print(f"Warning: Could not process decorated function {node.name}: {e}")
                                    continue
                                    
        except Exception as e:
            print(f"Warning: Could not parse file {file_path}: {e}")
            
        return configs

    def _extract_handler_config_from_ast(self, decorator_call, function_node, file_path: str):
        """Extract LambdaInfraConfig from AST without executing the decorator."""
        import ast
        from pathlib import Path
        from .lambda_config import LambdaInfraConfig, IAMRoleConfig
        
        # Extract arguments from the decorator call
        args = {}
        sqs_triggers = []
        sns_triggers = []
        
        # Process positional arguments (for backward compatibility)
        for i, arg_node in enumerate(decorator_call.args):
            arg_value = self._extract_ast_value(arg_node)
            
            # First string positional argument is function_name (backward compatibility)
            if i == 0 and isinstance(arg_value, str) and not arg_value.startswith('__'):
                args['function_name'] = arg_value
        
        # Get keyword arguments
        for keyword in decorator_call.keywords:
            if keyword.arg:
                arg_value = self._extract_ast_value(keyword.value)
                
                # Process named event source parameters
                if keyword.arg == 'input':
                    # Input parameter creates SQS triggers or SNS triggers
                    self._process_input_parameter(arg_value, file_path, sqs_triggers, sns_triggers)
                        
                # Store other arguments normally
                args[keyword.arg] = arg_value
        
        # Infer values from function name and docstring
        inferred_values = self._infer_values_from_function(function_node, args)
        
        # Merge inferred values with explicit args (explicit takes precedence)
        final_args = {**inferred_values, **args}
        
        # Create logical_id if not provided
        function_name = final_args.get('function_name', function_node.name)
        logical_id = final_args.get('logical_id')
        if logical_id is None:
            logical_id = function_name.replace('-', '').replace('_', '')
        
        # Use detected triggers directly (no legacy support)
        final_sqs_triggers = sqs_triggers
        final_sns_triggers = sns_triggers
        
        # Get source file info
        source_file = Path(file_path)
        source_dir = source_file.parent
        
        # Create LambdaInfraConfig
        config = LambdaInfraConfig(
            logical_id=logical_id,
            function_name=function_name,
            code_directory=source_dir,
            handler=f"{source_file.stem}.{function_node.name}",
            runtime=final_args.get('runtime', 'python3.11'),
            timeout=final_args.get('timeout', 3),
            memory_size=final_args.get('memory_size', 128),
            environment=final_args.get('environment', {}),
            tags=final_args.get('tags', {}),
            dependencies=final_args.get('dependencies'),
            sqs_triggers=final_sqs_triggers,
            sns_triggers=final_sns_triggers,
            role=final_args.get('role'),
            stack=final_args.get('stack', 'IICStack')
        )
        
        return config

    def _process_input_parameter(self, arg_value, file_path: str, sqs_triggers: List[str], sns_triggers: List[str]):
        """Process input parameter which can be SQS, SNS, string, or list of these."""
        if isinstance(arg_value, list):
            # Handle list of inputs
            for item in arg_value:
                self._process_single_input_source(item, file_path, sqs_triggers, sns_triggers)
        else:
            # Handle single input
            self._process_single_input_source(arg_value, file_path, sqs_triggers, sns_triggers)
    
    def _process_single_input_source(self, arg_value, file_path: str, sqs_triggers: List[str], sns_triggers: List[str]):
        """Process a single input source."""
        if isinstance(arg_value, str) and arg_value.startswith('__VARIABLE__:'):
            var_name = arg_value.split(':', 1)[1]
            # Try to resolve as SQS queue config
            queue_config = self._try_resolve_sqs_queue_config(var_name, file_path)
            if queue_config:
                sqs_triggers.append(queue_config['name'])
            else:
                # Try to resolve as SNS topic config
                topic_config = self._try_resolve_sns_topic_config(var_name, file_path)
                if topic_config:
                    sns_triggers.append(topic_config['name'])
                else:
                    # Try to resolve as string variable
                    string_value = self._try_resolve_string_variable(var_name, file_path)
                    if string_value:
                        if string_value.startswith('arn:aws:sns:'):
                            sns_triggers.append(string_value)
                        else:
                            # Assume it's a queue name
                            sqs_triggers.append(string_value)
        elif isinstance(arg_value, str) and not arg_value.startswith('__'):
            # String could be queue name or SNS ARN
            if arg_value.startswith('arn:aws:sns:'):
                sns_triggers.append(arg_value)
            else:
                # Assume it's a queue name
                sqs_triggers.append(arg_value)

    def _try_resolve_sns_topic_config(self, var_name: str, file_path: str) -> Optional[Dict[str, Any]]:
        """Try to resolve a variable name as an SNSTopicInfraConfig object."""
        import ast
        
        try:
            with open(file_path, 'r') as f:
                source = f.read()
            
            tree = ast.parse(source)
            
            # Look for variable assignments
            for node in ast.walk(tree):
                if isinstance(node, ast.Assign):
                    # Check if this assignment assigns to our variable
                    for target in node.targets:
                        if isinstance(target, ast.Name) and target.id == var_name:
                            # Check if the value is a call to SNSTopicInfraConfig
                            if isinstance(node.value, ast.Call):
                                if self._is_sns_topic_config_call(node.value):
                                    return self._extract_sns_config_from_call(node.value)
            
            return None
            
        except Exception as e:
            print(f"Warning: Could not resolve SNSTopicInfraConfig variable {var_name}: {e}")
            return None
    
    def _is_sns_topic_config_call(self, call_node) -> bool:
        """Check if an AST Call node represents a call to SNSTopicInfraConfig."""
        import ast
        
        # Direct call: SNSTopicInfraConfig(...)
        if isinstance(call_node.func, ast.Name) and call_node.func.id == 'SNSTopicInfraConfig':
            return True
        
        # Attribute call: iic.SNSTopicInfraConfig(...) or sns.SNSTopicInfraConfig(...)
        if isinstance(call_node.func, ast.Attribute) and call_node.func.attr == 'SNSTopicInfraConfig':
            return True
        
        return False
    
    def _extract_sns_config_from_call(self, call_node) -> Dict[str, Any]:
        """Extract configuration from an SNSTopicInfraConfig constructor call."""
        config = {}
        
        # Process keyword arguments
        for keyword in call_node.keywords:
            if keyword.arg == 'name':
                name_value = self._extract_ast_value(keyword.value)
                if isinstance(name_value, str):
                    config['name'] = name_value
        
        return config if config else None

    def _try_resolve_string_variable(self, var_name: str, file_path: str) -> Optional[str]:
        """Try to resolve a variable name as a string value."""
        import ast
        
        try:
            with open(file_path, 'r') as f:
                source = f.read()
            
            tree = ast.parse(source)
            
            # Look for variable assignments
            for node in ast.walk(tree):
                if isinstance(node, ast.Assign):
                    # Check if this assignment assigns to our variable
                    for target in node.targets:
                        if isinstance(target, ast.Name) and target.id == var_name:
                            # Check if the value is a string literal
                            if isinstance(node.value, ast.Str):  # Python 3.7 and earlier
                                return node.value.s
                            elif isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):  # Python 3.8+
                                return node.value.value
            
            return None
            
        except Exception as e:
            print(f"Warning: Could not resolve string variable {var_name}: {e}")
            return None

    def _try_resolve_sqs_queue_config(self, var_name: str, file_path: str) -> Optional[Dict[str, Any]]:
        """
        Try to resolve a variable name as an SQSQueueConfig object by analyzing the AST.
        
        This method looks for variable assignments in the same file that create
        SQSQueueConfig objects and extracts their configuration.
        
        Args:
            var_name: Name of the variable to resolve
            file_path: Path to the file being analyzed
            
        Returns:
            Dictionary with queue configuration if found, None otherwise
        """
        import ast
        
        try:
            with open(file_path, 'r') as f:
                source = f.read()
            
            tree = ast.parse(source)
            
            # Look for variable assignments
            for node in ast.walk(tree):
                if isinstance(node, ast.Assign):
                    # Check if this assignment assigns to our variable
                    for target in node.targets:
                        if isinstance(target, ast.Name) and target.id == var_name:
                            # Check if the value is a call to SQSQueueConfig
                            if isinstance(node.value, ast.Call):
                                if self._is_sqs_queue_config_call(node.value):
                                    return self._extract_sqs_config_from_call(node.value)
            
            return None
            
        except Exception as e:
            print(f"Warning: Could not resolve SQSQueueConfig variable {var_name}: {e}")
            return None
    
    def _is_sqs_queue_config_call(self, call_node) -> bool:
        """
        Check if an AST Call node represents a call to SQSQueueConfig.
        """
        import ast
        
        # Direct call: SQSQueueConfig(...)
        if isinstance(call_node.func, ast.Name) and call_node.func.id == 'SQSQueueConfig':
            return True
        
        # Attribute call: iic.SQSQueueConfig(...) or queue.SQSQueueConfig(...)
        if isinstance(call_node.func, ast.Attribute) and call_node.func.attr == 'SQSQueueConfig':
            return True
        
        return False
    
    def _extract_sqs_config_from_call(self, call_node) -> Dict[str, Any]:
        """
        Extract configuration from an SQSQueueConfig constructor call.
        """
        config = {}
        
        # Process keyword arguments
        for keyword in call_node.keywords:
            if keyword.arg == 'name':
                config['name'] = self._extract_ast_value(keyword.value)
            elif keyword.arg == 'fifo':
                config['fifo'] = self._extract_ast_value(keyword.value)
        
        # Process positional arguments (first argument is typically 'name')
        if call_node.args and len(call_node.args) > 0:
            # First positional argument is name
            name_value = self._extract_ast_value(call_node.args[0])
            if isinstance(name_value, str) and not name_value.startswith('__'):
                config['name'] = name_value
        
        return config

    def _infer_values_from_function(self, function_node, explicit_args):
        """Infer configuration values from function name (minimal inference)."""
        from ...config import get_config, default_naming_function
        
        inferred = {}
        
        # Only infer function_name from function name if not explicitly provided
        if 'function_name' not in explicit_args:
            config = get_config()
            # Check if there's a custom naming function
            if config.lambda_.custom_naming_function:
                inferred['function_name'] = config.lambda_.custom_naming_function(function_node.name)
            else:
                # Use the default naming function (camelCase)
                inferred['function_name'] = default_naming_function(function_node.name)
        
        return inferred

    def _extract_ast_value(self, node):
        """Extract value from AST node."""
        import ast
        
        if isinstance(node, ast.Str):  # Python 3.7 and earlier
            return node.s
        elif isinstance(node, ast.Constant):  # Python 3.8+
            return node.value
        elif isinstance(node, ast.List):
            return [self._extract_ast_value(item) for item in node.elts]
        elif isinstance(node, ast.Dict):
            return {
                self._extract_ast_value(k): self._extract_ast_value(v)
                for k, v in zip(node.keys, node.values)
            }
        elif isinstance(node, ast.NameConstant):  # Python 3.7 and earlier
            return node.value
        elif isinstance(node, ast.Num):  # Python 3.7 and earlier
            return node.n
        elif isinstance(node, ast.Name):
            # Return a placeholder for variable references (we'll resolve these later)
            return f"__VARIABLE__:{node.id}"
        elif isinstance(node, ast.Attribute):
            # Handle attribute access like obj.attr
            base = self._extract_ast_value(node.value)
            return f"__ATTRIBUTE__:{base}.{node.attr}"
        else:
            # For complex expressions, return a placeholder
            return f"__VARIABLE__:{node.__class__.__name__}"

    def _generate_lambda_resources(self, config) -> Dict[str, Resource]:
        """Generate all CloudFormation resources for a Lambda function."""
        resources = {}
        
        # Generate IAM Role
        role_resource_name = f"{config.role.logical_id}"
        resources[role_resource_name] = self._generate_iam_role(config.role)
        
        # S3 bucket for Lambda deployment is managed by CLI pre-deployment step
        # No need to include it in CloudFormation template
        
        # Generate Lambda Function
        function_resource_name = f"{config.logical_id}Function"
        # Properties are already created by map_properties in the parent class
        # We'll get them from the context or create them here
        lambda_properties = LambdaFunctionProperties(**config.to_cloudformation_properties())
        resources[function_resource_name] = Resource(
            logical_id=function_resource_name,
            type=self.CLOUDFORMATION_TYPE,
            properties=lambda_properties
        )
        
        # Generate SNS triggers
        for i, topic_arn in enumerate(config.sns_triggers):
            permission_name = f"{config.logical_id}SNSPermission{i}"
            subscription_name = f"{config.logical_id}SNSSubscription{i}"
            
            resources[permission_name] = self._generate_sns_permission(topic_arn, function_resource_name)
            resources[subscription_name] = self._generate_sns_subscription(topic_arn, function_resource_name)
        
        # Generate SQS triggers
        for i, queue_arn in enumerate(config.sqs_triggers):
            mapping_name = f"{config.logical_id}SQSMapping{i}"
            resources[mapping_name] = self._generate_sqs_mapping(queue_arn, function_resource_name)
        
        return resources

    def _generate_iam_role(self, role_config) -> Resource:
        """Generate IAM role CloudFormation resource."""
        assume_role_policy = {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {"Service": "lambda.amazonaws.com"},
                    "Action": "sts:AssumeRole"
                }
            ]
        }
        
        role_properties = IAMRoleProperties(
            AssumeRolePolicyDocument=assume_role_policy,
            ManagedPolicyArns=role_config.managed_policies or [],
            Policies=[
                {
                    "PolicyName": name,
                    "PolicyDocument": policy
                }
                for name, policy in (role_config.inline_policies or {}).items()
            ]
        )
        
        return Resource(
            logical_id=role_config.logical_id,
            type="AWS::IAM::Role",
            properties=role_properties
        )

    def _generate_sns_permission(self, topic_arn: str, function_name: str) -> Resource:
        """Generate Lambda permission for SNS trigger."""
        permission_properties = LambdaPermissionProperties(
            FunctionName={"Ref": function_name},
            Action="lambda:InvokeFunction",
            Principal="sns.amazonaws.com",
            SourceArn=topic_arn
        )
        
        return Resource(
            logical_id=f"{function_name}SNSPermission",
            type="AWS::Lambda::Permission",
            properties=permission_properties
        )

    def _generate_sns_subscription(self, topic_arn: str, function_name: str) -> Resource:
        """Generate SNS subscription for Lambda function."""
        subscription_properties = SNSSubscriptionProperties(
            Protocol="lambda",
            TopicArn=topic_arn,
            Endpoint={"Fn::GetAtt": [function_name, "Arn"]}
        )
        
        return Resource(
            logical_id=f"{function_name}SNSSubscription",
            type="AWS::SNS::Subscription",
            properties=subscription_properties
        )

    def _generate_sqs_mapping(self, queue_identifier: str, function_name: str) -> Resource:
        """Generate SQS event source mapping for Lambda function."""
        # Resolve queue identifier to ARN reference
        event_source_arn = self._resolve_queue_arn(queue_identifier)
        
        mapping_properties = LambdaEventSourceMappingProperties(
            EventSourceArn=event_source_arn,
            FunctionName={"Ref": function_name},
            BatchSize=10
        )
        
        return Resource(
            logical_id=f"{function_name}SQSMapping",
            type="AWS::Lambda::EventSourceMapping",
            properties=mapping_properties
        )

    def _resolve_queue_arn(self, queue_identifier: str):
        """Resolve queue identifier to CloudFormation ARN reference."""
        # If it's already a full ARN, return as-is
        if queue_identifier.startswith('arn:aws:sqs:'):
            return queue_identifier
        
        # If it's a queue name, resolve to CloudFormation GetAtt reference
        # Convert queue name to resource name format (remove hyphens/underscores)
        sanitized_name = queue_identifier.replace('-', '').replace('_', '')
        resource_name = f"{sanitized_name}Queue"
        
        # Return CloudFormation GetAtt reference for queue ARN
        return {"Fn::GetAtt": [resource_name, "Arn"]}


    def package_lambda_code(self, config, output_dir: Path) -> Path:
        """
        Package the Lambda function code into a ZIP file with dependency management.
        
        This method handles dependency management using standard pip installation.
        
        Args:
            config: LambdaInfraConfig instance
            output_dir: Directory to place the packaged ZIP file
            
        Returns:
            Path to the created ZIP file
        """
        # Create output directory if it doesn't exist
        output_dir.mkdir(parents=True, exist_ok=True)
        
        # Create temporary directory for packaging
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            
            # Copy source code
            self._copy_source_code(config.code_directory, temp_path)
            
            # Handle dependencies using pip install -t approach
            self._handle_dependencies(config, temp_path)
            
            # Create ZIP file
            zip_path = output_dir / f"{config.function_name}.zip"
            self._create_zip(temp_path, zip_path)
            
            return zip_path

    def _copy_source_code(self, source_dir: Path, temp_dir: Path):
        """Copy source code to temporary directory."""
        import shutil
        
        # Copy all Python files and other necessary files
        for item in source_dir.iterdir():
            if item.is_file() and (item.suffix == '.py' or item.name in ['requirements.txt', 'README.md']):
                shutil.copy2(item, temp_dir)
            elif item.is_dir() and not item.name.startswith('.'):
                # Copy subdirectories (but skip hidden directories)
                shutil.copytree(item, temp_dir / item.name, dirs_exist_ok=True)

    def _handle_dependencies(self, config, temp_dir: Path):
        """Handle dependencies using pip install with Linux platform targeting."""
        # Determine dependency source based on config options
        dependencies = self._get_dependencies(config)
        
        # Copy the current IIC package for Lambda functions that use it
        # This allows Lambda functions to use SQSQueueConfig, etc. at runtime
        self._copy_iic_source(temp_dir)
        
        if not dependencies:
            print("📋 No dependencies to install (IIC framework copied separately)")
            return
        
        print(f"📋 Installing {len(dependencies)} dependencies for Lambda runtime:")
        for dep in dependencies:
            print(f"    • {dep}")
        
        # Use direct pip install approach for simplicity and reliability
        if not self._install_dependencies_with_pip(dependencies, temp_dir):
            raise RuntimeError(
                f"Failed to install dependencies for Lambda function '{config.function_name}'. "
                f"Dependencies that failed to install: {dependencies}. "
                f"This may be due to missing system dependencies or network issues."
            )

    def _get_dependencies(self, config) -> List[str]:
        """Get dependencies from various sources in order of preference."""
        # Always include IIC's core dependencies for Lambda runtime
        iic_core_deps = ["pydantic", "boto3>=1.28.0", "PyYAML>=6.0"]
        
        user_deps = []
        
        # 1. Explicit dependencies (highest priority)
        if config.dependencies is not None:
            if isinstance(config.dependencies, list):
                if config.dependencies:
                    print("📦 Using explicit dependencies list")
                    user_deps = config.dependencies
                else:
                    print("📦 Explicitly no user dependencies specified")
                    user_deps = []
            elif isinstance(config.dependencies, Path):
                if config.dependencies.exists():
                    print(f"📦 Using specified requirements file: {config.dependencies}")
                    user_deps = self._read_requirements_file(config.dependencies)
                else:
                    raise FileNotFoundError(
                        f"Requirements file not found: {config.dependencies}. "
                        f"Please check that the file exists and the path is correct."
                    )
        else:
            # 2. Default requirements.txt in code directory
            default_requirements = config.code_directory / "requirements.txt"
            if default_requirements.exists():
                print(f"📦 Using default requirements file: {default_requirements}")
                user_deps = self._read_requirements_file(default_requirements)
            else:
                # 3. Project-level requirements.txt (if exists)
                project_requirements = Path("requirements.txt")
                if project_requirements.exists():
                    print(f"📦 Using project requirements file: {project_requirements}")
                    user_deps = self._read_requirements_file(project_requirements)
        
        # Combine IIC core dependencies with user dependencies
        all_deps = iic_core_deps + user_deps
        print(f"📦 Total dependencies: {len(all_deps)} (IIC: {len(iic_core_deps)}, User: {len(user_deps)})")
        
        return all_deps

    def _read_requirements_file(self, requirements_file: Path) -> List[str]:
        """Read dependencies from a requirements.txt file."""
        try:
            with open(requirements_file, 'r') as f:
                lines = f.readlines()
            
            dependencies = []
            for line in lines:
                line = line.strip()
                if line and not line.startswith('#'):
                    dependencies.append(line)
            
            return dependencies
        except Exception as e:
            print(f"❌ Failed to read requirements file {requirements_file}: {e}")
            return []

    def _copy_iic_source(self, temp_dir: Path):
        """Copy IIC source code to temporary directory for Lambda packaging."""
        import shutil
        
        # Find the IIC source directory (go up from this file to src/iic)
        current_file = Path(__file__)
        iic_src_dir = current_file.parent.parent.parent  # This should be src/iic
        
        if not iic_src_dir.exists() or iic_src_dir.name != 'iic':
            # Try alternative path resolution
            project_root = current_file.parent.parent.parent.parent  # Go to project root
            iic_src_dir = project_root / 'src' / 'iic'
        
        if not iic_src_dir.exists():
            print(f"⚠️  Warning: Could not find IIC source directory. Expected: {iic_src_dir}")
            return
        
        # Copy the entire IIC package to temp directory
        target_dir = temp_dir / 'iic'
        print(f"📦 Copying IIC framework from {iic_src_dir} to {target_dir}")
        
        try:
            shutil.copytree(iic_src_dir, target_dir, dirs_exist_ok=True)
            print("✅ IIC framework copied successfully")
        except Exception as e:
            print(f"❌ Failed to copy IIC framework: {e}")
            raise

    def _create_zip(self, source_dir: Path, zip_path: Path):
        """Create ZIP file from source directory."""
        with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for file_path in source_dir.rglob('*'):
                if file_path.is_file():
                    # Calculate relative path for ZIP
                    arcname = file_path.relative_to(source_dir)
                    zipf.write(file_path, arcname)

    def upload_lambda_code_to_s3(self, config, zip_path: Path, bucket_name: str, session: boto3.Session = None) -> str:
        """
        Upload packaged Lambda code to S3 bucket.
        
        Args:
            config: LambdaInfraConfig instance
            zip_path: Path to the packaged ZIP file
            bucket_name: Name of the S3 bucket
            session: Optional boto3 session to use
            
        Returns:
            S3 key of the uploaded object
        """
        if session is None:
            session = boto3.Session()
        
        s3_client = session.client('s3')
        
        # Create S3 key for the Lambda function
        s3_key = f"lambda/{config.function_name}.zip"
        
        try:
            print(f"📦 Uploading Lambda code to S3: s3://{bucket_name}/{s3_key}")
            
            # Upload the ZIP file to S3
            s3_client.upload_file(
                str(zip_path),
                bucket_name,
                s3_key,
                ExtraArgs={'ServerSideEncryption': 'AES256'}
            )
            
            print(f"✅ Successfully uploaded Lambda code to S3: s3://{bucket_name}/{s3_key}")
            return s3_key
            
        except Exception as e:
            print(f"❌ Failed to upload Lambda code to S3: {e}")
            raise RuntimeError(f"Failed to upload Lambda code to S3: {e}")

    def package_and_upload_lambda(self, config, bucket_name: str, session: boto3.Session = None) -> str:
        """
        Package Lambda code and upload to S3.
        
        Args:
            config: LambdaInfraConfig instance
            bucket_name: Name of the S3 bucket
            session: Optional boto3 session to use
            
        Returns:
            S3 key of the uploaded object
        """
        # Create temporary directory for packaging
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            
            # Package the Lambda code
            zip_path = self.package_lambda_code(config, temp_path)
            
            # Upload to S3
            s3_key = self.upload_lambda_code_to_s3(config, zip_path, bucket_name, session)
            
            return s3_key

    def test_lambda_execution(self, zip_path: Path, handler: str) -> bool:
        """
        Test that a packaged Lambda function can actually execute.
        
        This method extracts the ZIP file and attempts to run the Lambda handler
        to verify that all dependencies are properly packaged.
        
        Args:
            zip_path: Path to the packaged Lambda ZIP file
            handler: Lambda handler string (e.g., 'index.handler')
            
        Returns:
            True if the Lambda function executes successfully, False otherwise
        """
        try:
            print(f"🧪 Testing Lambda execution: {handler}")
            
            # Extract ZIP to temporary directory
            with tempfile.TemporaryDirectory() as extract_dir:
                extract_path = Path(extract_dir)
                
                with zipfile.ZipFile(zip_path, 'r') as zipf:
                    zipf.extractall(extract_path)
                
                # Add the extract directory to Python path
                import sys
                original_path = sys.path.copy()
                sys.path.insert(0, str(extract_path))
                
                try:
                    # Import and test the handler
                    module_name, function_name = handler.split('.')
                    module = __import__(module_name)
                    handler_func = getattr(module, function_name)
                    
                    # Create mock event and context
                    class MockContext:
                        def __init__(self):
                            self.function_name = "test-function"
                            self.memory_limit_in_mb = 128
                            self.remaining_time_in_millis = 30000
                    
                    event = {"test": "data", "source": "iic-test"}
                    context = MockContext()
                    
                    # Execute the handler
                    result = handler_func(event, context)
                    
                    print(f"✅ Lambda execution successful!")
                    print(f"📊 Result: {result}")
                    return True
                    
                finally:
                    # Restore original Python path
                    sys.path = original_path
                    
        except Exception as e:
            print(f"❌ Lambda execution failed: {e}")
            return False

    def _install_dependencies_with_pip(self, dependencies: List[str], temp_dir: Path) -> bool:
        """Install dependencies using simple pip install -t approach."""
        import subprocess
        
        try:
            print("📦 Installing dependencies using pip install...")
            
            # Use pip install -t with Linux platform targeting for Lambda compatibility
            # Target Python 3.11 (Lambda runtime version) instead of current Python version
            cmd = [
                "python3", "-m", "pip", "install", 
                *dependencies,
                "-t", str(temp_dir),
                "--platform", "manylinux2014_x86_64",
                "--python-version", "3.11",
                "--only-binary=:all:",
                "--no-cache-dir",
                "--upgrade"
            ]
            
            print(f"📦 Running: {' '.join(cmd)}")
            
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
            
            if result.returncode == 0:
                print("✅ Dependencies installed successfully")
                if result.stdout:
                    # Show last few lines of pip output
                    stdout_lines = result.stdout.strip().split('\n')[-5:]
                    for line in stdout_lines:
                        if line.strip():
                            print(f"   {line}")
                return True
            else:
                print(f"❌ Failed to install dependencies:")
                print(f"   stdout: {result.stdout[-500:]}")
                print(f"   stderr: {result.stderr[-500:]}")
                return False
                
        except subprocess.TimeoutExpired:
            print("❌ Dependency installation timed out")
            return False
        except Exception as e:
            print(f"❌ Error installing dependencies: {e}")
            return False
