"""Timing utilities for structured logging.

Provide decorators and context managers for timing operations with automatic
structured log output including duration metrics.
"""

from __future__ import annotations

import asyncio
import functools
import time
from contextlib import contextmanager
from typing import TYPE_CHECKING, ParamSpec, TypeVar

from .config import get_logger

if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable, Generator

P = ParamSpec("P")
T = TypeVar("T")


@contextmanager
def log_timing(
    operation: str,
    **context: str | int | float | None,
) -> Generator[None, None, None]:
    """Context manager for timing operations with structured logging.

    Log operation start and completion/failure with duration in milliseconds.
    Automatically handles timeout and general exceptions with appropriate
    log levels.

    Args:
        operation: Name of the operation being timed (used as event prefix).
        **context: Additional context fields to include in logs.

    Yields:
        None

    Example:
        with log_timing("ollama_availability_check", host=self._host):
            client.list()
    """
    logger = get_logger()
    # Filter out None values from context
    ctx = {k: v for k, v in context.items() if v is not None}

    logger.info(f"{operation}_started", **ctx)
    start = time.perf_counter()
    try:
        yield
        elapsed_ms = (time.perf_counter() - start) * 1000
        logger.info(f"{operation}_completed", duration_ms=round(elapsed_ms, 2), **ctx)
    except TimeoutError:
        elapsed_ms = (time.perf_counter() - start) * 1000
        logger.warning(f"{operation}_timeout", duration_ms=round(elapsed_ms, 2), **ctx)
        raise
    except Exception as exc:
        elapsed_ms = (time.perf_counter() - start) * 1000
        logger.error(
            f"{operation}_failed",
            duration_ms=round(elapsed_ms, 2),
            error=str(exc),
            error_type=type(exc).__name__,
            **ctx,
        )
        raise


def timed(operation: str) -> Callable[[Callable[P, T]], Callable[P, T]]:
    """Decorate function to log timing with structured output.

    Automatically detect sync vs async functions and wrap appropriately.
    Log operation start, completion with duration, or failure with error details.

    Args:
        operation: Name of the operation (used as log event prefix).

    Returns:
        Decorator that wraps function with timing logs.

    Example:
        @timed("transcribe_audio")
        async def transcribe_async(self, audio: bytes) -> list[AsrResult]:
            ...

        @timed("parse_config")
        def parse_config(self, path: Path) -> Config:
            ...
    """

    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        if asyncio.iscoroutinefunction(func):

            @functools.wraps(func)
            async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
                logger = get_logger()
                logger.info(f"{operation}_started")
                start = time.perf_counter()
                try:
                    # Cast needed for async function return type
                    coro: Awaitable[T] = func(*args, **kwargs)  # type: ignore[assignment]
                    result = await coro
                    elapsed_ms = (time.perf_counter() - start) * 1000
                    logger.info(
                        f"{operation}_completed",
                        duration_ms=round(elapsed_ms, 2),
                    )
                    return result
                except TimeoutError:
                    elapsed_ms = (time.perf_counter() - start) * 1000
                    logger.warning(
                        f"{operation}_timeout",
                        duration_ms=round(elapsed_ms, 2),
                    )
                    raise
                except Exception as exc:
                    elapsed_ms = (time.perf_counter() - start) * 1000
                    logger.error(
                        f"{operation}_failed",
                        duration_ms=round(elapsed_ms, 2),
                        error=str(exc),
                        error_type=type(exc).__name__,
                    )
                    raise

            return async_wrapper  # type: ignore[return-value]

        @functools.wraps(func)
        def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            with log_timing(operation):
                return func(*args, **kwargs)

        return sync_wrapper

    return decorator
