"""
Utility functions for the digit counter package.
"""

import time
import logging
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from contextlib import contextmanager
from decimal import Decimal
from functools import wraps

from .constants import DEFAULT_PRECISION, MAX_PRECISION, SCIENTIFIC_NOTATION_CHARS
from .exceptions import CalculationError
from .types import DigitCountResult, BatchResult


def create_digit_counter(
    precision: int = DEFAULT_PRECISION,
    enable_cache: bool = True,
    log_level: Union[int, str] = logging.INFO,
    **kwargs
):
    """Factory function to create DigitCounter instance."""
    from .core import DigitCounter  # Local import to avoid circular import
    return DigitCounter(
        precision=precision,
        enable_cache=enable_cache,
        log_level=log_level,
        **kwargs
    )


@contextmanager
def timer() -> Callable[[], float]:
    """Context manager for timing code execution."""
    start = time.perf_counter()

    def get_elapsed() -> float:
        return time.perf_counter() - start

    try:
        yield get_elapsed
    finally:
        _ = get_elapsed()


def measure_performance(
    func: Callable,
    *args,
    iterations: int = 1000,
    warmup: int = 100,
    **kwargs
) -> Dict[str, Any]:
    """Measure performance of a function."""
    # Warmup
    for _ in range(warmup):
        func(*args, **kwargs)

    times = []
    for _ in range(iterations):
        start = time.perf_counter()
        func(*args, **kwargs)
        end = time.perf_counter()
        times.append((end - start) * 1000)

    import statistics

    return {
        "iterations": iterations,
        "total_time_ms": sum(times),
        "average_time_ms": statistics.mean(times),
        "median_time_ms": statistics.median(times),
        "min_time_ms": min(times),
        "max_time_ms": max(times),
        "stdev_time_ms": (statistics.stdev(times) if len(times) > 1 else 0),
    }


def validate_precision(precision: int) -> int:
    """Validate and clamp precision value."""
    if precision is None:
        return DEFAULT_PRECISION
    if not isinstance(precision, int):
        raise ValueError(f"Precision must be an int: {precision!r}")
    if precision < 0:
        raise ValueError(f"Precision cannot be negative: {precision}")
    return min(precision, MAX_PRECISION)


def format_number(value: Union[int, float, Decimal, str], precision: Optional[int] = None) -> str:
    """Format number for a stable string representation."""
    if precision is None:
        precision = DEFAULT_PRECISION

    if isinstance(value, Decimal):
        # str(Decimal.normalize()) may use exponent for small/large numbers
        return format(value, "f").rstrip("0").rstrip(".") if "." in format(value, "f") else format(value, "f")
    if isinstance(value, float):
        # repr gives full precision; limit if precision provided
        return repr(value)
    return str(value)


def parse_scientific_notation(value_str: str) -> Tuple[Decimal, int]:
    """Parse scientific notation string into (mantissa, exponent)."""
    try:
        if "e" in value_str or "E" in value_str:
            mantissa_str, exponent_str = value_str.lower().split("e")
            mantissa = Decimal(mantissa_str)
            exponent = int(exponent_str)
            return mantissa, exponent
        raise ValueError("Not in scientific notation")
    except Exception as exc:
        raise ValueError(f"Cannot parse scientific notation '{value_str}': {exc}") from exc


def setup_logging(
    level: Union[int, str] = logging.INFO,
    name: str = "digit_counter",
    format_str: Optional[str] = None
) -> logging.Logger:
    """Setup logging configuration and return logger."""
    if format_str is None:
        format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

    if isinstance(level, str):
        level = getattr(logging, level.upper(), logging.INFO)

    # Avoid reconfiguring logging multiple times in interactive use
    logger = logging.getLogger(name)
    if not logger.handlers:
        handler = logging.StreamHandler()
        formatter = logging.Formatter(format_str)
        handler.setFormatter(formatter)
        logger.addHandler(handler)
    logger.setLevel(level)
    return logger


def retry_on_failure(
    max_attempts: int = 3,
    delay: float = 0.1,
    exceptions: tuple = (Exception,),
    logger: Optional[logging.Logger] = None
):
    """Decorator for retrying a function on failure."""
    def decorator(func: Callable):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as exc:
                    last_exception = exc
                    if logger:
                        logger.warning(f"Attempt {attempt + 1}/{max_attempts} failed: {exc}")
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            if logger:
                logger.error(f"All {max_attempts} attempts failed")
            raise last_exception
        return wrapper
    return decorator
