# Implements: REQ-p00050-A, REQ-p00050-C
# Implements: REQ-d00127-A, REQ-d00127-B, REQ-d00127-C, REQ-d00127-D
"""GraphNode - Unified node representation for traceability graph.

This module provides the core data structures for Architecture 3.0:
- NodeKind: Enum of node types
- GraphNode: Unified node with typed content and edge management
"""

from __future__ import annotations

from collections import deque
from collections.abc import Callable, Iterator
from dataclasses import InitVar, dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any
from uuid import uuid4

if TYPE_CHECKING:
    from elspais.graph.relations import Edge, EdgeKind


class NodeKind(Enum):
    """Types of nodes in the traceability graph."""

    REQUIREMENT = "requirement"
    ASSERTION = "assertion"
    CODE = "code"
    TEST = "test"
    RESULT = "result"
    USER_JOURNEY = "journey"
    REMAINDER = "remainder"
    # Implements: REQ-d00126-A
    FILE = "file"


# Implements: REQ-d00126-B
class FileType(Enum):
    """Classification of source files by their domain role.

    Determines which domain parser processes the file and what
    node kinds its content produces.
    """

    SPEC = "spec"
    JOURNEY = "journey"
    CODE = "code"
    TEST = "test"
    RESULT = "result"


@dataclass
class GraphNode:
    """A node in the traceability graph.

    GraphNode is the unified representation for all entities in the
    traceability graph. The `kind` field determines the expected
    structure of `content`.

    All parent-child relationships are created via link() with a typed
    EdgeKind. Use unlink() to sever all edges between two nodes.

    File path is accessed via ``file_node().get_field("relative_path")``.
    Line numbers are accessed via ``get_field("parse_line")`` and
    ``get_field("parse_end_line")``.

    Attributes:
        id: Unique identifier for this node (mutable via set_id()).
        kind: The type of node (requirement, assertion, etc.).
        uuid: Stable 32-char hex string for GUI/DOM referencing.

    Use get_label()/set_label() for label access.
    Use set_id() for ID mutations.
    """

    id: str
    kind: NodeKind
    label: InitVar[str] = ""  # Constructor parameter, stored in _label
    uuid: str = field(default_factory=lambda: uuid4().hex)

    # Label stored internally - use get_label()/set_label() or label property
    _label: str = field(default="", init=False)

    # Internal storage (prefixed) - excluded from constructor
    _children: list[GraphNode] = field(default_factory=list, init=False)
    _parents: list[GraphNode] = field(default_factory=list, init=False, repr=False)
    _outgoing_edges: list[Edge] = field(default_factory=list, init=False, repr=False)
    _incoming_edges: list[Edge] = field(default_factory=list, init=False, repr=False)
    _content: dict[str, Any] = field(default_factory=dict, init=False)
    _metrics: dict[str, Any] = field(default_factory=dict, init=False)

    # Implements: REQ-p00014-C
    def __post_init__(self, label: str) -> None:
        """Initialize internal label and default stereotype."""
        self._label = label
        from elspais.graph.relations import Stereotype

        self._content.setdefault("stereotype", Stereotype.CONCRETE)

    def set_id(self, value: str) -> None:
        """Set the node's unique identifier.

        Use this method for mutations - it allows graph updates
        without breaking the dataclass contract.

        Note: This only changes the node's internal ID. The graph's
        _index must be updated separately by the mutation method.
        """
        # Dataclass field assignment - directly mutate the id attribute
        object.__setattr__(self, "id", value)

    # Label accessors - encapsulated for future hooks (e.g., index updates)
    def get_label(self) -> str:
        """Get the node's display label."""
        return self._label

    def set_label(self, value: str) -> None:
        """Set the node's display label.

        Use this method for mutations - it may trigger graph updates
        in the future (e.g., search index, change tracking).
        """
        self._label = value

    # Implements: REQ-d00127-C
    # Iterator access with optional edge-kind filtering
    def iter_children(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Iterate over child nodes.

        Args:
            edge_kinds: If provided, only return children reachable via
                edges of these kinds. If None, return all children.
        """
        if edge_kinds is None:
            yield from self._children
        else:
            seen: set[str] = set()
            for edge in self._outgoing_edges:
                if edge.kind in edge_kinds and edge.target.id not in seen:
                    seen.add(edge.target.id)
                    yield edge.target

    def iter_parents(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Iterate over parent nodes.

        Args:
            edge_kinds: If provided, only return parents reachable via
                incoming edges of these kinds. If None, return all parents.
        """
        if edge_kinds is None:
            yield from self._parents
        else:
            seen: set[str] = set()
            for edge in self._incoming_edges:
                if edge.kind in edge_kinds and edge.source.id not in seen:
                    seen.add(edge.source.id)
                    yield edge.source

    def iter_outgoing_edges(self) -> Iterator[Edge]:
        """Iterate over outgoing edges."""
        yield from self._outgoing_edges

    def iter_incoming_edges(self) -> Iterator[Edge]:
        """Iterate over incoming edges."""
        yield from self._incoming_edges

    def iter_edges_by_kind(self, edge_kind: EdgeKind) -> Iterator[Edge]:
        """Iterate outgoing edges of a specific kind."""
        for e in self._outgoing_edges:
            if e.kind == edge_kind:
                yield e

    # Count and membership checks (avoid materializing lists)
    def child_count(self) -> int:
        """Return number of children."""
        return len(self._children)

    def parent_count(self) -> int:
        """Return number of parents."""
        return len(self._parents)

    def has_child(self, node: GraphNode) -> bool:
        """Check if node is a child."""
        return node in self._children

    def has_parent(self, node: GraphNode) -> bool:
        """Check if node is a parent."""
        return node in self._parents

    @property
    def is_root(self) -> bool:
        """True if this node has no parents."""
        return len(self._parents) == 0

    @property
    def is_leaf(self) -> bool:
        """True if this node has no children."""
        return len(self._children) == 0

    # Field accessors (return single value)
    def get_field(self, key: str, default: Any = None) -> Any:
        """Get a field from content."""
        return self._content.get(key, default)

    def set_field(self, key: str, value: Any) -> None:
        """Set a field in content."""
        self._content[key] = value

    def get_all_content(self) -> dict[str, Any]:
        """Get a copy of all content fields.

        Returns a shallow copy for serialization purposes.
        Prefer get_field() for accessing individual fields.
        """
        return dict(self._content)

    def get_metric(self, key: str, default: Any = None) -> Any:
        """Get a metric value."""
        return self._metrics.get(key, default)

    def set_metric(self, key: str, value: Any) -> None:
        """Set a metric value."""
        self._metrics[key] = value

    # Convenience properties for common fields
    @property
    def level(self) -> str | None:
        """Get the requirement level (PRD, OPS, DEV)."""
        return self._content.get("level")

    @property
    def status(self) -> str | None:
        """Get the requirement status."""
        return self._content.get("status")

    @property
    def hash(self) -> str | None:
        """Get the content hash."""
        return self._content.get("hash")

    # Implements: REQ-d00127-B
    def unlink(self, child: GraphNode) -> bool:
        """Remove a child node and clean up all links.

        Severs all edges between this node and the child and removes
        the bidirectional cache entries. Use remove_edge() to remove
        a single specific edge while preserving others.

        Args:
            child: The child node to unlink.

        Returns:
            True if the child was found and removed, False otherwise.
        """
        if child not in self._children:
            return False

        # Remove bidirectional node links
        self._children.remove(child)
        if self in child._parents:
            child._parents.remove(self)

        # Remove outgoing edges to this child
        self._outgoing_edges = [e for e in self._outgoing_edges if e.target is not child]

        # Remove incoming edges from this parent on the child
        child._incoming_edges = [e for e in child._incoming_edges if e.source is not self]

        return True

    def remove_edge(self, edge: Edge) -> bool:
        """Remove a single specific edge.

        Removes the edge from both outgoing and incoming lists. If no
        other edges remain between the two nodes, also removes the
        parent/child node links.

        Args:
            edge: The exact edge object to remove.

        Returns:
            True if the edge was found and removed, False otherwise.
        """
        if edge not in self._outgoing_edges:
            return False

        self._outgoing_edges.remove(edge)
        if edge in edge.target._incoming_edges:
            edge.target._incoming_edges.remove(edge)

        # Clean up parent/child links only if no more edges remain
        child = edge.target
        has_remaining = any(e.target is child for e in self._outgoing_edges)
        if not has_remaining:
            if child in self._children:
                self._children.remove(child)
            if self in child._parents:
                child._parents.remove(self)

        return True

    def link(
        self,
        child: GraphNode,
        edge_kind: EdgeKind,
        assertion_targets: list[str] | None = None,
    ) -> Edge:
        """Create a typed edge to a child node.

        Args:
            child: The child node to link.
            edge_kind: The type of relationship.
            assertion_targets: Optional list of assertion labels targeted.

        Returns:
            The created Edge.
        """
        from elspais.graph.relations import Edge

        # Create the edge
        edge = Edge(
            source=self,
            target=child,
            kind=edge_kind,
            assertion_targets=assertion_targets or [],
        )

        # Add bidirectional node links
        if child not in self._children:
            self._children.append(child)
        if self not in child._parents:
            child._parents.append(self)

        # Track edges
        self._outgoing_edges.append(edge)
        child._incoming_edges.append(edge)

        return edge

    @property
    def depth(self) -> int:
        """Calculate depth from root (0 for roots).

        For DAG structures, returns minimum depth (shortest path to root).
        FILE parents are excluded since they represent structural
        containment, not domain hierarchy.
        """
        domain_parents = [p for p in self._parents if p.kind != NodeKind.FILE]
        if not domain_parents:
            return 0
        return 1 + min(p.depth for p in domain_parents)

    # Implements: REQ-d00127-C
    def walk(
        self,
        order: str = "pre",
        edge_kinds: set[EdgeKind] | None = None,
    ) -> Iterator[GraphNode]:
        """Iterate over this node and descendants.

        Args:
            order: Traversal order:
                - "pre": Parent first (depth-first, pre-order)
                - "post": Children first (depth-first, post-order)
                - "level": Breadth-first (level order)
            edge_kinds: If provided, only traverse children reachable
                via edges of these kinds at each level. If None,
                traverse all children.

        Yields:
            GraphNode instances in the specified order.
        """
        if order == "pre":
            yield from self._walk_preorder(edge_kinds)
        elif order == "post":
            yield from self._walk_postorder(edge_kinds)
        elif order == "level":
            yield from self._walk_level(edge_kinds)
        else:
            raise ValueError(f"Unknown traversal order: {order}")

    def _walk_preorder(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Pre-order traversal (parent before children)."""
        yield self
        for child in self.iter_children(edge_kinds):
            yield from child._walk_preorder(edge_kinds)

    def _walk_postorder(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Post-order traversal (children before parent)."""
        for child in self.iter_children(edge_kinds):
            yield from child._walk_postorder(edge_kinds)
        yield self

    def _walk_level(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Level-order (breadth-first) traversal."""
        queue: deque[GraphNode] = deque([self])
        while queue:
            node = queue.popleft()
            yield node
            queue.extend(node.iter_children(edge_kinds))

    # Implements: REQ-d00127-C
    def ancestors(self, edge_kinds: set[EdgeKind] | None = None) -> Iterator[GraphNode]:
        """Iterate up through all ancestor paths (BFS).

        For DAG structures, visits each unique ancestor once.

        Args:
            edge_kinds: If provided, only traverse parents reachable
                via incoming edges of these kinds. If None, traverse
                all parents.

        Yields:
            Ancestor GraphNode instances.
        """
        visited: set[str] = set()
        queue: deque[GraphNode] = deque(self.iter_parents(edge_kinds))
        while queue:
            node = queue.popleft()
            if node.id not in visited:
                visited.add(node.id)
                yield node
                queue.extend(node.iter_parents(edge_kinds))

    # Implements: REQ-d00127-D, REQ-d00128-L
    def file_node(self) -> GraphNode | None:
        """Walk incoming edges upward to find the nearest FILE ancestor.

        Traversal path depends on node position:
        - Top-level content (REQUIREMENT, file-level REMAINDER):
          one hop via incoming CONTAINS edge to FILE.
        - ASSERTION or REMAINDER section: two hops -- incoming
          STRUCTURES edge to REQUIREMENT, then incoming CONTAINS
          edge to FILE.
        - INSTANCE node: returns None (no CONTAINS edge). To find
          the original file, navigate via INSTANCE edge to the
          original node, then call file_node() on it.

        Returns:
            The nearest FILE ancestor, or None if not found.
        """
        # INSTANCE nodes are virtual -- they have no physical file
        stereotype = self.get_field("stereotype")
        if stereotype is not None and getattr(stereotype, "value", None) == "instance":
            return None
        visited: set[str] = set()
        queue: deque[GraphNode] = deque(self._parents)
        while queue:
            node = queue.popleft()
            if node.id in visited:
                continue
            visited.add(node.id)
            if node.kind == NodeKind.FILE:
                return node
            queue.extend(node._parents)
        return None

    def find(self, predicate: Callable[[GraphNode], bool]) -> Iterator[GraphNode]:
        """Find all descendants matching predicate.

        Args:
            predicate: Function that returns True for matching nodes.

        Yields:
            Matching GraphNode instances.
        """
        for node in self.walk():
            if predicate(node):
                yield node

    def find_by_kind(self, kind: NodeKind) -> Iterator[GraphNode]:
        """Find all descendants of a specific kind.

        Args:
            kind: The NodeKind to filter by.

        Yields:
            GraphNode instances of the specified kind.
        """
        return self.find(lambda n: n.kind == kind)
