"""Resource Registry Service for managing resource types and resources.

Provides the application-layer service for:

- Registering / listing / showing **resource types** (built-in and custom)
- Registering / listing / showing **resource instances**
- Bootstrap registration of built-in types on startup (idempotent)

## Built-in Resource Types

CleverAgents ships with built-in resource types that cover common local
development workflows. Built-in types are **unnamespaced** (no
``namespace/`` prefix) and are available immediately without registration:

| Type              | Kind     | Sandbox          | Description                    |
|-------------------|----------|------------------|--------------------------------|
| ``git-checkout``  | physical | ``git_worktree`` | Git repository checkout        |
| ``fs-directory``  | physical | ``copy_on_write``| Filesystem directory           |
| ``container-runtime``| physical | ``none``      | Container runtime engine       |
| ``container-image``| physical | ``none``        | Container image (tag + digest) |
| ``container-mount``| physical | ``snapshot``    | Bind/volume mount in container |
| ``container-exec-env``| physical | ``snapshot`` | Container exec environment     |
| ``container-port`` | physical | ``snapshot``    | Container port mapping         |
| ``container-volume``| physical | ``snapshot``   | Named container volume         |
| ``container-network``| physical | ``none``      | Container network              |
| ``file``          | virtual  | ``none``         | Cross-layer file identity      |
| ``directory``     | virtual  | ``none``         | Cross-layer directory identity |
| ``commit``        | virtual  | ``none``         | Cross-repo commit identity     |
| ``branch``        | virtual  | ``none``         | Cross-repo branch identity     |
| ``tag``           | virtual  | ``none``         | Cross-repo tag identity        |
| ``tree``          | virtual  | ``none``         | Cross-repo tree identity       |
| ``remote``        | virtual  | ``none``         | Cross-repo remote identity     |
| ``submodule``     | virtual  | ``none``         | Cross-repo submodule identity  |
| ``symlink``       | virtual  | ``none``         | Cross-layer symlink identity   |
| ``git``           | physical | ``none``         | Git repository object database |
| ``git-remote``    | physical | ``none``         | Remote URL on a git repo       |
| ``git-branch``    | physical | ``none``         | Named branch ref               |
| ``git-tag``       | physical | ``none``         | Tag ref (lightweight/annotated)|
| ``git-commit``    | physical | ``none``         | Commit object                  |
| ``git-tree``      | physical | ``none``         | Tree object (directory listing)|
| ``git-tree-entry``| physical | ``none``         | Blob entry in a tree           |
| ``git-stash``     | physical | ``none``         | Stash entry                    |
| ``git-submodule`` | physical | ``none``         | Submodule reference            |
| ``fs-symlink``    | physical | ``copy_on_write``| Symbolic link                  |
| ``fs-hardlink``   | physical | ``copy_on_write``| Hard link                      |

Custom types must follow the ``namespace/name`` pattern and are registered
via YAML configuration files.

Based on:

- ``docs/specification.md`` -- Resource Registry
- ADR-003: Dependency Injection
- ADR-007: Repository Pattern

See Also:
    - [ResourceTypeSpec][cleveragents.domain.models.core.resource_type.ResourceTypeSpec]
    - [Resource][cleveragents.domain.models.core.resource.Resource]
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import yaml

from cleveragents.application.services._resource_registry_dag import ResourceDagMixin
from cleveragents.application.services._resource_registry_data import (
    BUILTIN_TYPES,
    db_to_spec,
    spec_to_db,
)
from cleveragents.application.services._resource_registry_ops import (
    ResourceInstanceMixin,
)
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.infrastructure.database.models import (
    ResourceModel,
    ResourceTypeModel,
)
from cleveragents.resource.inheritance import (
    ResourceTypeParentNotFoundError,
    ResourceTypeParentRemovalError,
    find_subtypes,
    is_subtype_of,
    resolve_inheritance_chain,
    validate_chain,
)

# Backward-compat aliases so test code that imports the old private
# names from this module continues to work.
_spec_to_db = spec_to_db
_db_to_spec = db_to_spec

logger = logging.getLogger(__name__)


class ResourceRegistryService(ResourceInstanceMixin, ResourceDagMixin):
    """Application service for the Resource Registry.

    Manages resource types and resource instances. Uses session-factory
    pattern for database access to match the repository conventions.

    Resource instance CRUD and DAG operations are provided by the
    ``ResourceInstanceMixin`` (see ``_resource_registry_ops``) and
    ``ResourceDagMixin`` (see ``_resource_registry_dag``) base classes.
    """

    def __init__(
        self,
        session_factory: Any,
    ) -> None:
        """Initialize the resource registry service.

        Automatically seeds built-in resource types (``fs-directory``,
        ``git-checkout``, virtual deferred types, etc.) on construction
        so they are available immediately after ``agents init``.  The
        bootstrap is idempotent and gracefully skipped when the database
        tables do not yet exist (e.g. before migrations have run).

        Args:
            session_factory: Callable returning a SQLAlchemy Session.
        """
        self._session_factory = session_factory
        try:
            self.bootstrap_builtin_types()
        except Exception:
            # Tables may not exist yet (pre-migration) or the session may
            # be a test mock.  Callers that need built-in types before
            # migrations complete should invoke bootstrap_builtin_types()
            # explicitly after the schema is ready.
            logger.debug(
                "Auto-bootstrap of built-in resource types deferred "
                "(tables may not exist yet)"
            )

    def _session(self) -> Any:
        """Convenience helper to obtain a session."""
        return self._session_factory()

    # -- Bootstrap -----------------------------------------------------------

    def bootstrap_builtin_types(self) -> list[str]:
        """Register built-in resource types if they are missing.

        Idempotent: existing types are not overwritten.

        Returns:
            List of type names that were newly registered.
        """
        registered: list[str] = []
        session = self._session()
        try:
            # Build a registry of already-persisted types so that
            # validate_chain can check inheritance for new built-ins
            # (e.g. devcontainer-instance inherits container-instance).
            existing_registry: dict[str, dict[str, Any]] = {}
            for row in session.query(ResourceTypeModel).all():
                rn = str(row.name)
                ri = getattr(row, "inherits", None)
                existing_registry[rn] = {
                    "inherits": str(ri) if ri else None,
                }

            for builtin_def in BUILTIN_TYPES:
                name = builtin_def["name"]
                if name in existing_registry:
                    continue

                existing = session.query(ResourceTypeModel).filter_by(name=name).first()
                if existing is not None:
                    existing_registry[name] = {
                        "inherits": getattr(existing, "inherits", None),
                    }
                    continue

                spec = ResourceTypeSpec.from_config(builtin_def)

                # Validate inheritance chain for built-ins that declare
                # inherits (e.g. devcontainer-instance -> container-instance).
                if spec.inherits is not None:
                    validate_chain(
                        spec.name,
                        spec.inherits,
                        existing_registry,
                        is_built_in=spec.built_in,
                    )

                db_model = spec_to_db(spec, source="builtin")
                session.add(db_model)
                session.flush()
                # Track the newly registered type so subsequent
                # built-ins can reference it as a parent.
                existing_registry[name] = {
                    "inherits": spec.inherits,
                }
                registered.append(name)
                logger.info("Registered built-in resource type: %s", name)

            session.commit()
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

        return registered

    # -- Resource Type operations --------------------------------------------

    def register_type(self, config_path: str | Path) -> ResourceTypeSpec:
        """Load a YAML resource type config, validate, and persist.

        If the type declares ``inherits``, the parent type must already
        be registered and the resulting chain is validated (ADR-042).

        Args:
            config_path: Path to the YAML configuration file.

        Returns:
            The validated ``ResourceTypeSpec``.

        Raises:
            ValidationError: If the config is invalid or chain rules
                are violated.
        """
        path = Path(config_path)
        if not path.exists():
            raise ValidationError(
                message=f"Config file not found: {path}",
                details={"path": str(path)},
            )

        raw_text = path.read_text(encoding="utf-8")
        config_data: dict[str, Any] = yaml.safe_load(raw_text) or {}
        spec = ResourceTypeSpec.from_config(config_data)

        session = self._session()
        try:
            # Acquire a row-level lock on the parent type (if any) to
            # prevent TOCTOU races where two concurrent registrations
            # could both pass cycle validation before either commits.
            if spec.inherits is not None:
                parent_row = (
                    session.query(ResourceTypeModel)
                    .filter_by(name=spec.inherits)
                    .with_for_update()
                    .first()
                )
                if parent_row is None:
                    raise ValidationError(
                        message=(
                            f"Parent type '{spec.inherits}' is not registered. "
                            f"Cannot register '{spec.name}' with "
                            f"inherits='{spec.inherits}'."
                        ),
                        details={"name": spec.name, "inherits": spec.inherits},
                    )

            existing = (
                session.query(ResourceTypeModel).filter_by(name=spec.name).first()
            )
            if existing is not None:
                raise ValidationError(
                    message=f"Resource type '{spec.name}' already exists",
                    details={"name": spec.name},
                )

            # Validate inheritance chain (ADR-042 rules 1-4).
            # NOTE: ADR-042 steps 3 (pre-compute merged fields) and 5
            # (validate handler loadability) are deferred to runtime.
            if spec.inherits is not None:
                registry = self._load_type_registry()
                try:
                    validate_chain(
                        spec.name,
                        spec.inherits,
                        registry,
                        is_built_in=spec.built_in,
                    )
                except (
                    ResourceTypeParentNotFoundError,
                    ValueError,
                ) as exc:
                    raise ValidationError(
                        message=str(exc),
                        details={"name": spec.name, "inherits": spec.inherits},
                    ) from exc

            db_model = spec_to_db(spec, source=str(path))
            session.add(db_model)
            session.flush()
            session.commit()
        except ValidationError:
            session.rollback()
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

        return spec

    def list_types(self, namespace: str | None = None) -> list[ResourceTypeSpec]:
        """List registered resource types.

        Args:
            namespace: Optional namespace filter. If None, lists all types.

        Returns:
            List of ``ResourceTypeSpec`` instances.
        """
        session = self._session()
        try:
            query = session.query(ResourceTypeModel)
            if namespace is not None:
                query = query.filter_by(namespace=namespace)
            query = query.order_by(ResourceTypeModel.name)
            rows = query.all()
            return [db_to_spec(row) for row in rows]
        finally:
            session.close()

    def show_type(self, name: str) -> ResourceTypeSpec:
        """Show details of a registered resource type.

        Args:
            name: Resource type name (built-in or namespaced).

        Returns:
            ``ResourceTypeSpec`` instance.

        Raises:
            NotFoundError: If the type is not found.
        """
        session = self._session()
        try:
            row = session.query(ResourceTypeModel).filter_by(name=name).first()
            if row is None:
                raise NotFoundError(
                    resource_type="resource_type",
                    resource_id=name,
                )
            return db_to_spec(row)
        finally:
            session.close()

    def remove_type(self, name: str) -> None:
        """Remove a custom resource type after safety checks.

        Enforces ADR-042 Rule 5: a type that is a parent of registered
        subtypes cannot be removed until the subtypes are removed first.

        Args:
            name: Name of the resource type to remove.

        Raises:
            NotFoundError: If the type is not registered.
            ValidationError: If the type is built-in or has resources.
            ResourceTypeParentRemovalError: If subtypes still reference
                this type via ``inherits``.
        """
        session = self._session()
        try:
            row = session.query(ResourceTypeModel).filter_by(name=name).first()
            if row is None:
                raise NotFoundError(
                    resource_type="resource_type",
                    resource_id=name,
                )

            spec = db_to_spec(row)
            if spec.built_in:
                raise ValidationError(
                    message=f"Cannot remove built-in resource type: {name}",
                    details={"name": name},
                )

            # Guard: reject if resources still reference this type
            resource_count: int = (
                session.query(ResourceModel)
                .filter(ResourceModel.type_name == name)
                .count()
            )
            if resource_count > 0:
                raise ValidationError(
                    message=(
                        f"Cannot remove type '{name}': "
                        f"{resource_count} resource(s) still reference it."
                    ),
                    details={"name": name, "resource_count": resource_count},
                )

            # Guard: ADR-042 Rule 5 — reject if subtypes exist
            registry = self._load_type_registry()
            subtypes = find_subtypes(name, registry)
            if subtypes:
                raise ResourceTypeParentRemovalError(
                    f"Cannot remove '{name}': subtypes exist: {subtypes}"
                )

            session.delete(row)
            session.commit()
        except (NotFoundError, ValidationError, ResourceTypeParentRemovalError):
            session.rollback()
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

    def resolve_type_inheritance_chain(self, type_name: str) -> list[str]:
        """Resolve the full inheritance chain for a resource type.

        Walks the ``inherits`` links from *type_name* to the root ancestor.

        Args:
            type_name: Name of the resource type to resolve.

        Returns:
            List of type names from child to root, e.g.
            ``["devcontainer-instance", "container-instance"]``.

        Raises:
            NotFoundError: If the type is not registered.
            ValidationError: If the chain is invalid (cycle, depth, etc.).
        """
        registry = self._load_type_registry()
        if type_name not in registry:
            raise NotFoundError(
                resource_type="resource_type",
                resource_id=type_name,
            )
        try:
            return resolve_inheritance_chain(type_name, registry)
        except ValueError as exc:
            raise ValidationError(
                message=str(exc),
                details={"type_name": type_name},
            ) from exc

    def is_subtype_of(self, type_name: str, ancestor_name: str) -> bool:
        """Check whether *type_name* is the same as or inherits from *ancestor_name*.

        Args:
            type_name: The concrete type to check.
            ancestor_name: The declared/expected type.

        Returns:
            ``True`` if *type_name* equals *ancestor_name* or
            transitively inherits from it.
        """
        registry = self._load_type_registry()
        return is_subtype_of(type_name, ancestor_name, registry)

    def _load_type_registry(self) -> dict[str, dict[str, Any]]:
        """Load all registered types into an in-memory dict for chain ops.

        .. note::

            Returns a **minimal** registry for chain resolution and subtype
            checks only.  It does NOT include ``handler``, ``cli_args``,
            ``capabilities``, or other fields needed by
            ``resolve_fields()`` or ``resolve_handler_polymorphic()``.

        Returns:
            Dict mapping type name to its config dict (with ``inherits``).
        """
        session = self._session()
        try:
            rows = session.query(ResourceTypeModel).all()
            registry: dict[str, dict[str, Any]] = {}
            for row in rows:
                name = str(row.name)
                raw_inherits = getattr(row, "inherits", None)
                # Derive built_in from the namespace column: built-in types
                # have namespace="builtin".  Fall back to the name heuristic
                # only when the column is unavailable.
                raw_ns = getattr(row, "namespace", None)
                is_built_in = (
                    str(raw_ns) == "builtin" if raw_ns is not None else "/" not in name
                )
                registry[name] = {
                    "inherits": str(raw_inherits) if raw_inherits else None,
                    "built_in": is_built_in,
                }
            return registry
        finally:
            session.close()
