# -*- coding: utf-8 -*-
"""
个股交易方案 (策略) 管理侧: 校验 / 挂载 / 暂停撤下 —— PER_STOCK_STRATEGY_PLAN.md §五/§十
运行侧 (每分钟评估、下单) 见 strategy_runner.py。

挂载前**先过约束校验** (validate): 违反仓位 / 存量 / 上限就挡下、给明确中文原因、**不写库**
—— 落实「理论上不允许违反, 无法操作要在页面提示」。真正下单时的合规由 rule_gate 再兜一道 (双保险)。
校验不写库、不抛异常; 读持仓失败按「暂不能校验」挡下 (不放行未校验的挂载)。
"""
from __future__ import annotations

import json
import logging
from datetime import datetime

from app.core import tradedays as td
from app.repo import pms_repo
from app.services import param_store, portfolio

logger = logging.getLogger("pms.strategy_svc")

TYPES = {"T0", "GRID", "TRAIL"}
AUTONOMY = {"auto", "confirm"}
STATUSES = {"ACTIVE", "PAUSED", "CANCELLED", "DONE"}


def _f(v, d=0.0):
    try:
        return float(v)
    except (TypeError, ValueError):
        return d


def _pos(view, code):
    for x in view["positions"]:
        if x["ts_code"] == code:
            return x
    return None


def validate(cfg: dict, view=None) -> dict:
    """校验一条策略配置。返回 {ok, reasons:[中文原因]}。不写库、不抛异常。"""
    cfg = cfg or {}
    reasons = []
    code = cfg.get("ts_code")
    typ = cfg.get("type")
    autonomy = cfg.get("autonomy") or "auto"
    params = cfg.get("params") or {}
    if not code:
        reasons.append("未指定股票")
    if typ not in TYPES:
        reasons.append(f"未知策略类型 {typ}(仅支持 做T=T0 / 网格=GRID / 跟踪止盈=TRAIL)")
    if autonomy not in AUTONOMY:
        reasons.append(f"自主档 {autonomy} 非法(仅 auto / confirm)")
    if reasons:
        return {"ok": False, "reasons": reasons}

    try:
        view = view or portfolio.positions_view()
    except Exception as e:  # noqa: BLE001 —— 读不到持仓不能放行未校验的挂载
        return {"ok": False, "reasons": [f"读持仓失败, 暂不能校验(不放行): {type(e).__name__}: {e}"]}
    pos = _pos(view, code)
    if not pos or int(pos.get("total_qty") or 0) <= 0:
        return {"ok": False, "reasons": [
            "该股当前没有持仓 —— 交易方案要在已有底仓上做差价(A股 T+1, 当日买入不可当日卖)"]}

    # 同股已有 ACTIVE 策略 → 不重复挂
    try:
        dup = pms_repo.list_strategies(ts_code=code, statuses=["ACTIVE"], limit=5)
    except Exception:  # noqa: BLE001
        dup = []
    if dup:
        reasons.append(f"该股已挂着一条 {dup[0].get('type')} 策略(生效中), 先撤下再挂新的")

    prm = view["params"]
    scale = _f(prm.get("scale"))
    stock_cap = _f(prm.get("stock_cap"))
    mv = _f(pos.get("market_value"))
    avail = int(pos.get("avail_qty") or 0)
    cap_room = max(0.0, stock_cap * scale - mv)   # 这只票离单股上限还差多少钱

    if typ == "T0":
        tr = _f(params.get("t_ratio"))
        if not (0 < tr <= 0.3334):
            reasons.append("做T 的 T 仓比例 t_ratio 必须在 0~1/3 之间(硬上限 1/3)")
        if avail <= 0:
            reasons.append("做T 需要 T+1 可卖的存量股(当前可卖为 0), 无法在存量上做差价")
    elif typ == "GRID":
        lo, hi = _f(params.get("lower")), _f(params.get("upper"))
        mid = _f(params.get("center")) or _f(pos.get("price"))
        step = _f(params.get("step"))
        max_cap = _f(params.get("max_capital"))
        if not (0 < lo < mid < hi):
            reasons.append("网格上下界不成立: 需 0 < 下界 < 中枢 < 上界")
        if step <= 0 and _f(params.get("step_pct")) <= 0:
            reasons.append("网格档距必须 > 0 (绝对档距 step 或百分比档距 step_pct 至少给一个)")
        if max_cap <= 0:
            reasons.append("网格最大投入额必须 > 0")
        elif max_cap > cap_room + 1e-6:
            reasons.append(
                f"网格最大投入 {max_cap:,.0f} 元超过该股离单股上限的余量 {cap_room:,.0f} 元"
                f"(单股上限 {stock_cap:.0%}×规模 {scale:,.0f}, 已占市值 {mv:,.0f})")
    elif typ == "TRAIL":
        gb = _f(params.get("giveback"))
        if not (0 < gb < 1):
            reasons.append("跟踪止盈的回撤比例 giveback 必须在 0~1 之间(如 0.05=从高点回落 5% 就卖)")
        if avail <= 0:
            reasons.append("跟踪止盈触发时要卖出, 但当前 T+1 可卖为 0")

    return {"ok": not reasons, "reasons": reasons}


def attach(cfg: dict, by: str = "user") -> dict:
    """挂载一条策略。先校验, 违反返回 {ok:false, errors}; 通过则写 pms_strategy(ACTIVE)。"""
    v = validate(cfg)
    if not v["ok"]:
        return {"ok": False, "errors": v["reasons"]}
    code = cfg["ts_code"]
    ymd = td.ymd()
    sid = f"STR_{ymd}_{code.replace('.', '')}_{int(datetime.now().timestamp()) % 1000000}"
    pms_repo.insert_strategy(strategy_id=sid, ts_code=code, stype=cfg["type"],
                             autonomy=cfg.get("autonomy") or "auto",
                             params=cfg.get("params") or {}, state={},
                             status="ACTIVE", note=cfg.get("note"))
    return {"ok": True, "strategy_id": sid, "ts_code": code}


def set_status(strategy_id: str, status: str, by: str = "user") -> dict:
    """暂停(PAUSED) / 恢复(ACTIVE) / 撤下(CANCELLED)。"""
    status = (status or "").upper()
    if status not in STATUSES:
        return {"ok": False, "error": f"非法状态 {status}"}
    st = pms_repo.get_strategy(strategy_id)
    if not st:
        return {"ok": False, "error": "策略不存在"}
    pms_repo.update_strategy(strategy_id, status=status)
    return {"ok": True, "strategy_id": strategy_id, "ts_code": st.get("ts_code"), "status": status}


# ================================================================ 买入暂停表 (决策系统风控预警)
# 挂了策略的票, 决策系统的风控卖出只提示、不自动清仓 (强制离场会推翻你特意设的策略);
# 这里把该票策略的**买入这一侧**暂停 —— 不平仓、不动卖出、可页面恢复。
# 用独立运行参数 PMS_STRATEGY_BUYPAUSE (按 ts_code 映射), **不放进策略 state_json**:
# 那份 state 每跳被 strategy_runner 重写, 放进去会被并发的 signal_digest / intraday_exec
# 互相覆盖 (与 portfolio.neg_streak_map 用独立参数、避开读改写竞态同一个道理)。
BUYPAUSE_KEY = "PMS_STRATEGY_BUYPAUSE"


def buypause_map() -> dict:
    """当前被暂停买入的票 {ts_code: {reason, source, at}}。读失败按空 (不误暂停)。"""
    try:
        raw = pms_repo.get_param(BUYPAUSE_KEY)
        return json.loads(raw) if raw else {}
    except Exception:  # noqa: BLE001
        return {}


def _save_buypause(m: dict) -> dict:
    try:
        pms_repo.set_param(BUYPAUSE_KEY, json.dumps(m, ensure_ascii=False), "system")
        return {"ok": True}
    except Exception as e:  # noqa: BLE001 —— 写不进要吭声, 别让"已暂停"变成静默没暂停
        logger.error("[strategy] 买入暂停表写入失败: %s —— 该票策略买入可能仍在跑", e)
        return {"ok": False, "error": f"{type(e).__name__}: {e}"}


def pause_buy(ts_code: str, *, reason: str = "", source: str = "signal") -> list:
    """暂停某票所有 ACTIVE 策略的买入这一侧 (决策系统风控预警触发)。返回受影响的 strategy_id。
    只标记、不平仓、不动卖出; 由页面「恢复买入」(resume_buy) 解除。"""
    if not ts_code:
        return []
    m = buypause_map()
    if ts_code not in m:
        m[ts_code] = {"reason": (str(reason)[:300] if reason else ""), "source": source,
                      "at": str(datetime.now())[:19]}
        _save_buypause(m)
    try:
        return [s.get("strategy_id") for s in
                pms_repo.list_strategies(ts_code=ts_code, statuses=["ACTIVE"], limit=10)]
    except Exception:  # noqa: BLE001
        return []


def resume_buy(strategy_id: str, by: str = "user") -> dict:
    """恢复买入 (按策略解除该票的买入暂停)。只恢复买入, 不影响卖出/平回。"""
    st = pms_repo.get_strategy(strategy_id)
    if not st:
        return {"ok": False, "error": "策略不存在"}
    code = st.get("ts_code")
    m = buypause_map()
    if code in m:
        m.pop(code, None)
        r = _save_buypause(m)
        if not r.get("ok"):
            return {"ok": False, "error": r.get("error")}
    return {"ok": True, "strategy_id": strategy_id, "ts_code": code}
