"""Shared state and utilities for devcontainer lifecycle modules.

Internal module providing the lifecycle registry, thread-safe
accessors, constants, and output-parsing helpers shared by
``devcontainer_lifecycle``, ``devcontainer_health``, and
``devcontainer_cleanup``.

Extracted from ``devcontainer.py`` to break circular dependencies
while keeping files under the 500-line limit (F28 fix).

.. warning::
    This is an internal module. External code should import from
    ``cleveragents.resource.handlers.devcontainer`` instead.
"""

from __future__ import annotations

import json
import logging
import re
import threading

from cleveragents.domain.models.core.container_lifecycle import (
    ContainerLifecycleState,
    ContainerLifecycleTracker,
)

logger = logging.getLogger(__name__)

# ── Constants ────────────────────────────────────────────────────

#: Default health check interval in seconds.
DEFAULT_HEALTH_CHECK_INTERVAL: float = 30.0

#: S1 fix: regex pattern for valid Docker container IDs (12-64 hex chars).
_CONTAINER_ID_PATTERN = re.compile(r"^[a-f0-9]{12,64}$")

#: Maximum number of terminal-state trackers before eviction.
_MAX_TERMINAL_TRACKERS: int = 200

# ── Shared mutable state ─────────────────────────────────────────

#: Registry of lifecycle trackers keyed by resource_id.
_lifecycle_registry: dict[str, ContainerLifecycleTracker] = {}
_registry_lock = threading.RLock()

#: Background health check threads keyed by resource_id.
_health_check_threads: dict[str, threading.Thread] = {}
_health_check_stop_events: dict[str, threading.Event] = {}

# ── Registry accessors ───────────────────────────────────────────


def get_lifecycle_tracker(resource_id: str) -> ContainerLifecycleTracker:
    """Get or create a lifecycle tracker for a resource.

    Args:
        resource_id: The ULID of the devcontainer resource.

    Returns:
        The current lifecycle tracker.
    """
    if not resource_id:
        raise ValueError("resource_id must not be empty")
    with _registry_lock:
        if resource_id not in _lifecycle_registry:
            _lifecycle_registry[resource_id] = ContainerLifecycleTracker(
                resource_id=resource_id,
            )
        return _lifecycle_registry[resource_id]


def set_lifecycle_tracker(tracker: ContainerLifecycleTracker) -> None:
    """Update the lifecycle tracker in the registry.

    Args:
        tracker: The tracker to store.
    """
    if not isinstance(tracker, ContainerLifecycleTracker):
        raise TypeError("tracker must be a ContainerLifecycleTracker")
    with _registry_lock:
        _lifecycle_registry[tracker.resource_id] = tracker


def list_active_containers() -> list[str]:
    """Return resource IDs of all containers in the ``running`` state.

    Returns:
        List of resource_id strings.
    """
    with _registry_lock:
        return [
            rid
            for rid, tracker in _lifecycle_registry.items()
            if tracker.current_state == ContainerLifecycleState.RUNNING
        ]


# ── Output parsing ───────────────────────────────────────────────


def _parse_devcontainer_up_output(stdout: str) -> tuple[str | None, str | None]:
    """Parse JSON output from ``devcontainer up``.

    The devcontainer CLI emits JSON like::

        {"outcome": "success", "containerId": "abc123",
         "remoteWorkspaceFolder": "/workspaces/project"}

    The CLI may emit log lines before the final JSON object (R4 fix).
    This function scans lines in reverse to find the last valid JSON
    object containing the expected fields.

    Args:
        stdout: Raw stdout from devcontainer up.

    Returns:
        Tuple of (container_id, workspace_path). Either may be None
        if the field is absent.
    """
    container_id: str | None = None
    workspace_path: str | None = None

    # R4 fix: iterate lines in reverse to handle log lines before JSON
    for line in reversed(stdout.splitlines()):
        line = line.strip()
        if not line:
            continue
        try:
            data = json.loads(line)
            if isinstance(data, dict):
                raw_id = data.get("containerId")
                # S1 fix: validate container_id format (12-64 hex chars)
                if raw_id and _CONTAINER_ID_PATTERN.match(raw_id):
                    container_id = raw_id
                elif raw_id:
                    logger.warning("Ignoring invalid container ID format: %s", raw_id)
                # F11 fix: validate remoteWorkspaceFolder is an absolute
                # path (must start with "/"), consistent with S1
                # container ID validation.
                raw_ws = data.get("remoteWorkspaceFolder")
                if raw_ws and isinstance(raw_ws, str) and raw_ws.startswith("/"):
                    workspace_path = raw_ws
                elif raw_ws:
                    logger.warning("Ignoring invalid workspace path: %s", raw_ws)
                return container_id, workspace_path
        except (json.JSONDecodeError, TypeError):
            continue

    # B3 fix: removed redundant fallback — the reverse-line loop above
    # already handles single-line JSON output.
    return container_id, workspace_path
