"""Domain models for large-project hierarchical decomposition.

Defines the configuration, tree nodes, dependency edges, and result
containers used by :class:`DecompositionService` to break a large file
set into bounded subplans suitable for independent execution.

Key models:

- :class:`DecompositionConfig` — tuneable heuristic parameters.
- :class:`DecompositionNode` — a single node in the decomposition tree.
- :class:`DecompositionResult` — full decomposition output with metrics.
- :class:`DependencyEdge` / :class:`DependencyGraph` — dependency DAG
  structures consumed by :class:`DependencyClosureComputer`.

Based on Forgejo issue #205.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum

from pydantic import BaseModel, ConfigDict, Field

# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------


class ClusterStrategy(StrEnum):
    """Strategy used to partition files into a cluster."""

    DIRECTORY = "directory"
    LANGUAGE = "language"
    SIZE = "size"
    FALLBACK = "fallback"


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class DecompositionConfig:
    """Tuneable parameters for hierarchical decomposition.

    Attributes:
        max_depth: Maximum recursion depth (root = 0).
        max_files_per_subplan: Upper bound on files in a single leaf.
        max_tokens_per_subplan: Upper bound on estimated tokens per leaf.
        min_files_per_subplan: Below this, no further splitting.
        language_priority: Ordered list of language extensions used for
            deterministic cluster ordering.
    """

    max_depth: int = 4
    max_files_per_subplan: int = 500
    max_tokens_per_subplan: int = 100_000
    min_files_per_subplan: int = 10
    language_priority: list[str] = field(
        default_factory=lambda: [".py", ".ts", ".js", ".go", ".rs", ".java"]
    )

    def __post_init__(self) -> None:
        if self.max_depth < 1:
            raise ValueError("max_depth must be >= 1")
        if self.max_files_per_subplan < 1:
            raise ValueError("max_files_per_subplan must be >= 1")
        if self.max_tokens_per_subplan < 1:
            raise ValueError("max_tokens_per_subplan must be >= 1")
        if self.min_files_per_subplan < 1:
            raise ValueError("min_files_per_subplan must be >= 1")


# ---------------------------------------------------------------------------
# Decomposition tree
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class DecompositionNode:
    """A single node in the decomposition hierarchy.

    Leaf nodes contain file paths assigned to a subplan.
    Internal nodes are structural grouping points.

    Attributes:
        node_id: Stable, deterministic identifier for this node.
        parent_id: ``node_id`` of the parent (``None`` for root).
        depth: Distance from root (root = 0).
        file_paths: Files assigned to this node.
        language: Dominant language extension (e.g. ``".py"``).
        directory_prefix: Common directory prefix for contained files.
        estimated_tokens: Estimated total token count for the files.
        strategy: Clustering strategy that produced this node.
        children_ids: Ordered list of child ``node_id`` values.
    """

    node_id: str
    parent_id: str | None
    depth: int
    file_paths: list[str]
    language: str
    directory_prefix: str
    estimated_tokens: int
    strategy: ClusterStrategy = ClusterStrategy.DIRECTORY
    children_ids: list[str] = field(default_factory=list)

    @property
    def is_leaf(self) -> bool:
        """Return ``True`` when the node has no children."""
        return len(self.children_ids) == 0


@dataclass(frozen=True)
class DecompositionResult:
    """Full output of a hierarchical decomposition pass.

    Attributes:
        nodes: Every node in the decomposition tree (BFS order).
        max_depth_reached: Deepest level actually produced.
        total_files: Number of unique files across all leaves.
        metrics: Arbitrary key/value diagnostic counters.
    """

    nodes: list[DecompositionNode]
    max_depth_reached: int
    total_files: int
    metrics: dict[str, int | float] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Dependency graph
# ---------------------------------------------------------------------------


class DependencyType(StrEnum):
    """Kind of dependency between two files."""

    IMPORT = "import"
    INCLUDE = "include"
    CALL = "call"
    INHERIT = "inherit"
    UNKNOWN = "unknown"


@dataclass(frozen=True)
class DependencyEdge:
    """Directed edge in the file dependency graph.

    Attributes:
        source_file: The file that depends on *target_file*.
        target_file: The file being depended upon.
        dependency_type: Kind of dependency.
    """

    source_file: str
    target_file: str
    dependency_type: DependencyType = DependencyType.IMPORT


class DependencyGraph(BaseModel):
    """Directed graph of file-level dependencies.

    Maintains an adjacency list and per-node in-degree for efficient
    topological sort and closure computation.

    Attributes:
        edges: All edges in the graph.
        file_nodes: Set of all known file paths (nodes).
        adjacency_list: Forward adjacency: source → [targets].
        in_degree: Number of incoming edges per node.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    edges: list[DependencyEdge] = Field(default_factory=list)
    file_nodes: set[str] = Field(default_factory=set)
    adjacency_list: dict[str, list[str]] = Field(default_factory=dict)
    in_degree: dict[str, int] = Field(default_factory=dict)

    # ------------------------------------------------------------------
    # Mutation helpers
    # ------------------------------------------------------------------

    def add_edge(self, edge: DependencyEdge) -> None:
        """Add an edge, updating adjacency and in-degree."""
        self.edges.append(edge)
        self.file_nodes.add(edge.source_file)
        self.file_nodes.add(edge.target_file)
        self.adjacency_list.setdefault(edge.source_file, []).append(edge.target_file)
        self.in_degree.setdefault(edge.source_file, 0)
        self.in_degree[edge.target_file] = self.in_degree.get(edge.target_file, 0) + 1

    def add_node(self, file_path: str) -> None:
        """Ensure *file_path* is registered as a graph node."""
        self.file_nodes.add(file_path)
        self.adjacency_list.setdefault(file_path, [])
        self.in_degree.setdefault(file_path, 0)

    def successors(self, file_path: str) -> list[str]:
        """Return direct successors (targets) of *file_path*."""
        return list(self.adjacency_list.get(file_path, []))


__all__: list[str] = [
    "ClusterStrategy",
    "DecompositionConfig",
    "DecompositionNode",
    "DecompositionResult",
    "DependencyEdge",
    "DependencyGraph",
    "DependencyType",
]
