"""
KnowledgeGraph persistence services.

This module bridges KnowledgeNode/KnowledgeEdge structures to Django's
KnowledgeLink model for persistent storage.

Key responsibilities:
1. Convert edge types to Django LinkType (via YAML config)
2. Parse node IDs into (type, id) tuples
3. Batch convert edges to KnowledgeLink DTOs
4. Compute node weights based on connectivity
5. Deduplicate links before persistence

All type mappings and valid values are loaded from:
    schema/definitions/knowledge_graph.yaml

Usage:
    from lightwave.data_quality.knowledge_graph import (
        batch_edges_to_links,
        compute_node_weight,
        deduplicate_links,
    )

    # Convert edges to Django-ready format
    links = batch_edges_to_links(emergence_edges)
    unique_links = deduplicate_links(links)

    # Compute node importance based on connections
    weight = compute_node_weight("task_abc123", emergence_edges)
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Protocol, Union

# Import typed schemas
from lightwave.schema.pydantic.models.knowledge_graph import (
    KnowledgeEdge,
    KnowledgeGraphConfig,
    KnowledgeLinkDTO,
    KnowledgeNode,
)

if TYPE_CHECKING:
    pass


# =============================================================================
# BACKWARDS-COMPATIBLE PROTOCOLS
# =============================================================================
# These protocols allow the module to accept both the new Pydantic models
# AND simple dataclass/dict objects that match the interface.


class KnowledgeEdgeLike(Protocol):
    """Protocol for KnowledgeEdge-like objects."""

    source_id: str
    target_id: str
    edge_type: str
    weight: float
    metadata: dict[str, Any]


class KnowledgeNodeLike(Protocol):
    """Protocol for KnowledgeNode-like objects."""

    id: str
    node_type: str
    label: str


# Type alias for functions that accept either typed or protocol objects
EdgeInput = Union[KnowledgeEdge, KnowledgeEdgeLike]
NodeInput = Union[KnowledgeNode, KnowledgeNodeLike]


# =============================================================================
# LINK TYPE CONSTANTS (derived from YAML)
# =============================================================================
# These provide convenience constants for common operations.
# The canonical values are in knowledge_graph.yaml.


class LinkType:
    """
    Django KnowledgeLink.LinkType choices.

    These values come from knowledge_graph.yaml and mirror the Django model.
    """

    RELATES_TO = "relates_to"
    DERIVED_FROM = "derived_from"
    SUPPORTS = "supports"
    CONTRADICTS = "contradicts"
    EXTENDS = "extends"
    IMPLEMENTS = "implements"


class EdgeType:
    """
    V-Emergence EdgeType values.

    These values come from knowledge_graph.yaml edge_types section.
    """

    # Semantic
    RELATES_TO = "relates_to"
    IS_PART_OF = "is_part_of"
    DEPENDS_ON = "depends_on"
    CONTRADICTS = "contradicts"
    SUPPORTS = "supports"
    DERIVED_FROM = "derived_from"
    # Temporal
    FOLLOWS = "follows"
    PRECEDES = "precedes"
    CONCURRENT_WITH = "concurrent_with"
    # Structural
    CONTAINS = "contains"
    REFERENCES = "references"
    SIMILAR_TO = "similar_to"


# Get mapping from YAML (cached)
EDGE_TO_LINK_TYPE_MAP: dict[str, str] = KnowledgeGraphConfig.get_edge_to_link_mapping()


# =============================================================================
# CORE FUNCTIONS
# =============================================================================


def parse_node_id(node_id: str) -> tuple[str, str]:
    """
    Parse a node ID into (type, id).

    Node IDs use format: "type_id" (e.g., "task_abc123", "concept_def456")

    Args:
        node_id: Node ID in "type_id" format

    Returns:
        Tuple of (type, id)

    Raises:
        ValueError: If node_id format is invalid

    Examples:
        >>> parse_node_id("task_abc123")
        ('task', 'abc123')
        >>> parse_node_id("concept_def456")
        ('concept', 'def456')
    """
    if not node_id or "_" not in node_id:
        raise ValueError(f"Invalid node_id format: {node_id}")

    parts = node_id.split("_", 1)
    if len(parts) != 2 or not parts[0] or not parts[1]:
        raise ValueError(f"Invalid node_id format: {node_id}")

    return parts[0], parts[1]


def map_edge_to_link_type(edge_type: str) -> str:
    """
    Map edge type to Django LinkType.

    Uses the mapping from knowledge_graph.yaml.

    Args:
        edge_type: Edge type string

    Returns:
        Django LinkType string (defaults to 'relates_to')
    """
    return KnowledgeGraphConfig.map_edge_to_link(edge_type)


def edge_to_link_dto(edge: EdgeInput) -> KnowledgeLinkDTO:
    """
    Convert a KnowledgeEdge to a KnowledgeLinkDTO.

    This is the core transformation from edge format to Django-persistable format.

    Args:
        edge: KnowledgeEdge or KnowledgeEdge-like object

    Returns:
        KnowledgeLinkDTO ready for Django persistence

    Raises:
        ValueError: If edge source_id or target_id is invalid
    """
    # If it's already a KnowledgeEdge, use its method
    if isinstance(edge, KnowledgeEdge):
        return KnowledgeLinkDTO.from_edge(edge)

    # Otherwise parse manually (for protocol objects)
    source_type, source_id = parse_node_id(edge.source_id)
    target_type, target_id = parse_node_id(edge.target_id)

    from uuid import uuid4

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


def batch_edges_to_links(edges: list[EdgeInput]) -> list[KnowledgeLinkDTO]:
    """
    Convert a batch of edges to link DTOs.

    Invalid edges are skipped (logged in production).

    Args:
        edges: List of KnowledgeEdge or KnowledgeEdge-like objects

    Returns:
        List of KnowledgeLinkDTO objects
    """
    links = []
    for edge in edges:
        try:
            links.append(edge_to_link_dto(edge))
        except ValueError:
            # Skip invalid edges
            continue
    return links


def deduplicate_links(links: list[KnowledgeLinkDTO]) -> list[KnowledgeLinkDTO]:
    """
    Remove duplicate links (same source/target pair).

    KnowledgeLink has a unique constraint on
    (source_type, source_id, target_type, target_id).

    Args:
        links: List of KnowledgeLinkDTO objects

    Returns:
        Deduplicated list of KnowledgeLinkDTO objects
    """
    seen: set[tuple[str, str, str, str]] = set()
    unique = []

    for link in links:
        key = (link.source_type, link.source_id, link.target_type, link.target_id)
        if key not in seen:
            seen.add(key)
            unique.append(link)

    return unique


# =============================================================================
# WEIGHT COMPUTATION
# =============================================================================


def compute_node_weight(
    node_id: str,
    edges: list[EdgeInput],
    weights_by_link_type: dict[str, float] | None = None,
) -> float:
    """
    Compute weight for a node based on its connectivity.

    The more connections a node has, the higher its weight.
    Different link types have different weight multipliers.

    Weight multipliers are loaded from knowledge_graph.yaml if not provided.

    Args:
        node_id: Node ID to compute weight for
        edges: List of edges in the graph
        weights_by_link_type: Optional custom weight multipliers

    Returns:
        Computed weight (0.0 if isolated)
    """
    if weights_by_link_type is None:
        # Load from YAML config
        config = KnowledgeGraphConfig.get_weight_config()
        weights_by_link_type = config.get("link_type_multipliers", {})
        # Add defaults for any missing types
        defaults = {
            LinkType.SUPPORTS: 1.5,
            LinkType.CONTRADICTS: 1.2,
            LinkType.DERIVED_FROM: 1.0,
            LinkType.RELATES_TO: 0.5,
            LinkType.EXTENDS: 1.0,
            LinkType.IMPLEMENTS: 1.2,
        }
        for k, v in defaults.items():
            weights_by_link_type.setdefault(k, v)

    # Get config values
    config = KnowledgeGraphConfig.get_weight_config()
    base_weight = config.get("base_weight", 1.0)
    max_bonus = config.get("max_connection_bonus", 2.0)
    bonus_per_edge = config.get("connection_bonus_per_edge", 0.1)

    total_weight = 0.0
    connection_count = 0

    for edge in edges:
        is_connected = edge.source_id == node_id or edge.target_id == node_id
        if is_connected:
            link_type = map_edge_to_link_type(edge.edge_type)
            type_weight = weights_by_link_type.get(link_type, 0.5)
            total_weight += edge.weight * type_weight
            connection_count += 1

    if connection_count == 0:
        return 0.0

    # Normalize: base weight + connection bonus
    # More connections = higher weight (diminishing returns)
    connection_bonus = min(connection_count * bonus_per_edge, max_bonus)
    average_weight = total_weight / connection_count

    return round(base_weight + connection_bonus + average_weight, 2)


def find_high_weight_nodes(
    nodes: list[NodeInput],
    edges: list[EdgeInput],
    threshold: float | None = None,
) -> list[tuple[str, float]]:
    """
    Find nodes with weight above threshold.

    Threshold defaults to 'high_weight' from knowledge_graph.yaml.

    Args:
        nodes: List of KnowledgeNode or KnowledgeNode-like objects
        edges: List of edges in the graph
        threshold: Minimum weight to include (default from YAML)

    Returns:
        List of (node_id, weight) tuples, sorted by weight descending
    """
    if threshold is None:
        config = KnowledgeGraphConfig.get_weight_config()
        threshold = config.get("thresholds", {}).get("high_weight", 2.5)

    weighted_nodes = []
    for node in nodes:
        weight = compute_node_weight(node.id, edges)
        if weight >= threshold:
            weighted_nodes.append((node.id, weight))

    return sorted(weighted_nodes, key=lambda x: x[1], reverse=True)


# =============================================================================
# DJANGO PERSISTENCE (requires Django context)
# =============================================================================


def persist_links_to_django(links: list[KnowledgeLinkDTO], batch_size: int = 100) -> int:
    """
    Persist KnowledgeLinkDTO objects to Django KnowledgeLink model.

    This function requires Django to be properly configured.

    Args:
        links: List of KnowledgeLinkDTO objects
        batch_size: Number of links to create per batch

    Returns:
        Number of links created
    """
    from uuid import UUID

    from django.apps import apps

    try:
        KnowledgeLink = apps.get_model("createos", "KnowledgeLink")
    except LookupError:
        raise RuntimeError("KnowledgeLink model not available. Is Django configured?")

    # Deduplicate first
    unique_links = deduplicate_links(links)

    created_count = 0
    for i in range(0, len(unique_links), batch_size):
        batch = unique_links[i : i + batch_size]

        link_objects = [
            KnowledgeLink(
                source_type=link.source_type,
                source_id=UUID(link.source_id) if len(link.source_id) == 36 else link.source_id,
                target_type=link.target_type,
                target_id=UUID(link.target_id) if len(link.target_id) == 36 else link.target_id,
                link_type=link.link_type,
                context=link.context,
            )
            for link in batch
        ]

        # Use bulk_create with ignore_conflicts to handle duplicates
        created = KnowledgeLink.objects.bulk_create(
            link_objects,
            ignore_conflicts=True,
        )
        created_count += len(created)

    return created_count


# =============================================================================
# RE-EXPORTS FOR CONVENIENCE
# =============================================================================

# Re-export typed schemas for convenience
__all__ = [
    # Typed schemas from schema module
    "KnowledgeNode",
    "KnowledgeEdge",
    "KnowledgeLinkDTO",
    "KnowledgeGraphConfig",
    # Protocols for backwards compatibility
    "KnowledgeEdgeLike",
    "KnowledgeNodeLike",
    # Type constants
    "LinkType",
    "EdgeType",
    "EDGE_TO_LINK_TYPE_MAP",
    # Core functions
    "parse_node_id",
    "map_edge_to_link_type",
    "edge_to_link_dto",
    "batch_edges_to_links",
    "deduplicate_links",
    # Weight computation
    "compute_node_weight",
    "find_high_weight_nodes",
    # Django persistence
    "persist_links_to_django",
]
