"""
Topological Dependency Resolver for ingestion job ordering.
Resolves dependencies based on Foreign Key relationships defined in YAML configurations.
"""

import logging
from typing import Dict, List, Set, Any, Optional

logger = logging.getLogger(__name__)


class CircularDependencyError(Exception):
    """Raised when a circular dependency is detected in the dependency graph."""

    pass


class DependencyResolver:
    """
    Resolves ingestion job dependencies using topological sorting.
    Orders jobs based on Foreign Key dependencies to ensure data integrity.
    """

    def __init__(self):
        """Initialize the dependency resolver."""
        self.dependency_graph: Dict[str, Set[str]] = {}
        self.table_to_config: Dict[str, Any] = {}

    def add_config(self, config: Dict[str, Any], config_path: Optional[str] = None):
        """
        Add a configuration to the dependency graph.

        Args:
            config: YAML configuration dictionary containing schema and dependencies
            config_path: Optional path to the configuration file for reference

        Raises:
            ValueError: If the configuration is missing required fields
        """
        if "target_table" not in config:
            raise ValueError(
                f"Configuration missing 'target_table': {config_path or 'unknown'}"
            )

        target_table = config["target_table"]

        # Store the config for later use
        self.table_to_config[target_table] = {
            "config": config,
            "path": config_path,
        }

        # Extract dependencies from the config
        dependencies = self._extract_dependencies(config)

        # Add to dependency graph
        if target_table not in self.dependency_graph:
            self.dependency_graph[target_table] = set()

        self.dependency_graph[target_table].update(dependencies)

        logger.debug(f"Added table '{target_table}' with dependencies: {dependencies}")

    def _extract_dependencies(self, config: Dict[str, Any]) -> Set[str]:
        """
        Extract Foreign Key dependencies from a configuration.

        Args:
            config: YAML configuration dictionary

        Returns:
            Set of table names that this configuration depends on
        """
        dependencies = set()

        # Check for explicit 'depends_on' field
        if "depends_on" in config:
            depends_on = config["depends_on"]
            if isinstance(depends_on, str):
                dependencies.add(depends_on)
            elif isinstance(depends_on, list):
                dependencies.update(depends_on)
            else:
                logger.warning(
                    f"Invalid 'depends_on' format for table '{config.get('target_table')}': {depends_on}"
                )

        # Check for ForeignKey fields in schema
        schema = config.get("schema", {})
        for field_name, field_def in schema.items():
            if isinstance(field_def, dict):
                field_type = field_def.get("type", "")
                # Check if it's a ForeignKey field
                if field_type == "ForeignKeyField" or "foreign_key" in field_def:
                    # Extract the referenced table
                    ref_table = field_def.get("references") or field_def.get(
                        "foreign_key"
                    )
                    if ref_table:
                        dependencies.add(ref_table)
                        logger.debug(f"Found foreign key: {field_name} -> {ref_table}")

        return dependencies

    def resolve_order(self) -> List[str]:
        """
        Resolve the dependency order using topological sorting (Kahn's algorithm).

        Returns:
            List of table names in the order they should be processed

        Raises:
            CircularDependencyError: If a circular dependency is detected
        """
        # Create a copy of the dependency graph for processing
        graph = {table: deps.copy() for table, deps in self.dependency_graph.items()}

        # Find all nodes (tables) in the graph
        all_tables = set(graph.keys())
        for deps in graph.values():
            all_tables.update(deps)

        # Initialize in-degree for all tables
        # In-degree = number of dependencies this table has
        in_degree = {table: 0 for table in all_tables}

        # Calculate in-degrees
        # For each table, count how many dependencies it has
        for table in graph:
            in_degree[table] = len(graph[table])

        # Queue of tables with no dependencies (in-degree = 0)
        queue = [table for table in all_tables if in_degree[table] == 0]
        sorted_order = []

        while queue:
            # Sort queue for deterministic ordering
            queue.sort()

            # Process the first table with no dependencies
            current = queue.pop(0)
            sorted_order.append(current)

            # For each table, check if it depends on current
            # If so, decrement its in-degree
            for table in all_tables:
                if table in graph and current in graph[table]:
                    in_degree[table] -= 1

                    # If table now has no dependencies, add to queue
                    if in_degree[table] == 0 and table not in sorted_order:
                        queue.append(table)

        # Check for circular dependencies
        if len(sorted_order) != len(all_tables):
            # Find the tables involved in the cycle
            remaining = all_tables - set(sorted_order)
            raise CircularDependencyError(
                f"Circular dependency detected among tables: {remaining}"
            )

        logger.info(
            "Resolved dependency order: %s",
            " -> ".join(sorted_order),
        )

        return sorted_order

    def get_ordered_configs(self) -> List[Dict[str, Any]]:
        """
        Get configurations ordered by their dependencies.

        Returns:
            List of configuration dictionaries in dependency order

        Raises:
            CircularDependencyError: If a circular dependency is detected
        """
        ordered_tables = self.resolve_order()

        ordered_configs = []
        for table in ordered_tables:
            if table in self.table_to_config:
                config_info = self.table_to_config[table]
                ordered_configs.append(
                    {
                        "table": table,
                        "config": config_info["config"],
                        "path": config_info["path"],
                    }
                )
            else:
                logger.warning(
                    f"Table '{table}' is referenced as a dependency but has no configuration"
                )

        logger.info(
            f"Resolved ingestion order: {[c['table'] for c in ordered_configs]}"
        )

        return ordered_configs

    def clear(self):
        """Clear all configurations and reset the resolver."""
        self.dependency_graph.clear()
        self.table_to_config.clear()


def resolve_ingestion_order(configs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    Convenience function to resolve the ingestion order for a list of configurations.

    Args:
        configs: List of YAML configuration dictionaries

    Returns:
        List of configurations ordered by dependencies

    Raises:
        CircularDependencyError: If a circular dependency is detected
    """
    resolver = DependencyResolver()

    for idx, config in enumerate(configs):
        resolver.add_config(config, config_path=f"config_{idx}")

    return resolver.get_ordered_configs()
