"""实时监控服务 — MPC 控制循环在后台线程中运行。

控制策略（真实硬件模式）：
  - 每步从数据包文件重新读取最新 seq_len 条记录作为真实传感器历史
  - 用历史末尾温度 y(k) 与上步预测 y(k|k-1) 计算反馈校正量
  - MPC + LSTM 多步滚动预测，最小化二次代价函数，输出最优电流 u(k)
  - 将 u(k) 发送给程控电源（_send_to_power_supply，待硬件实现）
  - 记录 y(k+1|k) 供下步反馈校正
  - 等待采样周期后重复
"""
from __future__ import annotations

import threading
from datetime import datetime
from pathlib import Path
from typing import Any

import numpy as np

from app.database import db_session
from app.ml.mpc_controller import CURRENT_IDX, FEATURE_COLS, MPCController, MPCParams, TARGET_IDX
from app.models import DataFile, DataPackage, DataPackageFile
from app.models.monitor import MonitorDataPoint, MonitorExperiment
from app.models.train_management import SavedModel

# ── 取消事件注册表 ────────────────────────────────────────────────────────────
_cancel_events: dict[int, threading.Event] = {}
_registry_lock = threading.Lock()

# 最大控制步数（安全上限，防止无限运行）
_MAX_STEPS = 600


class MonitorService:
    def __init__(self) -> None:
        self._base_dir = Path(__file__).resolve().parents[2]
        self._models_dir = self._base_dir / 'saved_models'

    # ── 模型 / 数据包下拉列表 ─────────────────────────────────────────────────

    def list_models(self) -> list[dict[str, Any]]:
        with db_session() as session:
            rows = session.query(SavedModel).order_by(SavedModel.created_at.desc()).all()
            return [
                {
                    'id': m.id,
                    'model_name': m.model_name,
                    'package_name': m.package_name,
                    'seq_len': m.params.get('seq_len', 20) if m.params else 20,
                }
                for m in rows
            ]

    def list_packages(self) -> list[dict[str, Any]]:
        with db_session() as session:
            rows = (
                session.query(DataPackage)
                .order_by(DataPackage.created_at.desc())
                .all()
            )
            return [{'id': p.id, 'name': p.name, 'data_count': p.data_count} for p in rows]

    # ── 试验 CRUD ─────────────────────────────────────────────────────────────

    def list_experiments(self) -> list[dict[str, Any]]:
        with db_session() as session:
            rows = (
                session.query(MonitorExperiment)
                .order_by(MonitorExperiment.created_at.desc())
                .all()
            )
            return [self._exp_to_dict(e) for e in rows]

    def get_experiment(self, exp_id: int) -> dict[str, Any]:
        with db_session() as session:
            exp = session.query(MonitorExperiment).filter(MonitorExperiment.id == exp_id).first()
            if not exp:
                raise ValueError('试验不存在')
            return self._exp_to_dict(exp)

    def create_experiment(
        self,
        name: str,
        model_id: int,
        input_csv_path: str,
        output_csv_path: str,
        target_temp: float,
        sampling_interval: float,
        mpc_params: dict,
    ) -> dict[str, Any]:
        with db_session() as session:
            model = session.query(SavedModel).filter(SavedModel.id == model_id).first()
            if not model:
                raise ValueError('模型不存在')

            input_path = Path(input_csv_path)
            if not input_path.exists():
                raise ValueError(f'输入CSV文件不存在: {input_csv_path}')

            exp = MonitorExperiment(
                name=name,
                model_id=model_id,
                model_name=model.model_name,
                package_id=None,
                package_name=None,
                input_csv_path=input_csv_path,
                output_csv_path=output_csv_path,
                target_temp=target_temp,
                sampling_interval=sampling_interval,
                mpc_params=mpc_params,
                status='idle',
                total_steps=0,
            )
            session.add(exp)
            session.commit()
            session.refresh(exp)
            return self._exp_to_dict(exp)

    def delete_experiment(self, exp_id: int) -> None:
        # 先停止正在运行的仿真
        self._cancel_thread(exp_id)
        with db_session() as session:
            exp = session.query(MonitorExperiment).filter(MonitorExperiment.id == exp_id).first()
            if not exp:
                raise ValueError('试验不存在')
            session.query(MonitorDataPoint).filter(
                MonitorDataPoint.experiment_id == exp_id
            ).delete()
            session.delete(exp)
            session.commit()

    # ── 控制操作 ──────────────────────────────────────────────────────────────

    def start_experiment(self, exp_id: int) -> None:
        with db_session() as session:
            exp = session.query(MonitorExperiment).filter(MonitorExperiment.id == exp_id).first()
            if not exp:
                raise ValueError('试验不存在')
            if exp.status == 'running':
                raise ValueError('试验正在运行中')

            exp.status = 'running'
            exp.start_time = datetime.now()
            exp.error_msg = None
            session.commit()

        # 若存在旧线程先取消
        self._cancel_thread(exp_id)

        cancel_event = threading.Event()
        with _registry_lock:
            _cancel_events[exp_id] = cancel_event

        t = threading.Thread(
            target=self._simulation_worker,
            args=(exp_id, cancel_event),
            daemon=True,
            name=f'mpc-sim-{exp_id}',
        )
        t.start()

    def stop_experiment(self, exp_id: int) -> None:
        self._cancel_thread(exp_id)
        with db_session() as session:
            exp = (
                session.query(MonitorExperiment)
                .filter(MonitorExperiment.id == exp_id)
                .first()
            )
            if exp and exp.status == 'running':
                exp.status = 'stopped'
                exp.stop_time = datetime.now()
                session.commit()

    # ── 数据查询 ──────────────────────────────────────────────────────────────

    def get_data_points(self, exp_id: int, from_step: int = 0) -> list[dict[str, Any]]:
        with db_session() as session:
            rows = (
                session.query(MonitorDataPoint)
                .filter(
                    MonitorDataPoint.experiment_id == exp_id,
                    MonitorDataPoint.step_idx >= from_step,
                )
                .order_by(MonitorDataPoint.step_idx)
                .all()
            )
            return [self._point_to_dict(p) for p in rows]

    # ── 控制线程 ───────────────────────────────────────────────────────────────

    def _simulation_worker(self, exp_id: int, cancel_event: threading.Event) -> None:
        """真实 MPC 控制循环。

        每步从数据包文件重新读取最新 seq_len 条记录作为真实传感器历史，
        运行 MPC 优化后将电流指令下发给程控电源，并进行单步预测误差反馈校正。
        """
        try:
            # ── 读取试验配置 ──────────────────────────────────────────────────
            with db_session() as session:
                exp = (
                    session.query(MonitorExperiment)
                    .filter(MonitorExperiment.id == exp_id)
                    .first()
                )
                if not exp:
                    return
                model_id = exp.model_id
                input_csv_path = exp.input_csv_path
                output_csv_path = exp.output_csv_path
                target_temp = float(exp.target_temp)
                sampling_interval = float(exp.sampling_interval or 1.0)
                mpc_params_dict = exp.mpc_params or {}
                start_step = int(exp.total_steps)

            # ── 加载模型文件 ──────────────────────────────────────────────────
            with db_session() as session:
                model_row = (
                    session.query(SavedModel)
                    .filter(SavedModel.id == model_id)
                    .first()
                )
                if not model_row:
                    self._set_error(exp_id, '模型不存在')
                    return
                model_path = self._base_dir / model_row.file_path

            if not model_path.exists():
                self._set_error(exp_id, f'模型文件不存在: {model_path}')
                return

            # ── 构造 MPC 参数 ─────────────────────────────────────────────────
            params = MPCParams(
                P=int(mpc_params_dict.get('P', 20)),
                M=int(mpc_params_dict.get('M', 5)),
                Q=float(mpc_params_dict.get('Q', 10.0)),
                R=float(mpc_params_dict.get('R', 0.1)),
                alpha=float(mpc_params_dict.get('alpha', 0.8)),
                u_min=float(mpc_params_dict.get('u_min', 0.0)),
                u_max=float(mpc_params_dict.get('u_max', 10.0)),
                du_min=float(mpc_params_dict.get('du_min', -2.0)),
                du_max=float(mpc_params_dict.get('du_max', 2.0)),
                y_min=float(mpc_params_dict.get('y_min', -50.0)),
                y_max=float(mpc_params_dict.get('y_max', 200.0)),
                correction_gain=float(mpc_params_dict.get('correction_gain', 0.5)),
            )

            ctrl = MPCController.from_checkpoint(model_path, params)
            seq_len = ctrl.predictor.seq_len

            # ── 初始热身：从输入CSV读取最新记录填满缓冲区 ─────────────────────
            if not input_csv_path:
                self._set_error(exp_id, '试验未配置输入CSV路径')
                return
            init_records = self._load_csv_records_tail(input_csv_path, seq_len + 10)
            if len(init_records) < seq_len:
                self._set_error(
                    exp_id,
                    f'输入CSV记录不足 {seq_len} 条（当前 {len(init_records)} 条），无法初始化',
                )
                return

            for rec in init_records:
                ctrl.update_measurement(rec)

            if not ctrl.ready():
                self._set_error(exp_id, '状态估计器初始化失败')
                return

            # y(k+1|k)：上一步对下一时刻温度的预测，初始设为 None
            y_pred_next: float | None = None

            # ── 主控制循环 ────────────────────────────────────────────────────
            for step in range(start_step, _MAX_STEPS):
                if cancel_event.is_set():
                    break

                # 1. 从输入CSV重新读取最新 seq_len 条真实传感器记录
                fresh_records = self._load_csv_records_tail(input_csv_path, seq_len)
                if len(fresh_records) < seq_len:
                    # 数据还未更新到足够条数，跳过本步等待
                    cancel_event.wait(sampling_interval)
                    continue

                # 2. 用真实数据刷新状态缓冲区（保留 correction_offset 和 u_prev）
                ctrl._estimator._buffer = []
                for rec in fresh_records:
                    ctrl.update_measurement(rec)

                # 3. 获取当前真实温度 y(k)
                state = ctrl._estimator.get_state()     # (seq_len, F)
                y_k = float(state[-1, TARGET_IDX])

                # 4. 反馈校正：误差 = y(k) - y(k|k-1)
                if y_pred_next is not None:
                    error = y_k - y_pred_next
                    ctrl._correction_offset += ctrl.p.correction_gain * error

                # 5. MPC 优化求 u(k)
                #    y_actual=None：校正已在步骤 4 手动完成，避免 ctrl.step() 内部重复
                try:
                    u_cmd = ctrl.step(yr=target_temp, y_actual=None)
                except Exception:
                    u_cmd = ctrl._u_prev

                # 6. 预测 y(k+1|k)，供下一步反馈校正使用
                #    将 u(k) 放入序列末尾，向前滚动一步预测
                new_row = state[-1].copy()
                new_row[CURRENT_IDX] = u_cmd
                seq_with_u = np.vstack([state[1:], new_row])   # (seq_len, F)
                y_pred_next = (
                    ctrl.predictor.predict_one(ctrl.predictor._norm(seq_with_u))
                    + ctrl._correction_offset
                )

                # 7. 参考轨迹第一步（用于前端展示）
                w_k = float(ctrl._reference_trajectory(y_k, target_temp)[0])

                # 8. 将电流指令发送给程控电源
                self._send_to_power_supply(u_cmd, exp_id)

                # 9. 持久化本步数据
                with db_session() as session:
                    session.add(
                        MonitorDataPoint(
                            experiment_id=exp_id,
                            step_idx=step,
                            actual_temp=round(y_k, 4),
                            reference_temp=round(w_k, 4),
                            current_output=round(u_cmd, 4),
                        )
                    )
                    session.query(MonitorExperiment).filter(
                        MonitorExperiment.id == exp_id
                    ).update({'total_steps': step + 1})
                    session.commit()

                # 10. 写入输出 CSV（每步追加一行）
                if output_csv_path:
                    self._append_output_row(output_csv_path, step, y_k, u_cmd, w_k, target_temp)

                # 11. 等待采样周期（可被 stop 信号提前中断）
                cancel_event.wait(sampling_interval)

            # ── 正常结束（达到最大步数） ──────────────────────────────────────
            with db_session() as session:
                exp_row = (
                    session.query(MonitorExperiment)
                    .filter(
                        MonitorExperiment.id == exp_id,
                        MonitorExperiment.status == 'running',
                    )
                    .first()
                )
                if exp_row:
                    exp_row.status = 'stopped'
                    exp_row.stop_time = datetime.now()
                    session.commit()

        except Exception as exc:
            self._set_error(exp_id, str(exc))
        finally:
            with _registry_lock:
                _cancel_events.pop(exp_id, None)

    # ── 工具方法 ──────────────────────────────────────────────────────────────

    def _cancel_thread(self, exp_id: int) -> None:
        with _registry_lock:
            event = _cancel_events.get(exp_id)
            if event:
                event.set()

    def _set_error(self, exp_id: int, msg: str) -> None:
        try:
            with db_session() as session:
                session.query(MonitorExperiment).filter(
                    MonitorExperiment.id == exp_id
                ).update(
                    {
                        'status': 'stopped',
                        'error_msg': msg,
                        'stop_time': datetime.now(),
                    }
                )
                session.commit()
        finally:
            with _registry_lock:
                _cancel_events.pop(exp_id, None)

    def _load_package_records(self, package_id: int) -> list[dict[str, Any]]:
        """从数据包加载所有记录。

        若数据包已保存合并 CSV（stored_name 非空），则直接读取该文件；
        否则回退为逐源文件读取合并。
        """
        from app.services.data_management_service import DataManagementService

        dm = DataManagementService()

        with db_session() as session:
            pkg = session.query(DataPackage).filter(DataPackage.id == package_id).first()
            if not pkg:
                return []
            pkg_stored_name: str | None = pkg.stored_name

            # 同时准备源文件信息（fallback 用）
            pf_rows = (
                session.query(DataPackageFile)
                .filter(DataPackageFile.package_id == package_id)
                .order_by(DataPackageFile.sort_order.asc())
                .all()
            )
            file_ids = [pf.file_id for pf in pf_rows]
            files = session.query(DataFile).filter(DataFile.id.in_(file_ids)).all() if file_ids else []
            file_map = {f.id: f for f in files}

        def _filter_valid(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
            return [
                r for r in records
                if r.get('actual_temperature') is not None
                and float(r['actual_temperature']) < 9000
            ]

        # 优先读取合并 CSV 文件
        if pkg_stored_name:
            pkg_file = dm._upload_dir / 'packages' / pkg_stored_name
            if pkg_file.exists():
                records, _ = dm._read_records(pkg_file, limit=None)
                return _filter_valid(records)

        # Fallback：逐源文件读取
        if not file_ids:
            return []
        all_records: list[dict[str, Any]] = []
        for fid in file_ids:
            if fid not in file_map:
                continue
            fmeta = file_map[fid]
            try:
                path = dm._resolve_local_file_path(fmeta.file_path, fmeta.stored_name)
                records, _ = dm._read_records(path, limit=None)
                all_records.extend(records)
            except Exception:
                continue
        return _filter_valid(all_records)

    def _load_package_records_tail(self, package_id: int, n: int) -> list[dict[str, Any]]:
        """从数据包加载最后 n 条有效记录（按文件顺序拼接后取尾部）。

        每次控制步调用，始终读取文件最新内容，模拟传感器实时写入场景。
        无效行（actual_temperature 为 None 或 ≥ 9000）已在 _load_package_records 过滤。
        """
        all_records = self._load_package_records(package_id)
        return all_records[-n:] if len(all_records) >= n else all_records

    def _send_to_power_supply(self, u_cmd: float, exp_id: int) -> None:
        """将电流指令发送到程控电源。

        TODO: 根据实际硬件接口实现，例如：
            - VISA / GPIB（仪器总线）
            - Modbus TCP/RTU
            - RS-232/RS-485 串口协议
        当前为占位实现，仅写入日志。
        """
        import logging
        logging.getLogger(__name__).info(
            '[exp=%d] 程控电源指令 → %.4f A', exp_id, u_cmd
        )

    def _load_csv_records_tail(self, csv_path: str, n: int) -> list[dict[str, Any]]:
        """从 CSV 文件加载最后 n 条有效记录，模拟传感器实时写入场景。"""
        from app.services.data_management_service import DataManagementService
        dm = DataManagementService()
        path = Path(csv_path)
        if not path.exists():
            return []
        records, _ = dm._read_records(path, limit=None)
        valid = [
            r for r in records
            if r.get('actual_temperature') is not None
            and float(r.get('actual_temperature', 9999)) < 9000
        ]
        return valid[-n:] if len(valid) >= n else valid

    @staticmethod
    def _append_output_row(
        csv_path: str,
        step: int,
        actual_temp: float,
        current_output: float,
        reference_temp: float,
        target_temp: float,
    ) -> None:
        """将每步控制结果追加写入输出 CSV 文件。"""
        import csv as csv_module
        path = Path(csv_path)
        path.parent.mkdir(parents=True, exist_ok=True)
        write_header = not path.exists() or path.stat().st_size == 0
        with path.open('a', newline='', encoding='utf-8-sig') as f:
            writer = csv_module.writer(f)
            if write_header:
                writer.writerow(['step', 'actual_temp', 'current_output', 'reference_temp', 'target_temp'])
            writer.writerow([
                step,
                round(actual_temp, 4),
                round(current_output, 4),
                round(reference_temp, 4),
                round(target_temp, 4),
            ])

    @staticmethod
    def _exp_to_dict(exp: MonitorExperiment) -> dict[str, Any]:
        return {
            'id': exp.id,
            'name': exp.name,
            'model_id': exp.model_id,
            'model_name': exp.model_name,
            'input_csv_path': exp.input_csv_path,
            'output_csv_path': exp.output_csv_path,
            'target_temp': exp.target_temp,
            'sampling_interval': exp.sampling_interval,
            'mpc_params': exp.mpc_params,
            'status': exp.status,
            'total_steps': exp.total_steps,
            'start_time': (
                exp.start_time.strftime('%Y-%m-%d %H:%M:%S')
                if exp.start_time and not isinstance(exp.start_time, str)
                else exp.start_time
            ),
            'stop_time': (
                exp.stop_time.strftime('%Y-%m-%d %H:%M:%S')
                if exp.stop_time and not isinstance(exp.stop_time, str)
                else exp.stop_time
            ),
            'error_msg': exp.error_msg,
            'exported': bool(exp.exported),
            'created_at': (
                exp.created_at.strftime('%Y-%m-%d %H:%M:%S')
                if exp.created_at and not isinstance(exp.created_at, str)
                else exp.created_at
            ),
        }

    @staticmethod
    def _point_to_dict(p: MonitorDataPoint) -> dict[str, Any]:
        return {
            'step_idx': p.step_idx,
            'actual_temp': p.actual_temp,
            'reference_temp': p.reference_temp,
            'current_output': p.current_output,
        }
