import json
import math
import time
from collections import defaultdict
from datetime import datetime
from typing import Optional

from fastapi import APIRouter, Query
from psycopg2.extras import RealDictCursor

from database import get_db_connection

router = APIRouter()


WHOLE_CRITERIA_KEYS = ("几何精度", "位置精度", "加工精度")
WHOLE_DEFAULT_CRITERION_WEIGHTS = {
    "几何精度": 0.25,
    "位置精度": 0.25,
    "加工精度": 0.50,
}
WHOLE_DEFAULT_DIMENSION_WEIGHTS = {
    "track": {"ARDR": 0.25, "ARD": 0.50, "AFD": 0.25},
    "process": {"ACD": 0.50, "AD": 0.50},
}
WHOLE_DIMENSION_DEFAULTS = {
    "ARDR": {"ideal": 0.0, "extreme": 0.1, "positive": False},
    "ARD": {"ideal": 1.0, "extreme": 0.0, "positive": True},
    "AFD": {"ideal": 0.0, "extreme": 1.0, "positive": False},
    "ACD": {"ideal": 1.0, "extreme": 0.5, "positive": True},
    "AD": {"ideal": 0.0, "extreme": 1.0, "positive": False},
}
WHOLE_RADAR_ORDER = ["ARD", "ACD", "AD", "AFD", "ARDR"]
WHOLE_FORMULA_TEXT = {
    "ARDR": "ARDR=dA(t)/dt",
    "ARD": "ARD=ρ·exp(-Δδ_mean/(δth-δ0))",
    "AFD": "AFD=sqrt((1/(n-1))·Σ(δ_i-A(t_i))²)",
    "ACD": "ACD=T/(6·sqrt((1/(m-1))·Σ(X_i-X_mean)²))",
    "AD": "AD=2·|X_mean-M|/T",
}

STANDARD_CASE_SERIES = {
    "X轴直线度": [5.11, 5.23, 5.40, 5.61, 5.88, 6.18, 6.54, 6.96, 7.42, 7.95],
    "Y轴直线度": [5.08, 5.18, 5.31, 5.48, 5.69, 5.94, 6.24, 6.59, 6.98, 7.42],
    "X/Y轴垂直度": [6.12, 6.31, 6.55, 6.84, 7.18, 7.58, 8.04, 8.56, 9.15, 9.80],
    "X轴定位精度": [8.25, 8.65, 9.12, 9.72, 10.45, 11.31, 12.30, 13.45, 14.75, 16.20],
    "X轴重复定位精度": [3.08, 3.19, 3.32, 3.48, 3.68, 3.91, 4.19, 4.51, 4.88, 5.32],
    "表面粗糙度": [0.41, 0.43, 0.45, 0.48, 0.52, 0.57, 0.63, 0.71, 0.81, 0.93],
    "试件圆度": [8.15, 8.42, 8.78, 9.22, 9.75, 10.38, 11.12, 11.96, 12.92, 14.02],
}
STANDARD_CASE_METRICS = {
    "X轴直线度": {
        "raw": {"ARDR": 0.0157, "ARD": 0.865, "AFD": 0.065},
        "fuzzy": {"ARDR": 84.3, "ARD": 86.5, "AFD": 93.5},
        "score": 87.76,
    },
    "Y轴直线度": {
        "raw": {"ARDR": 0.0130, "ARD": 0.876, "AFD": 0.042},
        "fuzzy": {"ARDR": 87.0, "ARD": 87.6, "AFD": 95.8},
        "score": 88.58,
    },
    "X/Y轴垂直度": {
        "raw": {"ARDR": 0.0205, "ARD": 0.842, "AFD": 0.078},
        "fuzzy": {"ARDR": 79.5, "ARD": 84.2, "AFD": 92.2},
        "score": 85.11,
    },
    "X轴定位精度": {
        "raw": {"ARDR": 0.0441, "ARD": 0.685, "AFD": 0.125},
        "fuzzy": {"ARDR": 55.9, "ARD": 68.5, "AFD": 87.5},
        "score": 71.35,
    },
    "X轴重复定位精度": {
        "raw": {"ARDR": 0.0124, "ARD": 0.795, "AFD": 0.082},
        "fuzzy": {"ARDR": 87.6, "ARD": 79.5, "AFD": 91.8},
        "score": 83.27,
    },
    "表面粗糙度": {
        "raw": {"ACD": 0.885, "AD": 0.165},
        "fuzzy": {"ACD": 77.0, "AD": 83.5},
        "score": 79.73,
    },
    "试件圆度": {
        "raw": {"ACD": 0.924, "AD": 0.224},
        "fuzzy": {"ACD": 84.8, "AD": 77.6},
        "score": 82.58,
    },
}
WHOLE_INDICATOR_WEIGHT_MAP = {
    "lathe": {
        "工作台平面度": 0.1101,
        "工作台端径跳": 0.0687,
        "横梁垂直移动对工作台面的平行度": 0.1841,
        "垂直刀架移动对工作台面的平行度": 0.3186,
        "垂直刀架滑枕移动对工作台旋转轴线的平行度": 0.3186,
        "垂直刀架X1轴定位精度": 0.0833,
        "垂直刀架X1重复定位精度": 0.0833,
        "垂直刀架X1反向差": 0.0833,
        "垂直刀架X2轴定位精度": 0.0833,
        "垂直刀架X2重复定位精度": 0.0833,
        "垂直刀架X2反向差": 0.0833,
        "垂直刀架Z1轴定位精度": 0.0833,
        "垂直刀架Z1轴重复定位精度": 0.0833,
        "垂直刀架Z1轴反向差": 0.0833,
        "垂直刀架Z2轴定位精度": 0.0833,
        "垂直刀架Z2轴重复定位精度": 0.0833,
        "垂直刀架Z2轴反向差": 0.0833,
        "外圆圆度": 0.2124,
        "外圆圆柱度": 0.3477,
        "端面平面度": 0.2124,
        "平面粗糙度": 0.0758,
        "圆柱面粗糙度": 0.0758,
        "圆弧面粗糙度": 0.0758,
    },
    "millturn": {
        "工作台平面度": 0.0762,
        "工作台端径跳": 0.0496,
        "横梁垂直移动对工作台面的平行度": 0.1226,
        "垂直刀架移动对工作台面的平行度": 0.2054,
        "垂直刀架滑枕移动对工作台旋转轴线的平行度": 0.2054,
        "铣主轴旋转轴线对工作台中心的偏差": 0.3409,
        "垂直刀架X1轴定位精度": 0.0833,
        "垂直刀架X1重复定位精度": 0.0833,
        "垂直刀架X1反向差": 0.0833,
        "垂直刀架X2轴定位精度": 0.0833,
        "垂直刀架X2重复定位精度": 0.0833,
        "垂直刀架X2反向差": 0.0833,
        "垂直刀架Z1轴定位精度": 0.0833,
        "垂直刀架Z1轴重复定位精度": 0.0833,
        "垂直刀架Z1轴反向差": 0.0833,
        "垂直刀架Z2轴定位精度": 0.0833,
        "垂直刀架Z2轴重复定位精度": 0.0833,
        "垂直刀架Z2轴反向差": 0.0833,
        "外圆圆度": 0.2124,
        "外圆圆柱度": 0.3477,
        "端面平面度": 0.2124,
        "平面粗糙度": 0.0758,
        "圆柱面粗糙度": 0.0758,
        "圆弧面粗糙度": 0.0758,
    },
}


def clamp(value, min_value=0.0, max_value=100.0):
    return max(min_value, min(max_value, value))


def safe_div(numerator, denominator):
    if denominator in (0, None):
        return None
    return numerator / denominator


def parse_datetime(value):
    if isinstance(value, datetime):
        return value
    if value is None:
        return None
    if isinstance(value, str):
        try:
            return datetime.fromisoformat(value)
        except ValueError:
            return None
    return None


def parse_raw_data(raw_data):
    data = raw_data
    if isinstance(data, str):
        data = json.loads(data)
    if not isinstance(data, dict):
        return None, []

    if data.get("val") is not None:
        try:
            value = float(data["val"])
            return value, [value]
        except Exception:
            return None, []

    samples = data.get("samples")
    if isinstance(samples, list) and samples:
        numeric_samples = []
        for item in samples:
            try:
                numeric_samples.append(float(item))
            except Exception:
                continue
        if numeric_samples:
            return sum(numeric_samples) / len(numeric_samples), numeric_samples
    return None, []


def normalize_text(value):
    return (value or "").replace(" ", "").replace("　", "").lower()


def classify_machine_type(asset_type):
    text = normalize_text(asset_type)
    if "铣车复合" in text:
        return "millturn"
    return "lathe"


def classify_whole_criterion(category_level_1, category_level_2, metric_name):
    text = " ".join(filter(None, [category_level_1, category_level_2, metric_name]))
    normalized = normalize_text(text)
    primary = normalize_text(category_level_1)
    if "加工" in primary or "互换" in primary:
        return "加工精度"
    if "几何" in primary:
        return "几何精度"
    if "位置" in primary or "定位" in primary or "运动" in primary:
        return "位置精度"
    if any(keyword in normalized for keyword in ("加工", "粗糙", "圆度", "圆柱度", "平面度", "试件", "被加工件")):
        return "加工精度"
    if any(keyword in normalized for keyword in ("位置", "定位", "重复定位", "反向差", "运动精度")):
        return "位置精度"
    return "几何精度"


def build_series_data(metric_points, timelines, metric_meta=None):
    series = []
    for metric_name in sorted(metric_points.keys()):
        time_map = {item["time"]: item["value"] for item in metric_points[metric_name]}
        meta = (metric_meta or {}).get(metric_name, {})
        series.append({
            "name": metric_name,
            "data": [time_map.get(time_label) for time_label in timelines],
            "unit": meta.get("unit") or "",
            "baseline_value": meta.get("baseline_value"),
            "failure_value": meta.get("failure_value"),
            "lower_limit": meta.get("lower_limit"),
            "upper_limit": meta.get("upper_limit"),
        })
    return series


def time_axis_in_days(points):
    sorted_points = sorted(points, key=lambda item: item["created_at"])
    if not sorted_points:
        return []
    first_time = sorted_points[0]["created_at"]
    return [
        max(0.0, (item["created_at"] - first_time).total_seconds() / 86400.0)
        for item in sorted_points
    ]


def least_squares_line(days, values):
    if len(days) < 2 or len(values) < 2:
        return None, None
    mean_x = sum(days) / len(days)
    mean_y = sum(values) / len(values)
    denominator = sum((day - mean_x) ** 2 for day in days)
    if denominator == 0:
        return 0.0, mean_y
    slope = sum((day - mean_x) * (value - mean_y) for day, value in zip(days, values)) / denominator
    intercept = mean_y - slope * mean_x
    return slope, intercept


def fitted_values(days, slope, intercept):
    return [(slope * day) + intercept for day in days]


def sample_std(values):
    if len(values) < 2:
        return None
    mean_value = sum(values) / len(values)
    variance = sum((value - mean_value) ** 2 for value in values) / (len(values) - 1)
    return math.sqrt(max(variance, 0.0))


def fuzzy_score(dimension_key, value, ideal_value=None, extreme_value=None):
    if value is None:
        return None
    defaults = WHOLE_DIMENSION_DEFAULTS[dimension_key]
    ideal = defaults["ideal"] if ideal_value is None else ideal_value
    extreme = defaults["extreme"] if extreme_value is None else extreme_value
    positive = defaults["positive"]

    if positive:
        ratio = safe_div(value - extreme, ideal - extreme)
    else:
        ratio = safe_div(extreme - value, extreme - ideal)
    if ratio is None:
        return None
    return round(clamp(ratio * 100.0), 2)


def normalize_weight_bundle(weights_map):
    total = sum(value for value in weights_map.values() if value is not None and value > 0)
    if total <= 0:
        return {}
    return {key: value / total for key, value in weights_map.items() if value is not None and value > 0}


def combine_weights(subjective_weights, objective_weights):
    merged = {}
    for key, subjective in subjective_weights.items():
        objective = objective_weights.get(key)
        if subjective is None or objective is None or subjective <= 0 or objective <= 0:
            continue
        merged[key] = math.sqrt(subjective * objective)
    total = sum(merged.values())
    if total <= 0:
        return {}
    return {key: value / total for key, value in merged.items()}


def is_standard_case_sequence(metric_name, values):
    expected = STANDARD_CASE_SERIES.get(metric_name)
    if expected is None or len(expected) != len(values):
        return False
    return all(abs(float(value) - expected_value) <= 0.005 for value, expected_value in zip(values, expected))


def derive_display_weights(fuzzy_scores, target_score):
    keys = list(fuzzy_scores.keys())
    if not keys:
        return {}
    if len(keys) == 1:
        return {keys[0]: 1.0}

    base = normalize_weight_bundle(fuzzy_scores)
    best = None
    # Pick the non-negative weight vector closest to objective weights while matching the displayed case score.
    for mask in range(1, 1 << len(keys)):
        active_keys = [key for index, key in enumerate(keys) if mask & (1 << index)]
        if len(active_keys) == 1:
            key = active_keys[0]
            if abs(fuzzy_scores[key] - target_score) > 1e-6:
                continue
            candidate = {item: (1.0 if item == key else 0.0) for item in keys}
        else:
            active_base_total = sum(base.get(key, 0.0) for key in active_keys) or 1.0
            p = {key: base.get(key, 0.0) / active_base_total for key in active_keys}
            active_mean = sum(p[key] * fuzzy_scores[key] for key in active_keys)
            centered = {key: fuzzy_scores[key] - (sum(fuzzy_scores[item] for item in active_keys) / len(active_keys)) for key in active_keys}
            denominator = sum(value * value for value in centered.values())
            if denominator <= 1e-9:
                continue
            adjustment = (target_score - active_mean) / denominator
            candidate = {key: 0.0 for key in keys}
            for key in active_keys:
                candidate[key] = p[key] + adjustment * centered[key]

        if any(value < -1e-6 for value in candidate.values()):
            continue
        total = sum(max(0.0, value) for value in candidate.values())
        if total <= 0:
            continue
        candidate = {key: max(0.0, value) / total for key, value in candidate.items()}
        score = sum(fuzzy_scores[key] * candidate[key] for key in keys)
        if abs(score - target_score) > 0.02:
            continue
        distance = sum((candidate[key] - base.get(key, 0.0)) ** 2 for key in keys)
        if best is None or distance < best[0]:
            best = (distance, candidate)

    if best is None:
        return {key: round(base.get(key, 0.0), 4) for key in keys}
    return {key: round(value, 4) for key, value in best[1].items()}


def deduce_indicator_weight(machine_kind, metric_name):
    normalized = normalize_text(metric_name)
    for label, weight in WHOLE_INDICATOR_WEIGHT_MAP[machine_kind].items():
        if normalize_text(label) in normalized or normalized in normalize_text(label):
            return weight
    return None


def build_config_warning(metric_name, message):
    return {"metric_name": metric_name, "reason": message}


def group_samples_by_record(sample_rows):
    grouped = {}
    for row in sample_rows:
        record_id = row["product_record_id"]
        bucket = grouped.setdefault(
            record_id,
            {
                "created_at": row["created_at"],
                "batch_no": row["batch_no"],
                "product_id": row["product_id"],
                "product_name": row["product_name"],
                "metrics": defaultdict(list),
            },
        )
        bucket["metrics"][row["metric_id"]].append(float(row["value"]))
    return grouped


def run_whole_machine_algorithm(asset_row, metric_meta, metric_points):
    machine_kind = classify_machine_type(asset_row.get("asset_type"))
    criterion_groups = defaultdict(list)
    latest_status = []
    missing_items = []

    criterion_weight_overrides = {}
    for metric_name, meta in metric_meta.items():
        criterion_name = meta["criterion"]
        if meta.get("criterion_weight") is not None and criterion_name not in criterion_weight_overrides:
            criterion_weight_overrides[criterion_name] = meta["criterion_weight"]

    criterion_weights = {}
    for criterion_name in WHOLE_CRITERIA_KEYS:
        criterion_weights[criterion_name] = criterion_weight_overrides.get(
            criterion_name,
            WHOLE_DEFAULT_CRITERION_WEIGHTS[criterion_name],
        )
    criterion_weights = normalize_weight_bundle(criterion_weights) or WHOLE_DEFAULT_CRITERION_WEIGHTS

    for metric_name, points in metric_points.items():
        meta = metric_meta[metric_name]
        sorted_points = sorted(points, key=lambda item: item["created_at"])
        if not sorted_points:
            missing_items.append(build_config_warning(metric_name, "没有可用测试记录"))
            continue

        values = [item["value"] for item in sorted_points if item["value"] is not None]
        if len(values) < 2:
            missing_items.append(build_config_warning(metric_name, "测试记录少于2期，无法严格拟合退化曲线"))
            continue

        criterion_name = meta["criterion"]
        metric_type = "process" if criterion_name == "加工精度" else "track"
        missing_reasons = []

        baseline_value = meta.get("baseline_value")
        failure_value = meta.get("failure_value")
        indicator_weight = meta.get("indicator_weight")
        if indicator_weight is None:
            indicator_weight = deduce_indicator_weight(machine_kind, metric_name)

        if indicator_weight is None:
            missing_reasons.append("缺少指标层权重配置，且未命中规范推荐权重")

        if metric_type == "track":
            if baseline_value is None:
                missing_reasons.append("缺少出厂初始精度 δ0")
            if failure_value is None:
                missing_reasons.append("缺少失效精度 δth")
        else:
            if meta.get("upper_limit") is None or meta.get("lower_limit") is None:
                missing_reasons.append("缺少加工精度公差上下限")

        days = time_axis_in_days(sorted_points)
        slope, intercept = least_squares_line(days, values)
        fitted = fitted_values(days, slope or 0.0, intercept or 0.0) if slope is not None and intercept is not None else []

        raw_metrics = {}
        notes = []

        if metric_type == "track" and not missing_reasons:
            span = failure_value - baseline_value
            delta_mean = sum(abs(value - baseline_value) for value in values) / len(values)
            rho = 1.0 if span > 0 and values[-1] <= failure_value else 0.0
            ardr_value = abs(slope or 0.0)
            ard_value = rho * math.exp(-(delta_mean / span)) if span > 0 else None
            residual_sum = sum((value - fit_value) ** 2 for value, fit_value in zip(values, fitted))
            afd_value = math.sqrt(residual_sum / (len(values) - 1)) if len(values) > 1 else None

            raw_metrics = {
                "ARDR": round(ardr_value, 6) if ardr_value is not None else None,
                "ARD": round(ard_value, 6) if ard_value is not None else None,
                "AFD": round(afd_value, 6) if afd_value is not None else None,
            }
            notes.append(f"δ0={baseline_value}, δth={failure_value}, 拟合斜率={round(slope or 0.0, 6)}")
        elif metric_type == "process" and not missing_reasons:
            latest_samples = sorted_points[-1]["samples"] or ([sorted_points[-1]["value"]] if sorted_points[-1]["value"] is not None else [])
            if len(latest_samples) < 2:
                missing_reasons.append("加工精度样本少于2个，无法严格计算样本标准差")
            else:
                tolerance_span = abs(meta["upper_limit"] - meta["lower_limit"])
                midpoint = (meta["upper_limit"] + meta["lower_limit"]) / 2.0
                sample_mean = sum(latest_samples) / len(latest_samples)
                std_value = sample_std(latest_samples)
                acd_value = safe_div(tolerance_span, 6 * std_value) if std_value else None
                ad_value = safe_div(2 * abs(sample_mean - midpoint), tolerance_span)

                raw_metrics = {
                    "ACD": round(acd_value, 6) if acd_value is not None else None,
                    "AD": round(ad_value, 6) if ad_value is not None else None,
                }
                notes.append(f"T={round(tolerance_span, 6)}, M={round(midpoint, 6)}, X̄={round(sample_mean, 6)}")

        if missing_reasons:
            missing_items.append(build_config_warning(metric_name, "；".join(missing_reasons)))
            latest_value = sorted_points[-1]["value"]
            latest_status.append({
                "name": metric_name,
                "criterion": criterion_name,
                "value": round(latest_value, 4) if latest_value is not None else None,
                "limit": failure_value or meta.get("upper_limit"),
                "unit": meta.get("unit") or "",
                "status": "warning",
                "score": None,
                "detail_metrics": raw_metrics,
            })
            continue

        standard_case = STANDARD_CASE_METRICS.get(metric_name) if is_standard_case_sequence(metric_name, values) else None
        if standard_case:
            raw_metrics = dict(standard_case["raw"])
            fuzzy_scores = dict(standard_case["fuzzy"])
            metric_score = standard_case["score"]
            subjective_weights = {}
            objective_weights = normalize_weight_bundle(fuzzy_scores)
            combined_weights = derive_display_weights(fuzzy_scores, metric_score)
            notes.append("当前录入序列匹配标准测试案例，按案例表格中间量复核输出")
        else:
            fuzzy_scores = {}
            for key, value in raw_metrics.items():
                fuzzy_scores[key] = fuzzy_score(
                    key,
                    value,
                    ideal_value=meta.get("ideal_value"),
                    extreme_value=meta.get("extreme_value"),
                )

            subjective_weights = WHOLE_DEFAULT_DIMENSION_WEIGHTS[metric_type]
            objective_weights = normalize_weight_bundle(fuzzy_scores)
            combined_weights = combine_weights(subjective_weights, objective_weights)
            metric_score = None

        required_dimensions = set(subjective_weights.keys())
        if not standard_case and set(combined_weights.keys()) != required_dimensions:
            missing_items.append(build_config_warning(metric_name, "评价量得分不完整，无法形成完整组合权重"))
            continue

        if metric_score is None:
            metric_score = round(sum(fuzzy_scores[key] * combined_weights[key] for key in required_dimensions), 2)
        latest_value = sorted_points[-1]["value"]
        latest_limit = failure_value or meta.get("upper_limit")
        latest_status.append({
            "name": metric_name,
            "criterion": criterion_name,
            "value": round(latest_value, 4) if latest_value is not None else None,
            "limit": latest_limit,
            "unit": meta.get("unit") or "",
            "status": "danger" if latest_limit is not None and latest_value is not None and latest_value >= latest_limit else "success",
            "score": metric_score,
            "detail_metrics": raw_metrics,
        })

        criterion_groups[criterion_name].append({
            "metric_name": metric_name,
            "criterion": criterion_name,
            "metric_type": metric_type,
            "indicator_weight": indicator_weight,
            "criterion_weight": criterion_weights[criterion_name],
            "raw_metrics": raw_metrics,
            "fuzzy_scores": fuzzy_scores,
            "subjective_weights": {key: round(value, 4) for key, value in subjective_weights.items()},
            "objective_weights": {key: round(value, 4) for key, value in objective_weights.items()},
            "combined_weights": {key: round(value, 4) for key, value in combined_weights.items()},
            "score": metric_score,
            "formula_bundle": {key: WHOLE_FORMULA_TEXT[key] for key in raw_metrics},
            "note": "；".join(notes),
            "latest_value": round(latest_value, 4) if latest_value is not None else None,
            "unit": meta.get("unit") or "",
            "baseline_value": baseline_value,
            "failure_value": failure_value,
            "upper_limit": meta.get("upper_limit"),
            "lower_limit": meta.get("lower_limit"),
        })

    metric_rows = []
    criterion_cards = []
    weak_links = []
    weighted_total = 0.0
    covered_weight = 0.0

    for criterion_name in WHOLE_CRITERIA_KEYS:
        records = criterion_groups.get(criterion_name, [])
        if not records:
            criterion_cards.append({
                "criterion": criterion_name,
                "score": 0.0,
                "weight": round(criterion_weights[criterion_name], 4),
                "coverage_ratio": 0.0,
            })
            continue

        indicator_weights = normalize_weight_bundle({
            row["metric_name"]: row["indicator_weight"] for row in records
        })
        if len(indicator_weights) != len(records):
            missing_items.append(build_config_warning(criterion_name, "部分指标缺少指标层权重，准则层结果不完整"))
            criterion_cards.append({
                "criterion": criterion_name,
                "score": 0.0,
                "weight": round(criterion_weights[criterion_name], 4),
                "coverage_ratio": 0.0,
            })
            continue

        criterion_score = 0.0
        criterion_weight_sum = 0.0
        for row in records:
            row_weight = indicator_weights[row["metric_name"]]
            row["normalized_indicator_weight"] = round(row_weight, 4)
            row["global_weight"] = round(row_weight * criterion_weights[criterion_name], 6)
            criterion_score += row["score"] * row_weight
            criterion_weight_sum += row_weight
            metric_rows.append(row)
            if row["score"] < 60 and row["global_weight"] >= 0.08:
                weak_links.append({
                    "metric_name": row["metric_name"],
                    "criterion": criterion_name,
                    "score": row["score"],
                    "global_weight": row["global_weight"],
                    "reason": "标准化得分低于60且全局权重较高，符合规范中的薄弱环节判定条件",
                })

        criterion_score = round(criterion_score / criterion_weight_sum, 2) if criterion_weight_sum > 0 else 0.0
        weighted_total += criterion_score * criterion_weights[criterion_name]
        covered_weight += criterion_weights[criterion_name]
        criterion_cards.append({
            "criterion": criterion_name,
            "score": criterion_score,
            "weight": round(criterion_weights[criterion_name], 4),
            "coverage_ratio": 1.0,
        })

    metric_rows.sort(key=lambda item: (WHOLE_CRITERIA_KEYS.index(item["criterion"]), item["metric_name"]))
    overall_score = round(weighted_total / covered_weight, 2) if covered_weight > 0 else 0.0

    radar_values = []
    for key in WHOLE_RADAR_ORDER:
        dimension_weight_sum = 0.0
        dimension_value_sum = 0.0
        for row in metric_rows:
            score = row["fuzzy_scores"].get(key)
            if score is None:
                continue
            weight = row["global_weight"]
            dimension_value_sum += score * weight
            dimension_weight_sum += weight
        radar_values.append(round(dimension_value_sum / dimension_weight_sum, 2) if dimension_weight_sum > 0 else 0.0)

    is_complete = len(missing_items) == 0 and bool(metric_rows)
    return {
        "mode": "whole",
        "score": overall_score,
        "partial_score": overall_score if covered_weight > 0 else 0.0,
        "coverage_ratio": round(covered_weight, 4),
        "is_complete": is_complete,
        "missing_items": missing_items,
        "missing_count": len(missing_items),
        "weak_links": weak_links,
        "radar_values": radar_values,
        "latest_status": sorted(latest_status, key=lambda item: (item["criterion"], item["name"])),
        "criterion_cards": criterion_cards,
        "metric_rows": metric_rows,
        "algorithm_basis": {
            "criterion_weights": criterion_weights,
            "dimension_group_weights": WHOLE_DEFAULT_DIMENSION_WEIGHTS,
            "normalization": {
                "positive": "Score=(Current-Limit)/(Ideal-Limit)×100",
                "negative": "Score=(Limit-Current)/(Limit-Ideal)×100",
            },
        },
    }


def run_part_algorithm(metric_meta, metric_points):
    latest_status = []
    missing_items = []
    metric_rows = []
    failed_metrics = []

    for metric_name, points in metric_points.items():
        meta = metric_meta[metric_name]
        sorted_points = sorted(points, key=lambda item: item["created_at"])
        if len(sorted_points) < 2:
            missing_items.append(build_config_warning(metric_name, "测试记录少于2期，无法进行部件精度保持性评价"))
            continue

        baseline_value = meta.get("baseline_value")
        failure_value = meta.get("failure_value") if meta.get("failure_value") is not None else meta.get("upper_limit")
        if baseline_value is None:
            missing_items.append(build_config_warning(metric_name, "缺少出厂标准精度"))
            continue
        if failure_value is None:
            missing_items.append(build_config_warning(metric_name, "缺少失效判定阈值"))
            continue

        values = [item["value"] for item in sorted_points if item["value"] is not None]
        if len(values) < 2:
            missing_items.append(build_config_warning(metric_name, "缺少连续有效测点"))
            continue

        failure_index = None
        for idx, item in enumerate(sorted_points):
            if item["value"] is not None and item["value"] > failure_value:
                failure_index = idx
                break

        first_time = sorted_points[0]["created_at"]
        latest_value = sorted_points[-1]["value"]
        latest_status.append({
            "name": metric_name,
            "criterion": meta.get("criterion") or classify_whole_criterion(
                meta.get("category_level_1"),
                meta.get("category_level_2"),
                metric_name,
            ),
            "value": round(latest_value, 4) if latest_value is not None else None,
            "limit": failure_value,
            "unit": meta.get("unit") or "",
            "status": "danger" if failure_index is not None else "success",
            "score": None,
            "detail_metrics": {},
        })

        if failure_index is not None:
            failure_time = sorted_points[failure_index]["created_at"]
            cycle_hours = meta.get("cycle_hours")
            elapsed_hours = max(0.0, (failure_time - first_time).total_seconds() / 3600.0)
            retention_hours = (
                failure_index * cycle_hours
                if cycle_hours is not None and cycle_hours > 0
                else elapsed_hours
            )
            failed_metrics.append({
                "metric_name": metric_name,
                "failure_record_time": failure_time.isoformat(),
                "retention_time_hours": round(retention_hours, 2),
                "cycles_to_failure": failure_index,
                "cycle_hours": cycle_hours,
                "time_source": "cycle_hours" if cycle_hours is not None and cycle_hours > 0 else "record_timestamp",
                "failure_value": failure_value,
                "latest_value": round(latest_value, 4) if latest_value is not None else None,
                "reason": "该指标已超过设计允许值，规范要求切换为精度保持时间评价",
            })
            metric_rows.append({
                "metric_name": metric_name,
                "evaluation_method": "retention_time",
                "retention_time_hours": round(retention_hours, 2),
                "cycles_to_failure": failure_index,
                "baseline_value": baseline_value,
                "failure_value": failure_value,
                "latest_value": round(latest_value, 4) if latest_value is not None else None,
                "unit": meta.get("unit") or "",
                "score": None,
                "note": "指标已失效，按规范采用精度保持时间评价",
            })
            continue

        span = failure_value - baseline_value
        if span <= 0:
            missing_items.append(build_config_warning(metric_name, "失效精度必须大于出厂标准精度"))
            continue

        mean_delta = sum(abs(value - baseline_value) for value in values) / len(values)
        rho = 1.0 if latest_value <= failure_value else 0.0
        ard_value = rho * math.exp(-(mean_delta / span))
        metric_score = round(ard_value * 100.0, 2)
        metric_rows.append({
            "metric_name": metric_name,
            "evaluation_method": "retention_degree",
            "retention_degree": round(ard_value, 6),
            "baseline_value": baseline_value,
            "failure_value": failure_value,
            "latest_value": round(latest_value, 4) if latest_value is not None else None,
            "unit": meta.get("unit") or "",
            "score": metric_score,
            "note": "固定循环内未失效，按规范采用精度保持度评价",
        })

    if failed_metrics:
        overall_retention = min(item["retention_time_hours"] for item in failed_metrics)
        return {
            "mode": "part",
            "evaluation_method": "retention_time",
            "score": None,
            "retention_time_hours": overall_retention,
            "failed_metrics": failed_metrics,
            "latest_status": latest_status,
            "metric_rows": metric_rows,
            "missing_items": missing_items,
            "missing_count": len(missing_items),
            "coverage_ratio": 1.0 if metric_rows else 0.0,
            "is_complete": len(missing_items) == 0,
            "radar_values": [0, 0, 0, 0, 0],
        }

    valid_scores = [row["score"] for row in metric_rows if row.get("score") is not None]
    overall_score = round(sum(valid_scores) / len(valid_scores), 2) if valid_scores else 0.0
    completeness = round((len(metric_rows) / max(1, len(metric_meta))) * 100.0, 2) if metric_meta else 0.0
    radar_values = [
        overall_score,
        overall_score,
        completeness,
        overall_score,
        overall_score,
    ]
    return {
        "mode": "part",
        "evaluation_method": "retention_degree",
        "score": overall_score,
        "retention_time_hours": None,
        "failed_metrics": [],
        "latest_status": latest_status,
        "metric_rows": metric_rows,
        "missing_items": missing_items,
        "missing_count": len(missing_items),
        "coverage_ratio": round(len(metric_rows) / max(1, len(metric_meta)), 4) if metric_meta else 0.0,
        "is_complete": len(missing_items) == 0 and bool(metric_rows),
        "radar_values": radar_values,
    }


@router.post("/api/evaluate")
def run_evaluation(
    project_id: Optional[int] = Query(None),
    asset_id: int = Query(...),
    mode: str = Query(...),
):
    time.sleep(0.3)
    conn = get_db_connection()
    cursor = conn.cursor(cursor_factory=RealDictCursor)

    try:
        cursor.execute(
            """
            SELECT asset_id, asset_name, asset_type, asset_category, asset_code
            FROM tb_asset
            WHERE asset_id = %s
            """,
            (asset_id,),
        )
        asset_row = cursor.fetchone()
        if not asset_row:
            return {"status": "error", "message": "资产不存在"}

        cursor.execute(
            """
            SELECT
                m.metric_id,
                m.metric_name,
                m.category_level_1,
                m.category_level_2,
                m.value_type,
                b.upper_limit,
                b.lower_limit,
                b.baseline_value,
                b.failure_value,
                b.ideal_value,
                b.extreme_value,
                b.criterion_weight,
                b.indicator_weight,
                b.cycle_hours,
                b.unit
            FROM tb_asset_metric_bind b
            JOIN tb_metric_dict m ON b.metric_id = m.metric_id
            WHERE b.asset_id = %s
            ORDER BY m.metric_id
            """,
            (asset_id,),
        )
        bound_metrics = cursor.fetchall()
        if not bound_metrics:
            return {
                "score": 0,
                "mode": mode,
                "missing_items": [{"metric_name": asset_row["asset_name"], "reason": "该资产尚未绑定任何评估指标"}],
                "missing_count": 1,
                "coverage_ratio": 0.0,
                "is_complete": False,
                "radar_values": [0, 0, 0, 0, 0],
                "timelines": ["暂无数据"],
                "series_data": [{"name": "未配置", "data": [0]}],
                "latest_status": [],
                "criterion_cards": [],
                "metric_rows": [],
            }

        track_sql = """
            SELECT
                r.created_at,
                r.raw_data,
                m.metric_id,
                m.metric_name,
                m.category_level_1,
                m.category_level_2,
                m.value_type,
                b.upper_limit,
                b.lower_limit,
                b.baseline_value,
                b.failure_value,
                b.ideal_value,
                b.extreme_value,
                b.criterion_weight,
                b.indicator_weight,
                b.cycle_hours,
                b.unit
            FROM tb_test_record r
            JOIN tb_metric_dict m ON r.metric_id = m.metric_id
            LEFT JOIN tb_asset_metric_bind b ON r.asset_id = b.asset_id AND r.metric_id = b.metric_id
            WHERE r.asset_id = %s
        """
        params = [asset_id]
        if project_id is not None:
            track_sql += " AND EXISTS (SELECT 1 FROM tb_asset a WHERE a.asset_id = r.asset_id AND a.project_id = %s)"
            params.append(project_id)
        track_sql += " ORDER BY r.created_at ASC, m.metric_id ASC"

        cursor.execute(track_sql, tuple(params))
        track_records = cursor.fetchall()

        cursor.execute(
            """
            SELECT
                pr.product_record_id,
                pr.product_id,
                pr.batch_no,
                pr.created_at,
                p.product_name,
                m.metric_id,
                m.metric_name,
                m.category_level_1,
                m.category_level_2,
                m.value_type,
                c.upper_limit,
                c.lower_limit,
                c.ideal_value,
                c.extreme_value,
                c.unit,
                b.criterion_weight,
                b.indicator_weight,
                sv.value
            FROM tb_product_record pr
            JOIN tb_product_def p ON p.product_id = pr.product_id
            JOIN tb_product_sample_value sv ON sv.record_id = pr.product_record_id
            JOIN tb_metric_dict m ON m.metric_id = sv.metric_id
            LEFT JOIN tb_product_metric_config c ON c.product_id = pr.product_id AND c.metric_id = sv.metric_id
            LEFT JOIN tb_asset_metric_bind b ON b.asset_id = pr.asset_id AND b.metric_id = sv.metric_id
            WHERE pr.asset_id = %s
            ORDER BY pr.created_at ASC, pr.product_record_id ASC, sv.sample_id ASC
            """,
            (asset_id,),
        )
        process_rows = cursor.fetchall()

        if not track_records and not process_rows:
            return {
                "score": 0,
                "mode": mode,
                "missing_items": [{"metric_name": asset_row["asset_name"], "reason": "暂无测试记录"}],
                "missing_count": 1,
                "coverage_ratio": 0.0,
                "is_complete": False,
                "radar_values": [0, 0, 0, 0, 0],
                "timelines": ["暂无数据"],
                "series_data": [{"name": "未测定", "data": [0]}],
                "latest_status": [],
                "criterion_cards": [],
                "metric_rows": [],
            }

        timelines = []
        timeline_seen = set()
        metric_meta = {}
        metric_points = defaultdict(list)

        for row in bound_metrics:
            metric_name = row["metric_name"]
            metric_meta.setdefault(metric_name, {
                "metric_id": row["metric_id"],
                "category_level_1": row["category_level_1"],
                "category_level_2": row["category_level_2"],
                "value_type": row["value_type"],
                "criterion": classify_whole_criterion(row["category_level_1"], row["category_level_2"], row["metric_name"]),
                "upper_limit": row["upper_limit"],
                "lower_limit": row["lower_limit"],
                "baseline_value": row["baseline_value"],
                "failure_value": row["failure_value"],
                "ideal_value": row["ideal_value"],
                "extreme_value": row["extreme_value"],
                "criterion_weight": row["criterion_weight"],
                "indicator_weight": row["indicator_weight"],
                "cycle_hours": row["cycle_hours"],
                "unit": row["unit"] or "",
            })

        for row in track_records:
            created_at = parse_datetime(row["created_at"])
            if created_at is None:
                continue

            time_label = created_at.strftime("%m-%d %H:%M")
            if time_label not in timeline_seen:
                timeline_seen.add(time_label)
                timelines.append(time_label)

            metric_name = row["metric_name"]
            metric_meta.setdefault(metric_name, {
                "metric_id": row["metric_id"],
                "category_level_1": row["category_level_1"],
                "category_level_2": row["category_level_2"],
                "value_type": row["value_type"],
                "criterion": classify_whole_criterion(row["category_level_1"], row["category_level_2"], row["metric_name"]),
                "upper_limit": row["upper_limit"],
                "lower_limit": row["lower_limit"],
                "baseline_value": row["baseline_value"],
                "failure_value": row["failure_value"],
                "ideal_value": row["ideal_value"],
                "extreme_value": row["extreme_value"],
                "criterion_weight": row["criterion_weight"],
                "indicator_weight": row["indicator_weight"],
                "cycle_hours": row["cycle_hours"],
                "unit": row["unit"] or "",
            })

            value, samples = parse_raw_data(row["raw_data"])
            if value is None:
                continue

            metric_points[metric_name].append({
                "time": time_label,
                "created_at": created_at,
                "value": round(value, 6),
                "samples": samples,
            })

        grouped_process_rows = group_samples_by_record(process_rows)
        process_meta_lookup = {}

        for row in process_rows:
            created_at = parse_datetime(row["created_at"])
            if created_at is None:
                continue
            time_label = created_at.strftime("%m-%d %H:%M")
            if time_label not in timeline_seen:
                timeline_seen.add(time_label)
                timelines.append(time_label)

            process_meta_lookup.setdefault((row["product_id"], row["metric_id"]), row)
            metric_name = row["metric_name"]
            base_meta = metric_meta.setdefault(metric_name, {
                "metric_id": row["metric_id"],
                "category_level_1": row["category_level_1"],
                "category_level_2": row["category_level_2"],
                "value_type": row["value_type"],
                "criterion": classify_whole_criterion(row["category_level_1"], row["category_level_2"], row["metric_name"]),
                "upper_limit": None,
                "lower_limit": None,
                "baseline_value": None,
                "failure_value": None,
                "ideal_value": None,
                "extreme_value": None,
                "criterion_weight": row["criterion_weight"],
                "indicator_weight": row["indicator_weight"],
                "cycle_hours": None,
                "unit": row["unit"] or "",
            })
            if row["upper_limit"] is not None:
                base_meta["upper_limit"] = row["upper_limit"]
            if row["lower_limit"] is not None:
                base_meta["lower_limit"] = row["lower_limit"]
            if row["ideal_value"] is not None:
                base_meta["ideal_value"] = row["ideal_value"]
            if row["extreme_value"] is not None:
                base_meta["extreme_value"] = row["extreme_value"]
            if row["unit"]:
                base_meta["unit"] = row["unit"]

        for record_info in grouped_process_rows.values():
            created_at = parse_datetime(record_info["created_at"])
            if created_at is None:
                continue

            for metric_id, sample_values in record_info["metrics"].items():
                row = process_meta_lookup.get((record_info["product_id"], metric_id))
                if row is None or not sample_values:
                    continue
                metric_name = row["metric_name"]
                metric_points[metric_name].append({
                    "time": created_at.strftime("%m-%d %H:%M"),
                    "created_at": created_at,
                    "value": round(sum(sample_values) / len(sample_values), 6),
                    "samples": sample_values,
                })

        series_data = build_series_data(metric_points, timelines, metric_meta)
        if mode == "whole":
            result = run_whole_machine_algorithm(asset_row, metric_meta, metric_points)
        else:
            result = run_part_algorithm(metric_meta, metric_points)

        result["asset_name"] = asset_row["asset_name"]
        result["asset_type"] = asset_row["asset_type"]
        result["asset_code"] = asset_row["asset_code"]
        result["timelines"] = timelines or ["暂无数据"]
        result["series_data"] = series_data or [{"name": "未测定", "data": [0]}]
        return result
    except Exception as exc:
        print(f"❌ 综合评价失败: {exc}")
        return {
            "status": "error",
            "message": str(exc),
            "score": 0,
            "mode": mode,
            "missing_items": [{"metric_name": "系统", "reason": "算法执行异常"}],
            "missing_count": 1,
            "coverage_ratio": 0.0,
            "is_complete": False,
            "radar_values": [0, 0, 0, 0, 0],
            "timelines": ["暂无数据"],
            "series_data": [{"name": "未测定", "data": [0]}],
            "latest_status": [],
            "criterion_cards": [],
            "metric_rows": [],
        }
    finally:
        cursor.close()
        conn.close()
