"""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)
                rules = col_spec.get("rules", [])
            else:
                col_name = col_spec.name
                expected_type_str = col_spec.type
                is_nullable = col_spec.nullable
                rules = col_spec.rules

            # 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,
                        )
                    )

            # Check custom rules
            if rules:
                from axm.catalog.rules.registry import RuleRegistry

                for rule_name in rules:
                    try:
                        rule = RuleRegistry.get(rule_name)
                    except KeyError:
                        errors.append(
                            ValidationError(
                                message=f"Rule '{rule_name}' not found",
                                severity=Severity.WARNING,
                                field=col_name,
                            )
                        )
                        continue

                    # Vectorized validation using map_elements
                    # We map the check function and look for False values
                    # return_dtype=pl.Boolean is important for performance
                    validation_mask = df[col_name].map_elements(
                        lambda x, r=rule: r.check(x), return_dtype=pl.Boolean
                    )

                    # Filter to find invalid rows
                    # We only take the first few errors to avoid flooding
                    invalid_rows = df.filter(validation_mask.not_()).head(5)

                    if invalid_rows.height > 0:
                        # We just report the first failure for now to keep it clean
                        first_fail_val = invalid_rows[col_name][0]
                        errors.append(
                            ValidationError(
                                message=(
                                    f"Rule '{rule.name}' failed: "
                                    f"{rule.description} (value: {first_fail_val})"
                                ),
                                severity=rule.severity,
                                field=col_name,
                            )
                        )

        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()
