"""Excel-vs-warehouse reconciliation and discrepancy classification for BLCE."""
from __future__ import annotations

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

from .contracts import (
    ExcelWarehouseCellResult,
    ExcelWarehouseReconciliation,
    ReconciliationStatus,
)


def _to_float(value: Any) -> Optional[float]:
    if value is None:
        return None
    if isinstance(value, (int, float)):
        return float(value)
    try:
        return float(str(value).replace(",", "").strip())
    except Exception:
        return None


def _normalize_rows(payload: Any, value_key_hint: str) -> Dict[str, float]:
    """Normalize input into {metric_key: value}."""
    if payload is None:
        return {}
    if isinstance(payload, dict):
        out: Dict[str, float] = {}
        for k, v in payload.items():
            fv = _to_float(v)
            if fv is not None:
                out[str(k)] = fv
        return out
    if isinstance(payload, list):
        out = {}
        for row in payload:
            if not isinstance(row, dict):
                continue
            key = str(
                row.get("metric_key")
                or row.get("metric")
                or row.get("name")
                or row.get("key")
                or ""
            ).strip()
            if not key:
                continue
            value = row.get(value_key_hint)
            if value is None:
                value = row.get("value")
            fv = _to_float(value)
            if fv is not None:
                out[key] = fv
        return out
    return {}


class ExcelWarehouseReconciler:
    """Compare Excel aggregates vs warehouse aggregates and classify discrepancies."""

    def reconcile(
        self,
        *,
        excel_values: Any,
        warehouse_values: Any,
        tolerance_pct: float = 0.01,
        analysis_id: str = "",
        report_name: str = "",
    ) -> ExcelWarehouseReconciliation:
        from src.report_parity.contracts import ParityTestResult
        from src.report_parity.discrepancy_classifier import DiscrepancyClassifier

        excel_map = _normalize_rows(excel_values, "excel_value")
        wh_map = _normalize_rows(warehouse_values, "warehouse_value")

        keys = sorted(set(excel_map.keys()) | set(wh_map.keys()))
        classifier = DiscrepancyClassifier(rounding_threshold=tolerance_pct / 2.0)
        results: List[ExcelWarehouseCellResult] = []
        classes: Dict[str, int] = {}

        for key in keys:
            e = excel_map.get(key)
            w = wh_map.get(key)
            if e is None and w is not None:
                status = ReconciliationStatus.EXTRA_IN_WAREHOUSE
                result = ExcelWarehouseCellResult(
                    metric_key=key,
                    excel_value=None,
                    warehouse_value=w,
                    difference=w,
                    difference_pct=1.0,
                    status=status,
                    classification_type="extra_data",
                    classification_confidence=0.9,
                    root_cause="Value appears in warehouse but not in Excel baseline.",
                    suggested_fix="Review warehouse filters/grouping and duplicate elimination.",
                )
            elif e is not None and w is None:
                status = ReconciliationStatus.MISSING_IN_WAREHOUSE
                result = ExcelWarehouseCellResult(
                    metric_key=key,
                    excel_value=e,
                    warehouse_value=None,
                    difference=-e,
                    difference_pct=1.0,
                    status=status,
                    classification_type="missing_data",
                    classification_confidence=0.9,
                    root_cause="Value appears in Excel but is missing in warehouse output.",
                    suggested_fix="Trace ETL lineage and validate source-to-DW load completeness.",
                )
            else:
                # Both present
                assert e is not None and w is not None
                diff = w - e
                diff_pct = abs(diff) / abs(e) if e != 0 else (0.0 if w == 0 else 1.0)
                if diff_pct <= tolerance_pct:
                    status = ReconciliationStatus.MATCH
                    result = ExcelWarehouseCellResult(
                        metric_key=key,
                        excel_value=e,
                        warehouse_value=w,
                        difference=diff,
                        difference_pct=diff_pct,
                        status=status,
                        classification_type="",
                        classification_confidence=1.0,
                    )
                else:
                    status = ReconciliationStatus.DISCREPANCY
                    tr = ParityTestResult(
                        line_item_id=key,
                        expected_value=e,
                        actual_value=w,
                        difference=diff,
                        difference_pct=diff_pct,
                        status="fail",
                        tolerance_pct=tolerance_pct,
                    )
                    disc = classifier.classify(tr)
                    result = ExcelWarehouseCellResult(
                        metric_key=key,
                        excel_value=e,
                        warehouse_value=w,
                        difference=diff,
                        difference_pct=diff_pct,
                        status=status,
                        classification_type=disc.discrepancy_type.value,
                        classification_confidence=disc.confidence,
                        root_cause=disc.root_cause_detail,
                        suggested_fix=disc.suggested_fix,
                    )

            if result.classification_type:
                classes[result.classification_type] = classes.get(result.classification_type, 0) + 1
            results.append(result)

        matched = sum(1 for r in results if r.status == ReconciliationStatus.MATCH)
        discrepancy = sum(1 for r in results if r.status == ReconciliationStatus.DISCREPANCY)
        missing = sum(1 for r in results if r.status == ReconciliationStatus.MISSING_IN_WAREHOUSE)
        extra = sum(1 for r in results if r.status == ReconciliationStatus.EXTRA_IN_WAREHOUSE)

        return ExcelWarehouseReconciliation(
            analysis_id=analysis_id,
            report_name=report_name,
            tolerance_pct=tolerance_pct,
            compared_count=len(results),
            matched_count=matched,
            discrepancy_count=discrepancy,
            missing_in_warehouse_count=missing,
            extra_in_warehouse_count=extra,
            classifications=classes,
            results=results,
        )


def build_resolution_plan(
    reconciliation: ExcelWarehouseReconciliation,
    *,
    include_row_details: bool = True,
) -> Dict[str, Any]:
    """Build a programmatic remediation plan from classified discrepancies."""
    actions = {
        "rounding": {
            "action": "adjust_precision",
            "description": "Adjust decimal precision/casting in DT_2 or semantic layer measure definition.",
        },
        "signage": {
            "action": "normalize_sign_convention",
            "description": "Apply consistent sign convention between Excel and warehouse formulas.",
        },
        "grain": {
            "action": "fix_aggregation_grain",
            "description": "Align GROUP BY keys to expected report grain.",
        },
        "filter": {
            "action": "align_filters",
            "description": "Apply missing include/exclude filters in warehouse SQL.",
        },
        "missing_data": {
            "action": "investigate_data_gaps",
            "description": "Trace source-to-target lineage for dropped records and load failures.",
        },
        "extra_data": {
            "action": "remove_extra_rows",
            "description": "Deduplicate or restrict joins/filters causing warehouse overcounts.",
        },
        "join": {
            "action": "fix_join_logic",
            "description": "Correct join keys and cardinality assumptions.",
        },
        "hierarchy": {
            "action": "fix_hierarchy_rollup",
            "description": "Align rollup levels and hierarchy mapping.",
        },
        "formula": {
            "action": "align_business_formula",
            "description": "Update warehouse calculation to match approved business formula.",
        },
    }

    grouped: Dict[str, List[ExcelWarehouseCellResult]] = {}
    for row in reconciliation.results:
        if not row.classification_type:
            continue
        grouped.setdefault(row.classification_type, []).append(row)

    plan_items: List[Dict[str, Any]] = []
    for cls, rows in sorted(grouped.items(), key=lambda x: len(x[1]), reverse=True):
        action_meta = actions.get(cls, {
            "action": "manual_review",
            "description": "Manual review required for unclassified discrepancy.",
        })
        item = {
            "classification_type": cls,
            "count": len(rows),
            "recommended_action": action_meta["action"],
            "description": action_meta["description"],
        }
        if include_row_details:
            item["affected_metrics"] = [r.metric_key for r in rows[:50]]
        plan_items.append(item)

    return {
        "reconciliation_id": reconciliation.reconciliation_id,
        "total_discrepancies": reconciliation.discrepancy_count
        + reconciliation.missing_in_warehouse_count
        + reconciliation.extra_in_warehouse_count,
        "classification_summary": reconciliation.classifications,
        "plan_items": plan_items,
    }

