"""Multi-project subplan support for CleverAgents.

This module provides domain models and a stateless resolver for
multi-project plan orchestration.  When a plan targets more than one
project (via ``project_links``), each project receives a scoped view
that tracks:

- Resource isolation (which resource IDs belong to which project)
- Access mode (read-only vs writable)
- Per-project changeset summaries
- Cross-project dependency metadata

## Key Classes

- ``ChangeSetSummary``       -- Per-project changeset tracking
- ``ProjectScope``           -- Scoped view of a project within a plan
- ``CrossProjectDependency`` -- Edge in the inter-project dependency graph
- ``MultiProjectMetadata``   -- Aggregation stored on the Plan model
- ``ProjectScopeResolver``   -- Stateless helper for scope creation and
  alias resolution

Based on ``docs/specification.md`` and Forgejo issue #199.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from pydantic import BaseModel, ConfigDict, Field, computed_field

if TYPE_CHECKING:
    from cleveragents.domain.models.core.plan import ProjectLink


class ChangeSetSummary(BaseModel):
    """Per-project changeset tracking.

    Records the number of files changed, added, and deleted for a
    single project within a multi-project plan.  Validation state is
    tracked to gate cross-project merges.
    """

    project_name: str = Field(
        ...,
        min_length=1,
        description="Namespaced project name this summary pertains to",
    )
    files_changed: int = Field(
        default=0,
        ge=0,
        description="Number of files modified",
    )
    files_added: int = Field(
        default=0,
        ge=0,
        description="Number of files created",
    )
    files_deleted: int = Field(
        default=0,
        ge=0,
        description="Number of files deleted",
    )
    total_lines_changed: int = Field(
        default=0,
        ge=0,
        description="Total lines added + removed across all file operations",
    )
    validation_passed: bool | None = Field(
        default=None,
        description="Whether per-project validation passed (None = not run)",
    )
    validation_errors: list[str] = Field(
        default_factory=list,
        description="Validation error messages for this project",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class ProjectScope(BaseModel):
    """Scoped view of a project within a multi-project plan.

    Created by ``ProjectScopeResolver.resolve_scopes`` for each
    ``ProjectLink`` in the plan.  Actors use the scope to understand
    which resources belong to a specific project and whether access is
    read-only.
    """

    project_name: str = Field(
        ...,
        min_length=1,
        description="Namespaced project name",
    )
    alias: str | None = Field(
        default=None,
        description="Optional alias for quick reference",
    )
    read_only: bool = Field(
        default=False,
        description="Whether this project is read-only in the plan",
    )
    resource_ids: list[str] = Field(
        default_factory=list,
        description="Resource IDs belonging to this project",
    )
    changeset_summary: ChangeSetSummary | None = Field(
        default=None,
        description="Per-project changeset summary (populated during Execute)",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class CrossProjectDependency(BaseModel):
    """An edge in the cross-project dependency graph.

    Captures a directional dependency between two projects within the
    same plan.  The ``dependency_type`` is a free-form label (e.g.
    ``"imports"``, ``"api-consumer"``, ``"shared-schema"``).
    """

    source_project: str = Field(
        ...,
        min_length=1,
        description="Project that depends on target",
    )
    target_project: str = Field(
        ...,
        min_length=1,
        description="Project that is depended upon",
    )
    dependency_type: str = Field(
        ...,
        min_length=1,
        description="Kind of dependency (e.g. 'imports', 'api-consumer')",
    )
    resource_ids: list[str] = Field(
        default_factory=list,
        description="Resource IDs involved in this dependency edge",
    )

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class MultiProjectMetadata(BaseModel):
    """Aggregated multi-project metadata stored on the Plan model.

    Created once during ``MultiProjectService.initialize_scopes`` and
    updated as execution progresses (changeset recording, dependency
    discovery).
    """

    project_scopes: list[ProjectScope] = Field(
        default_factory=list,
        description="Scoped views for each project in the plan",
    )
    cross_project_dependencies: list[CrossProjectDependency] = Field(
        default_factory=list,
        description="Discovered cross-project dependency edges",
    )
    allow_mixed_access: bool = Field(
        default=False,
        description=(
            "Whether mixed read-only and writable projects are "
            "permitted without explicit opt-in"
        ),
    )

    @computed_field  # type: ignore[prop-decorator]
    @property
    def total_projects(self) -> int:
        """Number of projects tracked in this metadata."""
        return len(self.project_scopes)

    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_assignment=True,
    )


class ProjectScopeResolver:
    """Stateless helper for project scope creation and alias resolution.

    All methods are designed to be called without persistent state,
    operating purely on the data passed to them.
    """

    def resolve_scopes(
        self,
        project_links: list[ProjectLink],
        available_resources: dict[str, list[str]],
    ) -> list[ProjectScope]:
        """Create ``ProjectScope`` objects for each project link.

        For each ``ProjectLink``, look up matching resource IDs from
        *available_resources* keyed by project name.

        Args:
            project_links: The plan's project links.
            available_resources: Mapping of project name to resource IDs.

        Returns:
            A list of ``ProjectScope`` objects.
        """
        scopes: list[ProjectScope] = []
        for link in project_links:
            resource_ids = available_resources.get(link.project_name, [])
            scope = ProjectScope(
                project_name=link.project_name,
                alias=link.alias,
                read_only=link.read_only,
                resource_ids=list(resource_ids),
            )
            scopes.append(scope)
        return scopes

    def resolve_alias(
        self,
        project_links: list[ProjectLink],
        alias_or_name: str,
    ) -> ProjectLink | None:
        """Resolve an alias or project name to a ``ProjectLink``.

        Checks aliases first (exact match), then falls back to
        project name matching.

        Args:
            project_links: The plan's project links.
            alias_or_name: Alias or project name to resolve.

        Returns:
            The matching ``ProjectLink``, or ``None`` if not found.
        """
        # Alias match first
        for link in project_links:
            if link.alias is not None and link.alias == alias_or_name:
                return link
        # Project name match
        for link in project_links:
            if link.project_name == alias_or_name:
                return link
        return None

    def validate_access_modes(
        self,
        project_links: list[ProjectLink],
        *,
        allow_mixed: bool = False,
    ) -> list[str]:
        """Validate that access modes are consistent.

        Returns error messages if there are mixed read-only and writable
        projects without ``allow_mixed=True``.

        Args:
            project_links: The plan's project links.
            allow_mixed: Whether mixed access is permitted.

        Returns:
            List of error message strings (empty if valid).
        """
        if allow_mixed:
            return []
        if len(project_links) < 2:
            return []

        has_read_only = any(link.read_only for link in project_links)
        has_writable = any(not link.read_only for link in project_links)

        if has_read_only and has_writable:
            ro_names = [link.project_name for link in project_links if link.read_only]
            rw_names = [
                link.project_name for link in project_links if not link.read_only
            ]
            return [
                f"Mixed access modes detected: read-only={ro_names}, "
                f"writable={rw_names}. Set allow_mixed_access=True "
                f"to permit this configuration."
            ]
        return []


__all__: list[str] = [
    "ChangeSetSummary",
    "CrossProjectDependency",
    "MultiProjectMetadata",
    "ProjectScope",
    "ProjectScopeResolver",
]
