"""Validator for Tabular DataEntry using Polars."""

from typing import Any

import polars as pl

from axm.catalog.entries import DataEntry
from axm.core import BaseValidator, Severity, ValidationError, ValidationResult


class TabularValidator(BaseValidator):
    """Validates dataframe against Tabular DataEntry schema."""

    def __init__(self, entry: DataEntry) -> None:
        """Initialize validator with data entry definition."""
        self.entry = entry

    def validate(self, data: Any) -> ValidationResult:
        """Validate tabular data (DataFrame)."""
        errors: list[ValidationError] = []

        if not isinstance(data, pl.DataFrame):
            return ValidationResult(
                is_valid=False,
                errors=[
                    ValidationError(
                        field="input",
                        message=f"Expected polars DataFrame, got {type(data)}",
                        severity=Severity.ERROR,
                    )
                ],
            )

        df = data
        spec = self.entry.data_schema.spec

        columns_spec = []
        if isinstance(spec, dict):
            columns_spec = spec.get("columns", [])
        elif hasattr(spec, "columns"):
            columns_spec = spec.columns

        # 2. Check columns existence and types
        for col_spec in columns_spec:
            # Handle both dict access and object attribute access
            if isinstance(col_spec, dict):
                col_name = col_spec["name"]
                expected_type_str = col_spec["type"]
                is_nullable = col_spec.get("nullable", True)
            else:
                col_name = col_spec.name
                expected_type_str = col_spec.type
                is_nullable = col_spec.nullable

            # Check existence
            if col_name not in df.columns:
                errors.append(
                    ValidationError(
                        message=f"Missing column: {col_name}",
                        severity=Severity.ERROR,
                        field=col_name,
                    )
                )
                continue

            # Check type
            actual_type = df.schema[col_name]
            if not self._check_type(actual_type, expected_type_str):
                errors.append(
                    ValidationError(
                        message=(
                            f"Type mismatch for column '{col_name}': "
                            f"expected {expected_type_str}, got {actual_type}"
                        ),
                        severity=Severity.ERROR,
                        field=col_name,
                    )
                )

            # Check nullability
            if not is_nullable:
                null_count = df[col_name].null_count()
                if null_count > 0:
                    errors.append(
                        ValidationError(
                            message=(
                                f"Column '{col_name}' is not nullable but "
                                f"contains {null_count} null values"
                            ),
                            severity=Severity.ERROR,
                            field=col_name,
                        )
                    )

        # 3. Enforce column rules via RuleEngine (only if schema is valid)
        if len(errors) == 0:
            rule_errors = self._validate_rules(df)
            errors.extend(rule_errors)

        return ValidationResult(is_valid=len(errors) == 0, errors=errors)

    def _check_type(self, actual: pl.DataType, expected: str) -> bool:
        """Check if Polars type matches expected string type."""
        # Expanded mapping with common aliases
        mapping = {
            # Common aliases (used in contracts)
            "integer": pl.Int64,
            "float": pl.Float64,
            "string": pl.String,
            "boolean": pl.Boolean,
            "bool": pl.Boolean,
            "str": pl.String,
            # Polars-specific types
            "int64": pl.Int64,
            "int32": pl.Int32,
            "int16": pl.Int16,
            "int8": pl.Int8,
            "uint64": pl.UInt64,
            "uint32": pl.UInt32,
            "uint16": pl.UInt16,
            "uint8": pl.UInt8,
            "float64": pl.Float64,
            "float32": pl.Float32,
            "date": pl.Date,
            "datetime": pl.Datetime,
            "duration": pl.Duration,
            "time": pl.Time,
            "object": pl.Object,
            "binary": pl.Binary,
            "utf8": pl.String,
        }

        # Exact match attempt
        target = mapping.get(expected)

        if target is not None:
            # Check if classes match (some are singleton instances, some classes)
            return actual == target or isinstance(actual, type(target))

        # Fallback for complex types (List, Struct) - basic string check for now
        # TODO: Implement recursive type checking for List/Struct
        return str(actual).lower() == expected.lower()

    def _validate_rules(self, df: pl.DataFrame) -> list[ValidationError]:
        """Enforce column rules via RuleEngine.

        Args:
            df: The DataFrame to validate.

        Returns:
            List of ValidationError for rule violations.
        """
        # Import RuleEngine's expected TabularSchema (from axm-core)
        from axm.catalog.entries.column import TabularSchema as RuleEngineSchema
        from axm.catalog.rules.engine import RuleEngine

        spec = self.entry.data_schema.spec

        # Get columns from spec
        if isinstance(spec, dict):
            columns_data = spec.get("columns", [])
        elif hasattr(spec, "columns"):
            columns_data = spec.columns
        else:
            return []

        # Filter columns with rules and convert to dicts for RuleEngine
        columns_with_rules: list[dict[str, Any]] = []
        for col in columns_data:
            if isinstance(col, dict):
                if col.get("rules"):
                    columns_with_rules.append(col)
            elif hasattr(col, "rules") and col.rules:
                # Convert pydantic model to dict
                columns_with_rules.append(col.model_dump())

        if not columns_with_rules:
            return []

        # Create TabularSchema for RuleEngine (axm-core version)
        schema = RuleEngineSchema(columns=columns_with_rules)

        # Run RuleEngine with batch validation (single Polars pass)
        engine = RuleEngine()
        report = engine.validate_batch(df, schema)

        # Convert failed results to ValidationError
        errors: list[ValidationError] = []
        for result in report.results:
            if not result.passed:
                msg = f"Rule '{result.rule}' failed: {result.violations} violations"
                errors.append(
                    ValidationError(
                        field=result.column,
                        message=msg,
                        severity=result.severity,
                    )
                )

        return errors
