"""Devcontainer session cleanup and registry eviction.

Provides session-scoped container cleanup and terminal-state tracker
eviction for the devcontainer lifecycle system.

Extracted from ``devcontainer.py`` to comply with the 500-line file
limit (F28 fix).  The ``devcontainer`` module re-exports all public
symbols from this module for backward compatibility.

Based on:
    - docs/specification.md -- Devcontainer, Resource Types
    - Issue #514: Devcontainer lifecycle management
"""

from __future__ import annotations

import logging
from typing import Any

from cleveragents.domain.models.core.container_lifecycle import (
    ContainerLifecycleState,
)
from cleveragents.resource.handlers._devcontainer_internals import (
    _MAX_TERMINAL_TRACKERS,
    _lifecycle_registry,
    _registry_lock,
    list_active_containers,
)
from cleveragents.resource.handlers.devcontainer_lifecycle import (
    stop_container,
)

logger = logging.getLogger(__name__)


def list_active_containers_for_session(session_id: str) -> list[str]:
    """Return resource IDs of running containers owned by *session_id*.

    When *session_id* is empty, behaves identically to
    :func:`list_active_containers` (returns all running containers).

    Containers that have no ``session_id`` set (None) are considered
    "unscoped" and match any session cleanup — this ensures containers
    activated before session-scoping was added still get cleaned up.

    Args:
        session_id: Session or plan ID to filter by.

    Returns:
        List of resource_id strings.
    """
    if not session_id:
        return list_active_containers()
    with _registry_lock:
        result: list[str] = []
        for rid, tracker in _lifecycle_registry.items():
            if tracker.current_state != ContainerLifecycleState.RUNNING:
                continue
            if tracker.session_id == session_id:
                result.append(rid)
            elif tracker.session_id is None:
                # BUG-3 fix: warn when an unscoped container is included
                # in session-scoped cleanup — the first session to close
                # will stop containers that may still be used by others.
                logger.warning(
                    "Including unscoped container %s in session '%s' cleanup "
                    "(container has no session_id set)",
                    rid,
                    session_id,
                )
                result.append(rid)
        return result


def stop_all_active_containers(
    *,
    run_command: Any = None,
    session_id: str = "",
) -> list[str]:
    """Stop active devcontainer instances, optionally scoped to a session.

    When *session_id* is provided, only containers associated with that
    session are stopped.  When empty, all running containers are stopped
    (backwards-compatible behavior).

    Used as a cleanup hook when sessions close or plans complete.

    Args:
        run_command: Optional callable for testing.
        session_id: Optional session/plan ID to scope cleanup.

    Returns:
        List of resource IDs that were stopped.
    """
    # R7-F1 fix: scope cleanup to session_id when provided.
    active_ids = list_active_containers_for_session(session_id)
    stopped: list[str] = []
    for rid in active_ids:
        try:
            stop_container(rid, run_command=run_command)
            stopped.append(rid)
            logger.info(
                "Stopped container %s (session=%s)",
                rid,
                session_id or "n/a",
            )
        except (RuntimeError, ValueError) as exc:
            logger.warning("Failed to stop container %s: %s", rid, exc)

    # R8-F1 fix: evict oldest terminal-state trackers when the
    # registry exceeds the cap.
    evict_terminal_trackers()

    return stopped


def evict_terminal_trackers() -> int:
    """Remove stopped/failed trackers when the registry exceeds the cap.

    Keeps at most :data:`_MAX_TERMINAL_TRACKERS` terminal-state entries.
    Evicts oldest (by last transition timestamp) first.

    Returns:
        Number of trackers evicted.
    """
    terminal_states = frozenset(
        {ContainerLifecycleState.STOPPED, ContainerLifecycleState.FAILED}
    )
    with _registry_lock:
        terminal_ids = [
            rid
            for rid, t in _lifecycle_registry.items()
            if t.current_state in terminal_states
        ]
        if len(terminal_ids) <= _MAX_TERMINAL_TRACKERS:
            return 0

        # Sort by last transition timestamp (oldest first)
        def _last_ts(rid: str) -> float:
            t = _lifecycle_registry[rid]
            if t.transitions:
                return t.transitions[-1].timestamp.timestamp()
            return 0.0

        terminal_ids.sort(key=_last_ts)
        to_evict = terminal_ids[: len(terminal_ids) - _MAX_TERMINAL_TRACKERS]
        for rid in to_evict:
            del _lifecycle_registry[rid]
        return len(to_evict)
