"""Excel vs data warehouse reconciliation engine.

Compares Excel report cell values against warehouse query results,
producing a cell-by-cell reconciliation report with discrepancy classification.
"""
from __future__ import annotations

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

from .contracts import (
    CellReconciliation,
    DiscrepancyType,
    ReconciliationReport,
)


class ExcelWarehouseReconciler:
    """Compare Excel report aggregations against data warehouse outputs."""

    def __init__(self, rounding_threshold: float = 0.01):
        self.rounding_threshold = rounding_threshold

    def reconcile(
        self,
        excel_data: Dict[str, Any],
        warehouse_data: Dict[str, Any],
        mapping: Dict[str, str],
    ) -> ReconciliationReport:
        """Produce cell-by-cell reconciliation report.

        Args:
            excel_data: {cells: [{address, value, formula}, ...]}
            warehouse_data: {rows: [{col: val}, ...], columns: [...]}
            mapping: {excel_cell_or_label: warehouse_column}
        """
        cells: List[CellReconciliation] = []

        # Build a lookup from warehouse columns → first-row values
        wh_lookup = self._build_warehouse_lookup(warehouse_data)
        excel_cells = excel_data.get("cells", [])

        for cell_info in excel_cells:
            address = cell_info.get("address", "")
            label = cell_info.get("label", address)
            excel_val = cell_info.get("value")

            # Find the warehouse column for this cell
            wh_col = mapping.get(address) or mapping.get(label)
            if wh_col is None:
                # No mapping — skip this cell
                continue

            warehouse_val = wh_lookup.get(wh_col)

            disc_type = self._classify_discrepancy(excel_val, warehouse_val)
            diff = self._compute_difference(excel_val, warehouse_val)

            cells.append(
                CellReconciliation(
                    cell_address=address,
                    excel_value=excel_val,
                    warehouse_value=warehouse_val,
                    discrepancy_type=disc_type,
                    difference=diff,
                    mapping_label=label,
                )
            )

        # Also check for warehouse columns not present in Excel
        mapped_wh_cols = set(mapping.values())
        for wh_col in wh_lookup:
            if wh_col not in mapped_wh_cols:
                cells.append(
                    CellReconciliation(
                        cell_address="",
                        excel_value=None,
                        warehouse_value=wh_lookup[wh_col],
                        discrepancy_type=DiscrepancyType.MISSING_EXCEL,
                        mapping_label=wh_col,
                    )
                )

        summary = self._compute_summary(cells)

        return ReconciliationReport(
            total_cells=len(cells),
            matched=summary.get("match", 0),
            rounding=summary.get("rounding", 0),
            genuine_diff=summary.get("genuine_diff", 0),
            missing_excel=summary.get("missing_excel", 0),
            missing_warehouse=summary.get("missing_warehouse", 0),
            cells=cells,
            rounding_threshold=self.rounding_threshold,
        )

    # ------------------------------------------------------------------
    # Classification
    # ------------------------------------------------------------------

    def _classify_discrepancy(
        self, excel_val: Any, warehouse_val: Any
    ) -> DiscrepancyType:
        """Classify: MATCH, ROUNDING, GENUINE_DIFF, MISSING_EXCEL, MISSING_WAREHOUSE."""
        if excel_val is None and warehouse_val is None:
            return DiscrepancyType.MATCH

        if excel_val is None:
            return DiscrepancyType.MISSING_EXCEL

        if warehouse_val is None:
            return DiscrepancyType.MISSING_WAREHOUSE

        # Try numeric comparison
        e_num = _to_float(excel_val)
        w_num = _to_float(warehouse_val)

        if e_num is not None and w_num is not None:
            diff = abs(e_num - w_num)
            if diff == 0.0:
                return DiscrepancyType.MATCH
            if diff <= self.rounding_threshold:
                return DiscrepancyType.ROUNDING
            return DiscrepancyType.GENUINE_DIFF

        # String comparison
        if str(excel_val).strip() == str(warehouse_val).strip():
            return DiscrepancyType.MATCH

        return DiscrepancyType.GENUINE_DIFF

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _build_warehouse_lookup(warehouse_data: Dict[str, Any]) -> Dict[str, Any]:
        """Build column → value lookup from warehouse result (uses first row)."""
        rows = warehouse_data.get("rows", [])
        if not rows:
            return {}
        first_row = rows[0]
        if isinstance(first_row, dict):
            return dict(first_row)
        # If rows are lists, pair with columns
        columns = warehouse_data.get("columns", [])
        return dict(zip(columns, first_row)) if columns else {}

    @staticmethod
    def _compute_difference(excel_val: Any, warehouse_val: Any) -> float:
        """Compute numeric difference between values (0.0 if non-numeric)."""
        e_num = _to_float(excel_val)
        w_num = _to_float(warehouse_val)
        if e_num is not None and w_num is not None:
            return abs(e_num - w_num)
        return 0.0

    @staticmethod
    def _compute_summary(cells: List[CellReconciliation]) -> Dict[str, int]:
        """Count matched, rounding, genuine, missing cells."""
        counts: Dict[str, int] = {}
        for c in cells:
            key = c.discrepancy_type.value
            counts[key] = counts.get(key, 0) + 1
        return counts


# ---------------------------------------------------------------------------
# Module-level helpers
# ---------------------------------------------------------------------------

def _to_float(val: Any) -> Optional[float]:
    """Try to convert a value to float, return None on failure."""
    if val is None:
        return None
    if isinstance(val, (int, float)):
        if math.isnan(val) or math.isinf(val):
            return None
        return float(val)
    try:
        return float(str(val).replace(",", "").strip())
    except (ValueError, TypeError):
        return None
