"""Timeout watchdog for wall-clock timeout enforcement.

Implements wall-clock timeout for agents per docs/INTERFACES.md Section H.4:
- Parse timeout strings like '10m', '2h', '30s'
- Start wall-clock timer when agent starts
- On expiry, transition agent state to failed via LifecycleEvent.timeout

This is a Shell module because it uses asyncio for timers.
Returns Result[T, E] from returns library.
"""

import asyncio
import threading
from typing import Callable

from returns.result import Failure, Result, Success

from anima.core.timeout_parser import parse_timeout
from anima.core.schemas import AnimaTimeoutError


class TimeoutWatchdog:
    """Wall-clock timeout enforcement for agent execution.

    Starts a timer that transitions agent to 'failed' state with TIMEOUT error
    when the wall-clock duration expires.

    Per docs/INTERFACES.md Section H.4:
    - Timeout fires during normal execution: immediate transition to failed
    - Timeout fires during grace: immediate transition to failed (no additional grace)
    - Timeout is a hard wall-clock limit, not a graceful shutdown

    Attributes:
        timeout_seconds: Timeout duration in seconds (or None for no timeout).
        instance_id: Agent instance identifier for state transitions.
        on_timeout: Callback invoked on timeout (receives instance_id).
        timer: Background thread timer (None if no timeout).
        started: Whether the watchdog has been started.
        cancelled: Whether the watchdog has been cancelled.
    """

    __slots__ = (
        "timeout_seconds",
        "instance_id",
        "on_timeout",
        "timer",
        "started",
        "cancelled",
        "_loop",
        "_timeout_task",
    )

    def __init__(
        self,
        timeout_seconds: int | float | None,
        instance_id: str,
        on_timeout: Callable[[str], None],
    ) -> None:
        """Initialize timeout watchdog.

        Args:
            timeout_seconds: Timeout in seconds, or None for no timeout.
            instance_id: Agent instance identifier passed to on_timeout callback.
            on_timeout: Callback invoked when timeout fires.
                Receives instance_id as argument. Should transition to failed.
        """
        self.timeout_seconds: int | float | None = timeout_seconds
        self.instance_id = instance_id
        self.on_timeout = on_timeout
        self.timer: threading.Timer | None = None
        self.started = False
        self.cancelled = False

    def start(self) -> Result[None, AnimaTimeoutError]:
        """Start the timeout watchdog.

        Returns:
            Success(None) if started successfully or no timeout configured.
            Failure(AnimaTimeoutError) if already started.

        Examples:
            >>> from anima.shell.timeout_watchdog import TimeoutWatchdog
            >>> called = []
            >>> watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
            >>> result = watchdog.start()
            >>> isinstance(result, Success)
            True
            >>> watchdog.started
            True
            >>> _ = watchdog.cancel()  # Returns Success(None)
            >>> watchdog.started
            False

            >>> # No timeout configured
            >>> watchdog_none = TimeoutWatchdog(None, "ag_456", lambda _: None)
            >>> result_none = watchdog_none.start()
            >>> isinstance(result_none, Success)
            True
            >>> watchdog_none.started
            False
        """
        # No timeout configured
        if self.timeout_seconds is None:
            return Success(None)

        # Already started
        if self.started:
            return Failure(AnimaTimeoutError("Timeout watchdog already started"))

        # Already cancelled
        if self.cancelled:
            return Failure(AnimaTimeoutError("Timeout watchdog already cancelled"))

        # Start timer in background thread
        # Using threading.Timer instead of asyncio to work in both sync and async contexts
        self.timer = threading.Timer(
            self.timeout_seconds,
            self._fire_timeout,
        )
        self.timer.daemon = True  # Don't block process exit
        self.timer.start()
        self.started = True

        return Success(None)

    def cancel(self) -> Result[None, AnimaTimeoutError]:
        """Cancel the timeout watchdog.

        Idempotent: safe to call multiple times.

        Returns:
            Success(None) if cancelled or not started.
            Failure(AnimaTimeoutError) if timeout already fired.

        Examples:
            >>> from anima.shell.timeout_watchdog import TimeoutWatchdog
            >>> watchdog = TimeoutWatchdog(60, "ag_123", lambda _: None)
            >>> _ = watchdog.start()
            >>> result = watchdog.cancel()
            >>> isinstance(result, Success)
            True
            >>> watchdog.cancelled
            True

            >>> # Cancel before start is idempotent
            >>> watchdog2 = TimeoutWatchdog(60, "ag_456", lambda _: None)
            >>> result2 = watchdog2.cancel()
            >>> isinstance(result2, Success)
            True
            >>> watchdog2.cancelled
            True
        """
        # Already started - check if fired
        if self.started:
            if self.timer is None:
                result: Result[None, AnimaTimeoutError] = Failure(
                    AnimaTimeoutError("Timeout already fired"),
                )
            else:
                # Timer still running - cancel it
                self.timer.cancel()
                self.timer = None
                self.started = False
                self.cancelled = True
                result = Success(None)
            return result

        # Not started yet - mark as cancelled
        self.cancelled = True
        return Success(None)

    def _fire_timeout(self) -> None:
        """Fire the timeout callback.

        Internal method called by timer thread.
        Transitions agent to failed state.
        """
        # Mark as fired
        self.timer = None
        self.started = False

        # Call the timeout callback
        try:
            self.on_timeout(self.instance_id)
        except Exception:
            # @broad-except: timeout callback safety net must never crash timer thread
            # Callback errors are logged but not raised
            # The state transition should handle errors internally
            pass

    @property
    def is_active(self) -> bool:
        """Check if the watchdog is currently active.

        Returns:
            True if started and not cancelled, False otherwise.
        """
        return self.started and not self.cancelled

    @property
    def time_remaining(self) -> float | None:
        """Get remaining time before timeout.

        Note: This is an approximation since the timer is in a background thread.

        Returns:
            Remaining seconds, or None if no timeout or not started.
            Returns 0.0 if already timed out.
        """
        if self.timeout_seconds is None:
            return None

        if not self.started or self.cancelled:
            return None

        # Timer thread doesn't expose remaining time directly
        # Return None for now as this is informational only
        return None


# @invar:allow dead_export: Factory function retained for CLI callers; enables timeout string parsing from user input
def create_timeout_watchdog(
    timeout: str | None,
    instance_id: str,
    on_timeout_callback: Callable[[str], None],
) -> Result[TimeoutWatchdog, AnimaTimeoutError]:
    """Create a timeout watchdog from a duration string.

    Shell factory function that parses timeout string and creates watchdog.

    Args:
        timeout: Timeout duration string (e.g., '10m', '2h', '30s'), or None.
        instance_id: Agent instance identifier for state transitions.
        on_timeout_callback: Callback invoked when timeout fires.

    Returns:
        Success with TimeoutWatchdog, or Failure with AnimaTimeoutError
        if timeout string is invalid.

    Examples:
        >>> from anima.shell.timeout_watchdog import create_timeout_watchdog
        >>> called = []
        >>> result = create_timeout_watchdog("10m", "inst_123", lambda iid: called.append(iid))
        >>> isinstance(result, Success)
        True
        >>> watchdog = result.unwrap()
        >>> watchdog.timeout_seconds
        600

        >>> # None timeout
        >>> result_none = create_timeout_watchdog(None, "inst_456", lambda _: None)
        >>> isinstance(result_none, Success)
        True
        >>> watchdog_none = result_none.unwrap()
        >>> watchdog_none.timeout_seconds is None
        True
    """
    # No timeout configured
    if timeout is None:
        return Success(TimeoutWatchdog(None, instance_id, on_timeout_callback))

    # Parse timeout string
    try:
        timeout_seconds = parse_timeout(timeout)
    except ValueError as e:
        return Failure(AnimaTimeoutError(f"Invalid timeout format: {e}"))

    # Create watchdog
    return Success(TimeoutWatchdog(timeout_seconds, instance_id, on_timeout_callback))


class AsyncTimeoutWatchdog:
    """Async version of timeout watchdog for use in async contexts.

    Uses asyncio.create_task instead of threading.Timer for async contexts.

    This is useful when the agent runner is already in an async context
    and you want the timeout to fire within the same event loop.

    Attributes:
        timeout_seconds: Timeout duration in seconds (or None for no timeout).
        instance_id: Agent instance identifier for state transitions.
        on_timeout: Async callback invoked on timeout (receives instance_id).
        timeout_task: Background asyncio task (None if no timeout).
        started: Whether the watchdog has been started.
        cancelled: Whether the watchdog has been cancelled.
    """

    __slots__ = (
        "timeout_seconds",
        "instance_id",
        "on_timeout",
        "timeout_task",
        "started",
        "cancelled",
    )

    def __init__(
        self,
        timeout_seconds: int | float | None,
        instance_id: str,
        on_timeout: Callable[[str], None],
    ) -> None:
        """Initialize async timeout watchdog.

        Args:
            timeout_seconds: Timeout in seconds, or None for no timeout.
            instance_id: Agent instance identifier passed to on_timeout callback.
            on_timeout: Callback invoked when timeout fires.
        """
        self.timeout_seconds: int | float | None = timeout_seconds
        self.instance_id = instance_id
        self.on_timeout = on_timeout
        self.timeout_task: asyncio.Task[None] | None = None
        self.started = False
        self.cancelled = False

    async def start(self) -> Result[None, AnimaTimeoutError]:
        """Start the async timeout watchdog.

        Returns:
            Success(None) if started successfully or no timeout configured.
            Failure(AnimaTimeoutError) if already started.
        """
        # No timeout configured
        if self.timeout_seconds is None:
            return Success(None)

        # Already started
        if self.started:
            return Failure(AnimaTimeoutError("Timeout watchdog already started"))

        # Already cancelled
        if self.cancelled:
            return Failure(AnimaTimeoutError("Timeout watchdog already cancelled"))

        # Start timeout task
        self.timeout_task = asyncio.create_task(self._timeout_coro())
        self.started = True

        return Success(None)

    async def cancel(self) -> Result[None, AnimaTimeoutError]:
        """Cancel the async timeout watchdog.

        Returns:
            Success(None) if cancelled or not started.
            Failure(AnimaTimeoutError) if timeout already fired.
        """
        if not self.started:
            return Success(None)

        if self.cancelled:
            return Success(None)

        # Timeout already fired
        if self.timeout_task is None:
            return Failure(AnimaTimeoutError("Timeout already fired"))

        # Cancel the task
        self.timeout_task.cancel()
        try:
            await self.timeout_task
        except asyncio.CancelledError:
            pass

        self.timeout_task = None
        self.started = False
        self.cancelled = True

        return Success(None)

    async def _timeout_coro(self) -> None:
        """Wait for timeout and fire callback."""
        # timeout_seconds is guaranteed to be int if this task is running
        # (checked in start())
        assert self.timeout_seconds is not None
        try:
            await asyncio.sleep(self.timeout_seconds)
            # Timeout fired
            self.timeout_task = None
            self.started = False
            # Call callback
            self.on_timeout(self.instance_id)
        except asyncio.CancelledError:
            # Cancelled before timeout
            self.started = False
            raise

    @property
    def is_active(self) -> bool:
        """Check if the watchdog is currently active."""
        return self.started and not self.cancelled


async def create_async_timeout_watchdog(
    timeout: str | None,
    instance_id: str,
    on_timeout_callback: Callable[[str], None],
) -> Result[AsyncTimeoutWatchdog, AnimaTimeoutError]:
    """Create an async timeout watchdog from a duration string.

    Args:
        timeout: Timeout duration string (e.g., '10m', '2h', '30s'), or None.
        instance_id: Agent instance identifier for state transitions.
        on_timeout_callback: Callback invoked when timeout fires.

    Returns:
        Success with AsyncTimeoutWatchdog, or Failure with AnimaTimeoutError
        if timeout string is invalid.
    """
    # No timeout configured
    if timeout is None:
        return Success(AsyncTimeoutWatchdog(None, instance_id, on_timeout_callback))

    # Parse timeout string
    try:
        timeout_seconds = parse_timeout(timeout)
    except ValueError as e:
        return Failure(AnimaTimeoutError(f"Invalid timeout format: {e}"))

    # Create watchdog
    return Success(
        AsyncTimeoutWatchdog(timeout_seconds, instance_id, on_timeout_callback)
    )
