"""Classify mismatches between report values and DW values.

Uses deterministic heuristic rules (no LLM needed) to categorize
the root cause of discrepancies.
"""
from __future__ import annotations

import logging
import math
from typing import Any, Dict, List, Optional

from .contracts import (
    Discrepancy,
    DiscrepancyType,
    ParityTestResult,
    _new_id,
)

logger = logging.getLogger(__name__)


class DiscrepancyClassifier:
    """Classify root causes of report-vs-DW mismatches."""

    def __init__(
        self,
        rounding_threshold: float = 0.001,
        grain_multiple_tolerance: float = 0.05,
    ):
        self._rounding_threshold = rounding_threshold
        self._grain_multiple_tolerance = grain_multiple_tolerance

    def classify(
        self,
        test_result: ParityTestResult,
        context: Optional[Dict[str, Any]] = None,
    ) -> Discrepancy:
        """Classify a single test result discrepancy.

        Args:
            test_result: Failed parity test result
            context: Optional context (other test results, spec info, etc.)
        """
        expected = test_result.expected_value
        actual = test_result.actual_value
        diff_pct = test_result.difference_pct

        # Rule 1: Rounding
        if diff_pct < self._rounding_threshold:
            return self._make_discrepancy(
                test_result, DiscrepancyType.ROUNDING, 0.95,
                f"Difference of {diff_pct:.4%} is within rounding tolerance",
                "No fix needed — within acceptable rounding tolerance",
            )

        # Rule 2: Signage (actual ≈ -expected)
        if expected != 0 and abs(actual + expected) / abs(expected) < 0.01:
            return self._make_discrepancy(
                test_result, DiscrepancyType.SIGNAGE, 0.90,
                f"Actual ({actual}) is approximately negative of expected ({expected})",
                "Check sign convention: negate the measure or adjust the formula sign",
            )

        # Rule 3: Grain (actual = expected × N, integer multiple)
        if expected != 0:
            ratio = actual / expected
            if ratio > 1 and abs(ratio - round(ratio)) < self._grain_multiple_tolerance:
                multiple = round(ratio)
                return self._make_discrepancy(
                    test_result, DiscrepancyType.GRAIN, 0.80,
                    f"Actual is ~{multiple}x expected — suggests wrong aggregation grain",
                    f"Add GROUP BY or filter to reduce grain from {multiple}x to 1x",
                )

        # Rule 4: Filter (actual > expected — extra rows)
        if actual > expected and expected != 0:
            excess_pct = (actual - expected) / abs(expected)
            if excess_pct > 0.05:
                return self._make_discrepancy(
                    test_result, DiscrepancyType.FILTER, 0.70,
                    f"Actual exceeds expected by {excess_pct:.1%} — suggests missing filter",
                    "Add WHERE clause to exclude extra rows",
                )

        # Rule 5: Missing data (actual < expected significantly)
        if actual < expected and expected != 0:
            deficit_pct = (expected - actual) / abs(expected)
            if deficit_pct > 0.10:
                return self._make_discrepancy(
                    test_result, DiscrepancyType.MISSING_DATA, 0.65,
                    f"Actual is {deficit_pct:.1%} below expected — suggests missing data",
                    "Check for missing rows in DW: verify ETL completeness",
                )

        # Rule 6: Check context for join issues (multiple measures wrong in same direction)
        if context:
            other_results = context.get("other_results", [])
            if self._detect_join_issue(test_result, other_results):
                return self._make_discrepancy(
                    test_result, DiscrepancyType.JOIN, 0.60,
                    "Multiple measures off in same direction — suggests missing or wrong JOIN",
                    "Review JOIN conditions: check for missing table joins",
                )

            # Rule 7: Hierarchy issue (rollup wrong but detail correct)
            if self._detect_hierarchy_issue(test_result, context):
                return self._make_discrepancy(
                    test_result, DiscrepancyType.HIERARCHY, 0.55,
                    "Rollup totals wrong but detail lines correct — hierarchy grouping issue",
                    "Check GROUP BY hierarchy levels and rollup logic",
                )

        # Default: Formula error
        return self._make_discrepancy(
            test_result, DiscrepancyType.FORMULA, 0.40,
            f"Unclassified difference of {diff_pct:.2%}",
            "Review measure calculation formula for errors",
        )

    def classify_batch(
        self,
        test_results: List[ParityTestResult],
    ) -> List[Discrepancy]:
        """Classify multiple test results, using batch context."""
        failed = [r for r in test_results if r.status in ("fail", "warn")]
        context = {"other_results": failed}

        discrepancies = []
        for result in failed:
            disc = self.classify(result, context)
            discrepancies.append(disc)

        return discrepancies

    def _detect_join_issue(
        self,
        result: ParityTestResult,
        other_results: List[ParityTestResult],
    ) -> bool:
        """Detect if multiple measures are wrong in the same direction."""
        if not other_results:
            return False

        my_direction = 1 if result.actual_value > result.expected_value else -1
        same_direction = 0
        for other in other_results:
            if other.test_id == result.test_id:
                continue
            if other.status not in ("fail", "warn"):
                continue
            other_dir = 1 if other.actual_value > other.expected_value else -1
            if other_dir == my_direction:
                same_direction += 1

        return same_direction >= 2

    @staticmethod
    def _detect_hierarchy_issue(
        result: ParityTestResult,
        context: Dict[str, Any],
    ) -> bool:
        """Detect hierarchy grouping issues from context."""
        detail_results = context.get("detail_results", [])
        if not detail_results:
            return False

        detail_pass_count = sum(1 for r in detail_results if r.status == "pass")
        if detail_pass_count > 0 and result.status == "fail":
            return True
        return False

    @staticmethod
    def _make_discrepancy(
        result: ParityTestResult,
        disc_type: DiscrepancyType,
        confidence: float,
        detail: str,
        fix: str,
    ) -> Discrepancy:
        return Discrepancy(
            test_result=result,
            discrepancy_type=disc_type,
            confidence=confidence,
            root_cause_detail=detail,
            suggested_fix=fix,
        )
