"""策略交易:Flask 路由注册(顺势加仓 + 趋势回调页).逻辑在 strategy_*_lib."""
from __future__ import annotations

from lib.paths import strategy_templates_dir

import html as html_module
import os
import re
from typing import Any, Optional

from flask import Flask, flash, jsonify, redirect, render_template, request, url_for
from jinja2 import ChoiceLoader, FileSystemLoader

from lib.strategy.strategy_db import init_strategy_tables
from lib.strategy.strategy_roll_lib import BREAKOUT_MODE, FIB_MODES, MARKET_MODE, preview_roll
from lib.strategy.strategy_roll_monitor_lib import (
    cancel_roll_pending_leg,
    count_filled_roll_legs,
    count_pending_roll_legs,
    sync_roll_after_external_close,
)


def _dedupe_strategy_snapshots_on_startup(cfg: dict[str, Any]) -> None:
    """启动时清理历史重复快照(同计划同结果仅保留最新一条)."""
    get_db = cfg.get("get_db")
    if not callable(get_db):
        return
    try:
        from lib.strategy.strategy_snapshot_lib import dedupe_strategy_snapshots

        conn = get_db()
        try:
            removed = dedupe_strategy_snapshots(conn)
            if removed:
                conn.commit()
                print(
                    f"[strategy] deduped {removed} duplicate strategy_trade_snapshots",
                    flush=True,
                )
        finally:
            conn.close()
    except Exception as e:
        print(f"[strategy] snapshot dedupe skipped: {e}", flush=True)


def install_strategy_trading(app: Flask, repo_root: str, app_module: Any = None, **build_kw) -> None:
    """在 app.py 末尾调用(login_required 已定义后).仅注册 POST API;页面由各 app 的 render_main_page 渲染."""
    from lib.strategy.strategy_config import build_strategy_config

    build_kw.pop("render_trend_page", None)
    attach_strategy_templates(app, repo_root)
    cfg = build_strategy_config(app_module, **build_kw)
    register_strategy_trading(app, cfg)
    from lib.strategy.strategy_records_register import register_strategy_records

    register_strategy_records(app, cfg)
    app.extensions["strategy_roll_cfg"] = cfg
    _dedupe_strategy_snapshots_on_startup(cfg)


def attach_strategy_templates(app: Flask, repo_root: str) -> None:
    strat_dir = strategy_templates_dir(repo_root)
    if not os.path.isdir(strat_dir):
        return
    existing = app.jinja_loader
    loaders = [FileSystemLoader(strat_dir)]
    if existing is not None:
        if isinstance(existing, ChoiceLoader):
            loaders = list(existing.loaders) + loaders
        else:
            loaders.insert(0, existing)
    app.jinja_loader = ChoiceLoader(loaders)


def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
    """cfg 由各市面 app 注入回调(仅 API / DB 差异)."""

    login_required = cfg["login_required"]

    def _lr(f):
        return login_required(f)

    @_lr
    @app.route("/strategy/roll/preview", methods=["POST"])
    def strategy_roll_preview():
        data = request.get_json(silent=True) or request.form
        err = _roll_preview_response(cfg, data, json_mode=request.is_json)
        if request.is_json:
            return jsonify(err)
        if err.get("ok"):
            p = err["preview"]
            flash(
                f"预览:约 {p.get('add_amount_display', '-')} 张,"
                f"合并均价 {p.get('avg_entry_after', '-')},"
                f"打到止损约 {p.get('loss_at_sl_usdt', '-')}U"
            )
        else:
            flash(err.get("msg") or "预览失败")
        return redirect(url_for("strategy_trading_page"))

    @_lr
    @app.route("/strategy/roll/execute", methods=["POST"])
    def strategy_roll_execute():
        data = request.form
        try:
            ok, msg = _roll_execute(cfg, data)
        except Exception as e:
            fe = cfg.get("friendly_error")
            msg = fe(e) if callable(fe) else str(e)
            ok = False
        flash(msg)
        return redirect(url_for("strategy_trading_page"))

    @_lr
    @app.route("/strategy/roll/cancel/<int:leg_id>", methods=["POST"])
    def strategy_roll_cancel_leg(leg_id: int):
        conn = cfg["get_db"]()
        try:
            init_strategy_tables(conn)
            ok, msg = cancel_roll_pending_leg(cfg, conn, leg_id)
        finally:
            conn.close()
        if request.is_json:
            return jsonify({"ok": ok, "msg": msg})
        flash(msg)
        return redirect(url_for("strategy_trading_page"))

    @_lr
    @app.route("/strategy/roll/docs")
    def strategy_roll_docs():
        path = _resolve_roll_doc_path()
        if not path:
            flash("滚仓说明文档不存在")
            return redirect(url_for("strategy_trading_page"))
        with open(path, encoding="utf-8") as f:
            raw = f.read()
        return render_template(
            "strategy_roll_docs.html",
            doc_html=_roll_doc_markdown_to_html(raw),
            exchange_display=cfg.get("exchange_display") or "",
        )


def _resolve_roll_doc_path() -> str | None:
    """滚仓说明 md:优先包内,否则仓库根目录(顺势加仓滚仓说明.md)."""
    here = os.path.dirname(os.path.abspath(__file__))
    name = "顺势加仓滚仓说明.md"
    for path in (
        os.path.join(here, name),
        os.path.normpath(os.path.join(here, "..", "..", name)),
    ):
        if os.path.isfile(path):
            return path
    return None


def _roll_doc_markdown_to_html(text: str) -> str:
    """轻量 Markdown → HTML(仅供滚仓说明页)."""
    lines = text.splitlines()
    out: list[str] = []
    i = 0
    in_code = False
    code_buf: list[str] = []

    def flush_code() -> None:
        nonlocal code_buf
        if code_buf:
            out.append(
                "<pre><code>"
                + html_module.escape("\n".join(code_buf))
                + "</code></pre>"
            )
            code_buf = []

    def inline_md(s: str) -> str:
        s = html_module.escape(s)
        s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
        s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
        return s

    while i < len(lines):
        line = lines[i]
        if line.strip().startswith("```"):
            if in_code:
                in_code = False
                flush_code()
            else:
                in_code = True
            i += 1
            continue
        if in_code:
            code_buf.append(line)
            i += 1
            continue
        if line.startswith("# "):
            out.append(f"<h1>{inline_md(line[2:].strip())}</h1>")
        elif line.startswith("## "):
            out.append(f"<h2>{inline_md(line[3:].strip())}</h2>")
        elif line.startswith("### "):
            out.append(f"<h3>{inline_md(line[4:].strip())}</h3>")
        elif line.strip() == "---":
            out.append("<hr>")
        elif line.startswith("|") and "|" in line[1:]:
            rows: list[str] = []
            while i < len(lines) and lines[i].startswith("|"):
                rows.append(lines[i])
                i += 1
            if len(rows) >= 2 and re.match(r"^\|[\s\-:|]+\|$", rows[1].strip()):
                out.append("<table>")
                hdr = [c.strip() for c in rows[0].strip("|").split("|")]
                out.append("<tr>" + "".join(f"<th>{inline_md(c)}</th>" for c in hdr) + "</tr>")
                for row in rows[2:]:
                    cells = [c.strip() for c in row.strip("|").split("|")]
                    out.append("<tr>" + "".join(f"<td>{inline_md(c)}</td>" for c in cells) + "</tr>")
                out.append("</table>")
            continue
        elif re.match(r"^[-*]\s+", line):
            out.append("<ul>")
            while i < len(lines) and re.match(r"^[-*]\s+", lines[i]):
                item = re.sub(r"^[-*]\s+", "", lines[i])
                out.append(f"<li>{inline_md(item)}</li>")
                i += 1
            out.append("</ul>")
            continue
        elif line.strip():
            out.append(f"<p>{inline_md(line.strip())}</p>")
        i += 1
    flush_code()
    return "\n".join(out)


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


def _count_active_trends(conn, cfg: dict) -> int:
    fn = cfg.get("count_active_trend_plans")
    if callable(fn):
        return int(fn(conn) or 0)
    try:
        return int(
            conn.execute(
                "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'"
            ).fetchone()[0]
        )
    except Exception:
        return 0


def _risk_from_monitor(mon: dict, cfg: dict) -> tuple[Optional[float], Optional[str]]:
    try:
        rp = float(mon.get("risk_percent") or cfg.get("default_risk_percent", 2))
    except (TypeError, ValueError):
        return None, "监控单风险%无效"
    if rp <= 0:
        return None, "监控单风险%须大于0"
    return rp, None


def _contract_size(cfg: dict, ex_sym: str) -> float:
    get_cs = cfg.get("get_contract_size")
    if callable(get_cs):
        try:
            return float(get_cs(ex_sym) or 1.0)
        except Exception:
            pass
    return 1.0


def _roll_context(cfg: dict, data: dict) -> tuple[Optional[dict], Optional[str]]:
    m = cfg.get("app_module")
    if m is not None:
        try:
            from lib.trade.position_sizing_lib import OPEN_SOURCE_ROLL, assert_open_source_allowed

            mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk"
            ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_ROLL)
            if not ok_src:
                return None, src_msg
        except Exception:
            pass
    get_db = cfg["get_db"]
    symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
    if not symbol:
        return None, "请选择或填写币种"
    direction = (data.get("direction") or "long").strip().lower()
    validate_fn = getattr(m, "validate_trade_policy_open", None) if m is not None else None
    if callable(validate_fn):
        ok_pol, pol_msg = validate_fn(symbol, direction)
        if not ok_pol:
            return None, pol_msg
    ex_sym = cfg["normalize_exchange_symbol"](symbol)
    conn = get_db()
    init_strategy_tables(conn)
    if _count_active_trends(conn, cfg) > 0:
        conn.close()
        return None, "存在运行中的趋势回调计划,请先结束后再滚仓"
    mon = _get_active_monitor(conn, cfg, symbol, direction)
    if not mon:
        conn.close()
        return None, "未找到该币种同向的下单监控持仓,请先在「实盘下单」开仓"
    rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon)
    if pending > 0:
        conn.close()
        return None, "已有监控中的滚仓腿,请等待成交/失效或先删除后再提交"
    conn_cap = get_db()
    try:
        capital = float(cfg["get_trading_capital_usdt"](conn_cap))
    finally:
        conn_cap.close()
    risk_pct, risk_err = _risk_from_monitor(mon, cfg)
    if risk_err:
        conn.close()
        return None, risk_err
    pos = cfg["get_position"](ex_sym, direction)
    qty = float(pos.get("contracts") or 0)
    if qty <= 0:
        conn.close()
        return None, "交易所无该方向持仓,无法滚仓"
    entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0)
    if entry <= 0:
        conn.close()
        return None, "无法获取持仓均价"
    mark_fn = cfg.get("get_mark_price") or cfg.get("get_price")
    mark = mark_fn(symbol) if callable(mark_fn) else cfg["get_price"](symbol)
    ctx = {
        "conn": conn,
        "mon": mon,
        "rg": rg,
        "legs_done": legs_done,
        "symbol": symbol,
        "direction": direction,
        "ex_sym": ex_sym,
        "qty": qty,
        "entry": entry,
        "mark": float(mark) if mark else None,
        "capital": capital,
        "risk_pct": float(risk_pct),
        "tp0": float(mon.get("take_profit") or rg.get("initial_take_profit") or 0),
        "contract_size": _contract_size(cfg, ex_sym),
    }
    return ctx, None


def _parse_roll_form(data: dict, ctx: dict) -> tuple[Optional[dict], Optional[str]]:
    add_mode = (data.get("add_mode") or MARKET_MODE).strip().lower()
    raw_sl = data.get("new_stop_loss") or data.get("sl")
    if raw_sl in (None, ""):
        return None, "请填写新止损价"
    try:
        new_sl = float(raw_sl)
    except (TypeError, ValueError):
        return None, "止损价格式错误"
    if new_sl <= 0:
        return None, "止损价须大于0"
    fib_u = fib_l = bp = None
    try:
        if data.get("fib_upper") not in (None, ""):
            fib_u = float(data.get("fib_upper"))
        if data.get("fib_lower") not in (None, ""):
            fib_l = float(data.get("fib_lower"))
        if data.get("breakthrough_price") not in (None, ""):
            bp = float(data.get("breakthrough_price"))
    except (TypeError, ValueError):
        return None, "价格参数格式错误"

    add_price = ctx.get("mark")
    if add_mode == MARKET_MODE:
        if add_price is None or add_price <= 0:
            return None, "无法获取市价快照"
    elif add_mode in FIB_MODES:
        if fib_u is None or fib_l is None:
            return None, "斐波须填写上沿 H 与下沿 L"
    elif add_mode == BREAKOUT_MODE:
        if bp is None:
            return None, "突破加仓须填写突破价"
        add_price = ctx.get("mark")
    else:
        return None, "加仓方式无效"

    return {
        "add_mode": add_mode,
        "new_stop_loss": new_sl,
        "fib_upper": fib_u,
        "fib_lower": fib_l,
        "breakthrough_price": bp,
        "add_price": add_price,
    }, None


def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
    ctx, err = _roll_context(cfg, data)
    if err:
        return {"ok": False, "msg": err}
    parsed, perr = _parse_roll_form(data, ctx)
    if perr:
        ctx["conn"].close()
        return {"ok": False, "msg": perr}
    conn = ctx["conn"]
    try:
        preview, perr2 = preview_roll(
            direction=ctx["direction"],
            symbol=ctx["symbol"],
            qty_existing=ctx["qty"],
            entry_existing=ctx["entry"],
            initial_take_profit=ctx["tp0"],
            add_mode=parsed["add_mode"],
            new_stop_loss=parsed["new_stop_loss"],
            risk_percent=ctx["risk_pct"],
            capital_base_usdt=ctx["capital"],
            add_price=parsed["add_price"],
            fib_upper=parsed["fib_upper"],
            fib_lower=parsed["fib_lower"],
            breakthrough_price=parsed["breakthrough_price"],
            legs_done=ctx["legs_done"],
            contract_size=ctx["contract_size"],
        )
    finally:
        conn.close()
    if perr2:
        return {"ok": False, "msg": perr2}
    amt_raw = float(preview["add_amount_raw"])
    amt_p = cfg["amount_to_precision"](ctx["ex_sym"], amt_raw)
    preview["add_amount_display"] = amt_p if amt_p is not None else amt_raw
    preview["risk_display"] = f"{ctx['risk_pct']:g}%≈{ctx['capital'] * ctx['risk_pct'] / 100:.2f}U"
    price_fmt = cfg.get("price_fmt")
    if callable(price_fmt):
        preview["add_price_display"] = price_fmt(ctx["symbol"], preview["add_price"])
        preview["new_sl_display"] = price_fmt(ctx["symbol"], preview["new_stop_loss"])
        preview["tp_display"] = price_fmt(ctx["symbol"], preview["initial_take_profit"])
    return {"ok": True, "preview": preview}


def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]:
    get_db = cfg["get_db"]
    conn = None
    try:
        ok_live, reason = cfg["ensure_live_ready"]()
        if not ok_live:
            return False, reason or "实盘未就绪"
        prev = _roll_preview_response(cfg, data)
        if not prev.get("ok"):
            return False, prev.get("msg") or "预览失败"
        preview = prev["preview"]
        symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
        direction = preview["direction"]
        ex_sym = cfg["normalize_exchange_symbol"](symbol)
        add_mode = preview["add_mode"]
        new_sl = float(preview["new_stop_loss"])
        tp0 = float(preview["initial_take_profit"])
        lev_fn = cfg.get("default_leverage")
        if not callable(lev_fn):
            lev_fn = lambda _s: 5
        leverage = int(data.get("leverage") or 0) or int(lev_fn(symbol))
        conn = get_db()
        init_strategy_tables(conn)
        mon = _get_active_monitor(conn, cfg, symbol, direction)
        if not mon:
            return False, "监控单已不存在"
        rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon)
        if pending > 0:
            return False, "已有监控中的滚仓腿,请先删除或等待结束"
        if add_mode == MARKET_MODE:
            amount = cfg["amount_to_precision"](ex_sym, float(preview["add_amount_raw"]))
            if amount is None or amount <= 0:
                return False, "加仓张数低于交易所最小精度"
            order = cfg["market_add"](ex_sym, direction, amount, leverage)
            fill = float(
                cfg.get("resolve_fill_price", lambda o, s, p: p)(
                    order, ex_sym, preview["add_price"]
                )
                or preview["add_price"]
            )
            oid = str(order.get("id") or "") if isinstance(order, dict) else ""
            cfg["replace_tpsl"](ex_sym, direction, new_sl, tp0, mon)
            conn.execute(
                """INSERT INTO roll_legs (
                    roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price,
                    breakthrough_price, fill_price, amount, new_stop_loss, exchange_order_id,
                    status, created_at
                ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    rg["id"],
                    legs_done + 1,
                    preview["add_mode_label"],
                    preview.get("fib_upper"),
                    preview.get("fib_lower"),
                    None,
                    preview.get("breakthrough_price"),
                    fill,
                    amount,
                    new_sl,
                    oid,
                    "filled",
                    cfg["app_now_str"](),
                ),
            )
            conn.execute(
                "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?",
                (legs_done + 1, new_sl, cfg["app_now_str"](), rg["id"]),
            )
            live_qty = float(mon.get("order_amount") or 0) + float(amount)
            try:
                from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty

                pos2 = cfg["get_position"](ex_sym, direction) or {}
                q2 = normalize_contracts_qty(pos2.get("contracts") or 0)
                if contracts_qty_is_open(q2):
                    live_qty = q2
                else:
                    live_qty = normalize_contracts_qty(live_qty)
            except Exception:
                from lib.hub.hub_position_metrics import normalize_contracts_qty

                live_qty = normalize_contracts_qty(live_qty)
            conn.execute(
                """UPDATE order_monitors SET stop_loss=?, order_amount=?,
                   breakeven_armed=0, breakeven_price=NULL WHERE id=?""",
                (new_sl, live_qty, mon["id"]),
            )
            conn.commit()
            _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new)
            return True, f"市价加仓第 {legs_done + 1} 腿已成交,止损已更新,止盈仍为首仓"
        # 程序监控:斐波 / 突破
        limit_px = None
        if add_mode in FIB_MODES:
            px_fn = cfg.get("price_to_precision")
            limit_px = float(preview["add_price"])
            if callable(px_fn):
                limit_px = float(px_fn(ex_sym, limit_px) or limit_px)
        mark_fn = cfg.get("get_mark_price") or cfg.get("get_price")
        last_mark = mark_fn(symbol) if callable(mark_fn) else preview["add_price"]
        conn.execute(
            """INSERT INTO roll_legs (
                roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price,
                breakthrough_price, new_stop_loss, last_mark_price, status, created_at
            ) VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
            (
                rg["id"],
                legs_done + 1,
                preview["add_mode_label"],
                preview.get("fib_upper"),
                preview.get("fib_lower"),
                limit_px,
                preview.get("breakthrough_price"),
                new_sl,
                last_mark,
                "pending",
                cfg["app_now_str"](),
            ),
        )
        conn.commit()
        _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new)
        return True, f"已提交{preview['add_mode_label']}监控,触价后将市价加仓并更新止损"
    except Exception as e:
        fe = cfg.get("friendly_error")
        return False, fe(e) if callable(fe) else str(e)
    finally:
        if conn is not None:
            try:
                conn.close()
            except Exception:
                pass


def _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, *, roll_is_new: bool) -> None:
    if not roll_is_new:
        return
    try:
        from lib.strategy.strategy_wechat_notify import notify_roll_group_started

        notify_roll_group_started(
            cfg,
            group_id=int(rg["id"]),
            symbol=symbol,
            direction=direction,
            order_monitor_id=int(mon["id"]),
            initial_take_profit=tp0,
            initial_stop_loss=float(mon.get("stop_loss") or new_sl),
        )
    except Exception:
        pass


def _get_active_monitor(conn, cfg: dict, symbol: str, direction: str) -> Optional[dict]:
    row = conn.execute(
        "SELECT * FROM order_monitors WHERE status='active' AND symbol=? AND direction=? ORDER BY id DESC LIMIT 1",
        (symbol, direction),
    ).fetchone()
    return _row_to_dict(row) if row else None


def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int, int, bool]:
    """返回 (roll_group, filled_legs, pending_legs, is_new_group)."""
    row = conn.execute(
        "SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active' ORDER BY id DESC LIMIT 1",
        (mon["id"],),
    ).fetchone()
    if row:
        d = _row_to_dict(row)
        gid = int(d["id"])
        filled = count_filled_roll_legs(conn, gid)
        pending = count_pending_roll_legs(conn, gid)
        return d, filled, pending, False
    now = mon.get("created_at") or ""
    cur = conn.execute(
        """INSERT INTO roll_groups (
            order_monitor_id, symbol, exchange_symbol, direction,
            initial_take_profit, initial_stop_loss, current_stop_loss,
            risk_percent, leg_count, status, created_at, updated_at
        ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
        (
            mon["id"],
            mon["symbol"],
            mon.get("exchange_symbol"),
            mon["direction"],
            mon.get("take_profit"),
            mon.get("stop_loss"),
            mon.get("stop_loss"),
            mon.get("risk_percent") or 2,
            0,
            "active",
            now,
            now,
        ),
    )
    gid = int(cur.lastrowid)
    return (
        {
            "id": gid,
            "leg_count": 0,
            "initial_take_profit": mon.get("take_profit"),
            "initial_stop_loss": mon.get("stop_loss"),
            "symbol": mon.get("symbol"),
            "direction": mon.get("direction"),
        },
        0,
        0,
        True,
    )


def roll_sync_after_external_close(cfg: dict, conn, symbol: str, direction: str) -> dict:
    """供 hub / del_order 调用的滚仓同步入口."""
    return sync_roll_after_external_close(
        cfg, conn, symbol, direction, reason="手动平仓,滚仓监控已结束"
    )

