"""Root SemanticModel - the top-level container for the entire semantic model."""

from datetime import datetime

from pydantic import Field

from semantic_model.base import SemanticBaseModel, SourceId, EntityId
from semantic_model.metadata import ModelMetadata
from semantic_model.indexing import IndexingState, ReindexJob, ReindexTrigger, ReindexJobType
from semantic_model.sources import DataSource
from semantic_model.entities import SemanticEntity
from semantic_model.entity_relationships import EntityRelationship, RelationshipGraph
from semantic_model.glossary import GlossaryTerm, Glossary


class SemanticModel(SemanticBaseModel):
    """The complete semantic model for an organization's data landscape.

    This is the root object containing:
    - Level 1: Data sources with tables, columns, and internal relationships
    - Level 2: Semantic entities, cross-source relationships, and unified attributes
    - Shared: Glossary terms and indexing state
    """

    # Metadata
    metadata: ModelMetadata

    # Level 1: Source-scoped semantics
    sources: list[DataSource] = Field(
        default_factory=list,
        description="Data sources indexed by Level 1 agent",
    )

    # Level 2: Cross-source semantics
    entities: list[SemanticEntity] = Field(
        default_factory=list,
        description="Semantic entities created by Level 2 agent",
    )
    entity_relationships: list[EntityRelationship] = Field(
        default_factory=list,
        description="Relationships between entities",
    )

    # Shared: Glossary
    glossary: list[GlossaryTerm] = Field(
        default_factory=list,
        description="Business terminology definitions",
    )

    # Indexing state
    indexing_state: IndexingState = Field(default_factory=IndexingState)

    # =========================================================================
    # Source operations
    # =========================================================================

    @property
    def source_count(self) -> int:
        """Get number of data sources."""
        return len(self.sources)

    @property
    def confirmed_sources(self) -> list[DataSource]:
        """Get all confirmed (production-ready) sources."""
        return [s for s in self.sources if s.is_confirmed]

    @property
    def staging_sources(self) -> list[DataSource]:
        """Get all sources in staging (needing review)."""
        return [s for s in self.sources if s.needs_review]

    def get_source(self, source_id: SourceId) -> DataSource | None:
        """Get a data source by ID."""
        for source in self.sources:
            if source.id == source_id:
                return source
        return None

    def get_source_by_name(self, name: str) -> DataSource | None:
        """Get a data source by name."""
        for source in self.sources:
            if source.name == name:
                return source
        return None

    def add_source(self, source: DataSource) -> "SemanticModel":
        """Add a new data source."""
        return self.model_copy(
            update={
                "sources": [*self.sources, source],
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    def update_source(self, source: DataSource) -> "SemanticModel":
        """Update an existing data source."""
        sources = [s if s.id != source.id else source for s in self.sources]
        return self.model_copy(
            update={
                "sources": sources,
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    def remove_source(self, source_id: SourceId) -> "SemanticModel":
        """Remove a data source."""
        sources = [s for s in self.sources if s.id != source_id]
        return self.model_copy(
            update={
                "sources": sources,
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    # =========================================================================
    # Entity operations
    # =========================================================================

    @property
    def entity_count(self) -> int:
        """Get number of semantic entities."""
        return len(self.entities)

    def get_entity(self, entity_id: EntityId) -> SemanticEntity | None:
        """Get a semantic entity by ID."""
        for entity in self.entities:
            if entity.id == entity_id:
                return entity
        return None

    def get_entity_by_name(self, name: str) -> SemanticEntity | None:
        """Get a semantic entity by name."""
        for entity in self.entities:
            if entity.name == name:
                return entity
        return None

    def add_entity(self, entity: SemanticEntity) -> "SemanticModel":
        """Add a new semantic entity."""
        return self.model_copy(
            update={
                "entities": [*self.entities, entity],
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    def update_entity(self, entity: SemanticEntity) -> "SemanticModel":
        """Update an existing semantic entity."""
        entities = [e if e.id != entity.id else entity for e in self.entities]
        return self.model_copy(
            update={
                "entities": entities,
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    def get_relationship_graph(self) -> RelationshipGraph:
        """Get the entity relationship graph for path finding."""
        return RelationshipGraph(self.entity_relationships)

    # =========================================================================
    # Glossary operations
    # =========================================================================

    def get_glossary(self) -> Glossary:
        """Get a Glossary object for lookups."""
        return Glossary(self.glossary)

    def add_glossary_term(self, term: GlossaryTerm) -> "SemanticModel":
        """Add a glossary term."""
        return self.model_copy(
            update={
                "glossary": [*self.glossary, term],
                "metadata": self.metadata.model_copy(update={"updated_at": datetime.utcnow()}),
            }
        )

    # =========================================================================
    # Indexing operations
    # =========================================================================

    def queue_reindex(
        self,
        job_type: ReindexJobType,
        trigger: ReindexTrigger,
        target_source_ids: list[SourceId],
        level: int = 1,
    ) -> "SemanticModel":
        """Queue a reindex job."""
        import uuid

        job = ReindexJob(
            id=str(uuid.uuid4()),
            job_type=job_type,
            triggered_by=trigger,
            target_source_ids=target_source_ids,
            level=level,
        )

        new_state = self.indexing_state.add_job(job)

        return self.model_copy(update={"indexing_state": new_state})

    def get_cascade_targets(self, source_id: SourceId) -> list[SourceId]:
        """Get source IDs that should be reindexed when a source changes.

        This finds all sources that depend on the given source.
        """
        targets = []

        for source in self.sources:
            for dep in source.dependencies:
                if dep.depends_on_source_id == source_id:
                    targets.append(source.id)
                    break

        return targets

    # =========================================================================
    # Prompt generation
    # =========================================================================

    def to_prompt_format(
        self,
        include_sources: bool = True,
        include_entities: bool = True,
        include_glossary: bool = True,
        include_columns: bool = False,
        max_glossary_terms: int = 30,
    ) -> str:
        """Convert the model to a compact format for LLM prompts.

        Args:
            include_sources: Include source descriptions
            include_entities: Include entity descriptions
            include_glossary: Include glossary terms
            include_columns: Include column details (verbose)
            max_glossary_terms: Maximum glossary terms to include

        Returns:
            Formatted string for LLM context
        """
        sections = [
            f"# Semantic Data Model: {self.metadata.id}",
            f"Version: {self.metadata.version}",
            "",
        ]

        if include_sources and self.sources:
            sections.append("## Data Sources\n")
            for source in self.confirmed_sources:
                sections.append(
                    source.to_prompt_format(
                        include_tables=True,
                        include_columns=include_columns,
                    )
                )
                sections.append("")

        if include_entities and self.entities:
            sections.append("## Business Entities\n")
            for entity in self.entities:
                sections.append(entity.to_prompt_format())
                sections.append("")

        if include_glossary and self.glossary:
            glossary = self.get_glossary()
            sections.append(glossary.to_prompt_format(max_terms=max_glossary_terms))

        return "\n".join(sections)

    # =========================================================================
    # Validation
    # =========================================================================

    def validate_references(self) -> list[str]:
        """Validate that all references in the model are valid.

        Returns:
            List of validation error messages (empty if valid)
        """
        errors = []
        source_ids = {s.id for s in self.sources}
        entity_ids = {e.id for e in self.entities}

        # Check source dependencies
        for source in self.sources:
            for dep in source.dependencies:
                if dep.depends_on_source_id not in source_ids:
                    errors.append(
                        f"Source '{source.name}' references non-existent source " f"'{dep.depends_on_source_id}'"
                    )

        # Check entity manifestations
        for entity in self.entities:
            for manifest in entity.manifestations:
                if manifest.source_id not in source_ids:
                    errors.append(
                        f"Entity '{entity.name}' manifestation references "
                        f"non-existent source '{manifest.source_id}'"
                    )

        # Check entity relationships
        for rel in self.entity_relationships:
            if rel.from_entity_id not in entity_ids:
                errors.append(f"Relationship '{rel.name}' references non-existent entity " f"'{rel.from_entity_id}'")
            if rel.to_entity_id not in entity_ids:
                errors.append(f"Relationship '{rel.name}' references non-existent entity " f"'{rel.to_entity_id}'")

        return errors


def create_empty_model(
    model_id: str,
    organization_id: str,
    version: str = "0.1.0",
) -> SemanticModel:
    """Create an empty semantic model.

    Args:
        model_id: Unique identifier for the model
        organization_id: Organization this model belongs to
        version: Initial version string

    Returns:
        Empty SemanticModel ready for indexing
    """
    return SemanticModel(
        metadata=ModelMetadata(
            id=model_id,
            version=version,
            organization_id=organization_id,
        ),
        sources=[],
        entities=[],
        entity_relationships=[],
        glossary=[],
        indexing_state=IndexingState(),
    )
