"""
Decorators for Gattopy.
"""

import inspect
import time
import tracemalloc
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Tuple

from .core import Profiler, ProfileResult


def gatto(
    func: Optional[Callable] = None,
    *,
    show_memory: bool = True,
    show_args: bool = True,
    precision: int = 6,
    return_data: bool = False,
    silent: bool = False,
    accumulate: bool = True,
) -> Callable:
    """
    Main decorator for Gattopy profiling.

    This is the primary interface for profiling functions with customizable options.
    All calls are automatically accumulated for later analysis.

    Args:
        func: The function to decorate (when used without parentheses)
        show_memory: Whether to track and display memory usage (default: True)
        show_args: Whether to capture and display function arguments (default: True)
        precision: Number of decimal places for time measurements (default: 6)
        return_data: If True, return (result, stats) tuple; if False, return only result (default: False)
        silent: If True, don't print reports for each call (default: False)
        accumulate: If True, automatically accumulate all calls for later analysis (default: True)

    Returns:
        Decorated function that profiles execution and provides analysis methods

    Methods available on decorated function:
        .print_summary() - Print summary of all accumulated calls
        .get_stats() - Get list of all statistics dictionaries
        .get_profiles() - Get list of all ProfileResult objects
        .export_stats(filepath) - Export to JSON/CSV/MD (auto-detects format)
        .call_count() - Get number of accumulated calls
        .clear_stats() - Clear all accumulated data

    Example:
        # Simple usage - just decorate and call
        @gatto
        def my_function(x, y):
            return x + y

        # Call normally - stats accumulate automatically
        result1 = my_function(5, 3)
        result2 = my_function(10, 20)

        # Analyze when needed
        my_function.print_summary()
        my_function.export_stats('results.json')

        # Silent mode for batch processing
        @gatto(silent=True)
        def process(data):
            return analyze(data)

        for item in big_list:
            process(item)  # No console spam

        process.print_summary()  # See results at the end
    """

    def decorator(f: Callable) -> Callable:
        # Storage for accumulated results
        all_profiles: List[ProfileResult] = []
        all_stats: List[Dict] = []

        @wraps(f)
        def wrapper(*args, **kwargs):
            # Step 1: Create and configure Profiler internally
            profiler = Profiler(name=f.__name__, track_memory=show_memory)

            # Step 2 & 3: Capture arguments if requested
            captured_args = None
            if show_args:
                captured_args = _capture_function_args(f, args, kwargs)

            # Step 4: Execute function with profiling
            with profiler:
                result = f(*args, **kwargs)

            # Step 5: Build statistics dictionary
            stats = _build_stats_dict(
                profiler.result, captured_args, precision, show_memory, show_args
            )

            # Step 6 & 7: Format and print report (unless silent)
            if not silent:
                from .reporting import format_profile_report

                report = format_profile_report(stats)
                print(report)

            # Store stats in function attributes
            wrapper.last_stats = stats
            wrapper.last_profile = profiler.result

            # Accumulate if enabled
            if accumulate:
                all_profiles.append(profiler.result)
                all_stats.append(stats)

            # Step 8: Return based on return_data flag
            if return_data:
                return result, stats
            else:
                return result

        # Attach attributes and methods
        wrapper.last_stats = None
        wrapper.last_profile = None
        wrapper._all_profiles = all_profiles
        wrapper._all_stats = all_stats

        # Helper methods
        def get_stats() -> List[Dict]:
            """Get all accumulated statistics."""
            return list(all_stats)

        def get_profiles() -> List[ProfileResult]:
            """Get all accumulated ProfileResult objects."""
            return list(all_profiles)

        def get_summary() -> str:
            """Get formatted summary of all accumulated calls."""
            if not all_profiles:
                return "No profiling data accumulated yet."

            from .reporting import Reporter

            reporter = Reporter()
            for profile in all_profiles:
                reporter.add_result(profile)
            return reporter.generate_summary()

        def print_summary():
            """Print summary of all accumulated calls."""
            print(get_summary())

        def export_stats(filepath: str, format: str = "auto"):
            """
            Export accumulated statistics to file.

            Args:
                filepath: Path to output file
                format: 'json', 'csv', 'md', or 'auto' (detect from extension)
            """
            import json
            from pathlib import Path

            path = Path(filepath)

            # Auto-detect format
            if format == "auto":
                ext = path.suffix.lower()
                if ext == ".json":
                    format = "json"
                elif ext == ".csv":
                    format = "csv"
                elif ext == ".md":
                    format = "md"
                else:
                    format = "json"

            # Create parent directory if needed
            path.parent.mkdir(parents=True, exist_ok=True)

            if format == "json":
                with open(path, "w", encoding="utf-8") as outfile:
                    json.dump(
                        {
                            "function": f.__name__,
                            "total_calls": len(all_stats),
                            "calls": all_stats,
                        },
                        outfile,
                        indent=2,
                        ensure_ascii=False,
                    )

            elif format == "csv":
                import csv

                if all_stats:
                    with open(path, "w", newline="", encoding="utf-8") as csvfile:
                        # Get all unique keys from all stats
                        all_keys = set()
                        for s in all_stats:
                            all_keys.update(s.keys())

                        # Remove nested dicts
                        keys = [
                            k
                            for k in all_keys
                            if not isinstance(all_stats[0].get(k), dict)
                        ]

                        writer = csv.DictWriter(csvfile, fieldnames=sorted(keys))
                        writer.writeheader()

                        for s in all_stats:
                            row = {k: s.get(k) for k in keys}
                            writer.writerow(row)

            elif format == "md":
                from .reporting import Reporter

                reporter = Reporter()
                for profile in all_profiles:
                    reporter.add_result(profile)
                reporter.save_markdown(filepath)

            return filepath

        def clear_stats():
            """Clear all accumulated statistics."""
            all_profiles.clear()
            all_stats.clear()

        def call_count() -> int:
            """Get number of accumulated calls."""
            return len(all_stats)

        # Attach methods to wrapper
        wrapper.get_stats = get_stats
        wrapper.get_profiles = get_profiles
        wrapper.get_summary = get_summary
        wrapper.print_summary = print_summary
        wrapper.export_stats = export_stats
        wrapper.clear_stats = clear_stats
        wrapper.call_count = call_count

        return wrapper

    # Handle both @gatto and @gatto()
    if func is None:
        return decorator
    else:
        return decorator(func)


def _capture_function_args(func: Callable, args: tuple, kwargs: dict) -> Dict[str, Any]:
    """
    Capture function arguments with their names.

    Args:
        func: The function being called
        args: Positional arguments
        kwargs: Keyword arguments

    Returns:
        Dictionary mapping argument names to values
    """
    sig = inspect.signature(func)
    bound_args = sig.bind(*args, **kwargs)
    bound_args.apply_defaults()

    return dict(bound_args.arguments)


def _build_stats_dict(
    profile_result: ProfileResult,
    captured_args: Optional[Dict[str, Any]],
    precision: int,
    show_memory: bool,
    show_args: bool,
) -> Dict[str, Any]:
    """
    Build comprehensive statistics dictionary.

    Args:
        profile_result: ProfileResult from profiler
        captured_args: Captured function arguments (if show_args=True)
        precision: Decimal precision for time values
        show_memory: Whether memory stats are included
        show_args: Whether arguments are included

    Returns:
        Dictionary with all profiling statistics
    """
    stats = {
        "function_name": profile_result.function_name,
        "execution_time": (
            round(profile_result.execution_time, precision)
            if profile_result.execution_time
            else None
        ),
        "execution_time_formatted": (
            profile_result._format_time(profile_result.execution_time)
            if profile_result.execution_time
            else None
        ),
    }

    if show_memory:
        stats["memory_peak_mb"] = (
            round(profile_result.memory_peak, precision)
            if profile_result.memory_peak
            else None
        )
        stats["memory_current_mb"] = (
            round(profile_result.memory_current, precision)
            if profile_result.memory_current
            else None
        )

    if show_args and captured_args:
        stats["arguments"] = _format_args_for_display(captured_args)

    stats["calls_count"] = profile_result.calls_count
    stats["metadata"] = profile_result.metadata

    return stats


def _format_args_for_display(args_dict: Dict[str, Any]) -> Dict[str, str]:
    """
    Format arguments for clean display.

    Args:
        args_dict: Dictionary of argument names and values

    Returns:
        Dictionary with formatted string representations
    """
    formatted = {}
    for name, value in args_dict.items():
        # Truncate long values
        str_value = str(value)
        if len(str_value) > 100:
            str_value = str_value[:97] + "..."
        formatted[name] = str_value

    return formatted


def profile_time(
    func: Optional[Callable] = None, *, print_result: bool = True
) -> Callable:
    """
    Decorator to profile execution time of a function.

    Args:
        func: The function to profile
        print_result: Whether to print the result automatically

    Example:
        @profile_time
        def my_function():
            time.sleep(1)

        @profile_time(print_result=False)
        def get_data():
            return expensive_operation()
    """

    def decorator(f: Callable) -> Callable:
        @wraps(f)
        def wrapper(*args, **kwargs):
            start_time = time.perf_counter()
            result = f(*args, **kwargs)
            end_time = time.perf_counter()

            execution_time = end_time - start_time
            profile_result = ProfileResult(
                function_name=f.__name__, execution_time=execution_time
            )

            # Store result as attribute
            wrapper.last_profile = profile_result

            if print_result:
                print(profile_result)

            return result

        wrapper.last_profile = None
        return wrapper

    # Handle both @profile_time and @profile_time()
    if func is None:
        return decorator
    else:
        return decorator(func)


def profile_memory(
    func: Optional[Callable] = None, *, print_result: bool = True
) -> Callable:
    """
    Decorator to profile memory usage of a function.

    Args:
        func: The function to profile
        print_result: Whether to print the result automatically

    Example:
        @profile_memory
        def create_large_list():
            return [i for i in range(1000000)]

        @profile_memory(print_result=False)
        def process_data():
            data = load_data()
            return transform(data)
    """

    def decorator(f: Callable) -> Callable:
        @wraps(f)
        def wrapper(*args, **kwargs):
            tracemalloc.start()
            result = f(*args, **kwargs)
            current, peak = tracemalloc.get_traced_memory()
            tracemalloc.stop()

            profile_result = ProfileResult(
                function_name=f.__name__,
                memory_peak=peak / 1024 / 1024,  # Convert to MB
                memory_current=current / 1024 / 1024,
            )

            # Store result as attribute
            wrapper.last_profile = profile_result

            if print_result:
                print(profile_result)

            return result

        wrapper.last_profile = None
        return wrapper

    # Handle both @profile_memory and @profile_memory()
    if func is None:
        return decorator
    else:
        return decorator(func)


def profile(
    func: Optional[Callable] = None,
    *,
    print_result: bool = True,
    track_memory: bool = True,
) -> Callable:
    """
    Decorator to profile both time and memory usage of a function.

    Args:
        func: The function to profile
        print_result: Whether to print the result automatically
        track_memory: Whether to track memory (can be disabled for performance)

    Example:
        @profile
        def complex_operation():
            data = [i**2 for i in range(1000000)]
            return sum(data)

        @profile(print_result=False, track_memory=False)
        def fast_function():
            return sum(range(1000))
    """

    def decorator(f: Callable) -> Callable:
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Start time tracking
            start_time = time.perf_counter()

            # Start memory tracking if enabled
            if track_memory:
                tracemalloc.start()

            # Execute function
            result = f(*args, **kwargs)

            # Stop time tracking
            end_time = time.perf_counter()
            execution_time = end_time - start_time

            # Stop memory tracking if enabled
            memory_peak = None
            memory_current = None
            if track_memory:
                current, peak = tracemalloc.get_traced_memory()
                tracemalloc.stop()
                memory_peak = peak / 1024 / 1024  # Convert to MB
                memory_current = current / 1024 / 1024

            # Create profile result
            profile_result = ProfileResult(
                function_name=f.__name__,
                execution_time=execution_time,
                memory_peak=memory_peak,
                memory_current=memory_current,
            )

            # Store result as attribute
            wrapper.last_profile = profile_result

            if print_result:
                print(profile_result)

            return result

        wrapper.last_profile = None
        return wrapper

    # Handle both @profile and @profile()
    if func is None:
        return decorator
    else:
        return decorator(func)
