from __future__ import annotations

from typing import Annotated, Literal

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from ..config import get_settings
from ..env_store import (
    binance_keys_configured,
    live_ready,
    mask_secret,
    okx_keys_configured,
    upsert_env_keys,
)
from ..exchange.runtime import (
    load_runtime_settings,
    normalize_exchange_name,
    persist_exchange_choice,
    reload_market_session,
)
from ..models.db import get_db
from ..sim.ledger import Ledger
from ..sim.matcher import Matcher
from .auth import require_user

router = APIRouter(prefix="/api/settings", tags=["settings"])

KEYS = (
    "fee_rate",
    "exit_move_pct",
    "exit_mode",
    "net_profit_target",
    "premium_exit_multiple",
    "rest_seconds",
    "live_order_interval_sec",
    "skip_weekends",
    "one_expiry_per_day",
    "initial_equity",
    "leverage",
    "perp_margin_mode",
    "min_option_hours",
    "min_option_leverage",
    "atm_open_offset_enabled",
    "max_atm_open_offset",
    "fixed_direction_enabled",
    "fixed_perp_side",
    "close_bid_mark_max_pct",
    "perp_qty_eth",
    "option_qty_eth",
    "show_manual_trade_buttons",
    "sizing_mode",
    "risk_leverage_basis",
    "risk_loss_mode",
    "risk_loss_pct",
    "risk_loss_usdt",
    "risk_capital_source",
    "risk_manual_capital_usdt",
    "risk_perp_unit",
    "risk_option_unit",
    "risk_exit_unit",
)


class StrategySettingsBody(BaseModel):
    fee_rate: float | None = Field(default=None, ge=0, le=0.05)
    exit_move_pct: float | None = Field(default=None, ge=0.1, le=50)
    exit_mode: str | None = Field(default=None, pattern="^(fixed_usdt|premium_multiple)$")
    net_profit_target: float | None = Field(default=None, ge=0.1, le=1_000_000)
    premium_exit_multiple: float | None = Field(default=None, ge=0.1, le=100)
    rest_seconds: int | None = Field(default=None, ge=0, le=3600)
    live_order_interval_sec: float | None = Field(default=None, ge=0.2, le=30)
    skip_weekends: bool | None = None
    one_expiry_per_day: bool | None = None
    initial_equity: float | None = Field(default=None, ge=1000, le=10_000_000)
    leverage: float | None = Field(default=None, ge=1, le=125)
    perp_margin_mode: str | None = Field(default=None, pattern="^(cross|isolated)$")
    min_option_hours: float | None = Field(default=None, ge=1, le=720)
    min_option_leverage: float | None = Field(default=None, ge=1, le=10000)
    atm_open_offset_enabled: bool | None = None
    max_atm_open_offset: float | None = Field(default=None, ge=0, le=100)
    fixed_direction_enabled: bool | None = None
    fixed_perp_side: str | None = Field(default=None, pattern="^(long|short)$")
    close_bid_mark_max_pct: float | None = Field(default=None, ge=1, le=100)
    perp_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
    option_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
    show_manual_trade_buttons: bool | None = None
    exchange: str | None = Field(default=None, pattern="^(okx|binance|bn)$")
    sizing_mode: str | None = Field(default=None, pattern="^(manual|risk_based)$")
    risk_leverage_basis: str | None = Field(
        default=None, pattern="^(actual|selection)$"
    )
    risk_loss_mode: str | None = Field(default=None, pattern="^(percent|absolute)$")
    risk_loss_pct: float | None = Field(default=None, ge=0.01, le=100)
    risk_loss_usdt: float | None = Field(default=None, ge=0.1, le=1_000_000)
    risk_capital_source: str | None = Field(
        default=None, pattern="^(trading_account|manual)$"
    )
    risk_manual_capital_usdt: float | None = Field(default=None, ge=1, le=100_000_000)
    risk_perp_unit: float | None = Field(default=None, ge=0.01, le=100)
    risk_option_unit: float | None = Field(default=None, ge=0.01, le=100)
    risk_exit_unit: float | None = Field(default=None, ge=0.1, le=1_000_000)


def _as_bool(raw: str | None, default: bool) -> bool:
    if raw is None or raw == "":
        return default
    return str(raw).strip().lower() in ("1", "true", "yes", "on")


def _risk_preview_safe() -> dict:
    try:
        from ..strategy.risk_sizing import preview_risk_sizing

        return preview_risk_sizing()
    except Exception as e:
        return {"ok": False, "detail": f"预览失败: {e}", "risk_based": False}


def _read_settings() -> dict:
    db = get_db()
    s = get_settings()
    rt = load_runtime_settings()
    mode = str(db.get_setting("exit_mode", s.exit_mode) or s.exit_mode)
    if mode not in ("fixed_usdt", "premium_multiple"):
        mode = "fixed_usdt"
    return {
        "fee_rate": float(db.get_setting("fee_rate", str(s.fee_rate)) or s.fee_rate),
        "exit_move_pct": float(
            db.get_setting("exit_move_pct", str(s.exit_move_pct)) or s.exit_move_pct
        ),
        "exit_mode": mode,
        "net_profit_target": float(
            db.get_setting("net_profit_target", str(s.net_profit_target))
            or s.net_profit_target
        ),
        "premium_exit_multiple": float(
            db.get_setting("premium_exit_multiple", str(s.premium_exit_multiple))
            or s.premium_exit_multiple
        ),
        "rest_seconds": int(
            float(db.get_setting("rest_seconds", str(s.rest_seconds)) or s.rest_seconds)
        ),
        "live_order_interval_sec": float(
            db.get_setting(
                "live_order_interval_sec", str(s.live_order_interval_sec)
            )
            or s.live_order_interval_sec
        ),
        "skip_weekends": _as_bool(
            db.get_setting("skip_weekends", str(s.skip_weekends)), s.skip_weekends
        ),
        "one_expiry_per_day": _as_bool(
            db.get_setting("one_expiry_per_day", str(s.one_expiry_per_day)),
            s.one_expiry_per_day,
        ),
        "initial_equity": float(
            db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
        ),
        "leverage": float(db.get_setting("leverage", str(s.leverage)) or s.leverage),
        "perp_margin_mode": (
            mm
            if (
                mm := str(
                    db.get_setting("perp_margin_mode", s.perp_margin_mode)
                    or s.perp_margin_mode
                    or "cross"
                )
                .strip()
                .lower()
            )
            in ("cross", "isolated")
            else "cross"
        ),
        "min_option_hours": float(
            db.get_setting("min_option_hours", str(s.min_option_hours))
            or s.min_option_hours
        ),
        "min_option_leverage": float(
            db.get_setting("min_option_leverage", str(s.min_option_leverage))
            or s.min_option_leverage
        ),
        "atm_open_offset_enabled": _as_bool(
            db.get_setting(
                "atm_open_offset_enabled", str(s.atm_open_offset_enabled)
            ),
            s.atm_open_offset_enabled,
        ),
        "max_atm_open_offset": float(
            db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset))
            or s.max_atm_open_offset
        ),
        "fixed_direction_enabled": _as_bool(
            db.get_setting(
                "fixed_direction_enabled", str(s.fixed_direction_enabled)
            ),
            s.fixed_direction_enabled,
        ),
        "fixed_perp_side": (
            side
            if (
                side := str(
                    db.get_setting("fixed_perp_side", s.fixed_perp_side)
                    or s.fixed_perp_side
                )
                .strip()
                .lower()
            )
            in ("long", "short")
            else "long"
        ),
        "close_bid_mark_max_pct": float(
            db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
            or s.close_bid_mark_max_pct
        ),
        "perp_qty_eth": float(
            db.get_setting("perp_qty_eth", str(s.perp_qty_eth)) or s.perp_qty_eth
        ),
        "option_qty_eth": float(
            db.get_setting("option_qty_eth", str(s.option_qty_eth)) or s.option_qty_eth
        ),
        "show_manual_trade_buttons": _as_bool(
            db.get_setting("show_manual_trade_buttons", "0"), False
        ),
        "sizing_mode": (
            sm
            if (
                sm := str(db.get_setting("sizing_mode", "manual") or "manual")
                .strip()
                .lower()
            )
            in ("manual", "risk_based")
            else "manual"
        ),
        "risk_leverage_basis": (
            rb
            if (
                rb := str(
                    db.get_setting(
                        "risk_leverage_basis",
                        str(s.risk_leverage_basis),
                    )
                    or s.risk_leverage_basis
                    or "selection"
                )
                .strip()
                .lower()
            )
            in ("actual", "selection")
            else "selection"
        ),
        "risk_loss_mode": (
            lm
            if (
                lm := str(db.get_setting("risk_loss_mode", "percent") or "percent")
                .strip()
                .lower()
            )
            in ("percent", "absolute")
            else "percent"
        ),
        "risk_loss_pct": float(db.get_setting("risk_loss_pct", "1") or 1),
        "risk_loss_usdt": float(db.get_setting("risk_loss_usdt", "15") or 15),
        "risk_capital_source": (
            cs
            if (
                cs := str(
                    db.get_setting("risk_capital_source", "trading_account")
                    or "trading_account"
                )
                .strip()
                .lower()
            )
            in ("trading_account", "manual")
            else "trading_account"
        ),
        "risk_manual_capital_usdt": float(
            db.get_setting("risk_manual_capital_usdt", "10000") or 10000
        ),
        "risk_perp_unit": float(db.get_setting("risk_perp_unit", "1") or 1),
        "risk_option_unit": float(db.get_setting("risk_option_unit", "2") or 2),
        "risk_exit_unit": float(db.get_setting("risk_exit_unit", "15") or 15),
        "risk_sizing_preview": _risk_preview_safe(),
        "exchange": rt.exchange,
        "perp_inst_id": rt.perp_inst_id,
        "option_inst_family": rt.option_inst_family,
        "index_inst_id": rt.index_inst_id,
        "ledger": Ledger(db).snapshot(),
    }


@router.get("/strategy")
async def get_strategy_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
    return _read_settings()


@router.put("/strategy")
async def put_strategy_settings(
    body: StrategySettingsBody,
    _user: Annotated[str, Depends(require_user)],
) -> dict:
    db = get_db()
    s = get_settings()
    data = body.model_dump(exclude_none=True)
    equity_to_apply: float | None = None
    switch_to: str | None = None

    if "exchange" in data:
        new_ex = normalize_exchange_name(str(data.pop("exchange")))
        old_ex = normalize_exchange_name(
            db.get_setting("exchange", s.exchange) or s.exchange
        )
        if new_ex != old_ex:
            if Matcher(db).has_open_position():
                raise HTTPException(
                    status_code=409,
                    detail="有未平仓，无法切换交易所；请先平仓后再改",
                )
            switch_to = new_ex

    if "initial_equity" in data:
        new_eq = float(data["initial_equity"])
        old_eq = float(
            db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
        )
        if abs(new_eq - old_eq) > 1e-9:
            if Matcher(db).has_open_position():
                raise HTTPException(
                    status_code=409,
                    detail="有未平仓，无法重置模拟资金；请先平仓后再改",
                )
            equity_to_apply = new_eq

    if "perp_margin_mode" in data:
        new_mm = str(data["perp_margin_mode"]).strip().lower()
        old_mm = str(
            db.get_setting("perp_margin_mode", s.perp_margin_mode) or s.perp_margin_mode
        ).strip().lower()
        if new_mm != old_mm and Matcher(db).has_open_position():
            raise HTTPException(
                status_code=409,
                detail="有未平仓，无法切换永续保证金模式；请先平仓后再改",
            )

    # 持仓中禁止改动会影响本组成交/出场的参数（基线：运行中无人工开平仓，策略锁定本组成交）
    if Matcher(db).has_open_position():
        locked_keys = (
            "net_profit_target",
            "exit_mode",
            "premium_exit_multiple",
            "perp_qty_eth",
            "option_qty_eth",
            "leverage",
            "sizing_mode",
            "risk_perp_unit",
            "risk_option_unit",
            "risk_exit_unit",
            "risk_loss_mode",
            "risk_loss_pct",
            "risk_loss_usdt",
            "risk_capital_source",
            "risk_manual_capital_usdt",
        )
        hit = [k for k in locked_keys if k in data]
        if hit:
            raise HTTPException(
                status_code=409,
                detail=f"有未平仓，禁止修改本组成交相关参数：{', '.join(hit)}；请先平仓",
            )

    # 以损定仓 ↔ 手动仓位互斥；开启以损定仓时强制 fixed_usdt，并忽略手填名义/出场
    sizing_mode = str(
        data.get(
            "sizing_mode",
            db.get_setting("sizing_mode", "manual") or "manual",
        )
    ).strip().lower()
    if sizing_mode == "risk_based":
        data["exit_mode"] = "fixed_usdt"
        data.pop("perp_qty_eth", None)
        data.pop("option_qty_eth", None)
        data.pop("net_profit_target", None)
        loss_mode = str(
            data.get(
                "risk_loss_mode",
                db.get_setting("risk_loss_mode", "percent") or "percent",
            )
        ).strip().lower()
        if loss_mode == "absolute":
            loss_u = data.get("risk_loss_usdt")
            if loss_u is None:
                loss_u = float(db.get_setting("risk_loss_usdt", "0") or 0)
            if float(loss_u) <= 0:
                raise HTTPException(
                    status_code=400,
                    detail="以损定仓选用亏损值时，须填写 risk_loss_usdt > 0",
                )
        else:
            src = str(
                data.get(
                    "risk_capital_source",
                    db.get_setting("risk_capital_source", "trading_account")
                    or "trading_account",
                )
            ).strip().lower()
            if src == "manual":
                cap = data.get("risk_manual_capital_usdt")
                if cap is None:
                    cap = float(db.get_setting("risk_manual_capital_usdt", "0") or 0)
                if float(cap) <= 0:
                    raise HTTPException(
                        status_code=400,
                        detail="以损定仓选用单独本金时，须填写 risk_manual_capital_usdt > 0",
                    )
            pct = data.get("risk_loss_pct")
            if pct is None:
                pct = float(db.get_setting("risk_loss_pct", "0") or 0)
            if float(pct) <= 0:
                raise HTTPException(
                    status_code=400,
                    detail="以损定仓选用亏损幅度时，须填写 risk_loss_pct > 0",
                )

    for k, v in data.items():
        if k in KEYS:
            db.set_setting(k, str(v))

    if equity_to_apply is not None:
        Ledger(db).reset_equity(
            equity_to_apply,
            note=f"设置模拟资金={equity_to_apply:.2f}",
        )

    if switch_to is not None:
        rt = persist_exchange_choice(switch_to)
        try:
            await reload_market_session(rt)
        except Exception as e:
            raise HTTPException(
                status_code=502,
                detail=f"交易所已切换为 {switch_to}，但行情重连失败: {e}",
            ) from e

    return _read_settings()


class RuntimeSettingsBody(BaseModel):
    mode: Literal["SIM", "LIVE"] | None = None
    confirm_live: bool | None = False
    confirm_live_phrase: str | None = None
    okx_api_key: str | None = None
    okx_api_secret: str | None = None
    okx_api_passphrase: str | None = None
    binance_api_key: str | None = None
    binance_api_secret: str | None = None


def _runtime_payload() -> dict:
    s = get_settings()
    rt = load_runtime_settings()
    mode = "SIM" if s.is_sim else "LIVE"
    ready, reason = live_ready(exchange=rt.exchange)
    return {
        "mode": mode,
        "exchange": rt.exchange,
        "okx_configured": okx_keys_configured(s),
        "binance_configured": binance_keys_configured(s),
        "okx_api_key_masked": mask_secret(s.okx_api_key),
        "okx_api_secret_masked": mask_secret(s.okx_api_secret),
        "okx_api_passphrase_masked": mask_secret(s.okx_api_passphrase),
        "binance_api_key_masked": mask_secret(s.binance_api_key),
        "binance_api_secret_masked": mask_secret(s.binance_api_secret),
        "live_ready": bool(ready) if mode == "LIVE" else True,
        "live_ready_reason": reason if mode == "LIVE" else "sim",
        "sim": s.is_sim,
    }


@router.get("/runtime")
async def get_runtime_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
    return _runtime_payload()


@router.put("/runtime")
async def put_runtime_settings(
    body: RuntimeSettingsBody,
    _user: Annotated[str, Depends(require_user)],
) -> dict:
    db = get_db()
    s = get_settings()
    cur_mode = "SIM" if s.is_sim else "LIVE"
    new_mode = (body.mode or cur_mode).strip().upper()
    if new_mode not in ("SIM", "LIVE"):
        raise HTTPException(status_code=400, detail="mode 须为 SIM 或 LIVE")

    if new_mode != cur_mode and Matcher(db).has_open_position():
        raise HTTPException(
            status_code=409,
            detail="有未平仓，无法切换 SIM/LIVE；请先平仓后再改",
        )

    if new_mode == "LIVE" and cur_mode != "LIVE":
        if not body.confirm_live:
            raise HTTPException(
                status_code=400,
                detail="切换到 LIVE 须二次确认（confirm_live=true）",
            )
        phrase = (body.confirm_live_phrase or "").strip()
        if phrase != "LIVE":
            raise HTTPException(
                status_code=400,
                detail="切换到 LIVE 须在 confirm_live_phrase 传入 LIVE",
            )
        secret = (s.auth_secret or "").strip()
        if not secret or secret == "change-me-eth-hedge-sim-secret":
            raise HTTPException(
                status_code=400,
                detail="切到 LIVE 前请在 .env 设置非默认 AUTH_SECRET",
            )

    updates: dict[str, str] = {}
    if body.okx_api_key is not None and body.okx_api_key.strip():
        updates["OKX_API_KEY"] = body.okx_api_key.strip()
    if body.okx_api_secret is not None and body.okx_api_secret.strip():
        updates["OKX_API_SECRET"] = body.okx_api_secret.strip()
    if body.okx_api_passphrase is not None and body.okx_api_passphrase.strip():
        updates["OKX_API_PASSPHRASE"] = body.okx_api_passphrase.strip()
    if body.binance_api_key is not None and body.binance_api_key.strip():
        updates["BINANCE_API_KEY"] = body.binance_api_key.strip()
    if body.binance_api_secret is not None and body.binance_api_secret.strip():
        updates["BINANCE_API_SECRET"] = body.binance_api_secret.strip()

    if new_mode != cur_mode:
        updates["MODE"] = new_mode

    if updates:
        upsert_env_keys(updates)

    s2 = get_settings()
    if new_mode == "LIVE":
        rt = load_runtime_settings()
        if rt.exchange == "okx" and not okx_keys_configured(s2):
            if cur_mode == "SIM":
                upsert_env_keys({"MODE": "SIM"})
            raise HTTPException(
                status_code=400,
                detail="切到 LIVE 前请先配置完整 OKX API Key/Secret/Passphrase",
            )
        if rt.exchange == "binance" and not binance_keys_configured(s2):
            if cur_mode == "SIM":
                upsert_env_keys({"MODE": "SIM"})
            raise HTTPException(
                status_code=400,
                detail="切到 LIVE 前请先配置完整币安 API Key/Secret",
            )

    # 热切 LIVE：强制暂停策略（对齐冷启动护栏；运行中禁止人工开平仓基线）
    if new_mode == "LIVE" and cur_mode != "LIVE":
        db.execute(
            "UPDATE strategy_state SET running=0, phase=?, last_error=? WHERE id=1",
            ("paused", "已切换 LIVE，策略已强制暂停；确认就绪后再启动"),
        )
        try:
            from ..strategy import get_engine

            # 同步停循环标志（pause 为 async，此处只写状态）
            get_engine()._set_state(
                running=0,
                phase="paused",
                last_error="已切换 LIVE，策略已强制暂停；确认就绪后再启动",
            )
        except Exception:
            pass

    try:
        from ..strategy import get_engine

        get_engine().refresh_executor()
    except Exception:
        pass

    return _runtime_payload()


class NotifySettingsBody(BaseModel):
    enabled: bool | None = None
    webhook_url: str | None = None
    machine_name: str | None = Field(default=None, max_length=64)


def _notify_payload() -> dict:
    from ..notify import wecom
    from ..env_store import mask_secret

    url = wecom.wecom_webhook_url()
    return {
        "enabled": wecom.wecom_enabled(),
        "webhook_configured": bool(url),
        "webhook_url_masked": mask_secret(url) if url else None,
        "venue_label": wecom.venue_label(),
        "machine_name": wecom.wecom_machine_name() or "",
    }


@router.get("/notify")
async def get_notify_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
    return _notify_payload()


@router.put("/notify")
async def put_notify_settings(
    body: NotifySettingsBody,
    _user: Annotated[str, Depends(require_user)],
) -> dict:
    updates: dict[str, str] = {}
    if body.enabled is not None:
        updates["WECOM_ENABLED"] = "1" if body.enabled else "0"
        get_db().set_setting("wecom_enabled", "1" if body.enabled else "0")
    if body.webhook_url is not None and body.webhook_url.strip():
        updates["WECOM_WEBHOOK_URL"] = body.webhook_url.strip()
        get_db().set_setting("wecom_webhook_url", body.webhook_url.strip())
    if body.machine_name is not None:
        name = body.machine_name.strip()[:64]
        updates["WECOM_MACHINE_NAME"] = name
        get_db().set_setting("wecom_machine_name", name)
    if updates:
        upsert_env_keys(updates)
        get_settings.cache_clear()
    return _notify_payload()


@router.post("/notify/test")
async def test_notify(_user: Annotated[str, Depends(require_user)]) -> dict:
    from ..notify import wecom

    ok, msg = wecom.notify_test()
    if not ok:
        raise HTTPException(status_code=400, detail=f"推送失败: {msg}")
    return {"ok": True, "detail": "测试消息已发送", **_notify_payload()}
