"""永期「以期权为主」:定仓、方向映射、目标位与净利口径(纯函数为主)."""
from __future__ import annotations

import math
import os
from typing import Any, Optional

PREMIUM_EXEC_FACTOR = 0.95
DEFAULT_MIN_HOURS = 36.0
DEFAULT_STRIKE_INTERVAL = 15.0
DEFAULT_PERP_LEVERAGE = 100
DEFAULT_OPT_LEVERAGE_ITM_ATM = 100.0
DEFAULT_OPT_LEVERAGE_OTM = 200.0
DEFAULT_RATIO_ITM_ATM = 2.0
DEFAULT_RATIO_OTM = 4.0
OTM_LEV_FLOOR = 180.0


def _sf(v: Any) -> Optional[float]:
    if v is None or v == "":
        return None
    try:
        return float(v)
    except (TypeError, ValueError):
        return None


def is_option_primary(body_or_plan: dict[str, Any] | None) -> bool:
    if not body_or_plan:
        return False
    v = body_or_plan.get("option_primary")
    if v in (True, 1, "1", "true", "yes", "on"):
        return True
    try:
        return int(v or 0) == 1
    except (TypeError, ValueError):
        return False


def fee_rate() -> float:
    try:
        return max(0.0, float(os.getenv("HEDGE_PLAN_FEE_RATE") or os.getenv("OKX_TAKER_FEE") or "0.0005"))
    except (TypeError, ValueError):
        return 0.0005


def floor2(v: float) -> float:
    """ETH 数量向下取两位小数."""
    if v <= 0:
        return 0.0
    return math.floor(float(v) * 100.0 + 1e-12) / 100.0


def opt_type_for_view(direction: str) -> str:
    """看法做多→Call,做空→Put."""
    return "P" if str(direction or "").strip().lower() == "short" else "C"


def perp_direction_for_view(direction: str) -> str:
    """看法做多→永续空,做空→永续多."""
    return "long" if str(direction or "").strip().lower() == "short" else "short"


def default_opt_leverage(moneyness: str) -> float:
    m = (moneyness or "").strip().lower()
    return DEFAULT_OPT_LEVERAGE_OTM if m == "otm" else DEFAULT_OPT_LEVERAGE_ITM_ATM


def default_ratio(moneyness: str) -> float:
    m = (moneyness or "").strip().lower()
    return DEFAULT_RATIO_OTM if m == "otm" else DEFAULT_RATIO_ITM_ATM


def effective_min_opt_leverage(moneyness: str, configured: Any) -> float:
    cfg = _sf(configured)
    base = cfg if cfg is not None and cfg > 0 else default_opt_leverage(moneyness)
    if (moneyness or "").strip().lower() == "otm":
        return max(base, OTM_LEV_FLOOR)
    return base


def hours_to_expiry_from_ms(exp_ms: Any, *, now_ms: Optional[float] = None) -> Optional[float]:
    exp = _sf(exp_ms)
    if exp is None or exp <= 0:
        return None
    # OKX exp 多为毫秒
    if exp < 1e12:
        exp *= 1000.0
    now = now_ms if now_ms is not None else __import__("time").time() * 1000.0
    return (exp - now) / 3600000.0


def target_hit(*, view_side: str, index_px: float, strike: float, points: float) -> bool:
    """相对 K 的点数目标:做多 index≥K+N;做空 index≤K−N.点数须 >0."""
    n = float(points or 0)
    k = float(strike)
    s = float(index_px)
    if n <= 0 or k <= 0 or s <= 0:
        return False
    side = str(view_side or "").strip().lower()
    if side == "short":
        return s <= (k - n)
    return s >= (k + n)


def option_bid_liquidity_ok(bid: Any, bid_sz: Any, *, need_sheets: float = 0) -> tuple[bool, str]:
    b = _sf(bid)
    if b is None or b <= 0:
        return False, "暂无买一报价,无法平期权"
    sz = _sf(bid_sz)
    if sz is not None and sz <= 0:
        return False, "买一深度为 0,无法平期权"
    need = float(need_sheets or 0)
    if need > 0 and sz is not None and sz + 1e-12 < need:
        return False, f"买一深度不足(需 {need:g} 张,买一 {sz:g})"
    return True, ""


def size_from_premium(
    *,
    premium_budget: float,
    ask: float,
    ct_mult: float,
    ratio: float,
    contract_size: float,
    exec_factor: float = PREMIUM_EXEC_FACTOR,
) -> dict[str, Any]:
    """权利金×0.95 → ETH 两位小数 → 期权张 → 永续跟比例."""
    budget = float(premium_budget or 0)
    a = float(ask or 0)
    ct = float(ct_mult or 0.01)
    r = float(ratio or 0)
    cs = float(contract_size or 0.01)
    usable = budget * float(exec_factor or PREMIUM_EXEC_FACTOR)
    if budget <= 0 or a <= 0 or ct <= 0 or r <= 0 or cs <= 0:
        return {
            "ok": False,
            "msg": "定仓参数无效",
            "usable_premium": round(usable, 4),
            "eth_qty": 0.0,
            "sheets": 0.0,
            "perp_eth": 0.0,
            "contracts": 0.0,
        }
    # ask 为每 1 币权利金;ETH 数量 = usable / ask
    eth_qty = floor2(usable / a)
    if eth_qty <= 0:
        return {
            "ok": False,
            "msg": "权利金不足以买入 0.01 ETH 名义期权",
            "usable_premium": round(usable, 4),
            "eth_qty": 0.0,
            "sheets": 0.0,
            "perp_eth": 0.0,
            "contracts": 0.0,
        }
    sheets = eth_qty / ct
    # 张数向下取整到整数张(OKX 期权常见整张)
    sheets_i = float(math.floor(sheets + 1e-12))
    if sheets_i <= 0:
        return {
            "ok": False,
            "msg": "换算期权张数不足 1 张",
            "usable_premium": round(usable, 4),
            "eth_qty": eth_qty,
            "sheets": 0.0,
            "perp_eth": 0.0,
            "contracts": 0.0,
        }
    # 用整张回写 ETH,保持与下单一致
    eth_qty = round(sheets_i * ct, 2)
    perp_eth = eth_qty / r
    contracts = perp_eth / cs
    premium_est = a * sheets_i * ct
    return {
        "ok": True,
        "msg": "",
        "usable_premium": round(usable, 4),
        "eth_qty": eth_qty,
        "sheets": sheets_i,
        "perp_eth": round(perp_eth, 6),
        "contracts": contracts,
        "premium_est": round(premium_est, 4),
        "ratio": r,
        "exec_factor": float(exec_factor or PREMIUM_EXEC_FACTOR),
    }


def estimate_combo_net_pnl(
    *,
    view_side: str,
    strike: float,
    index_px: float,
    ask_open: float,
    bid: float,
    sheets: float,
    ct_mult: float,
    perp_direction: str,
    perp_entry: float,
    perp_mark: float,
    contracts: float,
    contract_size: float,
    fee: Optional[float] = None,
) -> dict[str, Any]:
    """组合净利(扣费);平仓/卖出手续费按买入费率估算."""
    fr = fee if fee is not None else fee_rate()
    ct = float(ct_mult or 0.01)
    sh = float(sheets or 0)
    a = float(ask_open or 0)
    b = float(bid or 0)
    premium = a * sh * ct
    opt_proceeds = b * sh * ct
    opt_open_fee = premium * fr
    opt_close_fee = opt_proceeds * fr  # 卖出费用按买入费率
    opt_net = opt_proceeds - premium - opt_open_fee - opt_close_fee

    coins = float(contracts or 0) * float(contract_size or 0.01)
    entry = float(perp_entry or 0)
    mark = float(perp_mark or 0)
    pd = str(perp_direction or "").strip().lower()
    if pd == "short":
        perp_gross = (entry - mark) * coins
    else:
        perp_gross = (mark - entry) * coins
    perp_notional_open = abs(entry * coins)
    perp_notional_close = abs(mark * coins)
    perp_open_fee = perp_notional_open * fr
    perp_close_fee = perp_notional_close * fr
    perp_net = perp_gross - perp_open_fee - perp_close_fee
    total = opt_net + perp_net
    return {
        "opt_net": round(opt_net, 4),
        "perp_net": round(perp_net, 4),
        "net": round(total, 4),
        "fee_rate": fr,
        "premium": round(premium, 4),
        "opt_proceeds": round(opt_proceeds, 4),
    }


def validate_option_primary_moneyness(
    *,
    opt_type: str,
    strike: Any,
    index_px: Any,
    ask: Any = None,
    moneyness: str = "atm",
    strike_interval: Any = DEFAULT_STRIKE_INTERVAL,
    min_hours: Any = DEFAULT_MIN_HOURS,
    hours_to_expiry: Any = None,
    min_opt_leverage: Any = None,
) -> Optional[str]:
    from lib.hedge_plan.hedge_plan_moneyness_lib import (
        classify_moneyness,
        is_atm_or_otm,
        is_itm_or_atm,
        normalize_opt_type,
    )

    o = normalize_opt_type(opt_type)
    k = _sf(strike)
    s = _sf(index_px)
    if o not in ("C", "P"):
        return "期权类型无效"
    if k is None or s is None or s <= 0:
        return "行权价或指数无效"
    m_want = (moneyness or "atm").strip().lower()
    m_got = classify_moneyness(opt_type=o, strike=k, index_px=s)
    if m_want == "itm":
        if not is_itm_or_atm(opt_type=o, strike=k, index_px=s):
            return "所选须为实值或平值"
    elif m_want == "atm":
        # 平值:距指数在间隔内即可(不强制 classify==atm)
        pass
    elif m_want == "otm":
        if m_got == "itm":
            return "虚值模式不可选实值"
        if not is_atm_or_otm(opt_type=o, strike=k, index_px=s):
            return "虚值模式须选虚值或平值档"
    else:
        return "期权类型(实/平/虚)无效"

    interval = float(_sf(strike_interval) or DEFAULT_STRIKE_INTERVAL)
    if interval > 0 and abs(k - s) > interval + 1e-9:
        return f"行权价偏离指数 {abs(k - s):.1f} > 间隔 {interval:.0f}"

    min_h = float(_sf(min_hours) or DEFAULT_MIN_HOURS)
    h = _sf(hours_to_expiry)
    if min_h > 0 and h is not None and h < min_h:
        return f"剩余到期约 {h:.1f}h,低于最短 {min_h:.0f}h"

    a = _sf(ask)
    min_lev = effective_min_opt_leverage(m_want if m_want != "atm" else m_got or "atm", min_opt_leverage)
    if min_lev > 0 and a is not None and a > 0:
        lev = s / a
        if lev < min_lev:
            return f"期权杠杆 S/ask≈{lev:.0f} 低于门槛 {min_lev:.0f}"
    return None


def validate_option_primary_watch(body: dict[str, Any]) -> Optional[str]:
    """盯盘启动校验:只要参数,不要求已选具体合约."""
    need = (
        "direction",
        "exchange_symbol",
        "premium_budget",
        "option_target_points",
        "perp_target_points",
        "option_perp_ratio",
        "option_leverage",
    )
    for k in need:
        if body.get(k) in (None, ""):
            return f"缺少字段: {k}"
    try:
        if float(body["premium_budget"]) <= 0:
            return "权利金须大于 0"
        if float(body["option_target_points"]) <= 0 or float(body["perp_target_points"]) <= 0:
            return "目标位点数须大于 0"
        if float(body["option_perp_ratio"]) <= 0:
            return "期权永续比例须大于 0"
        if float(body["option_leverage"]) <= 0:
            return "期权杠杆须大于 0"
        lev_perp = _sf(body.get("leverage"))
        if lev_perp is not None and lev_perp <= 0:
            return "永续杠杆须大于 0"
    except (TypeError, ValueError):
        return "数值字段无效"
    direction = str(body.get("direction") or "").strip().lower()
    if direction not in ("long", "short"):
        return "方向须为 long 或 short"
    moneyness = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
    if moneyness not in ("itm", "atm", "otm"):
        return "期权类型(实/平/虚)无效"
    return None


def validate_option_primary_start(body: dict[str, Any]) -> Optional[str]:
    need = (
        "direction",
        "contracts",
        "opt_inst_id",
        "sheets",
        "exchange_symbol",
        "premium_budget",
        "option_target_points",
        "perp_target_points",
        "option_perp_ratio",
    )
    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["premium_budget"]) <= 0:
            return "权利金须大于 0"
        if float(body["option_target_points"]) <= 0 or float(body["perp_target_points"]) <= 0:
            return "目标位点数须大于 0"
        if float(body["option_perp_ratio"]) <= 0:
            return "期权永续比例须大于 0"
    except (TypeError, ValueError):
        return "数值字段无效"
    direction = str(body.get("direction") or "").strip().lower()
    if direction not in ("long", "short"):
        return "方向须为 long 或 short"
    opt_type = str(body.get("opt_type") or "").strip().upper()
    if not opt_type:
        inst = str(body.get("opt_inst_id") or "")
        if inst.upper().endswith("-P"):
            opt_type = "P"
        elif inst.upper().endswith("-C"):
            opt_type = "C"
    want = opt_type_for_view(direction)
    if opt_type != want:
        return f"以期权为主时做{'多' if direction == 'long' else '空'}须用 {'Call' if want == 'C' else 'Put'}"
    moneyness = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
    from lib.hedge_plan.hedge_plan_moneyness_lib import parse_strike_from_inst

    strike = body.get("strike")
    if strike in (None, ""):
        strike = parse_strike_from_inst(str(body.get("opt_inst_id") or ""))
    index_px = body.get("index_px") or body.get("entry")
    return validate_option_primary_moneyness(
        opt_type=opt_type,
        strike=strike,
        index_px=index_px,
        ask=body.get("ask"),
        moneyness=moneyness,
        strike_interval=body.get("strike_interval", DEFAULT_STRIKE_INTERVAL),
        min_hours=body.get("min_option_hours", DEFAULT_MIN_HOURS),
        hours_to_expiry=body.get("hours_to_expiry"),
        min_opt_leverage=body.get("option_leverage") or body.get("min_opt_leverage"),
    )


def pick_option_primary_candidate(
    chain: dict[str, Any],
    *,
    direction: str,
    moneyness: str = "otm",
    strike_interval: Any = DEFAULT_STRIKE_INTERVAL,
    min_hours: Any = DEFAULT_MIN_HOURS,
    min_opt_leverage: Any = None,
) -> Optional[dict[str, Any]]:
    """从期权链挑最近达标合约(间隔+虚实值+杠杆门)."""
    from lib.hedge_plan.hedge_plan_moneyness_lib import classify_moneyness

    want = opt_type_for_view(direction)
    m_want = (moneyness or "otm").strip().lower()
    interval = float(_sf(strike_interval) or DEFAULT_STRIKE_INTERVAL)
    min_h = float(_sf(min_hours) or DEFAULT_MIN_HOURS)
    try:
        idx = float(chain.get("index_px") or 0)
    except (TypeError, ValueError):
        idx = 0.0
    if idx <= 0:
        return None

    best: Optional[dict[str, Any]] = None
    best_dist: Optional[float] = None
    for exp in chain.get("expiries") or []:
        h = hours_to_expiry_from_ms(exp.get("exp_time"))
        if min_h > 0 and h is not None and h < min_h:
            continue
        for c in exp.get("contracts") or []:
            if str(c.get("opt_type") or "").upper() != want:
                continue
            try:
                k = float(c.get("strike") or 0)
                ask = float(c.get("ask") or 0)
            except (TypeError, ValueError):
                continue
            if k <= 0 or ask <= 0:
                continue
            if interval > 0 and abs(k - idx) > interval + 1e-9:
                continue
            m_got = classify_moneyness(opt_type=want, strike=k, index_px=idx)
            if m_want == "itm" and m_got not in ("itm", "atm"):
                continue
            if m_want == "atm" and m_got != "atm":
                continue
            if m_want == "otm" and m_got == "itm":
                continue
            min_lev = effective_min_opt_leverage(m_want if m_want != "atm" else (m_got or "atm"), min_opt_leverage)
            if min_lev > 0 and idx / ask < min_lev - 1e-9:
                continue
            dist = abs(k - idx)
            if best is None or best_dist is None or dist < best_dist:
                best = {
                    **dict(c),
                    "hours_to_expiry": h,
                    "exp_time": exp.get("exp_time"),
                    "moneyness": m_got,
                    "index_px": idx,
                    "leverage": round(idx / ask, 1),
                }
                best_dist = dist
    return best


def build_option_primary_preview(body: dict[str, Any]) -> dict[str, Any]:
    """情景:期权目标 / 永续目标粗估净利."""
    view = str(body.get("direction") or "long").lower()
    strike = float(body["strike"])
    n = float(body.get("option_target_points") or 0)
    m = float(body.get("perp_target_points") or 0)
    ask = float(body.get("ask") or 0)
    sheets = float(body.get("sheets") or 0)
    ct = float(body.get("ct_mult") or 0.01)
    contracts = float(body.get("contracts") or 0)
    cs = float(body.get("contract_size") or 0.01)
    entry = float(body.get("entry") or body.get("index_px") or 0)
    perp_dir = perp_direction_for_view(view)
    # 粗估到点时期权卖价:按内在价值近似(下限 0)
    def intrinsic(spot: float) -> float:
        o = opt_type_for_view(view)
        if o == "C":
            return max(0.0, spot - strike)
        return max(0.0, strike - spot)

    scenarios = []
    for label, pts, reason in (
        ("期权目标", n, "opt_target_points"),
        ("永续目标", m, "perp_target_points"),
    ):
        spot = strike + pts if view != "short" else strike - pts
        bid_est = max(ask * 0.5, intrinsic(spot) * 0.85)  # 保守估价
        net = estimate_combo_net_pnl(
            view_side=view,
            strike=strike,
            index_px=spot,
            ask_open=ask,
            bid=bid_est,
            sheets=sheets,
            ct_mult=ct,
            perp_direction=perp_dir,
            perp_entry=entry,
            perp_mark=spot,
            contracts=contracts,
            contract_size=cs,
        )
        scenarios.append(
            {
                "label": label,
                "reason": reason,
                "index": spot,
                "perp_pnl": net["perp_net"],
                "options_pnl": net["opt_net"],
                "total": net["net"],
                "note": "扣费净利估价;平仓费按买入费率",
            }
        )
    premium = ask * sheets * ct
    return {
        "plan_type": "perp_options",
        "option_primary": True,
        "summary": {
            "premium_paid": round(premium, 4),
            "usable_premium": round(float(body.get("premium_budget") or 0) * PREMIUM_EXEC_FACTOR, 4),
            "opt_target_total": scenarios[0]["total"] if scenarios else None,
            "perp_target_total": scenarios[1]["total"] if len(scenarios) > 1 else None,
            "perp_direction": perp_dir,
            "opt_type": opt_type_for_view(view),
        },
        "scenarios": scenarios,
    }
