"""
Knowledge Graph Pydantic Schemas.

Type-safe models for the knowledge graph system, validated against
schema/definitions/knowledge_graph.yaml at runtime.

This module provides:
- KnowledgeNode: A node in the knowledge graph
- KnowledgeEdge: A relationship between nodes
- EmergencePattern: A pattern discovered by emergence analysis
- EmergenceResult: Complete result from an emergence cycle
- KnowledgeGraphConfig: Runtime access to YAML configuration

Usage:
    from lightwave.schema.pydantic.models.knowledge_graph import (
        KnowledgeNode,
        KnowledgeEdge,
        EmergencePattern,
        EmergenceResult,
        KnowledgeGraphConfig,
    )

    # Create typed nodes and edges
    node = KnowledgeNode(
        id="task_abc123",
        node_type="task",
        label="Implement feature X",
    )

    edge = KnowledgeEdge(
        source_id="task_abc123",
        target_id="task_def456",
        edge_type="depends_on",
        weight=0.9,
    )

    # Validate against YAML config
    assert KnowledgeGraphConfig.is_valid_node_type("task")
    assert KnowledgeGraphConfig.is_valid_edge_type("depends_on")
"""

from __future__ import annotations

from datetime import datetime
from enum import StrEnum
from functools import lru_cache
from pathlib import Path
from typing import Any

import yaml
from pydantic import Field, field_validator, model_validator

from lightwave.schema.pydantic.core.base import LightwaveBaseSchema, SSTSchema

# =============================================================================
# YAML CONFIGURATION LOADER
# =============================================================================


def _get_yaml_path() -> Path:
    """Get path to knowledge_graph.yaml (from __index.yaml registry)."""
    from lightwave.schema.core.paths import get_sst_path

    return get_sst_path("knowledge_graph")


@lru_cache(maxsize=1)
def _load_knowledge_graph_yaml() -> dict[str, Any]:
    """Load and cache knowledge_graph.yaml."""
    yaml_path = _get_yaml_path()
    if not yaml_path.exists():
        # Return minimal defaults if YAML doesn't exist
        return {
            "node_types": {"options": []},
            "edge_types": {"categories": {}},
            "link_types": {"options": []},
            "edge_to_link_mapping": {},
            "emergence_patterns": {"options": []},
            "weight_scoring": {
                "base_weight": 1.0,
                "max_connection_bonus": 2.0,
                "connection_bonus_per_edge": 0.1,
                "link_type_multipliers": {},
                "thresholds": {"high_weight": 2.5, "medium_weight": 1.5, "low_weight": 1.0},
            },
            "confidence_levels": {"options": [], "actionable_threshold": 0.7},
            "emergence_scopes": {"options": []},
        }
    with open(yaml_path) as f:
        return yaml.safe_load(f)


class KnowledgeGraphConfig:
    """
    Runtime configuration loaded from knowledge_graph.yaml.

    Provides type-safe access to YAML-defined values with caching.
    """

    @classmethod
    def _yaml(cls) -> dict[str, Any]:
        """Get cached YAML data."""
        return _load_knowledge_graph_yaml()

    @classmethod
    def get_node_types(cls) -> list[str]:
        """Get all valid node type values."""
        options = cls._yaml().get("node_types", {}).get("options", [])
        return [opt["value"] for opt in options]

    @classmethod
    def is_valid_node_type(cls, node_type: str) -> bool:
        """Check if a node type is valid."""
        return node_type in cls.get_node_types()

    @classmethod
    def get_edge_types(cls) -> list[str]:
        """Get all valid edge type values (all categories)."""
        categories = cls._yaml().get("edge_types", {}).get("categories", {})
        edge_types = []
        for category in categories.values():
            edge_types.extend(opt["value"] for opt in category.get("options", []))
        return edge_types

    @classmethod
    def is_valid_edge_type(cls, edge_type: str) -> bool:
        """Check if an edge type is valid."""
        return edge_type in cls.get_edge_types()

    @classmethod
    def get_link_types(cls) -> list[str]:
        """Get all valid Django link type values."""
        options = cls._yaml().get("link_types", {}).get("options", [])
        return [opt["value"] for opt in options]

    @classmethod
    def is_valid_link_type(cls, link_type: str) -> bool:
        """Check if a link type is valid."""
        return link_type in cls.get_link_types()

    @classmethod
    def get_edge_to_link_mapping(cls) -> dict[str, str]:
        """Get the edge type to link type mapping."""
        return cls._yaml().get("edge_to_link_mapping", {})

    @classmethod
    def map_edge_to_link(cls, edge_type: str) -> str:
        """Map an edge type to its corresponding link type."""
        mapping = cls.get_edge_to_link_mapping()
        return mapping.get(edge_type, "relates_to")

    @classmethod
    def get_pattern_types(cls) -> list[str]:
        """Get all valid emergence pattern types."""
        options = cls._yaml().get("emergence_patterns", {}).get("options", [])
        return [opt["value"] for opt in options]

    @classmethod
    def is_valid_pattern_type(cls, pattern_type: str) -> bool:
        """Check if a pattern type is valid."""
        return pattern_type in cls.get_pattern_types()

    @classmethod
    def get_weight_config(cls) -> dict[str, Any]:
        """Get weight scoring configuration."""
        return cls._yaml().get("weight_scoring", {})

    @classmethod
    def get_link_type_multiplier(cls, link_type: str) -> float:
        """Get the weight multiplier for a link type."""
        config = cls.get_weight_config()
        multipliers = config.get("link_type_multipliers", {})
        return multipliers.get(link_type, 0.5)

    @classmethod
    def get_actionable_threshold(cls) -> float:
        """Get the confidence threshold for actionable patterns."""
        config = cls._yaml().get("confidence_levels", {})
        return config.get("actionable_threshold", 0.7)

    @classmethod
    def get_emergence_scopes(cls) -> list[str]:
        """Get all valid emergence scope values."""
        options = cls._yaml().get("emergence_scopes", {}).get("options", [])
        return [opt["value"] for opt in options]

    @classmethod
    def is_valid_scope(cls, scope: str) -> bool:
        """Check if an emergence scope is valid."""
        return scope in cls.get_emergence_scopes()

    @classmethod
    def clear_cache(cls) -> None:
        """Clear the YAML cache (for testing)."""
        _load_knowledge_graph_yaml.cache_clear()


# =============================================================================
# ENUMS (Runtime-validated against YAML)
# =============================================================================


class ConfidenceLevel(StrEnum):
    """Confidence levels for emergence discoveries."""

    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"


# =============================================================================
# KNOWLEDGE NODE
# =============================================================================


class KnowledgeNode(LightwaveBaseSchema):
    """
    A node in the knowledge graph.

    Nodes represent entities that can be connected by edges.
    Node IDs follow the format: "{type}_{identifier}"

    Attributes:
        id: Unique node identifier (format: "type_id")
        node_type: Type of node (validated against YAML)
        label: Human-readable label
        metadata: Additional node metadata
        weight: Computed importance weight (0.0 if uncomputed)
        created_at: When the node was created
    """

    id: str = Field(..., min_length=3, description="Node ID in format 'type_id'")
    node_type: str = Field(..., description="Node type from knowledge_graph.yaml")
    label: str = Field(..., min_length=1, description="Human-readable label")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
    weight: float = Field(0.0, ge=0.0, description="Computed importance weight")
    created_at: datetime | None = Field(None, description="Creation timestamp")

    @field_validator("id")
    @classmethod
    def validate_id_format(cls, v: str) -> str:
        """Validate node ID format."""
        if "_" not in v:
            raise ValueError(f"Node ID must be in format 'type_id': {v}")
        parts = v.split("_", 1)
        if not parts[0] or not parts[1]:
            raise ValueError(f"Node ID must have non-empty type and id: {v}")
        return v

    @field_validator("node_type")
    @classmethod
    def validate_node_type(cls, v: str) -> str:
        """Validate node type against YAML definition."""
        if not KnowledgeGraphConfig.is_valid_node_type(v):
            valid = KnowledgeGraphConfig.get_node_types()
            raise ValueError(f"Invalid node_type '{v}'. Valid types: {valid}")
        return v

    @model_validator(mode="after")
    def validate_id_matches_type(self) -> "KnowledgeNode":
        """Ensure node ID prefix matches node_type."""
        prefix = self.id.split("_", 1)[0]
        if prefix != self.node_type:
            raise ValueError(f"Node ID prefix '{prefix}' doesn't match node_type '{self.node_type}'")
        return self

    def get_entity_id(self) -> str:
        """Extract the entity ID from the node ID."""
        return self.id.split("_", 1)[1]


# =============================================================================
# KNOWLEDGE EDGE
# =============================================================================


class KnowledgeEdge(LightwaveBaseSchema):
    """
    An edge (relationship) between two nodes in the knowledge graph.

    Edges represent semantic, temporal, or structural relationships
    discovered by emergence analysis.

    Attributes:
        source_id: Source node ID
        target_id: Target node ID
        edge_type: Type of relationship (validated against YAML)
        weight: Edge weight/strength (0.0-1.0)
        confidence: Confidence in this edge (0.0-1.0)
        metadata: Additional edge metadata
        discovered_at: When the edge was discovered
        discovered_by: What discovered this edge (e.g., "emergence_v1")
    """

    source_id: str = Field(..., min_length=3, description="Source node ID")
    target_id: str = Field(..., min_length=3, description="Target node ID")
    edge_type: str = Field(..., description="Edge type from knowledge_graph.yaml")
    weight: float = Field(1.0, ge=0.0, le=10.0, description="Edge weight/strength")
    confidence: float = Field(1.0, ge=0.0, le=1.0, description="Discovery confidence")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
    discovered_at: datetime | None = Field(None, description="Discovery timestamp")
    discovered_by: str = Field("manual", description="Discovery source")

    @field_validator("source_id", "target_id")
    @classmethod
    def validate_node_id_format(cls, v: str) -> str:
        """Validate node ID format."""
        if "_" not in v:
            raise ValueError(f"Node ID must be in format 'type_id': {v}")
        parts = v.split("_", 1)
        if not parts[0] or not parts[1]:
            raise ValueError(f"Node ID must have non-empty type and id: {v}")
        return v

    @field_validator("edge_type")
    @classmethod
    def validate_edge_type(cls, v: str) -> str:
        """Validate edge type against YAML definition."""
        if not KnowledgeGraphConfig.is_valid_edge_type(v):
            valid = KnowledgeGraphConfig.get_edge_types()
            raise ValueError(f"Invalid edge_type '{v}'. Valid types: {valid}")
        return v

    @model_validator(mode="after")
    def validate_not_self_referential(self) -> "KnowledgeEdge":
        """Ensure edge doesn't connect node to itself."""
        if self.source_id == self.target_id:
            raise ValueError(f"Edge cannot connect node to itself: {self.source_id}")
        return self

    def get_link_type(self) -> str:
        """Get the Django KnowledgeLink.link_type for this edge."""
        return KnowledgeGraphConfig.map_edge_to_link(self.edge_type)

    def is_bidirectional(self) -> bool:
        """Check if this edge type is bidirectional."""
        # Bidirectional edges: relates_to, contradicts, concurrent_with, similar_to
        bidirectional = {"relates_to", "contradicts", "concurrent_with", "similar_to"}
        return self.edge_type in bidirectional


# =============================================================================
# EMERGENCE PATTERN
# =============================================================================


class EmergencePattern(LightwaveBaseSchema):
    """
    A pattern discovered by emergence analysis.

    Patterns are higher-level structures found in the knowledge graph,
    such as clusters, dependency chains, orphans, or bottlenecks.

    Attributes:
        id: Unique pattern identifier
        pattern_type: Type of pattern (validated against YAML)
        description: Human-readable description
        involved_nodes: List of node IDs involved in this pattern
        involved_edges: List of edges involved (source_id, target_id tuples)
        confidence: Confidence in this pattern (0.0-1.0)
        actionable: Whether this pattern suggests an action
        suggested_action: Suggested action if actionable
        metadata: Additional pattern metadata
        discovered_at: When the pattern was discovered
    """

    id: str = Field(..., min_length=1, description="Pattern identifier")
    pattern_type: str = Field(..., description="Pattern type from knowledge_graph.yaml")
    description: str = Field("", description="Human-readable description")
    involved_nodes: list[str] = Field(default_factory=list, description="Node IDs in pattern")
    involved_edges: list[tuple[str, str]] = Field(default_factory=list, description="Edges in pattern (source, target)")
    confidence: float = Field(1.0, ge=0.0, le=1.0, description="Discovery confidence")
    actionable: bool = Field(False, description="Whether this suggests an action")
    suggested_action: str = Field("", description="Suggested action if actionable")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
    discovered_at: datetime | None = Field(None, description="Discovery timestamp")

    @field_validator("pattern_type")
    @classmethod
    def validate_pattern_type(cls, v: str) -> str:
        """Validate pattern type against YAML definition."""
        if not KnowledgeGraphConfig.is_valid_pattern_type(v):
            valid = KnowledgeGraphConfig.get_pattern_types()
            raise ValueError(f"Invalid pattern_type '{v}'. Valid types: {valid}")
        return v

    @model_validator(mode="after")
    def set_actionable_from_confidence(self) -> "EmergencePattern":
        """Set actionable based on confidence threshold if not explicitly set."""
        threshold = KnowledgeGraphConfig.get_actionable_threshold()
        if self.confidence >= threshold and self.suggested_action:
            object.__setattr__(self, "actionable", True)
        return self

    def get_confidence_level(self) -> ConfidenceLevel:
        """Get the confidence level for this pattern."""
        if self.confidence >= 0.8:
            return ConfidenceLevel.HIGH
        elif self.confidence >= 0.5:
            return ConfidenceLevel.MEDIUM
        return ConfidenceLevel.LOW


# =============================================================================
# EMERGENCE RESULT
# =============================================================================


class EmergenceResult(SSTSchema):
    """
    Complete result from an emergence cycle.

    Contains all discovered nodes, edges, and patterns from
    a single emergence analysis run.

    Attributes:
        cycle_id: Unique identifier for this cycle
        scope: Scope of analysis (validated against YAML)
        started_at: When the cycle started
        completed_at: When the cycle completed
        nodes_analyzed: Number of entities analyzed
        edges_discovered: Number of new edges found
        patterns_discovered: Number of patterns found
        nodes: List of discovered/updated nodes
        edges: List of discovered edges
        patterns: List of discovered patterns
        errors: Any errors encountered during analysis
    """

    model_config = LightwaveBaseSchema.model_config  # Not frozen for mutability

    cycle_id: str = Field(..., min_length=1, description="Cycle identifier")
    scope: str = Field(..., description="Emergence scope from knowledge_graph.yaml")
    started_at: datetime = Field(..., description="Cycle start time")
    completed_at: datetime | None = Field(None, description="Cycle completion time")
    nodes_analyzed: int = Field(0, ge=0, description="Number of entities analyzed")
    edges_discovered: int = Field(0, ge=0, description="Number of edges found")
    patterns_discovered: int = Field(0, ge=0, description="Number of patterns found")
    nodes: list[KnowledgeNode] = Field(default_factory=list, description="Discovered nodes")
    edges: list[KnowledgeEdge] = Field(default_factory=list, description="Discovered edges")
    patterns: list[EmergencePattern] = Field(default_factory=list, description="Discovered patterns")
    errors: list[str] = Field(default_factory=list, description="Errors encountered")

    @field_validator("scope")
    @classmethod
    def validate_scope(cls, v: str) -> str:
        """Validate scope against YAML definition."""
        if not KnowledgeGraphConfig.is_valid_scope(v):
            valid = KnowledgeGraphConfig.get_emergence_scopes()
            raise ValueError(f"Invalid scope '{v}'. Valid scopes: {valid}")
        return v

    def is_successful(self) -> bool:
        """Check if the cycle completed without errors."""
        return self.completed_at is not None and len(self.errors) == 0

    def get_actionable_patterns(self) -> list[EmergencePattern]:
        """Get patterns that have suggested actions."""
        return [p for p in self.patterns if p.actionable]

    def get_high_confidence_edges(self, threshold: float = 0.8) -> list[KnowledgeEdge]:
        """Get edges with confidence above threshold."""
        return [e for e in self.edges if e.confidence >= threshold]


# =============================================================================
# KNOWLEDGE LINK DTO (for Django persistence)
# =============================================================================


class KnowledgeLinkDTO(LightwaveBaseSchema):
    """
    Data Transfer Object for persisting to Django KnowledgeLink model.

    This is the final form before saving to the database, with all
    validation complete and edge types mapped to link types.

    Attributes:
        id: UUID for the link
        source_type: Type of source entity
        source_id: ID of source entity
        target_type: Type of target entity
        target_id: ID of target entity
        link_type: Django LinkType value (from mapping)
        context: Additional context string
        weight: Link weight
        metadata: Additional metadata
    """

    id: str = Field(..., description="UUID for the link")
    source_type: str = Field(..., min_length=1, description="Source entity type")
    source_id: str = Field(..., min_length=1, description="Source entity ID")
    target_type: str = Field(..., min_length=1, description="Target entity type")
    target_id: str = Field(..., min_length=1, description="Target entity ID")
    link_type: str = Field(..., description="Django KnowledgeLink.LinkType value")
    context: str = Field("", description="Additional context")
    weight: float = Field(1.0, ge=0.0, description="Link weight")
    metadata: dict[str, Any] = Field(default_factory=dict, description="Additional metadata")

    @field_validator("link_type")
    @classmethod
    def validate_link_type(cls, v: str) -> str:
        """Validate link type against YAML definition."""
        if not KnowledgeGraphConfig.is_valid_link_type(v):
            valid = KnowledgeGraphConfig.get_link_types()
            raise ValueError(f"Invalid link_type '{v}'. Valid types: {valid}")
        return v

    @classmethod
    def from_edge(cls, edge: KnowledgeEdge) -> "KnowledgeLinkDTO":
        """
        Create a KnowledgeLinkDTO from a KnowledgeEdge.

        Parses the node IDs and maps the edge type to link type.
        """
        from uuid import uuid4

        # Parse source node ID
        source_parts = edge.source_id.split("_", 1)
        source_type, source_id = source_parts[0], source_parts[1]

        # Parse target node ID
        target_parts = edge.target_id.split("_", 1)
        target_type, target_id = target_parts[0], target_parts[1]

        return cls(
            id=str(uuid4()),
            source_type=source_type,
            source_id=source_id,
            target_type=target_type,
            target_id=target_id,
            link_type=edge.get_link_type(),
            context=edge.metadata.get("context", ""),
            weight=edge.weight,
            metadata=edge.metadata,
        )


# =============================================================================
# MODULE EXPORTS
# =============================================================================

__all__ = [
    # Configuration
    "KnowledgeGraphConfig",
    # Enums
    "ConfidenceLevel",
    # Core schemas
    "KnowledgeNode",
    "KnowledgeEdge",
    "EmergencePattern",
    "EmergenceResult",
    # DTO
    "KnowledgeLinkDTO",
]
