"""
Emergence Discovery Service.

This module provides services for discovering relationships between entities
and identifying patterns in the knowledge graph.

The emergence service:
1. Converts entities to typed KnowledgeNode objects
2. Discovers edges between entities (same sprint, dependencies, tags, etc.)
3. Identifies patterns (orphans, hubs, clusters, dependency chains)
4. Returns typed EmergenceResult ready for persistence

Usage:
    from lightwave.data_quality.emergence_service import (
        EmergenceService,
        run_emergence_cycle,
    )

    # Functional API
    result = run_emergence_cycle(tasks, entity_type="task", scope="active_sprint")

    # OOP API
    service = EmergenceService()
    service.add_entities(tasks, entity_type="task")
    service.add_entities(sprints, entity_type="sprint")
    result = service.run(scope="active_sprint")

All type validations use knowledge_graph.yaml as the single source of truth.
"""

from __future__ import annotations

from collections import defaultdict
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4

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

# =============================================================================
# CONSTANTS
# =============================================================================

# Minimum tag overlap for SIMILAR_TO edge
MIN_TAG_OVERLAP = 1

# Minimum connections to be considered a hub
HUB_MIN_CONNECTIONS = 5

# Confidence scores for different relationship types
CONFIDENCE_SAME_SPRINT = 0.8
CONFIDENCE_DEPENDS_ON = 1.0
CONFIDENCE_IS_PART_OF = 0.9
CONFIDENCE_TAG_OVERLAP_BASE = 0.3
CONFIDENCE_TAG_OVERLAP_PER_TAG = 0.1


# =============================================================================
# ENTITY TO NODE CONVERSION
# =============================================================================


def entities_to_nodes(entities: list[Any], entity_type: str) -> list[KnowledgeNode]:
    """
    Convert entities to typed KnowledgeNode objects.

    Args:
        entities: List of Django model instances or entity-like objects
        entity_type: One of the valid node types from knowledge_graph.yaml

    Returns:
        List of KnowledgeNode objects

    Raises:
        ValueError: If entity_type is not valid
    """
    # Validate entity type against YAML
    valid_types = KnowledgeGraphConfig.get_node_types()
    if entity_type not in valid_types:
        raise ValueError(f"Invalid entity_type '{entity_type}'. Valid types: {', '.join(valid_types)}")

    nodes = []
    for entity in entities:
        node = _entity_to_node(entity, entity_type)
        if node is not None:
            nodes.append(node)

    return nodes


def _entity_to_node(entity: Any, entity_type: str) -> KnowledgeNode | None:
    """
    Convert a single entity to a KnowledgeNode.

    Args:
        entity: A Django model instance or entity-like object
        entity_type: The node type

    Returns:
        KnowledgeNode or None if entity lacks required attributes
    """
    # Get entity ID
    entity_id = getattr(entity, "id", None)
    if entity_id is None:
        return None

    entity_id_str = str(entity_id)
    node_id = f"{entity_type}_{entity_id_str}"

    # Get label (title, name, or fallback to node_id)
    label = getattr(entity, "title", None) or getattr(entity, "name", None) or node_id

    # Extract metadata
    metadata = _extract_metadata(entity, entity_type)

    return KnowledgeNode(
        id=node_id,
        node_type=entity_type,
        label=str(label),
        metadata=metadata,
    )


def _extract_metadata(entity: Any, entity_type: str) -> dict[str, Any]:
    """
    Extract relevant metadata from an entity.

    Args:
        entity: The entity object
        entity_type: The node type

    Returns:
        Dict of metadata values
    """
    metadata: dict[str, Any] = {}

    # Common fields
    if hasattr(entity, "status"):
        metadata["status"] = entity.status

    if hasattr(entity, "tags"):
        metadata["tags"] = list(entity.tags) if entity.tags else []

    if hasattr(entity, "description"):
        metadata["description"] = entity.description

    # Task-specific
    if entity_type == "task":
        if hasattr(entity, "sprint_id") and entity.sprint_id:
            metadata["sprint_id"] = str(entity.sprint_id)
        if hasattr(entity, "epic_id") and entity.epic_id:
            metadata["epic_id"] = str(entity.epic_id)
        if hasattr(entity, "blocked_by") and entity.blocked_by:
            metadata["blocked_by"] = list(entity.blocked_by)

    # Sprint-specific
    if entity_type == "sprint":
        if hasattr(entity, "start_date"):
            metadata["start_date"] = str(entity.start_date) if entity.start_date else None
        if hasattr(entity, "end_date"):
            metadata["end_date"] = str(entity.end_date) if entity.end_date else None

    return metadata


# =============================================================================
# EDGE DISCOVERY
# =============================================================================


def discover_edges(
    nodes: list[KnowledgeNode],
    entities: list[Any],
    entity_type: str,
) -> list[KnowledgeEdge]:
    """
    Discover relationships between entities.

    Discovers the following relationship types:
    - RELATES_TO: Tasks in the same sprint
    - DEPENDS_ON: Tasks blocked by other tasks
    - IS_PART_OF: Tasks belonging to an epic
    - SIMILAR_TO: Tasks with overlapping tags

    Args:
        nodes: List of KnowledgeNode objects
        entities: Original entity objects (for accessing relationships)
        entity_type: The entity type being processed

    Returns:
        List of KnowledgeEdge objects
    """
    edges: list[KnowledgeEdge] = []

    if entity_type == "task":
        edges.extend(_discover_sprint_relationships(nodes, entities))
        edges.extend(_discover_blocking_relationships(nodes, entities))
        edges.extend(_discover_epic_relationships(nodes, entities))
        edges.extend(_discover_tag_similarity(nodes, entities))

    return edges


def _discover_sprint_relationships(
    nodes: list[KnowledgeNode],
    entities: list[Any],
) -> list[KnowledgeEdge]:
    """
    Discover RELATES_TO edges between tasks in the same sprint.
    """
    edges: list[KnowledgeEdge] = []

    # Group entities by sprint_id
    by_sprint: dict[str, list[tuple[KnowledgeNode, Any]]] = defaultdict(list)
    for node, entity in zip(nodes, entities, strict=False):
        sprint_id = getattr(entity, "sprint_id", None)
        if sprint_id:
            by_sprint[str(sprint_id)].append((node, entity))

    # Create edges between tasks in the same sprint
    for sprint_id, group in by_sprint.items():
        for i, (node_a, _) in enumerate(group):
            for node_b, _ in group[i + 1 :]:
                edges.append(
                    KnowledgeEdge(
                        source_id=node_a.id,
                        target_id=node_b.id,
                        edge_type="relates_to",
                        weight=1.0,
                        confidence=CONFIDENCE_SAME_SPRINT,
                        metadata={"reason": "same_sprint", "sprint_id": sprint_id},
                    )
                )

    return edges


def _discover_blocking_relationships(
    nodes: list[KnowledgeNode],
    entities: list[Any],
) -> list[KnowledgeEdge]:
    """
    Discover DEPENDS_ON edges from blocked_by relationships.
    """
    edges: list[KnowledgeEdge] = []

    # Build lookup from raw entity ID to node ID
    id_to_node = {}
    for node, entity in zip(nodes, entities, strict=False):
        raw_id = str(getattr(entity, "id", ""))
        id_to_node[raw_id] = node

    for node, entity in zip(nodes, entities, strict=False):
        blocked_by = getattr(entity, "blocked_by", None) or []

        for blocker_id in blocked_by:
            blocker_id_str = str(blocker_id)

            # Skip self-reference
            raw_entity_id = str(getattr(entity, "id", ""))
            if blocker_id_str == raw_entity_id:
                continue

            # Only create edge if blocker exists in our nodes
            if blocker_id_str in id_to_node:
                blocker_node = id_to_node[blocker_id_str]
                edges.append(
                    KnowledgeEdge(
                        source_id=node.id,  # The dependent task
                        target_id=blocker_node.id,  # The blocker
                        edge_type="depends_on",
                        weight=1.0,
                        confidence=CONFIDENCE_DEPENDS_ON,
                        metadata={"reason": "blocked_by"},
                    )
                )

    return edges


def _discover_epic_relationships(
    nodes: list[KnowledgeNode],
    entities: list[Any],
) -> list[KnowledgeEdge]:
    """
    Discover IS_PART_OF edges for tasks belonging to epics.
    """
    edges: list[KnowledgeEdge] = []

    for node, entity in zip(nodes, entities, strict=False):
        epic_id = getattr(entity, "epic_id", None)
        if epic_id:
            epic_node_id = f"epic_{epic_id}"
            edges.append(
                KnowledgeEdge(
                    source_id=node.id,
                    target_id=epic_node_id,
                    edge_type="is_part_of",
                    weight=1.0,
                    confidence=CONFIDENCE_IS_PART_OF,
                    metadata={"reason": "epic_membership", "epic_id": str(epic_id)},
                )
            )

    return edges


def _discover_tag_similarity(
    nodes: list[KnowledgeNode],
    entities: list[Any],
) -> list[KnowledgeEdge]:
    """
    Discover SIMILAR_TO edges based on tag overlap.
    """
    edges: list[KnowledgeEdge] = []

    # Build tag sets for each node
    node_tags: list[tuple[KnowledgeNode, set[str]]] = []
    for node, entity in zip(nodes, entities, strict=False):
        tags = getattr(entity, "tags", None) or []
        if tags:
            node_tags.append((node, set(tags)))

    # Compare all pairs
    for i, (node_a, tags_a) in enumerate(node_tags):
        for node_b, tags_b in node_tags[i + 1 :]:
            overlap = tags_a & tags_b
            if len(overlap) >= MIN_TAG_OVERLAP:
                # Confidence scales with overlap
                confidence = min(
                    CONFIDENCE_TAG_OVERLAP_BASE + len(overlap) * CONFIDENCE_TAG_OVERLAP_PER_TAG,
                    1.0,
                )
                edges.append(
                    KnowledgeEdge(
                        source_id=node_a.id,
                        target_id=node_b.id,
                        edge_type="similar_to",
                        weight=len(overlap) / max(len(tags_a), len(tags_b)),
                        confidence=confidence,
                        metadata={"reason": "tag_overlap", "shared_tags": list(overlap)},
                    )
                )

    return edges


# =============================================================================
# PATTERN DISCOVERY
# =============================================================================


def discover_patterns(
    nodes: list[KnowledgeNode],
    edges: list[KnowledgeEdge],
) -> list[EmergencePattern]:
    """
    Discover patterns in the knowledge graph.

    Identifies:
    - Orphans: Nodes with no connections
    - Hubs: Highly connected nodes
    - Dependency chains: Linear dependency sequences
    - Bottlenecks: Nodes that many others depend on

    Args:
        nodes: List of KnowledgeNode objects
        edges: List of KnowledgeEdge objects

    Returns:
        List of EmergencePattern objects
    """
    if not nodes:
        return []

    patterns: list[EmergencePattern] = []

    patterns.extend(_find_orphans(nodes, edges))
    patterns.extend(_find_hubs(nodes, edges))
    patterns.extend(_find_dependency_chains(nodes, edges))
    patterns.extend(_find_bottlenecks(nodes, edges))

    return patterns


def _find_orphans(
    nodes: list[KnowledgeNode],
    edges: list[KnowledgeEdge],
) -> list[EmergencePattern]:
    """Find nodes with no connections."""
    patterns: list[EmergencePattern] = []

    # Build set of connected node IDs
    connected: set[str] = set()
    for edge in edges:
        connected.add(edge.source_id)
        connected.add(edge.target_id)

    # Find orphans
    orphans = [n for n in nodes if n.id not in connected]

    for orphan in orphans:
        patterns.append(
            EmergencePattern(
                id=f"orphan_{str(uuid4())[:8]}",
                pattern_type="orphan",
                involved_nodes=[orphan.id],
                confidence=1.0,
                actionable=True,
                suggested_action=f"Review {orphan.label}: Consider linking to related entities or archiving if obsolete.",
                metadata={"node_type": orphan.node_type},
            )
        )

    return patterns


def _find_hubs(
    nodes: list[KnowledgeNode],
    edges: list[KnowledgeEdge],
) -> list[EmergencePattern]:
    """Find highly connected nodes."""
    patterns: list[EmergencePattern] = []

    # Count connections per node
    connection_count: dict[str, int] = defaultdict(int)
    for edge in edges:
        connection_count[edge.source_id] += 1
        connection_count[edge.target_id] += 1

    # Find hubs
    node_by_id = {n.id: n for n in nodes}
    for node_id, count in connection_count.items():
        if count >= HUB_MIN_CONNECTIONS and node_id in node_by_id:
            node = node_by_id[node_id]
            patterns.append(
                EmergencePattern(
                    id=f"hub_{str(uuid4())[:8]}",
                    pattern_type="hub",
                    involved_nodes=[node_id],
                    confidence=min(count / 10, 1.0),  # Higher count = higher confidence
                    actionable=False,
                    suggested_action=f"{node.label} is a hub with {count} connections - monitor for scope creep.",
                    metadata={"connection_count": count},
                )
            )

    return patterns


def _find_dependency_chains(
    nodes: list[KnowledgeNode],
    edges: list[KnowledgeEdge],
) -> list[EmergencePattern]:
    """Find linear dependency sequences."""
    patterns: list[EmergencePattern] = []

    # Build dependency graph (only DEPENDS_ON edges)
    depends_on = defaultdict(list)  # node -> [nodes it depends on]
    depended_by = defaultdict(list)  # node -> [nodes that depend on it]

    for edge in edges:
        if edge.edge_type == "depends_on":
            depends_on[edge.source_id].append(edge.target_id)
            depended_by[edge.target_id].append(edge.source_id)

    # Find chain starts (nodes with dependents but no dependencies)
    chain_ends: set[str] = set()
    for node_id in depends_on:
        # This node depends on others
        for dependency in depends_on[node_id]:
            if not depends_on.get(dependency):
                # Dependency has no further dependencies = potential chain end
                chain_ends.add(dependency)

    # Trace chains from ends
    visited: set[str] = set()
    for chain_end in chain_ends:
        if chain_end in visited:
            continue

        chain = _trace_chain(chain_end, depended_by, visited)
        if len(chain) >= 2:  # Need at least 2 nodes for a chain
            patterns.append(
                EmergencePattern(
                    id=f"chain_{str(uuid4())[:8]}",
                    pattern_type="dependency_chain",
                    involved_nodes=chain,
                    confidence=0.8,
                    actionable=True,
                    suggested_action=f"Dependency chain of {len(chain)} tasks - consider parallel execution opportunities.",
                    metadata={"chain_length": len(chain)},
                )
            )

    return patterns


def _trace_chain(
    start_node: str,
    depended_by: dict[str, list[str]],
    visited: set[str],
) -> list[str]:
    """Trace a dependency chain from a starting node."""
    chain = [start_node]
    visited.add(start_node)

    current = start_node
    while True:
        dependents = depended_by.get(current, [])
        unvisited = [d for d in dependents if d not in visited]

        if not unvisited:
            break

        # Follow the first unvisited dependent
        next_node = unvisited[0]
        chain.append(next_node)
        visited.add(next_node)
        current = next_node

    return chain


def _find_bottlenecks(
    nodes: list[KnowledgeNode],
    edges: list[KnowledgeEdge],
) -> list[EmergencePattern]:
    """Find nodes that many others depend on."""
    patterns: list[EmergencePattern] = []

    # Count how many nodes depend on each node
    dependency_count: dict[str, int] = defaultdict(int)
    for edge in edges:
        if edge.edge_type == "depends_on":
            dependency_count[edge.target_id] += 1

    # Find bottlenecks (nodes with 3+ dependents)
    BOTTLENECK_THRESHOLD = 3
    node_by_id = {n.id: n for n in nodes}

    for node_id, count in dependency_count.items():
        if count >= BOTTLENECK_THRESHOLD and node_id in node_by_id:
            node = node_by_id[node_id]
            patterns.append(
                EmergencePattern(
                    id=f"bottleneck_{str(uuid4())[:8]}",
                    pattern_type="bottleneck",
                    involved_nodes=[node_id],
                    confidence=min(count / 5, 1.0),
                    actionable=True,
                    suggested_action=f"{node.label} blocks {count} other tasks - prioritize or split to reduce risk.",
                    metadata={"dependent_count": count},
                )
            )

    return patterns


# =============================================================================
# MAIN ENTRY POINTS
# =============================================================================


def run_emergence_cycle(
    entities: list[Any],
    entity_type: str,
    scope: str,
) -> EmergenceResult:
    """
    Run a complete emergence cycle.

    This is the functional API for running emergence discovery.

    Args:
        entities: List of entity objects to analyze
        entity_type: The type of entities (task, sprint, etc.)
        scope: The scope of the cycle (from emergence_scopes in YAML)

    Returns:
        EmergenceResult with nodes, edges, and patterns

    Raises:
        ValueError: If scope is invalid
    """
    started_at = datetime.now(timezone.utc)

    # Validate scope
    valid_scopes = KnowledgeGraphConfig.get_emergence_scopes()
    if scope not in valid_scopes:
        raise ValueError(f"Invalid scope '{scope}'. Valid scopes: {', '.join(valid_scopes)}")

    # Convert entities to nodes
    nodes = entities_to_nodes(entities, entity_type)

    # Discover edges
    edges = discover_edges(nodes, entities, entity_type)

    # Discover patterns
    patterns = discover_patterns(nodes, edges)

    completed_at = datetime.now(timezone.utc)

    return EmergenceResult(
        cycle_id=str(uuid4()),
        scope=scope,
        started_at=started_at,
        completed_at=completed_at,
        nodes_analyzed=len(entities),
        edges_discovered=len(edges),
        patterns_discovered=len(patterns),
        nodes=nodes,
        edges=edges,
        patterns=patterns,
    )


class EmergenceService:
    """
    OOP interface for emergence discovery.

    Allows adding multiple entity types before running the cycle.

    Usage:
        service = EmergenceService()
        service.add_entities(tasks, entity_type="task")
        service.add_entities(sprints, entity_type="sprint")
        result = service.run(scope="active_sprint")
    """

    def __init__(self) -> None:
        self._entities: list[tuple[list[Any], str]] = []
        self._nodes: list[KnowledgeNode] = []
        self._edges: list[KnowledgeEdge] = []

    def add_entities(self, entities: list[Any], entity_type: str) -> None:
        """
        Add entities to be analyzed.

        Args:
            entities: List of entity objects
            entity_type: The type of entities
        """
        self._entities.append((entities, entity_type))

        # Convert to nodes immediately
        nodes = entities_to_nodes(entities, entity_type)
        self._nodes.extend(nodes)

        # Discover edges within this entity type
        edges = discover_edges(nodes, entities, entity_type)
        self._edges.extend(edges)

    def run(self, scope: str) -> EmergenceResult:
        """
        Run the emergence cycle.

        Args:
            scope: The scope of the cycle

        Returns:
            EmergenceResult with all discovered nodes, edges, and patterns
        """
        started_at = datetime.now(timezone.utc)

        # Validate scope
        valid_scopes = KnowledgeGraphConfig.get_emergence_scopes()
        if scope not in valid_scopes:
            raise ValueError(f"Invalid scope '{scope}'. Valid scopes: {', '.join(valid_scopes)}")

        # Discover cross-entity edges
        cross_edges = self._discover_cross_entity_edges()
        all_edges = self._edges + cross_edges

        # Discover patterns
        patterns = discover_patterns(self._nodes, all_edges)

        completed_at = datetime.now(timezone.utc)

        total_entities = sum(len(e) for e, _ in self._entities)

        return EmergenceResult(
            cycle_id=str(uuid4()),
            scope=scope,
            started_at=started_at,
            completed_at=completed_at,
            nodes_analyzed=total_entities,
            edges_discovered=len(all_edges),
            patterns_discovered=len(patterns),
            nodes=self._nodes,
            edges=all_edges,
            patterns=patterns,
        )

    def _discover_cross_entity_edges(self) -> list[KnowledgeEdge]:
        """
        Discover edges between different entity types.

        For example: task IS_PART_OF sprint, task RELATES_TO sprint, etc.
        """
        edges: list[KnowledgeEdge] = []

        # Build lookup by node type
        by_type: dict[str, list[KnowledgeNode]] = defaultdict(list)
        for node in self._nodes:
            by_type[node.node_type].append(node)

        # Link tasks to sprints they belong to
        tasks = by_type.get("task", [])
        sprints = by_type.get("sprint", [])

        if tasks and sprints:
            sprint_ids = {n.id for n in sprints}
            for task_node in tasks:
                sprint_id = task_node.metadata.get("sprint_id")
                if sprint_id:
                    sprint_node_id = f"sprint_{sprint_id}"
                    if sprint_node_id in sprint_ids:
                        edges.append(
                            KnowledgeEdge(
                                source_id=task_node.id,
                                target_id=sprint_node_id,
                                edge_type="is_part_of",
                                weight=1.0,
                                confidence=1.0,
                                metadata={"reason": "sprint_membership"},
                            )
                        )

        return edges


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

__all__ = [
    "EmergenceService",
    "discover_edges",
    "discover_patterns",
    "entities_to_nodes",
    "run_emergence_cycle",
]
