"""
Enhanced dry run engine for accurate cost prediction with anomaly profiles.

Executes the complete generation pipeline with automatic cleanup to provide
accurate final cost predictions for jobs with anomaly profiles.
"""

import time
import uuid
from typing import Optional, List
from pathlib import Path

from rich.console import Console
import polars as pl
import boto3
from botocore.exceptions import ClientError

from ..config.models import GenerationJob
from ..processing.cost_analysis import CostAnalysisResult, detect_anomaly_profiles, has_anomaly_profiles
from src.services.s3_data_service import S3DataService
from .streaming_engine import StreamingGenerationEngine


class EnhancedDryRunEngine:
    """
    Executes complete generation pipeline with automatic cleanup for accurate cost analysis.

    Uses identical processing components as real generation to ensure 100% accuracy
    in cost predictions while automatically managing temporary datasets.
    """

    def __init__(self, console: Console, memory_limit_gb: float = 2.0):
        """
        Initialize enhanced dry run engine.

        Args:
            console: Rich console for output
            memory_limit_gb: Memory limit for generation pipeline (optimized for dry runs)
        """
        self.console = console
        self.memory_limit_gb = memory_limit_gb
        self.s3_service = S3DataService()

    def analyze_job_cost(self, job: GenerationJob) -> CostAnalysisResult:
        """
        Execute full generation pipeline and return accurate cost analysis.

        This method runs the identical pipeline as real generation but automatically
        cleans up generated files after extracting cost analysis.

        Args:
            job: GenerationJob to analyze

        Returns:
            CostAnalysisResult with accurate cost predictions

        Raises:
            RuntimeError: If pipeline execution or cost analysis fails
        """
        # Detect anomaly profiles for analysis
        anomaly_profiles = detect_anomaly_profiles(job)
        primary_anomaly = anomaly_profiles[0] if anomaly_profiles else None

        # Start timing
        start_time = time.time()

        # Create temporary dataset name
        temp_dataset = self._create_temp_dataset_name()

        try:
            self.console.print(
                f"[dim]Running full pipeline analysis with dataset: {temp_dataset}[/dim]"
            )

            # Execute full generation pipeline using StreamingEngine
            generation_result = self._execute_generation_pipeline(job, temp_dataset)

            # Extract actual cost and monthly cost breakdown from generation result
            actual_cost = self._extract_cost_from_generation_result(generation_result, job)
            monthly_costs = self._extract_monthly_costs_from_dataset(temp_dataset, job)
            line_items = generation_result.get('generation', {}).get('total_records_generated', 0)
            total_resources = job.resource_count

            # Calculate max and median monthly costs
            max_monthly_cost = max(monthly_costs) if monthly_costs else None
            median_monthly_cost = self._calculate_median(monthly_costs) if monthly_costs else None

            # Calculate time range
            time_range = job.end_date - job.start_date
            time_range_hours = int(time_range.total_seconds() / 3600)

            duration = time.time() - start_time

            # Create analysis result
            result = CostAnalysisResult(
                configured_total_cost=job.total_cost,
                actual_final_cost=actual_cost,
                cost_multiplier=actual_cost / job.total_cost if job.total_cost > 0 else 0.0,
                max_monthly_cost=max_monthly_cost,
                median_monthly_cost=median_monthly_cost,
                monthly_costs=monthly_costs,
                anomaly_type=primary_anomaly.anomaly_type if primary_anomaly else None,
                anomaly_details=self._create_anomaly_details(anomaly_profiles),
                line_item_count=line_items,
                time_range_hours=time_range_hours,
                total_resources=total_resources,
                start_date=job.start_date,
                end_date=job.end_date,
                generation_duration_seconds=duration,
                temp_dataset_name=temp_dataset
            )

            return result

        except Exception as e:
            self.console.print(f"[red]Pipeline execution failed: {e}[/red]")
            raise RuntimeError(f"Cost analysis failed: {e}") from e

        finally:
            # Always cleanup temporary dataset
            self._cleanup_temp_dataset(temp_dataset)

    def can_analyze_job(self, job: GenerationJob) -> bool:
        """
        Check if this engine can analyze the given job.

        Currently only handles jobs with anomaly profiles. Jobs without anomaly
        profiles should use the existing fast estimation logic.

        Args:
            job: GenerationJob to check

        Returns:
            True if this engine should analyze the job
        """
        return has_anomaly_profiles(job)

    def _create_temp_dataset_name(self) -> str:
        """
        Create unique temporary dataset name.

        Uses UUID and timestamp to ensure uniqueness and easy identification.

        Returns:
            Unique temporary dataset name
        """
        timestamp = int(time.time())
        unique_id = uuid.uuid4().hex[:8]
        return f"dry-run-{unique_id}-{timestamp}"

    def _create_anomaly_details(self, anomaly_profiles) -> dict:
        """
        Create detailed anomaly information for analysis result.

        Args:
            anomaly_profiles: List of AnomalyProfileInfo objects

        Returns:
            Dictionary with anomaly details for reporting
        """
        if not anomaly_profiles:
            return {}

        details = {
            "total_anomaly_profiles": len(anomaly_profiles),
            "anomaly_types": [profile.anomaly_type for profile in anomaly_profiles],
            "profiles": []
        }

        for profile in anomaly_profiles:
            profile_details = {
                "type": profile.anomaly_type,
                "intensity": profile.intensity,
                "auto_calculated_intensity": profile.auto_calculated_intensity,
                "period": profile.period,
                "duration_hours": profile.duration_hours,
                "estimated_occurrences": profile.estimated_occurrences
            }
            details["profiles"].append(profile_details)

        return details

    def _cleanup_temp_dataset(self, dataset_name: str) -> None:
        """
        Clean up temporary dataset files from S3.

        Uses the existing S3DataService to remove all files associated with
        the temporary dataset. Handles errors gracefully and always attempts
        cleanup to prevent S3 storage leaks.

        Args:
            dataset_name: Temporary dataset name to clean up
        """
        try:
            self.console.print(f"[dim]Cleaning up temporary dataset: {dataset_name}[/dim]")

            # Use S3DataService to clean up the dataset
            # This removes all generated files in the dataset prefix
            self._cleanup_dataset_files(dataset_name)

            self.console.print(f"[dim]✓ Cleanup completed for {dataset_name}[/dim]")

        except Exception as e:
            # Log cleanup errors but don't fail the analysis
            self.console.print(f"[yellow]Warning: Cleanup failed for {dataset_name}: {e}[/yellow]")
            # Continue - this is a non-fatal error

    def _cleanup_dataset_files(self, dataset_name: str) -> None:
        """
        Remove all files associated with a dataset from S3.

        Args:
            dataset_name: Name of dataset to clean up

        Raises:
            Exception: If S3 operations fail
        """
        # Get the S3 prefix for this dataset
        # Dataset files are typically stored under patterns like:
        # - generated/{dataset_name}/
        # - processed/{dataset_name}/
        # - etc.

        prefixes_to_clean = [
            # Main dataset path used by S3DataService
            f"czbillgen/datasets/{dataset_name}/",
            # Legacy paths (in case they're used elsewhere)
            f"generated/{dataset_name}/",
            f"processed/{dataset_name}/",
            f"line-items/{dataset_name}/",
            f"anomalies/{dataset_name}/",
            f"variance/{dataset_name}/",
            f"k8s/{dataset_name}/",
            f"metadata/{dataset_name}/",
            f"usage/{dataset_name}/",
            f"discount/{dataset_name}/",
            f"tax/{dataset_name}/",
        ]

        import boto3
        from botocore.exceptions import ClientError

        s3_client = boto3.client('s3')
        bucket_name = self.s3_service.bucket
        total_files_deleted = 0

        self.console.print(f"[dim]  Searching {len(prefixes_to_clean)} prefixes in bucket: {bucket_name}[/dim]")

        for prefix in prefixes_to_clean:
            try:
                # Handle pagination for large file listings
                continuation_token = None
                prefix_files_deleted = 0

                while True:
                    # List objects with pagination support
                    list_kwargs = {
                        'Bucket': bucket_name,
                        'Prefix': prefix
                    }
                    if continuation_token:
                        list_kwargs['ContinuationToken'] = continuation_token

                    response = s3_client.list_objects_v2(**list_kwargs)

                    if 'Contents' in response:
                        # Delete all objects found in this batch
                        objects_to_delete = [{'Key': obj['Key']} for obj in response['Contents']]

                        if objects_to_delete:
                            s3_client.delete_objects(
                                Bucket=bucket_name,
                                Delete={'Objects': objects_to_delete}
                            )
                            prefix_files_deleted += len(objects_to_delete)
                            total_files_deleted += len(objects_to_delete)

                    # Check if there are more files to process
                    if not response.get('IsTruncated', False):
                        break

                    continuation_token = response.get('NextContinuationToken')

                # Report results for this prefix
                if prefix_files_deleted > 0:
                    self.console.print(f"[dim]  ✓ Deleted {prefix_files_deleted} files from {prefix}[/dim]")

            except ClientError as e:
                # Log individual prefix failures but continue with other prefixes
                self.console.print(f"[yellow]  ⚠️  Failed to clean {prefix}: {e}[/yellow]")
                continue

        self.console.print(f"[dim]  Total cleanup: {total_files_deleted} files removed[/dim]")

    def _get_cleanup_prefixes(self, dataset_name: str) -> list[str]:
        """
        Get all S3 prefixes that may contain files for this dataset.

        Args:
            dataset_name: Name of dataset

        Returns:
            List of S3 prefixes to clean up
        """
        return [
            f"generated/{dataset_name}/",
            f"processed/{dataset_name}/",
            f"line-items/{dataset_name}/",
            f"anomalies/{dataset_name}/",
            f"variance/{dataset_name}/",
            f"k8s/{dataset_name}/",
            f"metadata/{dataset_name}/",
            f"usage/{dataset_name}/",
            f"discount/{dataset_name}/",
            f"tax/{dataset_name}/",
            # Registry and tracking files
            f"registry/{dataset_name}.json",
            f"success-markers/{dataset_name}/",
            # Pipeline state files
            f"state/{dataset_name}/",
        ]

    def _execute_generation_pipeline(self, job: GenerationJob, dataset_name: str) -> dict:
        """
        Execute the complete generation pipeline for the given job.

        Uses StreamingEngine with memory optimization for dry run analysis.
        This is the same pipeline used for real generation, ensuring 100% accuracy.

        Args:
            job: GenerationJob to execute
            dataset_name: Temporary dataset name

        Returns:
            Dictionary with generation results and statistics

        Raises:
            Exception: If pipeline execution fails
        """
        try:
            # Create memory-optimized streaming engine for dry run
            engine = StreamingGenerationEngine(
                s3_service=self.s3_service,
                console=self.console,
                target_memory_bytes=int(self.memory_limit_gb * 1024 * 1024 * 1024),
                use_optimized_s3=True,
                use_vectorized_operations=True
            )

            # Execute the pipeline for this job
            # Create a temporary config with just this job
            from ..config.models import GenerateConfig
            temp_config = GenerateConfig(
                generation_jobs=[job]
            )

            self.console.print(f"[dim]  Executing pipeline for job: {job.name}[/dim]")
            generation_result = engine.generate_dataset(temp_config, dataset_name, verbose=False)

            self.console.print(f"[dim]  Pipeline execution completed successfully[/dim]")
            self.console.print(f"[dim]  Generation result keys: {list(generation_result.keys())}[/dim]")

            return generation_result

        except Exception as e:
            self.console.print(f"[red]  Pipeline execution failed: {e}[/red]")
            raise RuntimeError(f"Generation pipeline failed for job '{job.name}': {e}") from e

    def _create_memory_optimized_engine(self, dataset_name: str) -> StreamingGenerationEngine:
        """
        Create a StreamingEngine optimized for dry run analysis.

        Uses reduced memory limits and optimized settings for temporary analysis.

        Args:
            dataset_name: Temporary dataset name

        Returns:
            Configured StreamingEngine instance
        """
        return StreamingGenerationEngine(
            s3_service=self.s3_service,
            console=self.console,
            target_memory_bytes=int(self.memory_limit_gb * 1024 * 1024 * 1024),
            use_optimized_s3=True,
            use_vectorized_operations=True
        )

    def _calculate_actual_cost_from_generated_data(self, dataset_name: str, job: GenerationJob) -> tuple[float, int]:
        """
        Calculate actual final cost from generated parquet files.

        Reads the generated data files from S3 and sums all cost columns to get
        the true final cost after anomaly and variance effects.

        Args:
            dataset_name: Name of temporary dataset
            job: GenerationJob that was executed

        Returns:
            Tuple of (actual_final_cost, line_item_count)

        Raises:
            Exception: If unable to read or calculate costs from generated files
        """
        try:
            self.console.print(f"[dim]  Analyzing generated cost data for {dataset_name}[/dim]")

            # Find all generated parquet files for this dataset
            parquet_files = self._find_generated_parquet_files(dataset_name)

            if not parquet_files:
                # If no files found, this might be a very small job or error
                self.console.print(f"[yellow]  Warning: No parquet files found for {dataset_name}[/yellow]")
                return job.total_cost, 0

            total_cost = 0.0
            total_line_items = 0

            # Read and sum costs from all parquet files
            for file_path in parquet_files:
                try:
                    cost, line_items = self._read_cost_from_parquet_file(file_path)
                    total_cost += cost
                    total_line_items += line_items
                except Exception as e:
                    self.console.print(f"[yellow]  Warning: Failed to read {file_path}: {e}[/yellow]")
                    continue

            self.console.print(
                f"[dim]  Found ${total_cost:.2f} total cost across {total_line_items:,} line items[/dim]"
            )

            return total_cost, total_line_items

        except Exception as e:
            self.console.print(f"[red]  Cost analysis failed: {e}[/red]")
            # Fallback to configured cost if analysis fails
            return job.total_cost, 0

    def _find_generated_parquet_files(self, dataset_name: str) -> list[str]:
        """
        Find all parquet files generated for this dataset.

        Searches S3 for parquet files in common generation output locations.

        Args:
            dataset_name: Name of dataset

        Returns:
            List of S3 paths to parquet files
        """
        try:
            s3_client = boto3.client('s3')
            bucket_name = self.s3_service.bucket

            parquet_files = []

            # Common prefixes where generated parquet files are stored
            # Based on StreamingEngine output patterns
            search_prefixes = [
                f"generated/{dataset_name}/",
                f"line-items/{dataset_name}/",
                f"processed/{dataset_name}/",
                f"datasets/{dataset_name}/",
                f"temp/{dataset_name}/",
                f"staging/{dataset_name}/",
                # Also search without dataset prefix in case files are at root level
                "generated/",
                "line-items/",
                "processed/",
                "datasets/",
            ]

            for prefix in search_prefixes:
                try:
                    response = s3_client.list_objects_v2(
                        Bucket=bucket_name,
                        Prefix=prefix
                    )

                    if 'Contents' in response:
                        for obj in response['Contents']:
                            if obj['Key'].endswith('.parquet'):
                                s3_path = f"s3://{bucket_name}/{obj['Key']}"
                                parquet_files.append(s3_path)

                except ClientError as e:
                    self.console.print(f"[yellow]    Warning: Failed to list {prefix}: {e}[/yellow]")
                    continue

            return parquet_files

        except Exception as e:
            self.console.print(f"[red]  Failed to find parquet files: {e}[/red]")
            return []

    def _read_cost_from_parquet_file(self, file_path: str) -> tuple[float, int]:
        """
        Read cost data from a single parquet file.

        Args:
            file_path: S3 path to parquet file

        Returns:
            Tuple of (total_cost_in_file, line_item_count)
        """
        try:
            # Read parquet file using Polars
            df = pl.read_parquet(file_path)

            if len(df) == 0:
                return 0.0, 0

            # Find cost column - common names used in generated data
            cost_columns = [col for col in df.columns if 'cost' in col.lower()]

            if not cost_columns:
                # Try other common column names
                potential_cost_columns = ['amount', 'charge', 'price', 'total', 'hourly_cost']
                cost_columns = [col for col in df.columns
                              if any(cost_name in col.lower() for cost_name in potential_cost_columns)]

            if not cost_columns:
                self.console.print(f"[yellow]    Warning: No cost column found in {file_path}[/yellow]")
                return 0.0, len(df)

            # Use the first cost column found (or primary one if available)
            primary_cost_column = cost_columns[0]
            if 'hourly_cost' in cost_columns:
                primary_cost_column = 'hourly_cost'
            elif 'cost' in cost_columns:
                primary_cost_column = 'cost'

            # Sum all costs in the file
            total_cost = float(df[primary_cost_column].sum())
            line_count = len(df)

            return total_cost, line_count

        except Exception as e:
            # Re-raise with context
            raise Exception(f"Failed to read cost from {file_path}: {e}") from e

    def _get_cost_column_name(self, df: pl.DataFrame) -> Optional[str]:
        """
        Identify the primary cost column in a DataFrame.

        Args:
            df: DataFrame to analyze

        Returns:
            Name of cost column, or None if not found
        """
        # Priority order for cost column names
        priority_columns = ['hourly_cost', 'cost', 'amount', 'charge', 'total_cost']

        for col_name in priority_columns:
            if col_name in df.columns:
                return col_name

        # Fallback: find any column with 'cost' in the name
        cost_columns = [col for col in df.columns if 'cost' in col.lower()]
        if cost_columns:
            return cost_columns[0]

        return None

    def _extract_cost_from_generation_result(self, generation_result: dict, job: GenerationJob) -> float:
        """
        Extract actual cost from generation result.

        The generation result now includes actual cost from the StreamingEngine.
        If not found, fall back to configured cost.

        Args:
            generation_result: Dictionary returned by StreamingEngine
            job: GenerationJob that was executed

        Returns:
            Actual cost from generation, or configured cost as fallback
        """
        self.console.print(f"[dim]  Checking generation result for cost information[/dim]")
        self.console.print(f"[dim]  Available keys: {list(generation_result.keys())}[/dim]")

        # First check for the new direct cost information in generation results
        if 'generation' in generation_result:
            generation_section = generation_result['generation']
            self.console.print(f"[dim]  Generation section keys: {list(generation_section.keys())}[/dim]")

            # Check for total actual cost
            if 'total_actual_cost' in generation_section:
                actual_cost = float(generation_section['total_actual_cost'])
                self.console.print(f"[dim]  Found total_actual_cost: ${actual_cost:.2f}[/dim]")
                return actual_cost

            # Check for individual job costs (for single job)
            if 'job_costs' in generation_section and job.name in generation_section['job_costs']:
                actual_cost = float(generation_section['job_costs'][job.name])
                self.console.print(f"[dim]  Found job cost for '{job.name}': ${actual_cost:.2f}[/dim]")
                return actual_cost

        # Fallback to legacy cost extraction methods
        # Look for cost information in the generation result
        if 'line_items' in generation_result and 'total_cost' in generation_result['line_items']:
            actual_cost = float(generation_result['line_items']['total_cost'])
            self.console.print(f"[dim]  Found total_cost in line_items: ${actual_cost:.2f}[/dim]")
            return actual_cost
        elif 'analysis' in generation_result and 'total_cost' in generation_result['analysis']:
            actual_cost = float(generation_result['analysis']['total_cost'])
            self.console.print(f"[dim]  Found total_cost in analysis: ${actual_cost:.2f}[/dim]")
            return actual_cost

        # Print sections for debugging (truncated for readability)
        if 'line_items' in generation_result:
            line_items = generation_result['line_items']
            self.console.print(f"[dim]  Line_items keys: {list(line_items.keys())}[/dim]")
            if 'summary' in line_items:
                self.console.print(f"[dim]  Summary: {line_items['summary']}[/dim]")

        # Fallback to configured cost
        self.console.print(f"[dim]  No cost info found in generation result, using configured cost: ${job.total_cost:.2f}[/dim]")
        return job.total_cost

    def _extract_monthly_costs_from_dataset(self, dataset_name: str, job: GenerationJob) -> List[float]:
        """
        Extract monthly cost breakdown from generated dataset before cleanup.

        Args:
            dataset_name: Name of temporary dataset
            job: GenerationJob that was executed

        Returns:
            List of monthly costs in chronological order
        """
        try:
            self.console.print(f"[dim]  Extracting monthly costs from dataset: {dataset_name}[/dim]")

            # Read the generated parquet files from S3
            import polars as pl
            from collections import defaultdict

            # Get the generated files using S3DataService pattern
            dataset_files = self._list_dataset_files(dataset_name)
            if not dataset_files:
                self.console.print(f"[dim]  No dataset files found for monthly analysis[/dim]")
                return []

            monthly_costs = defaultdict(float)

            # Process each file to extract monthly costs
            for file_key in dataset_files:
                try:
                    # Download and read the parquet file
                    df = self._read_dataset_file(file_key)
                    if df is None or df.is_empty():
                        continue

                    # Extract billing period from file path (e.g., "Anthropic/2025-06.parquet" -> "2025-06")
                    import re
                    path_match = re.search(r'/(\d{4}-\d{2})\.parquet$', file_key)
                    if not path_match:
                        self.console.print(f"[dim]  Could not extract billing period from path: {file_key}[/dim]")
                        continue

                    billing_period = path_match.group(1)

                    # Find cost column
                    cost_col = None
                    if 'cost' in df.columns:
                        cost_col = 'cost'
                    else:
                        cost_columns = [col for col in df.columns if 'cost' in col.lower()]
                        if cost_columns:
                            cost_col = cost_columns[0]

                    if cost_col:
                        # Sum all costs for this month
                        total_cost = df[cost_col].sum()
                        monthly_costs[billing_period] += float(total_cost)

                except Exception as e:
                    self.console.print(f"[dim]  Warning: Failed to process file {file_key}: {e}[/dim]")
                    continue

            # Convert to sorted list of costs by month
            if monthly_costs:
                sorted_months = sorted(monthly_costs.keys())
                costs_list = [monthly_costs[month] for month in sorted_months]
                self.console.print(f"[dim]  Extracted {len(costs_list)} monthly cost entries[/dim]")
                return costs_list
            else:
                self.console.print(f"[dim]  No monthly cost data found in dataset[/dim]")
                return []

        except Exception as e:
            self.console.print(f"[dim]  Error extracting monthly costs: {e}[/dim]")
            return []

    def _calculate_median(self, values: List[float]) -> float:
        """
        Calculate median of a list of values.

        Args:
            values: List of numeric values

        Returns:
            Median value
        """
        if not values:
            return 0.0

        sorted_values = sorted(values)
        n = len(sorted_values)

        if n % 2 == 0:
            # Even number of values
            return (sorted_values[n // 2 - 1] + sorted_values[n // 2]) / 2.0
        else:
            # Odd number of values
            return sorted_values[n // 2]

    def _list_dataset_files(self, dataset_name: str) -> List[str]:
        """
        List all parquet files for a dataset in S3.

        Args:
            dataset_name: Name of the dataset

        Returns:
            List of S3 object keys for dataset files
        """
        try:
            import boto3
            s3_client = boto3.client('s3')
            bucket_name = self.s3_service.bucket

            # Try multiple possible prefixes where the files might be stored
            prefixes_to_try = [
                f"czbillgen/datasets/{dataset_name}/generate/",
                f"czbillgen/datasets/{dataset_name}/",
                f"datasets/{dataset_name}/generate/",
                f"datasets/{dataset_name}/",
                f"{dataset_name}/generate/",
                f"{dataset_name}/"
            ]

            file_keys = []
            for prefix in prefixes_to_try:
                paginator = s3_client.get_paginator('list_objects_v2')
                page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix)

                prefix_files = []
                for page in page_iterator:
                    if 'Contents' in page:
                        for obj in page['Contents']:
                            key = obj['Key']
                            if key.endswith('.parquet'):
                                prefix_files.append(key)

                if prefix_files:
                    self.console.print(f"[dim]  Found {len(prefix_files)} dataset files for monthly analysis[/dim]")
                    file_keys.extend(prefix_files)
                    break  # Use first prefix that has files

            return file_keys

        except Exception as e:
            self.console.print(f"[dim]  Error listing dataset files: {e}[/dim]")
            return []

    def _read_dataset_file(self, s3_key: str) -> Optional[pl.DataFrame]:
        """
        Read a parquet file from S3 into a Polars DataFrame.

        Args:
            s3_key: S3 object key for the file

        Returns:
            Polars DataFrame or None if read fails
        """
        try:
            import tempfile
            import os
            import boto3

            # Download file to temporary location
            with tempfile.NamedTemporaryFile(delete=False, suffix='.parquet') as temp_file:
                temp_path = temp_file.name

                s3_client = boto3.client('s3')
                s3_client.download_file(self.s3_service.bucket, s3_key, temp_path)

                # Read with Polars
                df = pl.read_parquet(temp_path)

                # Clean up temp file
                os.unlink(temp_path)

                return df

        except Exception as e:
            self.console.print(f"[dim]  Error reading dataset file {s3_key}: {e}[/dim]")
            return None