"""对冲计划开仓/平仓编排(可 dry_run 校验下单路径)."""
from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from typing import Any, Callable, Optional


def _now() -> str:
    return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S")


def _env_bool(key: str, default: bool = False) -> bool:
    raw = (os.getenv(key) or "").strip().lower()
    if not raw:
        return default
    return raw in ("1", "true", "yes", "on")


def open_order_mode() -> str:
    v = (os.getenv("HEDGE_PLAN_OPEN_ORDER") or "options_first").strip().lower()
    return v if v in ("options_first", "perp_first") else "options_first"


def build_po_path_plan(body: dict[str, Any]) -> list[dict[str, Any]]:
    """永期下单路径清单(不交易)."""
    mode = open_order_mode()
    opt = {
        "step": "options_buy_limit",
        "account": "options",
        "inst_id": body.get("opt_inst_id"),
        "sheets": float(body.get("sheets") or 1),
        "side": "buy",
        "price_hint": "ask",
    }
    perp = {
        "step": "perp_market_open",
        "account": "swap",
        "symbol": body.get("exchange_symbol"),
        "direction": body.get("direction") or "long",
        "contracts": float(body.get("contracts") or 0),
        "tp": body.get("tp"),
        "sl": body.get("sl"),
        "attach_tpsl": True,
    }
    return [opt, perp] if mode == "options_first" else [perp, opt]


def build_oo_path_plan(body: dict[str, Any]) -> list[dict[str, Any]]:
    return [
        {
            "step": "options_buy_limit",
            "account": "options",
            "leg": "a",
            "inst_id": (body.get("leg_a") or {}).get("inst_id"),
            "sheets": float((body.get("leg_a") or {}).get("sheets") or 1),
            "side": "buy",
            "price_hint": "ask",
        },
        {
            "step": "options_buy_limit",
            "account": "options",
            "leg": "b",
            "inst_id": (body.get("leg_b") or {}).get("inst_id"),
            "sheets": float((body.get("leg_b") or {}).get("sheets") or 1),
            "side": "buy",
            "price_hint": "ask",
        },
    ]


def _buy_option(
    cfg: dict[str, Any],
    *,
    inst_id: str,
    sheets: float,
    dry_run: bool,
) -> dict[str, Any]:
    from lib.exchange.okx_options_lib import (
        cap_option_buy_sheets_to_ask_depth,
        option_buy_liquidity_ok,
    )

    ex = cfg.get("exchange_options")
    quote_fn = cfg.get("quote_option_contract")
    place_fn = cfg.get("place_option_limit_order")
    td_buy = cfg.get("td_mode_for_option_buy")
    if not inst_id:
        return {"ok": False, "msg": "缺少期权合约"}
    if not callable(quote_fn) or ex is None:
        return {"ok": False, "msg": "期权报价能力未就绪"}
    q = quote_fn(ex, inst_id)
    if not q.get("ok"):
        return {"ok": False, "msg": q.get("msg") or "期权报价失败", "quote": q}
    ask = q.get("ask")
    ask_sz = q.get("ask_sz")
    can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz)
    if not can_open:
        return {
            "ok": False,
            "msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入",
            "quote": q,
            "mark": q.get("mark"),
            "ref_ask": q.get("ref_ask"),
            "can_open": False,
        }
    sheets_i = max(1, int(round(float(sheets))))
    capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets_i, ask_sz, min_sz=1)
    if capped is None:
        return {"ok": False, "msg": cap_msg or "卖一深度不足,无法买入", "quote": q}
    sheets_i = capped
    ct_mult = float(q.get("ct_mult") or 0.01)
    premium = float(ask) * sheets_i * ct_mult
    if dry_run:
        return {
            "ok": True,
            "dry_run": True,
            "inst_id": inst_id,
            "sheets": sheets_i,
            "ask": float(ask),
            "ask_sz": float(ask_sz),
            "premium": premium,
            "ct_mult": ct_mult,
            "tick_sz": q.get("tick_sz"),
            "meta": q.get("meta") or {},
            "strike": q.get("strike"),
            "exp_time": q.get("exp_time"),
            "opt_type": (q.get("meta") or {}).get("optType") or q.get("opt_type"),
            "can_open": True,
        }
    if not callable(place_fn):
        return {"ok": False, "msg": "期权限价下单未注入"}
    td = "isolated"
    if callable(td_buy):
        td = td_buy(cfg.get("options_td_mode") or "isolated")
    order = place_fn(
        ex,
        inst_id=inst_id,
        side="buy",
        sheets=sheets_i,
        price=float(ask),
        td_mode=td,
        tick_sz=q.get("tick_sz"),
    )
    if not order.get("ok"):
        return order
    return {
        "ok": True,
        "inst_id": inst_id,
        "sheets": sheets_i,
        "ask": float(ask),
        "ask_sz": float(ask_sz),
        "premium": premium,
        "ct_mult": ct_mult,
        "tick_sz": q.get("tick_sz"),
        "meta": q.get("meta") or {},
        "strike": q.get("strike"),
        "exp_time": q.get("exp_time"),
        "opt_type": (q.get("meta") or {}).get("optType") or q.get("opt_type"),
        "exchange_ord_id": (order.get("data") or {}).get("ordId"),
        "order": order,
        "can_open": True,
    }


def _open_perp(
    cfg: dict[str, Any],
    *,
    symbol: str,
    direction: str,
    contracts: float,
    leverage: int,
    tp: float,
    sl: float,
    dry_run: bool,
) -> dict[str, Any]:
    if not symbol or contracts <= 0:
        return {"ok": False, "msg": "永续符号或张数无效"}
    amount = float(contracts)
    to_prec = cfg.get("amount_to_precision")
    ex = cfg.get("exchange")
    if callable(to_prec) and ex is not None:
        try:
            amount = float(to_prec(symbol, amount))
        except Exception:
            pass
    if amount <= 0:
        return {"ok": False, "msg": "张数经精度舍入后为 0"}
    if dry_run:
        return {
            "ok": True,
            "dry_run": True,
            "symbol": symbol,
            "direction": direction,
            "contracts": amount,
            "leverage": leverage,
            "tp": tp,
            "sl": sl,
        }
    ensure = cfg.get("ensure_okx_live_ready")
    if callable(ensure):
        ok, msg = ensure()
        if not ok:
            return {"ok": False, "msg": msg or "实盘未就绪"}
    place = cfg.get("place_exchange_order")
    if not callable(place):
        return {"ok": False, "msg": "永续下单函数未注入"}
    try:
        order = place(symbol, direction, amount, leverage, stop_loss=sl, take_profit=tp)
    except Exception as e:
        return {"ok": False, "msg": f"永续开仓失败: {e}"}
    return {
        "ok": True,
        "symbol": symbol,
        "direction": direction,
        "contracts": amount,
        "leverage": leverage,
        "tp": tp,
        "sl": sl,
        "order": order,
        "exchange_ord_id": str((order or {}).get("id") or (order or {}).get("info", {}).get("ordId") or ""),
    }


def _sell_option(
    cfg: dict[str, Any],
    *,
    inst_id: str,
    sheets: float,
    dry_run: bool = False,
) -> dict[str, Any]:
    ex = cfg.get("exchange_options")
    quote_fn = cfg.get("quote_option_contract")
    place_fn = cfg.get("place_option_limit_order")
    if not callable(quote_fn) or ex is None:
        return {"ok": False, "msg": "期权报价能力未就绪"}
    q = quote_fn(ex, inst_id)
    bid = q.get("bid") if q.get("ok") else None
    if bid is None or float(bid) <= 0:
        return {"ok": False, "msg": "暂无买一价,无法平期权"}
    sheets_i = max(1, int(round(float(sheets))))
    if dry_run:
        return {"ok": True, "dry_run": True, "inst_id": inst_id, "sheets": sheets_i, "bid": float(bid)}
    if not callable(place_fn):
        return {"ok": False, "msg": "期权平仓未注入"}
    order = place_fn(
        ex,
        inst_id=inst_id,
        side="sell",
        sheets=sheets_i,
        price=float(bid),
        td_mode="isolated",
        tick_sz=q.get("tick_sz"),
        reduce_only=True,
    )
    return order if order.get("ok") else order


def execute_perp_options_start(
    cfg: dict[str, Any],
    body: dict[str, Any],
    *,
    dry_run: bool = False,
    persist: Optional[Callable[..., Any]] = None,
) -> dict[str, Any]:
    path = build_po_path_plan(body)
    results: list[dict[str, Any]] = []
    opt_res: Optional[dict[str, Any]] = None
    perp_res: Optional[dict[str, Any]] = None
    for step in path:
        if step["step"] == "options_buy_limit":
            opt_res = _buy_option(
                cfg,
                inst_id=str(body.get("opt_inst_id") or ""),
                sheets=float(body.get("sheets") or 1),
                dry_run=dry_run,
            )
            results.append({"step": step["step"], **opt_res})
            if not opt_res.get("ok"):
                return {"ok": False, "msg": opt_res.get("msg") or "期权开仓失败", "path": path, "results": results}
        else:
            perp_res = _open_perp(
                cfg,
                symbol=str(body.get("exchange_symbol") or ""),
                direction=str(body.get("direction") or "long"),
                contracts=float(body.get("contracts") or 0),
                leverage=int(body.get("leverage") or 10),
                tp=float(body["tp"]),
                sl=float(body["sl"]),
                dry_run=dry_run,
            )
            results.append({"step": step["step"], **perp_res})
            if not perp_res.get("ok"):
                # 半腿补偿:期权已成 + 配置允许则平期权
                if opt_res and opt_res.get("ok") and not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True):
                    close_r = _sell_option(
                        cfg,
                        inst_id=str(opt_res.get("inst_id") or body.get("opt_inst_id") or ""),
                        sheets=float(opt_res.get("sheets") or body.get("sheets") or 1),
                    )
                    results.append({"step": "options_auto_close_on_perp_fail", **close_r})
                msg = perp_res.get("msg") or "永续开仓失败"
                if not dry_run:
                    try:
                        from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail

                        notify_partial_fail(
                            cfg, plan_type="perp_options", msg=msg, results=results
                        )
                    except Exception:
                        pass
                return {
                    "ok": False,
                    "msg": msg,
                    "path": path,
                    "results": results,
                    "partial": True,
                }

    out = {
        "ok": True,
        "dry_run": dry_run,
        "plan_type": "perp_options",
        "path": path,
        "results": results,
        "option": opt_res,
        "perp": perp_res,
        "opened_at": _now(),
    }
    if persist and not dry_run:
        out["plan_id"] = persist(out, body)
    return out


def execute_options_options_start(
    cfg: dict[str, Any],
    body: dict[str, Any],
    *,
    dry_run: bool = False,
    persist: Optional[Callable[..., Any]] = None,
) -> dict[str, Any]:
    path = build_oo_path_plan(body)
    results: list[dict[str, Any]] = []
    leg_a = body.get("leg_a") or {}
    leg_b = body.get("leg_b") or {}
    a_res = _buy_option(cfg, inst_id=str(leg_a.get("inst_id") or ""), sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run)
    results.append({"step": "options_buy_limit", "leg": "a", **a_res})
    if not a_res.get("ok"):
        return {"ok": False, "msg": a_res.get("msg") or "腿A开仓失败", "path": path, "results": results}
    b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
    results.append({"step": "options_buy_limit", "leg": "b", **b_res})
    if not b_res.get("ok"):
        if not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True):
            close_r = _sell_option(cfg, inst_id=str(a_res.get("inst_id") or ""), sheets=float(a_res.get("sheets") or 1))
            results.append({"step": "options_auto_close_leg_a", **close_r})
        msg = b_res.get("msg") or "腿B开仓失败"
        if not dry_run:
            try:
                from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail

                notify_partial_fail(cfg, plan_type="options_options", msg=msg, results=results)
            except Exception:
                pass
        return {
            "ok": False,
            "msg": msg,
            "path": path,
            "results": results,
            "partial": True,
        }
    out = {
        "ok": True,
        "dry_run": dry_run,
        "plan_type": "options_options",
        "path": path,
        "results": results,
        "leg_a": a_res,
        "leg_b": b_res,
        "opened_at": _now(),
    }
    if persist and not dry_run:
        out["plan_id"] = persist(out, body)
    return out


def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
    pt = (plan_type or "").strip().lower()
    if pt == "perp_options":
        need = ("direction", "entry", "tp", "sl", "contracts", "opt_inst_id", "sheets", "exchange_symbol")
        for k in need:
            if body.get(k) in (None, ""):
                return f"缺少字段: {k}"
        try:
            if float(body["contracts"]) <= 0 or float(body["sheets"]) <= 0:
                return "张数必须大于 0"
            if float(body["tp"]) <= 0 or float(body["sl"]) <= 0:
                return "止盈/止损无效"
        except (TypeError, ValueError):
            return "数值字段无效"
        return None
    if pt == "options_options":
        a = body.get("leg_a") or {}
        b = body.get("leg_b") or {}
        if not a.get("inst_id") or not b.get("inst_id"):
            return "请选用两条期权腿"
        up = body.get("target_price_up")
        down = body.get("target_price_down")
        legacy = body.get("target_price")
        if up in (None, "") and legacy not in (None, ""):
            up = legacy
        if down in (None, "") and legacy not in (None, ""):
            down = legacy
        if up in (None, "") or down in (None, ""):
            return "请填写上破与下破目标价"
        try:
            if float(up) <= float(down):
                return "上破目标价必须大于下破目标价"
        except (TypeError, ValueError):
            return "目标价无效"
        return None
    return "未知计划类型"


def dump_preview(preview: Any) -> str:
    try:
        return json.dumps(preview, ensure_ascii=False)[:8000]
    except Exception:
        return ""
