import logging
import timeit
from functools import wraps
from typing import Callable
from typing_extensions import Self

logger = logging.getLogger(__name__)


class ExecTimer:
    """Times a section of code using wall-clock elapsed time.

    Can be used as a context manager or as a function decorator.
    """

    def __init__(
        self,
        logging_text: str | None = None,
        post_action: Callable[[float], None] | None = None,
        log_at_entry: bool = False,
        log_level: int = logging.INFO,
        stack_level: int = 2,
    ):
        """Constructor.

        Args:
            logging_text: Message included in log output. If None, no logging is emitted.
            post_action: Callable invoked with the elapsed time (seconds) after the block exits.
            log_at_entry: If True, logs the text with a trailing "..." on block entry.
            log_level: Python logging level used for all log calls.
            stack_level: Stack depth offset forwarded to the logger.
        """
        self._logging_text = logging_text
        self._post_action = post_action
        self._log_at_entry = log_at_entry
        self._log_level = log_level
        self._stack_level = stack_level
        self._start: float | None = None
        self._interval: float | None = None

    def __enter__(self) -> Self:
        if self._log_at_entry and self._logging_text is not None:
            logger.log(
                self._log_level,
                "%s...",
                self._logging_text.capitalize(),
                stacklevel=self._stack_level,
            )
        self._start = timeit.default_timer()
        return self

    def __exit__(self, *_) -> None:
        self._interval = timeit.default_timer() - self._start
        if self._logging_text is not None:
            logger.log(
                self._log_level,
                "Took %.3f sec for %s",
                self._interval,
                self._logging_text,
                stacklevel=self._stack_level,
            )
        if self._post_action is not None:
            self._post_action(self._interval)

    def __call__(self, func: Callable) -> Callable:
        """Returns a decorator that wraps *func* with this timer."""

        @wraps(func)
        def wrapper(*args, **kwargs):
            with type(self)(
                self._logging_text or f"running {func.__name__}",
                self._post_action,
                self._log_at_entry,
                self._log_level,
                3,
            ):
                return func(*args, **kwargs)

        return wrapper

    @property
    def interval(self) -> float | None:
        """Elapsed wall-clock time in seconds, or None if the timer has not yet exited."""
        return self._interval

    @property
    def text(self) -> str | None:
        """Logging text supplied at construction time."""
        return self._logging_text
