"""
本地训练脚本 — 生成合成数据、训练 MLP、导出 ONNX

运行方式（在 backend/ 目录下）：
    python train_models.py                  # 纯合成数据
    python train_models.py --use-real-data  # 混入生产实绩（10× 权重）

依赖（仅本地训练用，不进 Docker）：
    pip install torch onnx onnxruntime scikit-learn numpy
"""
import sys, json, time, argparse
from pathlib import Path

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

sys.path.insert(0, str(Path(__file__).parent))
from app.services.prediction import (
    AcidSpeedModel, TensionModel, QualityPredictionModel,
    _SAMPLE_FILE, get_sample_stats,
)

PT_DIR = Path(__file__).parent / "app" / "services" / "pt_models"
PT_DIR.mkdir(parents=True, exist_ok=True)

SEED = 2024
N    = 12000
np.random.seed(SEED)
torch.manual_seed(SEED)

TENSION_ZONES = [
    "inlet", "s1_roller", "acid_entry",
    "acid1", "acid2", "acid3",
    "rinse", "leveler", "s2_roller", "outlet",
]

REAL_SAMPLE_WEIGHT = 10   # 每条真实样本复制次数（等效权重）


# ─── 网络结构 ───────────────────────────────────────────────────────────────

class MLP(nn.Module):
    def __init__(self, in_dim: int, out_dim: int, hidden=(128, 64, 32)):
        super().__init__()
        layers: list = []
        prev = in_dim
        for h in hidden:
            layers += [nn.Linear(prev, h), nn.ReLU()]
            prev = h
        layers.append(nn.Linear(prev, out_dim))
        self.net = nn.Sequential(*layers)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)


# ─── 训练通用函数 ───────────────────────────────────────────────────────────

def fit(model: nn.Module, X: np.ndarray, y: np.ndarray,
        epochs=300, lr=1e-3, batch_size=512) -> nn.Module:
    Xt = torch.from_numpy(X)
    yt = torch.from_numpy(y)
    dl = DataLoader(TensorDataset(Xt, yt), batch_size=batch_size, shuffle=True)
    opt   = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
    sched = optim.lr_scheduler.CosineAnnealingLR(opt, epochs)
    loss_fn = nn.MSELoss()

    model.train()
    for ep in range(1, epochs + 1):
        tot = 0.0
        for xb, yb in dl:
            opt.zero_grad()
            loss = loss_fn(model(xb), yb)
            loss.backward()
            opt.step()
            tot += loss.item() * len(xb)
        sched.step()
        if ep % 100 == 0:
            print(f"    ep {ep:3d}/{epochs}  RMSE={((tot/len(Xt))**0.5):.5f}")
    return model


def z_scale(arr: np.ndarray, mean=None, std=None):
    if mean is None:
        mean = arr.mean(axis=0)
        std  = arr.std(axis=0) + 1e-8
    return ((arr - mean) / std).astype(np.float32), mean, std


def export_onnx(model: nn.Module, in_dim: int, path: Path):
    model.eval()
    dummy = torch.zeros(1, in_dim)
    torch.onnx.export(
        model, dummy, str(path),
        input_names=["input"], output_names=["output"],
        dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
        opset_version=17,
    )
    print(f"  → {path.name}  ({path.stat().st_size//1024} KB)")


# ─── 读取生产实绩样本 ────────────────────────────────────────────────────────

def load_real_samples(model_name: str):
    """
    从 production_samples.jsonl 读取指定模型的实绩样本，
    返回 (X_real, y_real) numpy 数组，或 (None, None)。
    """
    if not _SAMPLE_FILE.exists():
        return None, None

    Xs, ys = [], []
    with open(_SAMPLE_FILE) as f:
        for line in f:
            try:
                r = json.loads(line)
                if r.get("model") != model_name:
                    continue
                inp = r.get("inputs")
                if not inp:
                    continue

                if model_name == "acid_speed":
                    spd = r.get("actual_speed")
                    if spd is None: continue
                    Xs.append(inp[:14])
                    ys.append([spd])

                elif model_name == "tension":
                    kn = r.get("actual_kn")
                    if kn is None: continue
                    zone = r.get("zone")
                    if zone not in TENSION_ZONES: continue
                    # 单区段样本：只校准该区段，其他用模型预测填充
                    m = TensionModel(inp[0], inp[1], inp[2], inp[3])
                    res = m.calculate()
                    tensions = [res["zones"][z]["tension_kN"] for z in TENSION_ZONES]
                    tensions[TENSION_ZONES.index(zone)] = kn
                    Xs.append(inp[:4])
                    ys.append(tensions)

                elif model_name == "quality":
                    ag = r.get("actual_grade")
                    if ag is None: continue
                    grade_map = {"A1": 95.0, "A2": 85.0, "B1": 75.0, "B2": 65.0, "C": 50.0}
                    target_pi = grade_map.get(ag, 75.0)
                    Xs.append(inp[:6])
                    ys.append([target_pi, target_pi])  # pi ≈ surface as proxy

            except Exception:
                continue

    if not Xs:
        return None, None

    print(f"  实绩样本: {model_name} = {len(Xs)} 条 (将按 {REAL_SAMPLE_WEIGHT}× 权重混入)")
    return np.array(Xs, np.float32), np.array(ys, np.float32)


def mix_with_real(X_syn: np.ndarray, y_syn: np.ndarray,
                  X_real, y_real) -> tuple:
    """将真实样本重复 REAL_SAMPLE_WEIGHT 次后拼接到合成数据尾部。"""
    if X_real is None or len(X_real) == 0:
        return X_syn, y_syn
    X_r = np.tile(X_real, (REAL_SAMPLE_WEIGHT, 1))
    y_r = np.tile(y_real, (REAL_SAMPLE_WEIGHT, 1))
    return np.concatenate([X_syn, X_r], axis=0), np.concatenate([y_syn, y_r], axis=0)


# ─── 1. 酸洗速度模型 ────────────────────────────────────────────────────────
# 输入(14): thickness, scale_weight, conc×6, temp×6
# 输出(1):  max_speed

def gen_acid_speed(n: int):
    rng = np.random.default_rng(SEED)
    Xs, ys = [], []
    skip = 0
    while len(Xs) < n:
        t    = rng.uniform(0.5, 8.0)
        sw   = rng.uniform(4.0, 18.0)
        conc = rng.uniform(60, 240, 6).tolist()
        temp = rng.uniform(52, 87, 6).tolist()
        tpi  = rng.uniform(88, 97)
        try:
            m   = AcidSpeedModel(thickness=t, width=1000.0, steel_grade="Q235",
                                 acid_conc_list=conc, acid_temp_list=temp,
                                 scale_weight=sw, target_pi=tpi)
            spd = float(m.calculate()["max_speed"])
        except Exception:
            skip += 1
            continue

        steel_factor = rng.choice([0.92, 0.96, 1.00, 1.03, 1.06])
        noise = rng.normal(1.0, 0.06)
        spd_n = float(np.clip(spd * noise * steel_factor, 20, 180))
        Xs.append([t, sw] + conc + temp)
        ys.append([spd_n])

    print(f"  合成样本: acid_speed = {len(Xs)} 条  (skipped {skip})")
    return np.array(Xs, np.float32), np.array(ys, np.float32)


# ─── 2. 张力模型 ────────────────────────────────────────────────────────────
# 输入(4):   thickness, width, yield_strength, tension_coef
# 输出(10):  10 区段张力 kN

def gen_tension(n: int):
    rng = np.random.default_rng(SEED + 1)
    Xs, ys = [], []
    while len(Xs) < n:
        t  = rng.uniform(0.5, 8.0)
        w  = rng.uniform(600, 1600)
        ys_= rng.uniform(150, 600)
        tc = rng.uniform(0.15, 0.35)

        m = TensionModel(thickness=t, width=w, yield_strength=ys_, tension_coef=tc)
        res = m.calculate()
        tensions = [res["zones"][z]["tension_kN"] for z in TENSION_ZONES]
        noise = rng.normal(1.0, 0.04, 10)
        tensions_n = [float(np.clip(v * noise[i], 0.1, 9999)) for i, v in enumerate(tensions)]
        Xs.append([t, w, ys_, tc])
        ys.append(tensions_n)

    print(f"  合成样本: tension = {len(Xs)} 条")
    return np.array(Xs, np.float32), np.array(ys, np.float32)


# ─── 3. 质量预测模型 ─────────────────────────────────────────────────────────
# 输入(6):  thickness, avg_speed, acid_conc_avg, acid_temp_avg, scale_weight, fe_conc_avg
# 输出(2):  pi_score, surface_score

def gen_quality(n: int):
    rng = np.random.default_rng(SEED + 2)
    Xs, ys = [], []
    while len(Xs) < n:
        t    = rng.uniform(0.5, 8.0)
        spd  = rng.uniform(20, 180)
        conc = rng.uniform(60, 240)
        temp = rng.uniform(50, 90)
        sw   = rng.uniform(4, 18)
        fe   = rng.uniform(20, 130)

        m = QualityPredictionModel(thickness=t, avg_speed=spd,
                                   acid_conc_avg=conc, acid_temp_avg=temp,
                                   scale_weight=sw, fe_conc_avg=fe)
        res = m.calculate()
        pi_n  = float(np.clip(res["pi_score"]      * rng.normal(1.0, 0.06), 0, 100))
        suf_n = float(np.clip(res["surface_score"]  * rng.normal(1.0, 0.06), 0, 100))
        Xs.append([t, spd, conc, temp, sw, fe])
        ys.append([pi_n, suf_n])

    print(f"  合成样本: quality = {len(Xs)} 条")
    return np.array(Xs, np.float32), np.array(ys, np.float32)


# ─── 主流程 ─────────────────────────────────────────────────────────────────

def main(use_real_data: bool = False):
    scalers: dict = {}
    t0 = time.time()

    if use_real_data:
        stats = get_sample_stats()
        print(f"\n生产实绩样本统计: {stats}")

    # ── 酸洗速度 ──
    print("\n[1/3] 酸洗速度模型")
    X, y = gen_acid_speed(N)
    if use_real_data:
        X, y = mix_with_real(X, y, *load_real_samples("acid_speed"))
    Xn, Xm, Xs = z_scale(X)
    yn, ym, ys_ = z_scale(y)
    model = MLP(14, 1, hidden=(128, 64, 32))
    print("  训练中...")
    fit(model, Xn, yn, epochs=300)
    export_onnx(model, 14, PT_DIR / "acid_speed.onnx")
    scalers["acid_speed"] = {
        "X_mean": Xm.tolist(), "X_std": Xs.tolist(),
        "y_mean": ym.tolist(), "y_std": ys_.tolist(),
    }

    # ── 张力 ──
    print("\n[2/3] 张力模型")
    X, y = gen_tension(N)
    if use_real_data:
        X, y = mix_with_real(X, y, *load_real_samples("tension"))
    Xn, Xm, Xs = z_scale(X)
    yn, ym, ys_ = z_scale(y)
    model = MLP(4, 10, hidden=(64, 64, 32))
    print("  训练中...")
    fit(model, Xn, yn, epochs=300)
    export_onnx(model, 4, PT_DIR / "tension.onnx")
    scalers["tension"] = {
        "X_mean": Xm.tolist(), "X_std": Xs.tolist(),
        "y_mean": ym.tolist(), "y_std": ys_.tolist(),
        "zone_names": TENSION_ZONES,
    }

    # ── 质量 ──
    print("\n[3/3] 质量预测模型")
    X, y = gen_quality(N)
    if use_real_data:
        X, y = mix_with_real(X, y, *load_real_samples("quality"))
    Xn, Xm, Xs = z_scale(X)
    yn, ym, ys_ = z_scale(y)
    model = MLP(6, 2, hidden=(64, 32))
    print("  训练中...")
    fit(model, Xn, yn, epochs=300)
    export_onnx(model, 6, PT_DIR / "quality.onnx")
    scalers["quality"] = {
        "X_mean": Xm.tolist(), "X_std": Xs.tolist(),
        "y_mean": ym.tolist(), "y_std": ys_.tolist(),
    }

    scaler_path = PT_DIR / "scalers.json"
    with open(scaler_path, "w") as f:
        json.dump(scalers, f, indent=2)
    print(f"\n  scalers → {scaler_path.name}")
    print(f"\n完成  ({time.time()-t0:.1f}s)\n")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--use-real-data", action="store_true",
                        help="将 production_samples.jsonl 中的实绩混入训练集")
    args = parser.parse_args()
    main(use_real_data=args.use_real_data)
