#!/usr/bin/env python3
# File: src/noveler/infrastructure/di/service_locator.py
# Purpose: Provide a pytest-xdist aware service locator with worker-isolated caches.
# Context: Infrastructure dependency resolution entry-point consumed by application services and tests.

"""Service locator implementation with pytest-xdist worker isolation.

This module exposes the infrastructure-facing `ServiceLocator` that lazily
creates services and scopes its cache per pytest-xdist worker (or OS process
when xdist is not active). The design prevents cross-worker leakage while
preserving the existing lazy-loading semantics used by the application layer.
"""

from __future__ import annotations

import atexit
import os
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, TypeVar, cast

from noveler.domain.interfaces.service_locator_protocol import IServiceLocator
from noveler.infrastructure.factories.base_service_factory import get_service_factory
from noveler.infrastructure.patterns.lazy_proxy import LazyProxy

if TYPE_CHECKING:
    from noveler.domain.protocols import (
        IConfigurationServiceProtocol,
        IConsoleServiceProtocol,
        ILoggerProtocol,
        IPathServiceProtocol,
        IRepositoryFactoryProtocol,
        IUnitOfWorkProtocol,
    )

T = TypeVar("T")


class ServiceLocator(IServiceLocator):
    """Lazy service locator that isolates caches per pytest-xdist worker.

    Purpose:
        Lazily create infrastructure services and scope them per worker to avoid
        cache leakage when pytest-xdist spawns multiple workers.

    Preconditions:
        - `noveler.infrastructure.factories.base_service_factory.get_service_factory`
          returns a factory exposing the expected `create_*` callables.

    Side Effects:
        - Registers an `atexit` handler that clears the cache for the active worker.
        - Stores lazy proxies in memory for the lifetime of the process/worker.
    """

    def __init__(self) -> None:
        self._factory = get_service_factory()
        self._worker_id = self._resolve_worker_id()
        # Cache structure: {worker_id: {service_type: LazyProxy | Service}}
        self._cache: dict[str, dict[str, Any]] = {}
        atexit.register(self._cleanup_worker_cache)

    def _resolve_worker_id(self) -> str:
        """Resolve the pytest-xdist worker identifier or fall back to process ID."""
        worker = os.environ.get("PYTEST_XDIST_WORKER")
        if worker:
            return worker
        return f"pid-{os.getpid()}"

    def _get_worker_id(self) -> str:
        """Return the worker identifier used for cache partitioning."""
        return self._worker_id

    def _get_cache_for_worker(self) -> dict[str, Any]:
        """Return (and initialise when needed) the cache bucket for the worker."""
        worker_id = self._get_worker_id()
        if worker_id not in self._cache:
            self._cache[worker_id] = {}
        return self._cache[worker_id]

    def _cleanup_worker_cache(self) -> None:
        """Remove cache entries for the current worker when the process exits."""
        worker_id = self._get_worker_id()
        if worker_id in self._cache:
            self._cache.pop(worker_id)

    def _get_or_create(self, service_type: str, factory: Callable[[], T]) -> LazyProxy[T]:
        """Return a cached service proxy for the worker, creating it when missing."""
        cache = self._get_cache_for_worker()
        if service_type not in cache:
            cache[service_type] = LazyProxy(factory)
        return cast("LazyProxy[T]", cache[service_type])

    def clear_cache(self) -> None:
        """Clear cached services for the active worker only."""
        cache = self._get_cache_for_worker()
        cache.clear()

    def is_cached(self, service_type: str) -> bool:
        """Check whether the given service type exists in the worker cache."""
        return service_type in self._get_cache_for_worker()

    def is_initialized(self, service_type: str) -> bool:
        """Determine whether the cached proxy has already materialised its service."""
        cache = self._get_cache_for_worker()
        if service_type not in cache:
            return False
        proxy = cache[service_type]
        if isinstance(proxy, LazyProxy):
            return proxy.is_initialized
        return True

    def get_logger_service(self) -> ILoggerProtocol:
        """Return the logger service proxy scoped to the worker."""
        return cast(
            "ILoggerProtocol",
            self._get_or_create("ILoggerProtocol", self._factory.create_logger_service),
        )

    def get_unit_of_work(self) -> IUnitOfWorkProtocol:
        """Return the unit of work service proxy scoped to the worker."""
        return cast(
            "IUnitOfWorkProtocol",
            self._get_or_create("IUnitOfWorkProtocol", self._factory.create_unit_of_work),
        )

    def get_console_service(self) -> IConsoleServiceProtocol:
        """Return the console service proxy scoped to the worker."""
        return cast(
            "IConsoleServiceProtocol",
            self._get_or_create("IConsoleServiceProtocol", self._factory.create_console_service),
        )

    def get_path_service(self, project_root: str | None = None) -> IPathServiceProtocol:
        """Return a path service proxy; bypass cache when a custom root is provided."""
        if project_root is not None:
            return cast(
                "IPathServiceProtocol",
                LazyProxy(lambda: self._factory.create_path_service(project_root)),
            )
        return cast(
            "IPathServiceProtocol",
            self._get_or_create("IPathServiceProtocol", self._factory.create_path_service),
        )

    def get_configuration_service(self) -> IConfigurationServiceProtocol:
        """Return the configuration service proxy scoped to the worker."""
        return cast(
            "IConfigurationServiceProtocol",
            self._get_or_create(
                "IConfigurationServiceProtocol",
                self._factory.create_configuration_service,
            ),
        )

    def get_repository_factory(self) -> IRepositoryFactoryProtocol:
        """Return the repository factory proxy scoped to the worker."""
        return cast(
            "IRepositoryFactoryProtocol",
            self._get_or_create(
                "IRepositoryFactoryProtocol",
                self._factory.create_repository_factory,
            ),
        )


class ServiceLocatorManager:
    """Singleton manager that exposes a process-wide ServiceLocator instance."""

    _instance: ServiceLocatorManager | None = None
    _locator: ServiceLocator | None = None

    def __new__(cls) -> ServiceLocatorManager:
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    @property
    def locator(self) -> ServiceLocator:
        """Return the lazily created ServiceLocator singleton."""
        if self._locator is None:
            self._locator = ServiceLocator()
        return self._locator

    def set_locator(self, locator: ServiceLocator) -> None:
        """Override the managed locator (primarily for tests)."""
        self._locator = locator

    def reset(self) -> None:
        """Reset the managed locator so a new instance is created on next access."""
        self._locator = None


def get_service_locator() -> ServiceLocator:
    """Convenience accessor mirroring the historical global helper."""
    manager = ServiceLocatorManager()
    return manager.locator
