"""
Vedic Constraint Engine — 16 Sutra-Based Verification Patterns
Based on Bharati Krishna Tirthaji's Vedic Mathematics (16 sutras).

Each sutra maps to a constraint-satisfaction pattern that evaluates
solutions holistically — like an energy-based model finding the lowest
energy state, but grounded in 3,000-year-old optimization heuristics.
"""

from enum import Enum, auto
from dataclasses import dataclass, field
from typing import Any, Callable, Optional


class Sutra(Enum):
    """The 16 Vedic Mathematics Sutras as constraint patterns."""

    # 1. Ekadhikena Purvena — By one more than the previous
    # Pattern: Sequential constraint — each step must build on the last
    EKADHIKENA = "ekadhikena_purvena"

    # 2. Nikhilam — All from 9, last from 10
    # Pattern: Complement verification — check from the inverse
    NIKHILAM = "nikhilam"

    # 3. Urdhva Tiryagbhyam — Vertically and crosswise
    # Pattern: Multi-dimensional simultaneous check
    URDHVA_TIRYAK = "urdhva_tiryagbhyam"

    # 4. Paravartya Yojayet — Transpose and apply
    # Pattern: Transform constraints to simpler equivalent form
    PARAVARTYA = "paravartya_yojayet"

    # 5. Shunyam Saamyasamuccaye — When the sum is same, it is zero
    # Pattern: Equilibrium / balance verification
    SHUNYAM = "shunyam_saamyasamuccaye"

    # 6. Anurupyena — Proportionately
    # Pattern: Scale invariance — constraints hold at any scale
    ANURUPYENA = "anurupyena"

    # 7. Sankalana Vyavakalanabhyam — By addition and subtraction
    # Pattern: Additive consistency — parts must sum to whole
    SANKALANA = "sankalana_vyavakalanabhyam"

    # 8. Puranapuranabhyam — By completion or non-completion
    # Pattern: Completeness check — all required fields present
    PURANAPURANA = "puranapuranabhyam"

    # 9. Chalana Kalanabhyam — Differences and similarities
    # Pattern: Delta analysis — changes must be within bounds
    CHALANA = "chalana_kalanabhyam"

    # 10. Yavadunam — Whatever the extent of deficiency
    # Pattern: Gap analysis — measure and bound the deficit
    YAVADUNAM = "yavadunam"

    # 11. Vyashtisamanstih — Part and whole
    # Pattern: Compositional integrity — parts consistent with whole
    VYASHTI = "vyashtisamanstih"

    # 12. Shesanyankena Charamena — The remainders by the last digit
    # Pattern: Residual check — modular arithmetic verification
    SHESHANYANKENA = "shesanyankena_charamena"

    # 13. Sopaantyadvayamantyam — The ultimate and twice the penultimate
    # Pattern: Boundary verification — edge cases valid
    SOPAANTYA = "sopaantyadvayamantyam"

    # 14. Ekanyunena Purvena — By one less than the previous
    # Pattern: Decremental constraint — diminishing returns check
    EKANYUNENA = "ekanyunena_purvena"

    # 15. Gunitasamuchyah — The product of the sum is the sum of products
    # Pattern: Distributive consistency — aggregation preserves truth
    GUNITASAMUCHYA = "gunitasamuchyah"

    # 16. Gunakasamuchyah — The factors of the sum is the sum of the factors
    # Pattern: Factorization integrity — decomposition is valid
    GUNAKA = "gunakasamuchyah"


@dataclass
class SutraResult:
    """Result of applying a single sutra verification."""
    sutra: str
    passed: bool
    score: float          # 0.0 (total fail) - 1.0 (perfect)
    detail: str
    violations: list = field(default_factory=list)


@dataclass
class VedicVerdict:
    """Aggregate result of all sutra verifications."""
    energy_score: float     # Lower = better (like EBM)
    passed: bool
    sutras_applied: list    # List of SutraResult
    recommendation: str     # "VERIFIED" | "FLAGGED" | "REJECTED"
    reasoning: str


class VedicEngine:
    """
    Vedic constraint satisfaction engine.

    Applies sutra-based verification patterns to data.
    Each sutra is a reusable verification primitive.

    Usage:
        engine = VedicEngine()

        # Complement check (Nikhilam) — verify from the inverse
        result = engine.nikhilam(
            value=95,
            universe=100,
            tolerance=0.05
        )

        # Multi-dimensional check (Urdhva Tiryagbhyam)
        result = engine.urdhva_tiryak(
            dimensions={"cpu": 0.7, "mem": 0.8, "disk": 0.5},
            limits={"cpu": 0.9, "mem": 0.85, "disk": 0.95}
        )

        # Full verification with multiple sutras
        verdict = engine.verify(data, constraints, sutras=[...])
    """

    def nikhilam(self, value: float, universe: float,
                 tolerance: float = 0.0) -> SutraResult:
        """
        Sutra 2: Nikhilam — All from 9, last from 10.
        Verify by checking the COMPLEMENT instead of the value directly.
        If complement is within expected range, value is valid.

        Use case: Instead of "is this lead good?" ask "what makes it bad?"
        and check that badness is below threshold.
        """
        complement = universe - value
        expected_complement = universe * (1.0 - tolerance) if tolerance else 0.0
        complement_ratio = complement / universe if universe != 0 else 0

        passed = complement >= 0 and value <= universe * (1.0 + tolerance)

        return SutraResult(
            sutra=Sutra.NIKHILAM.value,
            passed=passed,
            score=1.0 - abs(complement_ratio) if passed else 0.0,
            detail=f"Value {value}, complement {complement} "
                   f"(universe {universe}, tolerance {tolerance})",
            violations=[] if passed else [
                f"Value {value} exceeds universe {universe} "
                f"(tolerance {tolerance})"
            ]
        )

    def urdhva_tiryak(self, dimensions: dict,
                      limits: dict) -> SutraResult:
        """
        Sutra 3: Urdhva Tiryagbhyam — Vertically and crosswise.
        Check ALL dimensions simultaneously, not one at a time.
        Cross-correlate: if dimension A is high, dimension B limits tighten.

        Use case: Server health — high CPU + high memory is worse than
        either alone. Task routing — match ALL capabilities at once.
        """
        violations = []
        dimension_scores = {}
        cross_pressure = 1.0
        minor_exceedances = []

        for dim, value in dimensions.items():
            limit = limits.get(dim)
            if limit is None:
                continue
            if limit == 0:
                ratio = 0.0 if value == 0 else float('inf')
            else:
                ratio = value / limit
            dimension_scores[dim] = min(ratio, 2.0)
            cross_pressure *= max(ratio, 1e-10)

            if value > limit:
                msg = (f"{dim}: {value} exceeds limit {limit} "
                       f"({ratio:.1%} of capacity)")
                if ratio <= 1.05:
                    minor_exceedances.append(msg)
                else:
                    violations.append(msg)

        # Cross-check: combined pressure across dimensions
        n_dims = len(dimension_scores)
        if n_dims > 0:
            avg_pressure = cross_pressure ** (1.0 / n_dims)
            near_limit = sum(1 for v in dimension_scores.values() if v > 0.9)
            high_saturation = sum(1 for v in dimension_scores.values() if v > 0.95)

            # Minor exceedances (<5% over) are promoted to violations when:
            # - majority of dimensions are near their limits (true cross-pressure)
            # - or the overall system is under high pressure
            # Otherwise, isolated minor exceedances with headroom elsewhere are tolerated
            if minor_exceedances:
                majority_near = near_limit > n_dims / 2
                if majority_near or avg_pressure > 0.95:
                    violations.extend(minor_exceedances)

            # Flag if any dimension exceeds and others are near limit
            if avg_pressure > 1.0 and near_limit >= 2:
                violations.append(
                    f"Cross-dimensional pressure {avg_pressure:.2f} "
                    f"exceeds unity across {near_limit} near-limit dimensions"
                )
            # Flag if nearly all dimensions are at high saturation (>0.95)
            elif n_dims >= 3 and high_saturation >= n_dims - 1 and avg_pressure > 0.95:
                violations.append(
                    f"Cross-dimensional saturation: {high_saturation}/{n_dims} "
                    f"dimensions above 95% capacity (avg pressure {avg_pressure:.3f})"
                )

        score = max(0.0, 1.0 - max(0, (cross_pressure ** (1.0 / max(n_dims, 1)) - 0.5)))
        passed = len(violations) == 0

        return SutraResult(
            sutra=Sutra.URDHVA_TIRYAK.value,
            passed=passed,
            score=round(min(1.0, max(0.0, score)), 4),
            detail=f"Checked {n_dims} dimensions simultaneously, "
                   f"cross-pressure: {cross_pressure ** (1.0/max(n_dims,1)):.3f}",
            violations=violations
        )

    def shunyam(self, inputs: dict, outputs: dict,
                tolerance: float = 0.001) -> SutraResult:
        """
        Sutra 5: Shunyam Saamyasamuccaye — When sums are same, result is zero.
        Verify EQUILIBRIUM: inputs must balance outputs within tolerance.

        Use case: Financial transactions (money in = money out + fees),
        energy systems, mass balance in manufacturing.
        """
        sum_in = sum(inputs.values())
        sum_out = sum(outputs.values())
        delta = abs(sum_in - sum_out)
        relative_delta = delta / max(abs(sum_in), abs(sum_out), 1e-10)

        passed = relative_delta <= tolerance

        return SutraResult(
            sutra=Sutra.SHUNYAM.value,
            passed=passed,
            score=round(max(0.0, 1.0 - relative_delta / max(tolerance, 1e-10)), 4),
            detail=f"Inputs sum: {sum_in}, Outputs sum: {sum_out}, "
                   f"Delta: {delta:.6f} ({relative_delta:.4%})",
            violations=[] if passed else [
                f"Equilibrium violated: delta {delta:.6f} "
                f"exceeds tolerance {tolerance}"
            ]
        )

    def anurupyena(self, values: list, ratios: list = None,
                   tolerance: float = 0.1) -> SutraResult:
        """
        Sutra 6: Anurupyena — Proportionately.
        Verify SCALE INVARIANCE: proportional relationships hold.

        Use case: If revenue doubles, costs should scale proportionally.
        If batch size changes, quality metrics should hold.
        """
        if len(values) < 2:
            return SutraResult(
                sutra=Sutra.ANURUPYENA.value,
                passed=True,
                score=1.0,
                detail="Insufficient data for proportion check",
            )

        violations = []
        if ratios:
            # Check explicit ratio expectations
            for i in range(min(len(values) - 1, len(ratios))):
                actual_ratio = values[i + 1] / values[i] if values[i] != 0 else float('inf')
                expected = ratios[i]
                deviation = abs(actual_ratio - expected) / max(abs(expected), 1e-10)
                if deviation > tolerance:
                    violations.append(
                        f"Ratio [{i}→{i+1}]: expected {expected:.3f}, "
                        f"got {actual_ratio:.3f} (deviation: {deviation:.1%})"
                    )
        else:
            # Check self-consistency of ratios (should be stable)
            # Skip zero values entirely — they break ratio analysis
            computed_ratios = []
            nonzero_pairs = [(values[i], values[i+1]) for i in range(len(values)-1)
                             if values[i] != 0 and values[i+1] != 0]
            for a, b in nonzero_pairs:
                computed_ratios.append(b / a)
            if len(computed_ratios) >= 2:
                mean_ratio = sum(computed_ratios) / len(computed_ratios)
                for i, r in enumerate(computed_ratios):
                    deviation = abs(r - mean_ratio) / max(abs(mean_ratio), 1e-10)
                    if deviation > tolerance:
                        violations.append(
                            f"Ratio [{i}→{i+1}] = {r:.3f} deviates from "
                            f"mean {mean_ratio:.3f} by {deviation:.1%}"
                        )

        passed = len(violations) == 0
        score = max(0.0, 1.0 - len(violations) * 0.2)

        return SutraResult(
            sutra=Sutra.ANURUPYENA.value,
            passed=passed,
            score=round(score, 4),
            detail=f"Checked proportionality across {len(values)} values",
            violations=violations
        )

    def sankalana(self, parts: dict, whole: float,
                  tolerance: float = 0.01) -> SutraResult:
        """
        Sutra 7: Sankalana Vyavakalanabhyam — By addition and subtraction.
        Verify ADDITIVE CONSISTENCY: parts must sum to the whole.

        Use case: Budget allocations, resource distribution,
        percentage breakdowns, vote counts.
        """
        parts_sum = sum(parts.values())
        delta = abs(parts_sum - whole)
        relative_delta = delta / max(abs(whole), 1e-10)

        passed = relative_delta <= tolerance

        return SutraResult(
            sutra=Sutra.SANKALANA.value,
            passed=passed,
            score=round(max(0.0, 1.0 - relative_delta / max(tolerance, 1e-10)), 4),
            detail=f"Parts sum: {parts_sum}, Expected whole: {whole}, "
                   f"Delta: {delta:.6f}",
            violations=[] if passed else [
                f"Parts ({parts_sum}) don't sum to whole ({whole}), "
                f"delta: {delta:.6f}"
            ]
        )

    def puranapurana(self, data: dict,
                     required_fields: list) -> SutraResult:
        """
        Sutra 8: Puranapuranabhyam — By completion or non-completion.
        Verify COMPLETENESS: all required fields/components present.

        Use case: Form validation, API request verification,
        construction checklist, compliance document checks.
        """
        present = [f for f in required_fields if f in data and data[f] is not None]
        missing = [f for f in required_fields if f not in data or data[f] is None]

        completeness = len(present) / max(len(required_fields), 1)
        passed = len(missing) == 0

        return SutraResult(
            sutra=Sutra.PURANAPURANA.value,
            passed=passed,
            score=round(completeness, 4),
            detail=f"Completeness: {len(present)}/{len(required_fields)} "
                   f"fields present",
            violations=[f"Missing required field: {f}" for f in missing]
        )

    def chalana(self, current: dict, previous: dict,
                max_delta: dict) -> SutraResult:
        """
        Sutra 9: Chalana Kalanabhyam — Differences and similarities.
        Verify CHANGE BOUNDS: deltas between states must be within limits.

        Use case: Anomaly detection, drift monitoring,
        financial variance checks, sensor validation.
        """
        violations = []
        for key in max_delta:
            if key in current and key in previous:
                delta = abs(current[key] - previous[key])
                limit = max_delta[key]
                if delta > limit:
                    violations.append(
                        f"{key}: changed by {delta:.4f} "
                        f"(max allowed: {limit})"
                    )

        checked = len(max_delta)
        passed = len(violations) == 0
        score = max(0.0, 1.0 - len(violations) / max(checked, 1))

        return SutraResult(
            sutra=Sutra.CHALANA.value,
            passed=passed,
            score=round(score, 4),
            detail=f"Checked {checked} dimensions for change bounds",
            violations=violations
        )

    def yavadunam(self, actual: dict, target: dict,
                  max_gap: dict = None) -> SutraResult:
        """
        Sutra 10: Yavadunam — Whatever the extent of deficiency.
        Measure and bound the GAP between actual and target.

        Use case: KPI tracking, SLA compliance, quality metrics,
        project progress verification.
        """
        gaps = {}
        violations = []
        for key in target:
            if key in actual:
                gap = target[key] - actual[key]
                gaps[key] = gap
                if max_gap and key in max_gap:
                    if abs(gap) > max_gap[key]:
                        violations.append(
                            f"{key}: gap {gap:.4f} exceeds max {max_gap[key]}"
                        )

        if not gaps:
            return SutraResult(
                sutra=Sutra.YAVADUNAM.value, passed=True, score=1.0,
                detail="No overlapping keys to check"
            )

        avg_gap_ratio = sum(
            abs(g) / max(abs(target[k]), 1e-10)
            for k, g in gaps.items()
        ) / len(gaps)
        passed = len(violations) == 0
        score = max(0.0, 1.0 - avg_gap_ratio)

        return SutraResult(
            sutra=Sutra.YAVADUNAM.value,
            passed=passed,
            score=round(score, 4),
            detail=f"Average gap ratio: {avg_gap_ratio:.4f} "
                   f"across {len(gaps)} metrics",
            violations=violations
        )

    def verify(self, data: dict, constraints: dict,
               sutras: list = None,
               custom_checks: list = None) -> VedicVerdict:
        """
        Run multiple sutra verifications and produce aggregate verdict.

        Args:
            data: The data/state to verify
            constraints: Hard constraints and limits
            sutras: List of Sutra enums to apply (default: auto-detect)
            custom_checks: List of (name, callable) for domain-specific checks

        Returns:
            VedicVerdict with energy_score (lower=better) and recommendation
        """
        results = []

        # Auto-detect applicable sutras if not specified
        if sutras is None:
            sutras = self._auto_detect_sutras(data, constraints)

        # Apply each sutra
        for sutra in sutras:
            result = self._apply_sutra(sutra, data, constraints)
            if result:
                results.append(result)

        # Apply custom domain checks
        if custom_checks:
            for name, check_fn in custom_checks:
                try:
                    result = check_fn(data, constraints)
                    if isinstance(result, SutraResult):
                        results.append(result)
                except Exception as e:
                    results.append(SutraResult(
                        sutra=name, passed=False, score=0.0,
                        detail=f"Custom check error: {e}",
                        violations=[str(e)]
                    ))

        # Compute energy score (lower = better, like EBM)
        if results:
            avg_score = sum(r.score for r in results) / len(results)
            energy = round(1.0 - avg_score, 4)
            all_passed = all(r.passed for r in results)
            any_critical = any(
                r.score < 0.3 for r in results
            )
        else:
            energy = 0.5
            all_passed = False
            any_critical = False

        # Determine recommendation
        if all_passed and energy <= 0.4:
            recommendation = "VERIFIED"
        elif any_critical or energy > 0.7:
            recommendation = "REJECTED"
        elif all_passed:
            recommendation = "VERIFIED"
        else:
            recommendation = "FLAGGED"

        # Build reasoning summary
        passed_sutras = [r.sutra for r in results if r.passed]
        failed_sutras = [r.sutra for r in results if not r.passed]
        all_violations = []
        for r in results:
            all_violations.extend(r.violations)

        reasoning = (
            f"Applied {len(results)} sutras. "
            f"Passed: {len(passed_sutras)}, Failed: {len(failed_sutras)}. "
            f"Energy: {energy} (lower=better). "
            f"Violations: {len(all_violations)}."
        )

        return VedicVerdict(
            energy_score=energy,
            passed=all_passed,
            sutras_applied=results,
            recommendation=recommendation,
            reasoning=reasoning
        )

    def _auto_detect_sutras(self, data: dict, constraints: dict) -> list:
        """Auto-detect which sutras are applicable based on data shape."""
        applicable = []

        # Always check completeness
        if "required_fields" in constraints:
            applicable.append(Sutra.PURANAPURANA)

        # Check dimensional limits
        if "limits" in constraints or any(
            isinstance(v, (int, float)) for v in constraints.values()
        ):
            applicable.append(Sutra.URDHVA_TIRYAK)

        # Check balance/equilibrium
        if "inputs" in data and "outputs" in data:
            applicable.append(Sutra.SHUNYAM)

        # Check parts-to-whole
        if "parts" in data and "total" in constraints:
            applicable.append(Sutra.SANKALANA)

        # Check change bounds
        if "previous" in data and "current" in data:
            applicable.append(Sutra.CHALANA)

        # Check gap analysis
        if "target" in constraints and "actual" in data:
            applicable.append(Sutra.YAVADUNAM)

        # Check proportionality (sequence of values)
        if "values" in data and isinstance(data["values"], list):
            applicable.append(Sutra.ANURUPYENA)

        # Default: at least check dimensions
        if not applicable:
            applicable.append(Sutra.URDHVA_TIRYAK)

        return applicable

    def _apply_sutra(self, sutra: Sutra, data: dict,
                     constraints: dict) -> Optional[SutraResult]:
        """Apply a specific sutra to data."""
        if sutra == Sutra.URDHVA_TIRYAK:
            dims = {k: v for k, v in data.items()
                    if isinstance(v, (int, float))}
            limits = constraints.get("limits", {})
            if not limits:
                limits = {k: v for k, v in constraints.items()
                          if isinstance(v, (int, float))}
            if dims and limits:
                return self.urdhva_tiryak(dims, limits)

        elif sutra == Sutra.SHUNYAM:
            inputs = data.get("inputs", {})
            outputs = data.get("outputs", {})
            tol = constraints.get("balance_tolerance", 0.001)
            if inputs and outputs:
                return self.shunyam(inputs, outputs, tol)

        elif sutra == Sutra.SANKALANA:
            parts = data.get("parts", {})
            whole = constraints.get("total", 0)
            tol = constraints.get("sum_tolerance", 0.01)
            if parts:
                return self.sankalana(parts, whole, tol)

        elif sutra == Sutra.PURANAPURANA:
            required = constraints.get("required_fields", [])
            if required:
                return self.puranapurana(data, required)

        elif sutra == Sutra.CHALANA:
            current = data.get("current", {})
            previous = data.get("previous", {})
            max_d = constraints.get("max_delta", {})
            if current and previous and max_d:
                return self.chalana(current, previous, max_d)

        elif sutra == Sutra.YAVADUNAM:
            actual = data.get("actual", data)
            target = constraints.get("target", {})
            max_gap = constraints.get("max_gap", {})
            if target:
                return self.yavadunam(actual, target, max_gap)

        elif sutra == Sutra.ANURUPYENA:
            values = data.get("values", [])
            ratios = constraints.get("expected_ratios", [])
            tol = constraints.get("ratio_tolerance", 0.1)
            if values:
                return self.anurupyena(values, ratios or None, tol)

        elif sutra == Sutra.NIKHILAM:
            value = data.get("value", 0)
            universe = constraints.get("universe", 100)
            tol = constraints.get("complement_tolerance", 0.0)
            return self.nikhilam(value, universe, tol)

        return None
