"""Quality gate engine — composable data validation predicates.

Each gate is a pure function: data -> pass/fail + details.
All results logged to AuditLog for SR 11-7 compliance.
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass

from orxaq.fabric.schema import SchemaSpec
from orxaq.kernel import AuditLog


class QualityError(Exception):
    """Raised when data fails quality gates."""


@dataclass(frozen=True)
class GateResult:
    """Result of a single quality gate check."""

    gate_name: str
    passed: bool
    score: float  # 0.0 to 1.0
    failures: tuple[dict, ...] = ()
    checked_at: str = ""


@dataclass(frozen=True)
class PipelineResult:
    """Result of running all quality gates."""

    all_passed: bool
    gate_results: tuple[GateResult, ...]
    overall_score: float


def _iso_now() -> str:
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())


class QualityGate:
    """Base class for quality gates."""

    name: str = "base"

    def check(self, data: list[dict]) -> GateResult:
        raise NotImplementedError


class CompletenessGate(QualityGate):
    """Check that columns have >= threshold% non-null values."""

    def __init__(self, columns: list[str], threshold: float = 0.95) -> None:
        self.name = "completeness"
        self._columns = columns
        self._threshold = threshold

    def check(self, data: list[dict]) -> GateResult:
        if not data:
            return GateResult(self.name, True, 1.0, checked_at=_iso_now())

        failures: list[dict] = []
        n = len(data)
        scores: list[float] = []

        for col in self._columns:
            non_null = sum(1 for row in data if row.get(col) is not None)
            pct = non_null / n
            scores.append(pct)
            if pct < self._threshold:
                failures.append(
                    {"column": col, "completeness": round(pct, 4), "threshold": self._threshold}
                )

        avg_score = sum(scores) / len(scores) if scores else 1.0
        return GateResult(
            self.name,
            passed=len(failures) == 0,
            score=avg_score,
            failures=tuple(failures),
            checked_at=_iso_now(),
        )


class BoundsGate(QualityGate):
    """Check all values in a column are within bounds."""

    def __init__(
        self, column: str, min_val: float | None = None, max_val: float | None = None
    ) -> None:
        self.name = f"bounds:{column}"
        self._column = column
        self._min = min_val
        self._max = max_val

    def check(self, data: list[dict]) -> GateResult:
        if not data:
            return GateResult(self.name, True, 1.0, checked_at=_iso_now())

        failures: list[dict] = []
        n = len(data)

        for i, row in enumerate(data):
            val = row.get(self._column)
            if val is None:
                continue
            try:
                num = float(val)
            except (ValueError, TypeError):
                failures.append({"row": i, "value": val, "reason": "not numeric"})
                continue

            if self._min is not None and num < self._min:
                failures.append({"row": i, "value": num, "reason": f"below {self._min}"})
            if self._max is not None and num > self._max:
                failures.append({"row": i, "value": num, "reason": f"above {self._max}"})

        passing = n - len(failures)
        score = passing / n if n > 0 else 1.0
        return GateResult(
            self.name,
            passed=len(failures) == 0,
            score=score,
            failures=tuple(failures),
            checked_at=_iso_now(),
        )


class TypeGate(QualityGate):
    """Check all values match expected schema types."""

    def __init__(self, schema: SchemaSpec) -> None:
        self.name = "type_check"
        self._schema = schema

    def check(self, data: list[dict]) -> GateResult:
        result = self._schema.validate(data)
        score = 1.0 - (result.error_count / max(result.total_rows * len(self._schema.columns), 1))
        failures = tuple(
            {"row": e.row_index, "column": e.column, "message": e.message} for e in result.errors
        )
        return GateResult(
            self.name,
            passed=result.valid,
            score=max(0.0, score),
            failures=failures,
            checked_at=_iso_now(),
        )


class UniquenessGate(QualityGate):
    """Check for duplicate values in specified columns."""

    def __init__(self, columns: list[str]) -> None:
        self.name = "uniqueness"
        self._columns = columns

    def check(self, data: list[dict]) -> GateResult:
        if not data:
            return GateResult(self.name, True, 1.0, checked_at=_iso_now())

        # Build composite key
        seen: dict[tuple, int] = {}
        failures: list[dict] = []

        for i, row in enumerate(data):
            key = tuple(row.get(c) for c in self._columns)
            if key in seen:
                failures.append({"row": i, "duplicate_of": seen[key], "key": str(key)})
            else:
                seen[key] = i

        n = len(data)
        score = (n - len(failures)) / n if n > 0 else 1.0
        return GateResult(
            self.name,
            passed=len(failures) == 0,
            score=score,
            failures=tuple(failures),
            checked_at=_iso_now(),
        )


class FreshnessGate(QualityGate):
    """Check data isn't stale based on a timestamp column."""

    def __init__(self, timestamp_column: str, max_age_hours: float) -> None:
        self.name = f"freshness:{timestamp_column}"
        self._column = timestamp_column
        self._max_age = max_age_hours

    def check(self, data: list[dict]) -> GateResult:
        if not data:
            return GateResult(self.name, True, 1.0, checked_at=_iso_now())

        now = time.time()
        failures: list[dict] = []

        for i, row in enumerate(data):
            val = row.get(self._column)
            if val is None:
                continue
            try:
                ts = float(val)
            except (ValueError, TypeError):
                continue
            age_hours = (now - ts) / 3600
            if age_hours > self._max_age:
                failures.append({"row": i, "age_hours": round(age_hours, 2), "max": self._max_age})

        n = len(data)
        score = (n - len(failures)) / n if n > 0 else 1.0
        return GateResult(
            self.name,
            passed=len(failures) == 0,
            score=score,
            failures=tuple(failures),
            checked_at=_iso_now(),
        )


class CustomGate(QualityGate):
    """User-defined quality gate via callable predicate."""

    def __init__(self, name: str, predicate: Callable[..., bool]) -> None:
        self.name = name
        self._predicate = predicate

    def check(self, data: list[dict]) -> GateResult:
        passed = self._predicate(data)
        return GateResult(
            self.name, passed=bool(passed), score=1.0 if passed else 0.0, checked_at=_iso_now()
        )


class QualityPipeline:
    """Run a sequence of quality gates and aggregate results."""

    def __init__(self, gates: list[QualityGate], audit_log: AuditLog | None = None) -> None:
        self._gates = gates
        self._audit = audit_log

    def run(self, data: list[dict]) -> PipelineResult:
        """Run all gates and return aggregate result."""
        results: list[GateResult] = []

        for gate in self._gates:
            result = gate.check(data)
            results.append(result)
            if self._audit is not None:
                status = "PASS" if result.passed else "FAIL"
                self._audit.append(
                    action=f"quality_gate:{gate.name}",
                    actor="fabric.quality",
                    details=f"{status} score={result.score:.2f} failures={len(result.failures)}",
                )

        all_passed = all(r.passed for r in results)
        overall_score = sum(r.score for r in results) / len(results) if results else 1.0

        return PipelineResult(
            all_passed=all_passed,
            gate_results=tuple(results),
            overall_score=overall_score,
        )

    def run_or_reject(self, data: list[dict]) -> list[dict]:
        """Run all gates; raise QualityError if any fail."""
        result = self.run(data)
        if not result.all_passed:
            failed = [r.gate_name for r in result.gate_results if not r.passed]
            msg = f"Quality gates failed: {', '.join(failed)}"
            raise QualityError(msg)
        return data
