"""Schema definitions and validation for the data fabric.

Column definitions with type, nullability, bounds, and allowed values.
SchemaSpec validates list[dict] data at system boundaries.
"""

from __future__ import annotations

from dataclasses import dataclass

from orxaq.kernel import SensitivityLevel


@dataclass(frozen=True)
class ColumnDef:
    """Definition of a single data column."""

    name: str
    dtype: str = "float"  # "float", "int", "str", "bool"
    nullable: bool = True
    bounds: tuple[float | None, float | None] = (None, None)
    allowed_values: tuple[str, ...] | None = None
    sensitivity: SensitivityLevel = SensitivityLevel.INTERNAL
    description: str = ""


@dataclass(frozen=True)
class ValidationError:
    """A single validation failure."""

    row_index: int
    column: str
    message: str


@dataclass(frozen=True)
class ValidationResult:
    """Result of schema validation."""

    valid: bool
    total_rows: int
    error_count: int
    errors: tuple[ValidationError, ...] = ()


@dataclass(frozen=True)
class SchemaSpec:
    """Schema specification for tabular data."""

    name: str
    columns: tuple[ColumnDef, ...] = ()
    version: str = "1.0"

    def validate(self, data: list[dict]) -> ValidationResult:
        """Validate data against this schema."""
        errors: list[ValidationError] = []

        for row_idx, row in enumerate(data):
            for col in self.columns:
                value = row.get(col.name)

                # Nullability check
                if value is None:
                    if not col.nullable:
                        errors.append(
                            ValidationError(
                                row_idx, col.name, f"null value in non-nullable column '{col.name}'"
                            )
                        )
                    continue

                # Type check
                if not self._check_type(value, col.dtype):
                    errors.append(
                        ValidationError(
                            row_idx, col.name, f"expected {col.dtype}, got {type(value).__name__}"
                        )
                    )
                    continue

                # Bounds check (numeric only)
                if col.dtype in ("float", "int") and col.bounds != (None, None):
                    num_val = float(value)
                    low, high = col.bounds
                    if low is not None and num_val < low:
                        errors.append(
                            ValidationError(
                                row_idx, col.name, f"value {num_val} below minimum {low}"
                            )
                        )
                    if high is not None and num_val > high:
                        errors.append(
                            ValidationError(
                                row_idx, col.name, f"value {num_val} above maximum {high}"
                            )
                        )

                # Allowed values check
                if col.allowed_values is not None and str(value) not in col.allowed_values:
                    errors.append(
                        ValidationError(row_idx, col.name, f"value '{value}' not in allowed values")
                    )

        return ValidationResult(
            valid=len(errors) == 0,
            total_rows=len(data),
            error_count=len(errors),
            errors=tuple(errors),
        )

    @staticmethod
    def _check_type(value: object, dtype: str) -> bool:
        """Check if value matches expected dtype."""
        if dtype == "float":
            return isinstance(value, int | float)
        if dtype == "int":
            return isinstance(value, int) and not isinstance(value, bool)
        if dtype == "str":
            return isinstance(value, str)
        if dtype == "bool":
            return isinstance(value, bool)
        return True


def credit_schema() -> SchemaSpec:
    """Return schema matching the 19-variable credit ontology."""
    return SchemaSpec(
        name="credit_risk",
        columns=tuple(
            [
                ColumnDef("unemployment_rate", "float", bounds=(0.0, 0.30)),
                ColumnDef("fed_funds_rate", "float", bounds=(-0.01, 0.20)),
                ColumnDef("hpi", "float", bounds=(0.0, None)),
                ColumnDef("gdp_growth", "float", bounds=(-0.10, 0.15)),
                ColumnDef("cpi", "float", bounds=(0.0, None)),
                ColumnDef("credit_score", "float", bounds=(300.0, 850.0)),
                ColumnDef("dti", "float", bounds=(0.0, 1.0)),
                ColumnDef("income", "float", sensitivity=SensitivityLevel.CONFIDENTIAL),
                ColumnDef("employment_length", "float", bounds=(0.0, 50.0)),
                ColumnDef(
                    "age", "float", bounds=(18.0, 100.0), sensitivity=SensitivityLevel.RESTRICTED
                ),
                ColumnDef("loan_amount", "float", bounds=(1000.0, None)),
                ColumnDef("interest_rate", "float"),
                ColumnDef("ltv", "float", bounds=(0.0, 2.0)),
                ColumnDef("term", "float"),
                ColumnDef("dpd_30", "float"),
                ColumnDef("dpd_60", "float"),
                ColumnDef("dpd_90", "float"),
                ColumnDef("current_balance", "float"),
                ColumnDef("default_flag", "float"),
            ]
        ),
        version="1.0",
    )
