"""Entity relationships - cross-source connections between entities (Level 2)."""

from enum import Enum

from pydantic import Field

from semantic_model.base import (
    NamedModel,
    SemanticBaseModel,
    EntityId,
    FullyQualifiedName,
    SqlExpression,
)
from semantic_model.overrides import ExpertOverride


class Cardinality(str, Enum):
    """Cardinality of an entity relationship."""

    ONE_TO_ONE = "one_to_one"
    ONE_TO_MANY = "one_to_many"
    MANY_TO_MANY = "many_to_many"


class JoinType(str, Enum):
    """SQL join type."""

    INNER = "inner"
    LEFT = "left"
    RIGHT = "right"
    FULL = "full"


class JoinStep(SemanticBaseModel):
    """A single step in a join path."""

    from_table: FullyQualifiedName
    from_column: str
    to_table: FullyQualifiedName
    to_column: str
    join_type: JoinType = Field(default=JoinType.INNER)
    additional_conditions: SqlExpression | None = Field(
        default=None,
        description="Extra WHERE conditions for this join",
    )

    def to_sql(
        self,
        from_alias: str | None = None,
        to_alias: str | None = None,
    ) -> str:
        """Generate SQL join clause.
        
        Args:
            from_alias: Alias for the from table
            to_alias: Alias for the to table
            
        Returns:
            SQL join clause
        """
        join_keyword = f"{self.join_type.value.upper()} JOIN"
        
        from_ref = from_alias or self.from_table
        to_ref = to_alias or self.to_table
        
        sql = f"{join_keyword} {self.to_table}"
        if to_alias:
            sql += f" AS {to_alias}"
        
        sql += f" ON {from_ref}.{self.from_column} = {to_ref}.{self.to_column}"
        
        if self.additional_conditions:
            sql += f" AND {self.additional_conditions}"
        
        return sql


class JoinPath(NamedModel):
    """A path to join two entities, potentially through intermediate tables."""

    # Steps to complete the join
    steps: list[JoinStep] = Field(
        default_factory=list,
        min_length=1,
        description="Ordered steps to join the entities",
    )

    # Usage guidance
    use_when: str = Field(
        default="",
        description="When to use this join path",
    )
    performance_notes: str = Field(
        default="",
        description="Performance considerations",
    )
    freshness: str = Field(
        default="",
        description="Data freshness when using this path",
    )

    @property
    def table_count(self) -> int:
        """Number of tables involved in this path."""
        tables = set()
        for step in self.steps:
            tables.add(step.from_table)
            tables.add(step.to_table)
        return len(tables)

    @property
    def is_direct(self) -> bool:
        """Check if this is a direct (single-step) join."""
        return len(self.steps) == 1

    def to_sql(self, start_alias: str = "a") -> str:
        """Generate full SQL join clause for the path.
        
        Args:
            start_alias: Alias for the starting table
            
        Returns:
            Complete SQL join clause
        """
        if not self.steps:
            return ""
        
        # Generate sequential aliases
        aliases = [start_alias]
        for i in range(len(self.steps)):
            aliases.append(chr(ord(start_alias) + i + 1))
        
        parts = []
        for i, step in enumerate(self.steps):
            parts.append(step.to_sql(
                from_alias=aliases[i],
                to_alias=aliases[i + 1],
            ))
        
        return "\n".join(parts)


class EntityRelationship(NamedModel):
    """A relationship between two semantic entities."""

    # Entities involved
    from_entity_id: EntityId = Field(..., description="Source entity")
    to_entity_id: EntityId = Field(..., description="Target entity")
    cardinality: Cardinality = Field(default=Cardinality.MANY_TO_MANY)

    # Join paths (multiple ways to connect)
    join_paths: list[JoinPath] = Field(default_factory=list)
    recommended_path_id: str | None = Field(
        default=None,
        description="ID of the recommended join path",
    )
    path_selection_guidance: str = Field(
        default="",
        description="How to choose between paths",
    )

    # Confidence
    confidence: float = Field(default=0.5, ge=0.0, le=1.0)

    # Expert overrides
    expert_overrides: list[ExpertOverride] = Field(default_factory=list)

    @property
    def recommended_path(self) -> JoinPath | None:
        """Get the recommended join path."""
        if not self.recommended_path_id:
            return self.join_paths[0] if self.join_paths else None
        
        for path in self.join_paths:
            if path.id == self.recommended_path_id:
                return path
        return None

    @property
    def has_direct_path(self) -> bool:
        """Check if there's a direct (single-step) join path."""
        return any(p.is_direct for p in self.join_paths)

    def get_path(self, path_id: str) -> JoinPath | None:
        """Get a join path by ID."""
        for path in self.join_paths:
            if path.id == path_id:
                return path
        return None

    def to_prompt_format(self) -> str:
        """Convert to a compact format for LLM prompts."""
        lines = [
            f"{self.from_entity_id} -> {self.to_entity_id} ({self.cardinality.value})",
        ]
        
        if self.description:
            lines.append(f"  {self.description}")
        
        recommended = self.recommended_path
        if recommended:
            lines.append(f"  Recommended path: {recommended.name}")
            if recommended.steps:
                for step in recommended.steps:
                    lines.append(
                        f"    {step.from_table}.{step.from_column} "
                        f"-> {step.to_table}.{step.to_column}"
                    )
        
        return "\n".join(lines)


class RelationshipGraph:
    """Utility class for working with entity relationships as a graph."""

    def __init__(self, relationships: list[EntityRelationship]):
        self.relationships = relationships
        self._build_index()

    def _build_index(self) -> None:
        """Build indices for fast lookup."""
        self._from_index: dict[str, list[EntityRelationship]] = {}
        self._to_index: dict[str, list[EntityRelationship]] = {}
        
        for rel in self.relationships:
            if rel.from_entity_id not in self._from_index:
                self._from_index[rel.from_entity_id] = []
            self._from_index[rel.from_entity_id].append(rel)
            
            if rel.to_entity_id not in self._to_index:
                self._to_index[rel.to_entity_id] = []
            self._to_index[rel.to_entity_id].append(rel)

    def get_relationships_from(self, entity_id: EntityId) -> list[EntityRelationship]:
        """Get all relationships from an entity."""
        return self._from_index.get(entity_id, [])

    def get_relationships_to(self, entity_id: EntityId) -> list[EntityRelationship]:
        """Get all relationships to an entity."""
        return self._to_index.get(entity_id, [])

    def get_relationship(
        self,
        from_entity_id: EntityId,
        to_entity_id: EntityId,
    ) -> EntityRelationship | None:
        """Get the relationship between two specific entities."""
        for rel in self.get_relationships_from(from_entity_id):
            if rel.to_entity_id == to_entity_id:
                return rel
        return None

    def get_connected_entities(self, entity_id: EntityId) -> set[EntityId]:
        """Get all entities connected to a given entity."""
        connected = set()
        
        for rel in self.get_relationships_from(entity_id):
            connected.add(rel.to_entity_id)
        
        for rel in self.get_relationships_to(entity_id):
            connected.add(rel.from_entity_id)
        
        return connected

    def find_path(
        self,
        from_entity_id: EntityId,
        to_entity_id: EntityId,
        max_hops: int = 3,
    ) -> list[EntityRelationship] | None:
        """Find a path of relationships between two entities (BFS).
        
        Args:
            from_entity_id: Starting entity
            to_entity_id: Target entity
            max_hops: Maximum number of relationships to traverse
            
        Returns:
            List of relationships forming the path, or None if no path found
        """
        if from_entity_id == to_entity_id:
            return []
        
        # BFS
        from collections import deque
        
        queue: deque[tuple[EntityId, list[EntityRelationship]]] = deque()
        queue.append((from_entity_id, []))
        visited = {from_entity_id}
        
        while queue:
            current, path = queue.popleft()
            
            if len(path) >= max_hops:
                continue
            
            for rel in self.get_relationships_from(current):
                next_entity = rel.to_entity_id
                
                if next_entity == to_entity_id:
                    return path + [rel]
                
                if next_entity not in visited:
                    visited.add(next_entity)
                    queue.append((next_entity, path + [rel]))
            
            # Also check reverse relationships
            for rel in self.get_relationships_to(current):
                next_entity = rel.from_entity_id
                
                if next_entity == to_entity_id:
                    return path + [rel]
                
                if next_entity not in visited:
                    visited.add(next_entity)
                    queue.append((next_entity, path + [rel]))
        
        return None
