"""
Schema Drift Detector for comparing YAML schemas against actual database structure.
"""

import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Any, List, Optional, Set

from atomsql import Database
from atomsql.orm import fields

from atomingest.core.loader import SchemaLoader

logger = logging.getLogger(__name__)


class DriftType(Enum):
    """Types of schema drift that can be detected."""

    MISSING_TABLE = "missing_table"
    EXTRA_TABLE = "extra_table"
    MISSING_COLUMN = "missing_column"
    EXTRA_COLUMN = "extra_column"
    TYPE_MISMATCH = "type_mismatch"
    NULLABLE_MISMATCH = "nullable_mismatch"


@dataclass
class DriftIssue:
    """Represents a single schema drift issue."""

    drift_type: DriftType
    table_name: str
    column_name: Optional[str] = None
    expected: Optional[Any] = None
    actual: Optional[Any] = None
    message: Optional[str] = None

    def __str__(self) -> str:
        """Human-readable representation of the drift issue."""
        if self.message:
            return self.message

        if self.drift_type == DriftType.MISSING_TABLE:
            return (
                f"Table '{self.table_name}' is defined in YAML but missing in database"
            )
        elif self.drift_type == DriftType.EXTRA_TABLE:
            return (
                f"Table '{self.table_name}' exists in database but not defined in YAML"
            )
        elif self.drift_type == DriftType.MISSING_COLUMN:
            return (
                f"Column '{self.column_name}' in table '{self.table_name}' "
                f"is defined in YAML but missing in database"
            )
        elif self.drift_type == DriftType.EXTRA_COLUMN:
            return (
                f"Column '{self.column_name}' in table '{self.table_name}' "
                f"exists in database but not defined in YAML"
            )
        elif self.drift_type == DriftType.TYPE_MISMATCH:
            return (
                f"Column '{self.column_name}' in table '{self.table_name}' "
                f"has type mismatch: expected {self.expected}, got {self.actual}"
            )
        elif self.drift_type == DriftType.NULLABLE_MISMATCH:
            return (
                f"Column '{self.column_name}' in table '{self.table_name}' "
                f"has nullable mismatch: expected {self.expected}, got {self.actual}"
            )
        return f"Unknown drift: {self.drift_type}"


@dataclass
class DriftReport:
    """Report containing all detected schema drift issues."""

    issues: List[DriftIssue] = field(default_factory=list)
    tables_checked: List[str] = field(default_factory=list)
    timestamp: Optional[str] = None

    @property
    def has_drift(self) -> bool:
        """Check if any drift was detected."""
        return len(self.issues) > 0

    @property
    def drift_count(self) -> int:
        """Total number of drift issues."""
        return len(self.issues)

    def issues_by_type(self, drift_type: DriftType) -> List[DriftIssue]:
        """Get all issues of a specific type."""
        return [issue for issue in self.issues if issue.drift_type == drift_type]

    def issues_by_table(self, table_name: str) -> List[DriftIssue]:
        """Get all issues for a specific table."""
        return [issue for issue in self.issues if issue.table_name == table_name]

    def summary(self) -> Dict[str, int]:
        """Get a summary count of issues by type."""
        summary_dict = {drift_type.value: 0 for drift_type in DriftType}
        for issue in self.issues:
            summary_dict[issue.drift_type.value] += 1
        return summary_dict

    def __str__(self) -> str:
        """Human-readable report."""
        if not self.has_drift:
            return "No schema drift detected."

        lines = [f"Schema Drift Report: {self.drift_count} issue(s) found"]
        lines.append("=" * 60)

        for table in self.tables_checked:
            table_issues = self.issues_by_table(table)
            if table_issues:
                lines.append(f"\nTable: {table}")
                for issue in table_issues:
                    lines.append(f"  - {issue}")

        lines.append("\n" + "=" * 60)
        lines.append("Summary by type:")
        for drift_type, count in self.summary().items():
            if count > 0:
                lines.append(f"  {drift_type}: {count}")

        return "\n".join(lines)


class SchemaDriftDetector:
    """
    Detects schema drift between YAML configuration and actual database structure.
    Uses AtomSQL's own backend field type mapping for accurate comparison.
    """

    def __init__(self, db: Database):
        """
        Initialize the drift detector.

        Args:
            db: Database connection to introspect
        """
        self.db = db
        self.schema_loader = SchemaLoader()

    def detect_drift(
        self, yaml_configs: List[str], check_extra_tables: bool = False
    ) -> DriftReport:
        """
        Detect drift between YAML schemas and database structure.

        Args:
            yaml_configs: List of paths to YAML configuration files
            check_extra_tables: If True, report tables in DB not defined in any YAML

        Returns:
            DriftReport containing all detected issues
        """
        from datetime import datetime

        report = DriftReport(timestamp=datetime.now().isoformat())

        # Load all YAML schemas
        yaml_schemas = {}
        for yaml_path in yaml_configs:
            try:
                schema = self.schema_loader.load_from_yaml(yaml_path)
                table_name = schema["target_table"]
                yaml_schemas[table_name] = schema
                logger.debug(f"Loaded schema for table '{table_name}' from {yaml_path}")
            except Exception as e:
                logger.error(f"Failed to load YAML config {yaml_path}: {e}")
                continue

        # Check each table defined in YAML
        for table_name, schema in yaml_schemas.items():
            report.tables_checked.append(table_name)
            self._check_table_drift(table_name, schema, report)

        # Optionally check for extra tables in database
        if check_extra_tables:
            self._check_extra_tables(set(yaml_schemas.keys()), report)

        logger.info(
            f"Drift detection complete: {report.drift_count} issue(s) found "
            f"across {len(report.tables_checked)} table(s)"
        )

        return report

    def _check_table_drift(
        self, table_name: str, schema: Dict[str, Any], report: DriftReport
    ) -> None:
        """Check for drift in a specific table."""
        # Check if table exists in database
        db_tables = self.db.backend.get_tables()

        if table_name not in db_tables:
            report.issues.append(
                DriftIssue(
                    drift_type=DriftType.MISSING_TABLE,
                    table_name=table_name,
                )
            )
            logger.warning(f"Table '{table_name}' not found in database")
            return

        # Get database columns
        db_columns = self._get_db_columns(table_name)

        # Get YAML schema columns
        yaml_fields = schema["schema"]

        # Check for missing and mismatched columns
        for field_name, field_obj in yaml_fields.items():
            if field_name == "id":
                # Skip the auto-generated id field
                continue

            if field_name not in db_columns:
                report.issues.append(
                    DriftIssue(
                        drift_type=DriftType.MISSING_COLUMN,
                        table_name=table_name,
                        column_name=field_name,
                    )
                )
                logger.warning(f"Column '{field_name}' missing in table '{table_name}'")
                continue

            # Check column properties
            db_col = db_columns[field_name]
            self._check_column_drift(table_name, field_name, field_obj, db_col, report)

        # Check for extra columns in database
        yaml_column_names = set(yaml_fields.keys())
        db_column_names = set(db_columns.keys())

        # Exclude 'id' from extra column check (it's auto-generated)
        extra_columns = db_column_names - yaml_column_names - {"id"}

        for extra_col in extra_columns:
            report.issues.append(
                DriftIssue(
                    drift_type=DriftType.EXTRA_COLUMN,
                    table_name=table_name,
                    column_name=extra_col,
                )
            )
            logger.debug(f"Extra column '{extra_col}' found in table '{table_name}'")

    def _check_column_drift(
        self,
        table_name: str,
        column_name: str,
        field_obj: fields.Field,
        db_column: Dict[str, Any],
        report: DriftReport,
    ) -> None:
        """Check for drift in a specific column."""
        # Check data type
        expected_type = self._get_expected_db_type(field_obj)
        actual_type = db_column["type"]

        if not self._types_match(expected_type, actual_type):
            report.issues.append(
                DriftIssue(
                    drift_type=DriftType.TYPE_MISMATCH,
                    table_name=table_name,
                    column_name=column_name,
                    expected=expected_type,
                    actual=actual_type,
                )
            )
            logger.warning(
                f"Type mismatch in '{table_name}.{column_name}': "
                f"expected {expected_type}, got {actual_type}"
            )

        # Check nullable constraint
        expected_nullable = getattr(field_obj, "nullable", False)
        actual_nullable = not db_column["notnull"]

        if expected_nullable != actual_nullable:
            report.issues.append(
                DriftIssue(
                    drift_type=DriftType.NULLABLE_MISMATCH,
                    table_name=table_name,
                    column_name=column_name,
                    expected=f"nullable={expected_nullable}",
                    actual=f"nullable={actual_nullable}",
                )
            )
            logger.debug(
                f"Nullable mismatch in '{table_name}.{column_name}': "
                f"expected {expected_nullable}, got {actual_nullable}"
            )

    def _check_extra_tables(self, yaml_tables: Set[str], report: DriftReport) -> None:
        """Check for tables in database that aren't defined in any YAML."""
        db_tables = set(self.db.backend.get_tables())

        # Filter out system tables
        system_tables = {"_migrations", "atom_keystore", "sqlite_sequence"}
        db_tables = db_tables - system_tables

        extra_tables = db_tables - set(yaml_tables)

        for table in extra_tables:
            report.issues.append(
                DriftIssue(
                    drift_type=DriftType.EXTRA_TABLE,
                    table_name=table,
                )
            )
            logger.debug(f"Extra table '{table}' found in database")

    def _get_db_columns(self, table_name: str) -> Dict[str, Dict[str, Any]]:
        """
        Get column information from the database.

        Returns:
            Dictionary mapping column name to column properties
        """
        columns = {}
        raw_columns = self.db.backend.get_table_columns(table_name)

        # SQLite PRAGMA table_info returns:
        # (cid, name, type, notnull, dflt_value, pk)
        for col in raw_columns:
            col_name = col[1]
            columns[col_name] = {
                "type": col[2],
                "notnull": bool(col[3]),
                "default": col[4],
                "pk": bool(col[5]),
            }

        return columns

    def _get_expected_db_type(self, field_obj: fields.Field) -> str:
        """
        Get the expected database type for a field using AtomSQL's backend.

        This delegates to AtomSQL's own field-to-database-type mapping,
        ensuring consistency with how AtomSQL creates tables.

        Args:
            field_obj: AtomSQL Field instance

        Returns:
            Expected database type as string
        """
        # Use AtomSQL's backend to get the field type
        # This ensures we use the same logic AtomSQL uses when creating tables
        try:
            field_type: str = self.db.backend.get_field_type(field_obj)
            return field_type
        except Exception as e:
            logger.warning(
                f"Failed to get field type for {field_obj.__class__.__name__}: {e}"
            )
            # Fallback to TEXT as a safe default
            return "TEXT"

    def _types_match(self, expected: str, actual: str) -> bool:
        """
        Check if expected and actual types match.

        Handles variations in type names (e.g., VARCHAR vs TEXT).

        Args:
            expected: Expected database type
            actual: Actual database type from DB

        Returns:
            True if types are compatible
        """
        expected = expected.upper()
        actual = actual.upper()

        # Exact match
        if expected == actual:
            return True

        # Handle TEXT variants
        text_types = {"TEXT", "VARCHAR", "CHAR", "CLOB", "STRING"}
        if expected in text_types and actual in text_types:
            return True

        # Handle INTEGER variants
        integer_types = {"INTEGER", "INT", "SMALLINT", "BIGINT", "TINYINT"}
        if expected in integer_types and actual in integer_types:
            return True

        # Handle REAL/FLOAT/DOUBLE variants
        float_types = {"REAL", "FLOAT", "DOUBLE", "NUMERIC"}
        if expected in float_types and actual in float_types:
            return True

        return False


def detect_drift(
    db: Database, yaml_configs: List[str], check_extra_tables: bool = False
) -> DriftReport:
    """
    Convenience function to detect schema drift.

    Args:
        db: Database connection
        yaml_configs: List of YAML configuration file paths
        check_extra_tables: Whether to check for extra tables in database

    Returns:
        DriftReport with all detected issues
    """
    detector = SchemaDriftDetector(db)
    return detector.detect_drift(yaml_configs, check_extra_tables)
