"""Pydantic models for Carve.

Core data structures for the tree store, including TreeNode, NodeMetadata,
and the in-memory TreeStore class for single-file storage.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from pydantic import BaseModel, Field


class NodeMetadata(BaseModel):
    """Metadata attached to each node tracking intent and history.

    This is what makes Carve different from plain Tree-sitter: we track
    the prompt/intent that created each piece of code.
    """

    prompt: str | None = Field(
        default=None,
        description="The prompt/intent that created or last modified this node",
    )
    model: str | None = Field(
        default=None,
        description="The AI model that generated this code (e.g., 'qwen2.5-coder-7b')",
    )
    created_at: datetime = Field(
        default_factory=lambda: datetime.now(UTC),
        description="When this node was first created",
    )
    updated_at: datetime = Field(
        default_factory=lambda: datetime.now(UTC),
        description="When this node was last modified",
    )
    version: int = Field(
        default=1,
        description="Version counter, incremented on each update",
    )
    history: list[dict[str, Any]] = Field(
        default_factory=list,
        description="History of previous versions with prompts and timestamps",
    )

    def record_update(self, prompt: str | None = None, model: str | None = None) -> None:
        """Record an update, pushing current state to history."""
        # Save current state to history
        self.history.append(
            {
                "version": self.version,
                "prompt": self.prompt,
                "model": self.model,
                "timestamp": self.updated_at.isoformat(),
            }
        )

        # Update current state
        self.version += 1
        self.updated_at = datetime.now(UTC)
        if prompt is not None:
            self.prompt = prompt
        if model is not None:
            self.model = model


class TreeNode(BaseModel):
    """A node in the Carve tree store.

    Represents a structural element of code: module, class, function, etc.
    Contains both the source code and metadata about its creation.
    """

    node_id: str = Field(
        description="Hierarchical ID like 'module::ClassName::method_name'",
    )
    node_type: str = Field(
        description="The type of node (e.g., 'class_definition', 'function_definition')",
    )
    source: str = Field(
        description="The source code for this node",
    )
    start_byte: int = Field(
        default=0,
        description="Start byte offset in the original source",
    )
    end_byte: int = Field(
        default=0,
        description="End byte offset in the original source",
    )
    start_point: tuple[int, int] = Field(
        default=(0, 0),
        description="Start position as (row, column)",
    )
    end_point: tuple[int, int] = Field(
        default=(0, 0),
        description="End position as (row, column)",
    )
    children: list[TreeNode] = Field(
        default_factory=list,
        description="Child nodes in the tree",
    )
    metadata: NodeMetadata = Field(
        default_factory=NodeMetadata,
        description="Metadata about this node's creation and history",
    )
    language: str = Field(
        default="",
        description="The programming language of this node",
    )

    model_config = {"frozen": False}

    def find_child(self, child_id: str) -> TreeNode | None:
        """Find a direct child by ID."""
        for child in self.children:
            if child.node_id == child_id:
                return child
        return None

    def find_descendant(self, node_id: str) -> TreeNode | None:
        """Find a descendant node by ID (recursive search)."""
        if self.node_id == node_id:
            return self

        for child in self.children:
            found = child.find_descendant(node_id)
            if found is not None:
                return found

        return None

    def walk(self) -> list[TreeNode]:
        """Walk all nodes in the subtree (depth-first)."""
        result: list[TreeNode] = [self]
        for child in self.children:
            result.extend(child.walk())
        return result

    def signature(self) -> str:
        """Get a short signature for this node (first line of source)."""
        lines = self.source.strip().split("\n")
        return lines[0] if lines else ""


class TreeStore(BaseModel):
    """In-memory storage for a single file's tree.

    This is the "source of truth" for Phase 1 - a single-file in-memory store.
    Later phases will add SQLite persistence and multi-file support.
    """

    file_path: str | None = Field(
        default=None,
        description="Path to the source file (if loaded from disk)",
    )
    language: str = Field(
        default="",
        description="The detected programming language",
    )
    source: str = Field(
        default="",
        description="The full source code",
    )
    root: TreeNode | None = Field(
        default=None,
        description="The root node of the tree",
    )

    model_config = {"frozen": False}

    def get_node(self, node_id: str) -> TreeNode | None:
        """Get a node by its ID."""
        if self.root is None:
            return None
        return self.root.find_descendant(node_id)

    def all_nodes(self) -> list[TreeNode]:
        """Get all nodes in the tree."""
        if self.root is None:
            return []
        return self.root.walk()

    def find_by_type(self, node_type: str) -> list[TreeNode]:
        """Find all nodes of a specific type."""
        return [n for n in self.all_nodes() if n.node_type == node_type]

    def find_by_name_pattern(self, pattern: str) -> list[TreeNode]:
        """Find nodes whose ID contains the pattern."""
        return [n for n in self.all_nodes() if pattern in n.node_id]


class ProjectStore(BaseModel):
    """Multi-file project store. The filesystem IS the tree.

    Holds a unified tree where directory nodes contain file nodes,
    and file nodes contain code nodes (from tree-sitter). Each file's
    source is stored separately, and byte offsets are per-file.
    """

    project_path: str | None = Field(
        default=None,
        description="Path to the project root directory",
    )
    root: TreeNode | None = Field(
        default=None,
        description="The root node of the project tree",
    )
    file_sources: dict[str, str] = Field(
        default_factory=dict,
        description="Per-file source storage: maps file node_id -> source string",
    )
    file_languages: dict[str, str] = Field(
        default_factory=dict,
        description="Per-file language: maps file node_id -> language",
    )

    model_config = {"frozen": False}

    def get_node(self, node_id: str) -> TreeNode | None:
        """Get a node by its ID from the unified tree."""
        if self.root is None:
            return None
        return self.root.find_descendant(node_id)

    def all_nodes(self) -> list[TreeNode]:
        """Get all nodes in the project tree."""
        if self.root is None:
            return []
        return self.root.walk()

    def get_file_for_node(self, node_id: str) -> str | None:
        """Find the file node_id that contains a given node.

        Checks if node_id starts with any known file node_id prefix.
        """
        # Direct match (the node IS a file)
        if node_id in self.file_sources:
            return node_id

        # Check if node_id starts with a file path prefix followed by ::
        for file_id in self.file_sources:
            if node_id.startswith(file_id + "::"):
                return file_id
        return None

    def get_source_for_node(self, node_id: str) -> str | None:
        """Get the file source string that contains a code node."""
        file_id = self.get_file_for_node(node_id)
        if file_id is not None:
            return self.file_sources.get(file_id)
        return None

    def get_language_for_node(self, node_id: str) -> str | None:
        """Get the language for a node based on its containing file."""
        file_id = self.get_file_for_node(node_id)
        if file_id is not None:
            return self.file_languages.get(file_id)
        return None

    def find_by_type(self, node_type: str) -> list[TreeNode]:
        """Find all nodes of a specific type."""
        return [n for n in self.all_nodes() if n.node_type == node_type]

    def find_by_name_pattern(self, pattern: str) -> list[TreeNode]:
        """Find nodes whose ID contains the pattern."""
        return [n for n in self.all_nodes() if pattern in n.node_id]


class Operation(BaseModel):
    """A replayable tool call for undo/redo."""

    tool: str
    kwargs: dict[str, Any]


class UndoEntry(BaseModel):
    """A single undoable operation."""

    description: str
    timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
    undo_op: Operation | None = None
    redo_op: Operation | None = None
    checkpoint_name: str | None = None


Store = TreeStore | ProjectStore
