"""从 .env.example 构建 env 配置 schema(分组,敏感,重启标注)."""
from __future__ import annotations

import os
import re
from typing import Any, Optional

from lib.env.env_file_lib import env_get, env_get_all, read_env_lines

_GROUP_RE = re.compile(r"^#\s*=+\s*(.+?)\s*=+\s*$")
_SEPARATOR_RE = re.compile(r"^#\s*=+\s*$")
_SECTION_DASH_RE = re.compile(r"^#\s*---\s*(.+?)\s*---\s*$")
_KEY_LINE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=")

RESTART_REQUIRED_EXACT = frozenset({
    "APP_HOST",
    "APP_PORT",
    "APP_DEBUG",
    "DB_PATH",
    "UPLOAD_DIR",
    "FLASK_SECRET_KEY",
    "POSITION_SIZING_MODE",
    "LIVE_TRADING_ENABLED",
    "OKX_TD_MODE",
    "OKX_POS_MODE",
    "OKX_POSITION_INST_TYPE",
    "BINANCE_MARGIN_MODE",
    "BINANCE_POSITION_MODE",
    "GATE_TD_MODE",
    "GATE_POS_MODE",
    "PM2_APP_NAME",
})

RESTART_REQUIRED_PREFIXES = (
    "OKX_API_",
    "OKX_OPTIONS_API_",
    "BINANCE_API_",
    "GATE_API_",
    "OKX_SOCKS_",
    "OKX_HTTP_",
    "OKX_HTTPS_",
    "BINANCE_HTTP_",
    "BINANCE_HTTPS_",
    "GATE_HTTP_",
    "GATE_HTTPS_",
)

HOT_RELOAD_EXACT = frozenset({
    "RISK_PERCENT",
    "MAX_ACTIVE_POSITIONS",
    "MANUAL_MIN_PLANNED_RR",
    "KEY_AUTO_MIN_PLANNED_RR",
    "DAILY_OPEN_ALERT_THRESHOLD",
    "DAILY_OPEN_HARD_LIMIT",
    "TRADING_DAY_RESET_HOUR",
    "TRADING_DAY_RESET_OPEN_GUARD_ENABLED",
    "RISK_CONTROL_ENABLED",
    "RISK_COOLING_HOURS_MANUAL",
    "RISK_COOLING_HOURS_MANUAL_JOURNAL",
    "RISK_MANUAL_CLOSE_DAILY_LIMIT",
    "RISK_MOOD_ISSUES_DAILY_FREEZE",
    "KEY_AUTO_ORDER_ENABLED",
    "TRADE_DIRECTION_RESTRICT_ENABLED",
    "TRADE_DIRECTION",
    "TRADE_SYMBOL_RESTRICT_ENABLED",
    "TRADE_SYMBOL_WHITELIST",
    "BALANCE_REFRESH_SECONDS",
    "PRICE_REFRESH_SECONDS",
    "MONITOR_POLL_SECONDS",
    "AUTO_TRANSFER_ENABLED",
    "AUTO_TRANSFER_AMOUNT",
    "AUTO_TRANSFER_BJ_HOUR",
    "FORCE_CLOSE_ENABLED",
    "FORCE_CLOSE_BJ_HOUR",
    "BTC_LEVERAGE",
    "ALT_LEVERAGE",
    "DAILY_START_CAPITAL",
    "DAILY_LOSS_CAPITAL",
    "DAILY_PROFIT_CAPITAL",
    "FULL_MARGIN_BUFFER_RATIO",
    "APP_USERNAME",
    "APP_PASSWORD",
    "APP_AUTH_DISABLED",
    "WECHAT_WEBHOOK",
    "HEDGE_PLAN_ENABLED",
    "HEDGE_PLAN_LIVE_ORDER",
    "HEDGE_PLAN_OPEN_ORDER",
    "HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
    "HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
    "HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
    "MAX_ACTIVE_HEDGE_PLANS",
    "HEDGE_PLAN_MONITOR_POLL_SECONDS",
    "HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
})

SENSITIVE_EXACT = frozenset({
    "APP_PASSWORD",
    "FLASK_SECRET_KEY",
    "HUB_BRIDGE_TOKEN",
    "OPENAI_API_KEY",
})

SENSITIVE_SUBSTR = ("_SECRET", "_PASSPHRASE", "_API_KEY", "_PASSWORD")


def _is_sensitive(key: str) -> bool:
    if key in SENSITIVE_EXACT:
        return True
    return any(s in key for s in SENSITIVE_SUBSTR)


def _restart_required(key: str) -> bool:
    if key in HOT_RELOAD_EXACT:
        return False
    if key in RESTART_REQUIRED_EXACT:
        return True
    return any(key.startswith(p) for p in RESTART_REQUIRED_PREFIXES)


def _hot_reload(key: str) -> bool:
    if key in HOT_RELOAD_EXACT:
        return True
    if _restart_required(key):
        return False
    return key.startswith(("KEY_", "KLINE_", "BREAKEVEN_", "RECONCILE_", "ORDER_CHART_"))


def _field_type(key: str, value: str) -> str:
    low = (value or "").strip().lower()
    if low in ("true", "false"):
        return "bool"
    if key.endswith("_ENABLED") or key.startswith("RISK_MOOD_"):
        return "bool"
    try:
        if "." in low:
            float(low)
            return "float"
        int(low)
        return "int"
    except ValueError:
        pass
    return "text"


def _mask_value(key: str, value: Optional[str]) -> dict[str, Any]:
    if value is None or value == "":
        return {"value": "", "masked": "", "tail": "", "has_value": False}
    if not _is_sensitive(key):
        return {"value": value, "masked": value, "tail": "", "has_value": True}
    tail = value[-4:] if len(value) >= 4 else value
    return {"value": "", "masked": f"****{tail}", "tail": tail, "has_value": True}


def parse_env_example_schema(example_path: str) -> list[dict[str, Any]]:
    if not os.path.isfile(example_path):
        return []
    lines = read_env_lines(example_path)
    groups: list[dict[str, Any]] = []
    group_map: dict[str, dict[str, Any]] = {}
    current_group = "基础配置"
    pending_note: list[str] = []
    in_section_block = False
    section_title_set = False
    allow_section_blocks = False

    def _ensure_group(title: str) -> dict[str, Any]:
        title = (title or "").strip() or "其他"
        if title not in group_map:
            group_map[title] = {"title": title, "fields": []}
            groups.append(group_map[title])
        return group_map[title]

    for raw in lines:
        line = raw.rstrip()
        stripped = line.strip()
        if not stripped:
            pending_note = []
            continue
        if _SEPARATOR_RE.match(stripped):
            if not allow_section_blocks:
                continue
            if not in_section_block:
                in_section_block = True
                section_title_set = False
            else:
                in_section_block = False
            continue
        if in_section_block and stripped.startswith("#"):
            note = stripped.lstrip("#").strip()
            if note and not section_title_set:
                current_group = note
                _ensure_group(current_group)
                section_title_set = True
            elif note:
                pending_note.append(note)
            continue
        gm = _GROUP_RE.match(stripped)
        if gm:
            title = gm.group(1).strip()
            if title and title != "=":
                current_group = title
                _ensure_group(current_group)
            in_section_block = False
            section_title_set = False
            pending_note = []
            continue
        dash = _SECTION_DASH_RE.match(stripped)
        if dash:
            allow_section_blocks = True
            current_group = dash.group(1).strip()
            _ensure_group(current_group)
            in_section_block = False
            section_title_set = False
            pending_note = []
            continue
        if stripped.startswith("#"):
            note = stripped.lstrip("#").strip()
            if note and not note.startswith("="):
                pending_note.append(note)
            continue
        km = _KEY_LINE.match(stripped)
        if not km:
            continue
        key = km.group(1)
        allow_section_blocks = True
        default_val = env_get(lines, key) or ""
        grp = _ensure_group(current_group)
        note = " ".join(pending_note).strip()
        grp["fields"].append(
            {
                "key": key,
                "label": key,
                "note": note,
                "default": default_val,
                "type": _field_type(key, default_val),
                "sensitive": _is_sensitive(key),
                "restart_required": _restart_required(key),
                "hot_reload": _hot_reload(key),
            }
        )
        pending_note = []
    return [g for g in groups if g.get("fields")]


def build_env_payload(example_path: str, env_path: str) -> dict[str, Any]:
    groups = parse_env_example_schema(example_path)
    env_lines = read_env_lines(env_path)
    values = env_get_all(env_lines)
    for group in groups:
        for field in group.get("fields") or []:
            key = field["key"]
            val = values.get(key)
            if val is None:
                val = field.get("default") or ""
            masked = _mask_value(key, val)
            field["current"] = masked["value"] if not field["sensitive"] else ""
            field["masked"] = masked["masked"]
            field["has_value"] = masked["has_value"]
    return {"groups": groups}


def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[dict[str, str], list[str]]:
    allowed = {}
    for group in groups:
        for field in group.get("fields") or []:
            allowed[field["key"]] = field
    clean: dict[str, str] = {}
    errors: list[str] = []
    for key, value in (updates or {}).items():
        if key not in allowed:
            errors.append(f"未知配置项: {key}")
            continue
        if value is None:
            continue
        val = str(value).strip()
        if allowed[key].get("sensitive") and (val == "" or (val.startswith("****") and len(val) <= 8)):
            continue
        # API Key 被密码管理器/自动填充成登录密码时通常很短;OKX Key 一般为 36 位
        if key.endswith("_API_KEY") and 0 < len(val) < 16:
            errors.append(f"{key} 长度异常,疑似自动填充;留空则不修改已有密钥")
            continue
        ftype = allowed[key].get("type")
        if ftype == "bool":
            low = val.lower()
            if low not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
                errors.append(f"{key} 须为 true/false")
                continue
            val = "true" if low in ("true", "1", "yes", "on") else "false"
        clean[key] = val
    return clean, errors


def updates_need_restart(groups: list[dict], changed_keys: list[str]) -> bool:
    field_map = {}
    for group in groups:
        for field in group.get("fields") or []:
            field_map[field["key"]] = field
    for key in changed_keys:
        meta = field_map.get(key) or {}
        if meta.get("restart_required"):
            return True
        if not meta.get("hot_reload"):
            return True
    return False
