from __future__ import annotations

import difflib
import logging
from decimal import Decimal
from typing import Any

from .models import GroundTruthRow, QSDrillScorecard, TakeoffItemEvaluation

logger = logging.getLogger("dscons.takeoff_training.evaluator")


class QSDrillEvaluator:
    """Bộ chấm điểm và đánh giá sai số bóc tách BoQ so với Ground Truth."""

    @classmethod
    def evaluate_takeoff(
        cls,
        drill_id: str,
        project_name: str,
        project_code: str,
        ground_truth_items: list[GroundTruthRow],
        ai_takeoff_items: list[dict[str, Any]],
    ) -> QSDrillScorecard:
        """So sánh từng dòng bóc tách của AI với bảng dự toán đối chứng chuẩn."""
        if not ground_truth_items:
            return QSDrillScorecard(
                drill_id=drill_id,
                project_name=project_name,
                project_code=project_code,
                overall_grade="F",
                is_production_ready=False,
            )

        evaluated_list: list[TakeoffItemEvaluation] = []
        matched_count = 0
        total_mae = Decimal("0.0")
        formulas_count = 0

        # Normalization and Matching
        used_ai_indices = set()

        for gt in ground_truth_items:
            best_match_idx = -1
            best_similarity = 0.0

            for idx, ai_item in enumerate(ai_takeoff_items):
                if idx in used_ai_indices:
                    continue

                ai_name = str(
                    ai_item.get("task_name") or ai_item.get("item_name") or ""
                ).lower()
                gt_name = gt.task_name.lower()

                # 1. Exact or high string similarity
                sim = difflib.SequenceMatcher(None, gt_name, ai_name).ratio()

                # 2. Match by Norm Code if available
                ai_code = str(ai_item.get("norm_code") or ai_item.get("code") or "")
                if gt.norm_code and ai_code and gt.norm_code.upper() == ai_code.upper():
                    sim = max(sim, 0.95)

                if sim > best_similarity:
                    best_similarity = sim
                    best_match_idx = idx

            if best_match_idx >= 0 and best_similarity >= 0.55:
                used_ai_indices.add(best_match_idx)
                ai_match = ai_takeoff_items[best_match_idx]
                actual_qty = Decimal(
                    str(ai_match.get("quantity") or ai_match.get("qty") or 0)
                )
                expected_qty = gt.quantity

                if expected_qty > Decimal("0.0"):
                    diff = abs(actual_qty - expected_qty)
                    dev_pct = (diff / expected_qty) * Decimal("100.0")
                else:
                    dev_pct = (
                        Decimal("0.0")
                        if actual_qty == Decimal("0.0")
                        else Decimal("100.0")
                    )

                f_present = bool(
                    ai_match.get("formula_breakdown")
                    or ai_match.get("diendan_congthuc")
                )
                if f_present:
                    formulas_count += 1

                if dev_pct <= Decimal("3.0"):
                    status = "EXACT_MATCH"
                    cause = None
                elif dev_pct <= Decimal("10.0"):
                    status = "MINOR_DEVIATION"
                    cause = "Lệch làm tròn hoặc trừ giao cấu kiện nhỏ"
                else:
                    status = "MAJOR_DEVIATION"
                    cause = "Sai lệch kích thước hình học hoặc chưa trừ cọc/lỗ mở"

                matched_count += 1
                total_mae += dev_pct

                evaluated_list.append(
                    TakeoffItemEvaluation(
                        task_name=gt.task_name,
                        norm_code=gt.norm_code,
                        unit=gt.unit,
                        expected_quantity=expected_qty,
                        actual_quantity=actual_qty,
                        deviation_pct=round(dev_pct, 2),
                        formula_present=f_present,
                        formula_breakdown=ai_match.get("formula_breakdown"),
                        match_status=status,
                        error_cause=cause,
                    )
                )
            else:
                evaluated_list.append(
                    TakeoffItemEvaluation(
                        task_name=gt.task_name,
                        norm_code=gt.norm_code,
                        unit=gt.unit,
                        expected_quantity=gt.quantity,
                        actual_quantity=Decimal("0.0"),
                        deviation_pct=Decimal("100.0"),
                        formula_present=False,
                        match_status="MISSED",
                        error_cause="Bỏ sót đầu việc so với bản vẽ thiết kế",
                    )
                )

        total_gt = len(ground_truth_items)
        total_ai = max(len(ai_takeoff_items), 1)
        recall = Decimal(str(round((matched_count / total_gt) * 100, 2)))
        precision = Decimal(str(round((matched_count / total_ai) * 100, 2)))
        avg_mae = Decimal(str(round(float(total_mae / max(matched_count, 1)), 2)))
        formula_cov = Decimal(str(round((formulas_count / total_ai) * 100, 2)))

        # Grading logic
        if (
            recall >= Decimal("95.0")
            and avg_mae <= Decimal("3.0")
            and formula_cov >= Decimal("85.0")
        ):
            grade = "S"
            is_ready = True
        elif recall >= Decimal("85.0") and avg_mae <= Decimal("6.0"):
            grade = "A"
            is_ready = True
        elif recall >= Decimal("75.0") and avg_mae <= Decimal("12.0"):
            grade = "B"
            is_ready = False
        elif recall >= Decimal("60.0"):
            grade = "C"
            is_ready = False
        else:
            grade = "F"
            is_ready = False

        return QSDrillScorecard(
            drill_id=drill_id,
            project_name=project_name,
            project_code=project_code,
            total_ground_truth_items=total_gt,
            matched_items=matched_count,
            recall_rate_pct=recall,
            precision_rate_pct=precision,
            volume_mae_pct=avg_mae,
            formula_coverage_pct=formula_cov,
            overall_grade=grade,
            is_production_ready=is_ready,
            evaluated_items=evaluated_list,
        )
