#!/usr/bin/env python3
"""补录缺失的趋势回调 trade_records(策略快照已有,交易记录漏写).

典型原因:gate insert_trade_record 曾不接受 entry_reason,_finalize_plan 写快照后插入失败.

用法:
  python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --dry-run
  python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --apply
"""
from __future__ import annotations

import argparse
import json
import sqlite3
import sys
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from lib.strategy.strategy_snapshot_lib import STRATEGY_TREND  # noqa: E402
from lib.strategy.strategy_trade_labels import ENTRY_REASON_TREND_PULLBACK, MONITOR_TYPE_TREND_PULLBACK  # noqa: E402

STATUS_TO_RESULT = {
    "stopped_sl": "止损",
    "stopped_tp": "止盈",
    "stopped_manual": "手动平仓",
}


def _row_dict(row) -> dict:
    if row is None:
        return {}
    try:
        return dict(row)
    except Exception:
        return {}


def _hold_minutes(hold_seconds: int) -> int:
    try:
        return max(0, int(round(float(hold_seconds) / 60.0)))
    except (TypeError, ValueError):
        return 0


def backfill_one(conn: sqlite3.Connection, snap: dict, *, apply: bool) -> dict:
    plan_id = int(snap.get("source_id") or 0)
    if plan_id <= 0:
        return {"plan_id": plan_id, "skipped": True, "reason": "invalid source_id"}
    exists = conn.execute(
        "SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1", (plan_id,)
    ).fetchone()
    if exists:
        return {"plan_id": plan_id, "skipped": True, "reason": "trade_exists"}

    try:
        payload = json.loads(snap.get("snapshot_json") or "{}")
    except Exception:
        payload = {}

    plan = conn.execute(
        "SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
    ).fetchone()
    plan_d = _row_dict(plan)

    symbol = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol")
    direction = snap.get("direction") or plan_d.get("direction") or payload.get("direction") or "long"
    result = (snap.get("result_label") or "").strip() or STATUS_TO_RESULT.get(
        plan_d.get("status") or "", "手动平仓"
    )
    opened_at = snap.get("opened_at") or plan_d.get("opened_at")
    closed_at = snap.get("closed_at")
    pnl_amount = snap.get("pnl_amount")
    if pnl_amount is None:
        pnl_amount = payload.get("pnl_amount")

    trigger_price = payload.get("avg_entry_price") or plan_d.get("avg_entry_price")
    stop_loss = payload.get("stop_loss") or plan_d.get("stop_loss")
    take_profit = payload.get("take_profit") or plan_d.get("take_profit")
    margin_capital = payload.get("plan_margin_capital") or plan_d.get("plan_margin_capital")
    leverage = payload.get("leverage") or plan_d.get("leverage")

    opened_ms = plan_d.get("opened_at_ms")
    closed_ms = None

    hold_seconds = 0
    if opened_at and closed_at:
        try:
            from datetime import datetime

            fmt = "%Y-%m-%d %H:%M:%S"
            o = datetime.strptime(str(opened_at).strip()[:19], fmt)
            c = datetime.strptime(str(closed_at).strip()[:19], fmt)
            hold_seconds = max(0, int((c - o).total_seconds()))
        except Exception:
            hold_seconds = 0

    row = {
        "symbol": symbol,
        "monitor_type": MONITOR_TYPE_TREND_PULLBACK,
        "direction": direction,
        "trigger_price": trigger_price,
        "stop_loss": stop_loss,
        "initial_stop_loss": plan_d.get("initial_stop_loss") or stop_loss,
        "take_profit": take_profit,
        "margin_capital": margin_capital,
        "leverage": leverage,
        "pnl_amount": pnl_amount,
        "hold_seconds": hold_seconds,
        "trade_style": "trend_pullback",
        "result": result,
        "opened_at": opened_at,
        "opened_at_ms": opened_ms,
        "closed_at": closed_at,
        "closed_at_ms": closed_ms,
        "entry_reason": ENTRY_REASON_TREND_PULLBACK,
        "trend_plan_id": plan_id,
    }

    if not apply:
        return {"plan_id": plan_id, "dry_run": True, "row": row}

    conn.execute(
        """INSERT INTO trade_records (
            symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss,
            take_profit, margin_capital, leverage, pnl_amount, hold_seconds, trade_style,
            hold_minutes, opened_at, opened_at_ms, closed_at, closed_at_ms, result,
            entry_reason, trend_plan_id
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
        (
            row["symbol"],
            row["monitor_type"],
            row["direction"],
            row["trigger_price"],
            row["stop_loss"],
            row["initial_stop_loss"],
            row["take_profit"],
            row["margin_capital"],
            row["leverage"],
            row["pnl_amount"],
            row["hold_seconds"],
            row["trade_style"],
            _hold_minutes(hold_seconds),
            row["opened_at"],
            row["opened_at_ms"],
            row["closed_at"],
            row["closed_at_ms"],
            row["result"],
            row["entry_reason"],
            row["trend_plan_id"],
        ),
    )
    return {"plan_id": plan_id, "inserted": True}


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--db", required=True, help="实例 sqlite 路径")
    ap.add_argument("--apply", action="store_true", help="写入数据库(默认 dry-run)")
    args = ap.parse_args()
    db_path = Path(args.db)
    if not db_path.is_file():
        print(f"数据库不存在: {db_path}")
        return 1
    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row
    snaps = conn.execute(
        """SELECT * FROM strategy_trade_snapshots
           WHERE strategy_type=? ORDER BY id DESC""",
        (STRATEGY_TREND,),
    ).fetchall()
    out = []
    for s in snaps:
        r = backfill_one(conn, _row_dict(s), apply=args.apply)
        out.append(r)
        print(r)
    if args.apply:
        conn.commit()
    conn.close()
    inserted = sum(1 for x in out if x.get("inserted"))
    print(f"done: inserted={inserted} total_snapshots={len(snaps)} apply={args.apply}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
