"""Application service for namespaced project management.

Provides a clean application-layer facade over the domain model
``NamespacedProject`` and its repository, so that the CLI layer
never needs to import from ``cleveragents.domain`` directly.

This service enforces Architectural Invariant #3:
    CLI layer → Application Services → Domain layer

Spec references:
- Project Data Model (lines 6477-6511)
- Namespaces (lines 6524-6584)
- ADR-009 (CLI Framework)
- Forgejo issue #7464
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import structlog

from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.project import (
    NamespacedProject,
    ParsedName,
    parse_namespaced_name,
)
from cleveragents.infrastructure.database.repositories import ProjectNotFoundError

if TYPE_CHECKING:
    from cleveragents.infrastructure.database.repositories import (
        NamespacedProjectRepository,
    )

_logger = structlog.get_logger(__name__)


class NamespacedProjectService:
    """Application service for namespaced project CRUD operations.

    Encapsulates all domain model construction so that callers (e.g. the
    CLI layer) never need to import from ``cleveragents.domain`` directly.

    Args:
        project_repo: Repository for persisting ``NamespacedProject`` records.
    """

    def __init__(self, project_repo: NamespacedProjectRepository) -> None:
        self._repo = project_repo

    # ------------------------------------------------------------------
    # Parsing helpers (expose domain parsing without domain import)
    # ------------------------------------------------------------------

    def parse_project_name(self, name: str) -> ParsedName:
        """Parse a ``[[server:]namespace/]name`` string.

        Args:
            name: The raw project name string from user input.

        Returns:
            A :class:`~cleveragents.domain.models.core.project.ParsedName`
            with ``server``, ``namespace``, and ``name`` components.

        Raises:
            ValueError: If the name is empty, has invalid characters,
                or uses a reserved/provider namespace.
        """
        return parse_namespaced_name(name)

    # ------------------------------------------------------------------
    # Create
    # ------------------------------------------------------------------

    def create_project(
        self,
        name: str,
        description: str | None = None,
    ) -> NamespacedProject:
        """Parse *name* and persist a new :class:`NamespacedProject`.

        Args:
            name: Raw project name (bare or ``namespace/name`` or
                ``server:namespace/name``).
            description: Optional human-readable description.

        Returns:
            The newly created :class:`NamespacedProject`.

        Raises:
            ValueError: If *name* is invalid or uses a reserved namespace.
            DatabaseError: If a project with the same namespaced name
                already exists or a persistence error occurs.
        """
        parsed = parse_namespaced_name(name)
        project = NamespacedProject(
            name=parsed.name,
            namespace=parsed.namespace,
            server=parsed.server,
            description=description,
        )
        self._repo.create(project)
        _logger.info(
            "namespaced_project_created",
            namespaced_name=project.namespaced_name,
        )
        return project

    # ------------------------------------------------------------------
    # Read
    # ------------------------------------------------------------------

    def get_project(self, namespaced_name: str) -> NamespacedProject:
        """Retrieve a project by its namespaced name.

        Args:
            namespaced_name: The ``namespace/name`` identifier.

        Returns:
            The matching :class:`NamespacedProject`.

        Raises:
            NotFoundError: If no project with that name exists.
        """
        try:
            return self._repo.get(namespaced_name)
        except ProjectNotFoundError as exc:
            raise NotFoundError(
                resource_type="project",
                resource_id=namespaced_name,
            ) from exc

    def list_projects(
        self,
        namespace: str | None = None,
    ) -> list[NamespacedProject]:
        """List all projects, optionally filtered by namespace.

        Args:
            namespace: If provided, only return projects in this namespace.

        Returns:
            List of :class:`NamespacedProject` instances.

        Raises:
            DatabaseError: If a persistence error occurs.
        """
        return self._repo.list_projects(namespace=namespace)

    # ------------------------------------------------------------------
    # Delete
    # ------------------------------------------------------------------

    def delete_project(self, namespaced_name: str) -> bool:
        """Delete a project by its namespaced name.

        Args:
            namespaced_name: The ``namespace/name`` identifier.

        Returns:
            ``True`` if the project was deleted, ``False`` otherwise.

        Raises:
            DatabaseError: If a persistence error occurs.
        """
        return self._repo.delete(namespaced_name)

    # ------------------------------------------------------------------
    # Validation helpers
    # ------------------------------------------------------------------

    def validate_project_name(self, name: str) -> ParsedName:
        """Validate and parse a project name without persisting.

        Useful for pre-flight validation in CLI commands.

        Args:
            name: The raw project name string.

        Returns:
            A :class:`~cleveragents.domain.models.core.project.ParsedName`.

        Raises:
            ValueError: If the name is invalid.
        """
        return parse_namespaced_name(name)

    # ------------------------------------------------------------------
    # Introspection helpers
    # ------------------------------------------------------------------

    def project_to_dict(self, project: NamespacedProject) -> dict[str, Any]:
        """Serialize a project to a spec-aligned dictionary.

        Keys: ``namespaced_name``, ``namespace``, ``name``,
        ``description``, ``linked_resources``, ``created_at``,
        ``updated_at``.

        Args:
            project: The project to serialize.

        Returns:
            A plain ``dict`` suitable for JSON/YAML output.
        """
        linked: list[dict[str, Any]] = []
        for lr in project.linked_resources:
            linked.append(
                {
                    "resource_id": lr.resource_id,
                    "read_only": lr.project_read_only,
                    "alias": lr.alias,
                    "linked_at": lr.linked_at.isoformat()
                    if hasattr(lr.linked_at, "isoformat")
                    else str(lr.linked_at),
                }
            )

        return {
            "namespaced_name": project.namespaced_name,
            "namespace": project.namespace,
            "name": project.name,
            "description": project.description,
            "linked_resources": linked,
            "created_at": project.created_at.isoformat()
            if hasattr(project.created_at, "isoformat")
            else str(project.created_at),
            "updated_at": project.updated_at.isoformat()
            if hasattr(project.updated_at, "isoformat")
            else str(project.updated_at),
        }
