"""
kis_trader/engine/daily_profit_halt.py — 일일 익절 목표 달성 시 신규 매수 중단 (2단)
==============================================================================
1단(마스터): ``DAILY_PROFIT_*`` — 봇 당일 실현손익 총합 → 전 전략 매수 OFF
2단(서브)  : ``{STRATEGY}_DAILY_PROFIT_*`` — 해당 전략만 매수 OFF

목표 판정: 원(KRW) · 운용한도 대비 %(PCT) **둘 중 하나라도** 달성 시 트리거.
분모(%) : ``DAILY_PROFIT_TARGET_BUDGET_KRW`` 또는 ON 전략 ``*_TOTAL_BUDGET_KRW`` 합.

**활성화(``*_TARGET_ENABLED``)** ON + 목표 설정 → 달성 시 **무조건** 신규매수 차단.
**신규매수 중단(``*_HALT_NEW_BUYS``)** = 목표와 무관한 **수동** 매수 잠금(별도 스위치).

매도(손절·익절)는 계속 — **신규 매수만** 차단.
모든 임계값 env/DB — 하드코딩 금지.
"""
from __future__ import annotations

import threading
import time
from datetime import datetime as dt
from typing import Any, Callable, Dict, List, Optional, Tuple

from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from kis_trader.utils.logger import get_logger

logger = get_logger("kis_trader.daily_profit_halt")

# 전략 ID → 운용한도 env 키 (bot_pct 분모와 동일)
_STRATEGY_BUDGET_ENV: Dict[str, str] = {
    "SCALP": "SCALP_TOTAL_BUDGET_KRW",
    "SHORT": "TAIL_TOTAL_BUDGET_KRW",
    "MOMENTUM": "MOMENTUM_TOTAL_BUDGET_KRW",
    "BREAKOUT": "BREAKOUT_TOTAL_BUDGET_KRW",
    "RANGE_BREAK": "RANGE_BREAK_TOTAL_BUDGET_KRW",
    "UPDOW": "UPDOW_TOTAL_BUDGET_KRW",
    "DBBAND": "DBBAND_TOTAL_BUDGET_KRW",
}


def _strategy_prefix(strategy_id: str) -> str:
    return (strategy_id or "").strip().upper() or "STRATEGY"


def _enabled_flag(common_key: str, strategy_key: str, default: bool = False) -> bool:
    raw = str(get_env_from_db(strategy_key, "")).strip()
    if raw != "":
        return get_env_bool(strategy_key, default)
    return get_env_bool(common_key, default)


def _target_krw(common_key: str, strategy_key: str, default: float = 0.0) -> float:
    sk = strategy_key
    if str(get_env_from_db(sk, "")).strip():
        return max(0.0, float(get_env_float(sk, default)))
    return max(0.0, float(get_env_float(common_key, default)))


def _target_pct(common_key: str, strategy_key: str, default: float = 0.0) -> float:
    sk = strategy_key
    if str(get_env_from_db(sk, "")).strip():
        return max(0.0, float(get_env_float(sk, default)))
    return max(0.0, float(get_env_float(common_key, default)))


def _mode(common_key: str, strategy_key: str, default: str = "fixed") -> str:
    """익절 판정 방식 — 전략 서브값 우선 → 마스터 → 기본(fixed).

    fixed   : 기존 동작(고정 목표 KRW/PCT 도달 시 중단)
    trailing: 당일 손익 고점 대비 되돌림(trail) 발동
    both    : 고정(하드캡) OR 트레일 — 먼저 닿는 쪽
    """
    raw = str(get_env_from_db(strategy_key, "")).strip()
    if raw != "":
        return raw.lower()
    g = str(get_env_from_db(common_key, "")).strip()
    return (g or default).lower()


def _halt_new_buys_flag(common_key: str, strategy_key: str, default: bool = False) -> bool:
    """수동 신규매수 중단 — 전략 서브값 우선 → 마스터 → 기본 OFF."""
    raw = str(get_env_from_db(strategy_key, "")).strip()
    if raw != "":
        return get_env_bool(strategy_key, default)
    return get_env_bool(common_key, default)


def load_global_profit_target() -> Dict[str, Any]:
    return {
        "enabled": get_env_bool("DAILY_PROFIT_TARGET_ENABLED", False),
        "krw": max(0.0, float(get_env_float("DAILY_PROFIT_TARGET_KRW", 0.0))),
        "pct": max(0.0, float(get_env_float("DAILY_PROFIT_TARGET_PCT", 0.0))),
        "budget_krw": max(0.0, float(get_env_float("DAILY_PROFIT_TARGET_BUDGET_KRW", 0.0))),
        "halt_new_buys": _halt_new_buys_flag(
            "DAILY_PROFIT_HALT_NEW_BUYS", "DAILY_PROFIT_HALT_NEW_BUYS", False,
        ),
        "notify_mm": get_env_bool("DAILY_PROFIT_NOTIFY_MM", True),
        # 트레일링 익절 (당일 손익 고점 추적) — fixed 기본이라 미설정 시 동작 불변
        "mode": _mode("DAILY_PROFIT_MODE", "DAILY_PROFIT_MODE", "fixed"),
        # 다단계 트레일 tier(우선) — '수익원:컷%,…'. 비면 단일 drop_pct 폴백.
        "trail_tiers": str(get_env_from_db("DAILY_PROFIT_TRAIL_TIERS", "")).strip(),
        "trail_drop_pct": max(0.0, float(get_env_float("DAILY_PROFIT_TRAIL_DROP_PCT", 0.0))),
        "trail_arm_krw": max(0.0, float(get_env_float("DAILY_PROFIT_TRAIL_ARM_KRW", 0.0))),
        "trail_arm_pct": max(0.0, float(get_env_float("DAILY_PROFIT_TRAIL_ARM_PCT", 0.0))),
    }


def load_strategy_profit_target(strategy_id: str) -> Dict[str, Any]:
    pfx = _strategy_prefix(strategy_id)
    return {
        "enabled": _enabled_flag(
            "DAILY_PROFIT_TARGET_ENABLED",
            f"{pfx}_DAILY_PROFIT_TARGET_ENABLED",
            False,
        ),
        "krw": _target_krw(
            "DAILY_PROFIT_TARGET_KRW",
            f"{pfx}_DAILY_PROFIT_TARGET_KRW",
            0.0,
        ),
        "pct": _target_pct(
            "DAILY_PROFIT_TARGET_PCT",
            f"{pfx}_DAILY_PROFIT_TARGET_PCT",
            0.0,
        ),
        "budget_env": _STRATEGY_BUDGET_ENV.get(pfx, f"{pfx}_TOTAL_BUDGET_KRW"),
        "halt_new_buys": _halt_new_buys_flag(
            "DAILY_PROFIT_HALT_NEW_BUYS",
            f"{pfx}_DAILY_PROFIT_HALT_NEW_BUYS",
            False,
        ),
        # 트레일링 익절 — 전략 서브값 우선 → 마스터 폴백 (fixed 기본)
        "mode": _mode("DAILY_PROFIT_MODE", f"{pfx}_DAILY_PROFIT_MODE", "fixed"),
        # 다단계 트레일 tier — 전략 서브값 우선 → 마스터 폴백 (비면 단일 drop_pct)
        "trail_tiers": _mode("DAILY_PROFIT_TRAIL_TIERS", f"{pfx}_DAILY_PROFIT_TRAIL_TIERS", ""),
        "trail_drop_pct": _target_krw(
            "DAILY_PROFIT_TRAIL_DROP_PCT", f"{pfx}_DAILY_PROFIT_TRAIL_DROP_PCT", 0.0,
        ),
        "trail_arm_krw": _target_krw(
            "DAILY_PROFIT_TRAIL_ARM_KRW", f"{pfx}_DAILY_PROFIT_TRAIL_ARM_KRW", 0.0,
        ),
        "trail_arm_pct": _target_pct(
            "DAILY_PROFIT_TRAIL_ARM_PCT", f"{pfx}_DAILY_PROFIT_TRAIL_ARM_PCT", 0.0,
        ),
    }


def resolve_strategy_budget_krw(strategy_id: str) -> float:
    pfx = _strategy_prefix(strategy_id)
    env_key = _STRATEGY_BUDGET_ENV.get(pfx, f"{pfx}_TOTAL_BUDGET_KRW")
    v = get_env_int(env_key, 0)
    if v > 0:
        return float(v)
    # SLOT × MAX_STOCKS 폴백
    slot_key = f"{pfx}_SLOT_MONEY"
    max_key = f"{pfx}_MAX_STOCKS"
    slot = get_env_int(slot_key, 0)
    mx = get_env_int(max_key, 0)
    if slot > 0 and mx > 0:
        return float(slot * mx)
    return 0.0


def resolve_global_operating_budget_krw(active_strategy_ids: List[str]) -> float:
    explicit = max(0.0, float(get_env_float("DAILY_PROFIT_TARGET_BUDGET_KRW", 0.0)))
    if explicit > 0:
        return explicit
    total = 0.0
    for sid in active_strategy_ids:
        b = resolve_strategy_budget_krw(sid)
        if b > 0:
            total += b
    return total


def _target_configured(cfg: Dict[str, Any]) -> bool:
    if not cfg.get("enabled"):
        return False
    return float(cfg.get("krw") or 0) > 0 or float(cfg.get("pct") or 0) > 0


def _target_reached(pnl_krw: float, cfg: Dict[str, Any], budget_krw: float) -> bool:
    if not _target_configured(cfg):
        return False
    krw_tgt = float(cfg.get("krw") or 0)
    pct_tgt = float(cfg.get("pct") or 0)
    if krw_tgt > 0 and pnl_krw >= krw_tgt:
        return True
    if pct_tgt > 0 and budget_krw > 0:
        need = budget_krw * pct_tgt / 100.0
        if pnl_krw >= need:
            return True
    return False


def _guard_active(cfg: Dict[str, Any]) -> bool:
    """모드별 가드 작동 여부 — fixed:목표값 / trailing:trail_drop / both:둘 중 하나.

    (기존 _target_configured 는 fixed 전용으로 유지하고, buy_allowed 는 이 함수를 쓴다.)
    """
    if not cfg.get("enabled"):
        return False
    mode = str(cfg.get("mode") or "fixed").lower()
    if mode in ("fixed", "both"):
        if float(cfg.get("krw") or 0) > 0 or float(cfg.get("pct") or 0) > 0:
            return True
    if mode in ("trailing", "both"):
        if parse_trail_tiers(cfg.get("trail_tiers")):
            return True
        if float(cfg.get("trail_drop_pct") or 0) > 0:
            return True
    return False


def _trail_arm_krw(cfg: Dict[str, Any], budget_krw: float) -> float:
    """트레일 발동 최소수익(원) — KRW·PCT 중 큰 값. 0 이면 트레일 비활성(과민발동 방지)."""
    arm = float(cfg.get("trail_arm_krw") or 0)
    arm_pct = float(cfg.get("trail_arm_pct") or 0)
    if arm_pct > 0 and budget_krw > 0:
        arm = max(arm, budget_krw * arm_pct / 100.0)
    return arm


def parse_trail_tiers(raw: Any) -> List[Tuple[float, float]]:
    """다단계 트레일 tier 파싱: '수익원:컷%,…' → [(arm_krw, drop_pct), …] (arm 오름차순).

    개별포지션 ratchet_tiers 와 동일 철학을 당일 누적손익에 적용.
    예) '30000:50,100000:35,300000:25'
        = 누적 3만↑ 고점대비 -50% / 10만↑ -35% / 30만↑ -25% (벌수록 타이트)
    잘못된 토큰은 건너뜀. 빈 문자열 → [].
    """
    if raw is None:
        return []
    s = str(raw).strip()
    if s == "" or s.lower() == "off":
        return []
    tiers: List[Tuple[float, float]] = []
    for tok in s.split(","):
        tok = tok.strip()
        if not tok or ":" not in tok:
            continue
        a, _, d = tok.partition(":")
        try:
            arm = float(a.strip())
            drop = float(d.strip())
        except (TypeError, ValueError):
            continue
        if arm < 0 or drop <= 0:
            continue
        tiers.append((arm, drop))
    tiers.sort(key=lambda x: x[0])
    return tiers


def _active_tier_drop(peak_krw: float, tiers: List[Tuple[float, float]]) -> float:
    """peak 가 도달한 가장 높은 tier 의 컷% 반환. 최저 tier 미만이면 0(비발동)."""
    drop = 0.0
    for arm, d in tiers:  # arm 오름차순
        if peak_krw >= arm:
            drop = d
        else:
            break
    return drop


def _trail_reached(
    pnl_krw: float, peak_krw: float, cfg: Dict[str, Any], budget_krw: float,
) -> bool:
    """당일 손익 고점(peak) 대비 되돌림 시 트리거.

    우선순위: 다단계 tier(trail_tiers) → 단일 trail_drop_pct(하위호환).
    tier: peak 가 속한 구간의 컷% 적용 → 벌수록 타이트(이익 보존 강화).
    안전장치: tier·drop 없거나 ARM 미달이면 비활성(설정 없이 과민발동 금지).
    peak 가 ARM 이상으로 올라간 뒤부터만 감시 → 작은 수익에 조기 종료 방지.
    """
    # 1) 다단계 tier 우선
    tiers = parse_trail_tiers(cfg.get("trail_tiers"))
    if tiers:
        drop = _active_tier_drop(peak_krw, tiers)
        if drop <= 0:
            return False  # 최저 tier(=arm) 미달
        cut = peak_krw * (1.0 - drop / 100.0)
        return pnl_krw <= cut

    # 2) 단일 drop_pct 폴백 (기존 동작)
    drop = float(cfg.get("trail_drop_pct") or 0)
    if drop <= 0:
        return False
    arm = _trail_arm_krw(cfg, budget_krw)
    if arm <= 0:
        return False
    if peak_krw < arm:
        return False
    cut = peak_krw * (1.0 - drop / 100.0)
    return pnl_krw <= cut


def _format_hit_detail(
    pnl_krw: float, cfg: Dict[str, Any], budget_krw: float,
) -> str:
    parts = [f"실현 {pnl_krw:+,.0f}원"]
    krw_tgt = float(cfg.get("krw") or 0)
    pct_tgt = float(cfg.get("pct") or 0)
    if krw_tgt > 0:
        parts.append(f"목표 {krw_tgt:,.0f}원")
    if pct_tgt > 0 and budget_krw > 0:
        parts.append(f"목표 {pct_tgt:.2f}% (한도 {budget_krw:,.0f}원)")
    return " · ".join(parts)


def describe_profit_guard_startup(cfg: Dict[str, Any], *, scope: str = "마스터") -> str:
    """기동 로그용 — 설정이 매수에 미치는 영향을 한 줄로."""
    if cfg.get("halt_new_buys"):
        return (
            f"⛔ [일일익절·{scope}] 수동 신규매수 중단 ON — "
            f"목표·손익과 무관하게 신규매수 차단 (매도·손절 유지)"
        )
    if not cfg.get("enabled"):
        return (
            f"ℹ️ [일일익절·{scope}] 손익 감시 OFF — "
            f"목표 달성 시에도 신규매수 차단 없음"
        )
    if not _guard_active(cfg):
        return (
            f"ℹ️ [일일익절·{scope}] 손익 감시 ON 이지만 목표 미설정 "
            f"(금액·%·트레일 없음) — 달성 차단 없음"
        )
    krw = float(cfg.get("krw") or 0)
    pct = float(cfg.get("pct") or 0)
    return (
        f"🎯 [일일익절·{scope}] 손익 감시 ON — 목표 {krw:,.0f}원 / {pct:.2f}% "
        f"· 달성 시 신규매수 차단 (매도·손절 유지)"
    )


class DailyProfitHaltGuard:
    """
  Orchestrator 가 주입 — ``buy_allowed(strategy_id)`` 로 신규 매수 차단 여부 판단.
    """

    def __init__(
        self,
        *,
        global_pnl_fn: Callable[[str], Tuple[float, int]],
        strategy_pnl_fn: Callable[[str, str], Tuple[float, int]],
        active_strategies_fn: Callable[[], List[str]],
        notify_fn: Optional[Callable[[str, Optional[str]], None]] = None,
    ):
        self._global_pnl_fn = global_pnl_fn
        self._strategy_pnl_fn = strategy_pnl_fn
        self._active_strategies_fn = active_strategies_fn
        self._notify_fn = notify_fn
        self._lock = threading.Lock()
        self._notified_keys: set = set()
        self._last_log_ts: Dict[str, float] = {}
        # 트레일링용 당일 손익 고점 추적 {scope_key: (today, peak_krw)}
        self._peaks: Dict[str, Tuple[str, float]] = {}

    def _update_peak(self, scope_key: str, today: str, pnl_krw: float) -> float:
        """당일 손익 고점 갱신·반환. 날짜가 바뀌면 리셋."""
        with self._lock:
            day, peak = self._peaks.get(scope_key, (today, pnl_krw))
            if day != today:
                peak = pnl_krw
            peak = max(peak, pnl_krw)
            self._peaks[scope_key] = (today, peak)
            return peak

    def buy_allowed(self, strategy_id: str) -> Tuple[bool, str]:
        """신규 매수 허용 여부. (False, 사유) 이면 매수 스킵."""
        today = dt.now().strftime("%Y-%m-%d")
        sid = _strategy_prefix(strategy_id)

        gcfg = load_global_profit_target()

        # 1) 수동 신규매수 중단 (목표·손익과 무관)
        if gcfg.get("halt_new_buys"):
            self._throttled_log(
                sid,
                "⛔ [신규매수중단·총합] 수동 중단 ON → 신규매수 차단",
            )
            return False, "탈락-신규매수중단(총합)"

        # 2) 일일익절 — 활성화 ON이면 목표 달성 시 무조건 신규매수 차단
        if _guard_active(gcfg):
            gpnl, gcnt = self._global_pnl_fn(today)
            gbudget = resolve_global_operating_budget_krw(self._active_strategies_fn())
            gpeak = self._update_peak(f"global:{today}", today, gpnl)
            hit, extra = self._check_hit(gpnl, gpeak, gcfg, gbudget)
            if hit:
                detail = _format_hit_detail(gpnl, gcfg, gbudget) + extra
                self._notify_once(f"global:{today}", "GLOBAL", detail, gcnt)
                self._throttled_log(sid, f"⛔ [일일익절·총합] {detail} → 신규매수 중단")
                return False, "탈락-일일익절(총합)"

        scfg = load_strategy_profit_target(sid)

        if scfg.get("halt_new_buys"):
            self._throttled_log(
                sid,
                f"⛔ [신규매수중단·{sid}] 수동 중단 ON → 해당전략 신규매수 차단",
            )
            return False, f"탈락-신규매수중단({sid})"

        if _guard_active(scfg):
            spnl, scnt = self._strategy_pnl_fn(today, sid)
            sbudget = resolve_strategy_budget_krw(sid)
            speak = self._update_peak(f"{sid}:{today}", today, spnl)
            hit, extra = self._check_hit(spnl, speak, scfg, sbudget)
            if hit:
                detail = _format_hit_detail(spnl, scfg, sbudget) + extra
                self._notify_once(f"{sid}:{today}", sid, detail, scnt)
                self._throttled_log(
                    sid,
                    f"⛔ [일일익절·{sid}] {detail} → 해당전략 신규매수 중단",
                )
                return False, f"탈락-일일익절({sid})"

        return True, ""

    def _check_hit(
        self, pnl_krw: float, peak_krw: float, cfg: Dict[str, Any], budget_krw: float,
    ) -> Tuple[bool, str]:
        """모드별 트리거 판정. (hit, 로그 부가설명) 반환.

        both: 고정(하드캡) 먼저 검사 → 트레일 — 먼저 닿는 쪽 발동.
        """
        mode = str(cfg.get("mode") or "fixed").lower()
        if mode in ("fixed", "both") and _target_reached(pnl_krw, cfg, budget_krw):
            return True, ""
        if mode in ("trailing", "both") and _trail_reached(pnl_krw, peak_krw, cfg, budget_krw):
            tiers = parse_trail_tiers(cfg.get("trail_tiers"))
            drop = (
                _active_tier_drop(peak_krw, tiers) if tiers
                else float(cfg.get("trail_drop_pct") or 0)
            )
            tag = "트레일·다단" if tiers else "트레일"
            return True, (
                f" · {tag}(고점 {peak_krw:+,.0f}원 대비 -{drop:.0f}% 되돌림)"
            )
        return False, ""

    def _throttled_log(self, strategy_id: str, msg: str) -> None:
        now = time.time()
        key = strategy_id
        if now - self._last_log_ts.get(key, 0.0) < 60.0:
            return
        self._last_log_ts[key] = now
        logger.info(msg)

    def _notify_once(
        self, notify_key: str, scope: str, detail: str, trade_cnt: int,
    ) -> None:
        gcfg = load_global_profit_target()
        if not gcfg.get("notify_mm", True) or not self._notify_fn:
            return
        with self._lock:
            if notify_key in self._notified_keys:
                return
            self._notified_keys.add(notify_key)
        title = "🎯 **일일 익절 목표 달성**"
        if scope == "GLOBAL":
            body = (
                f"{title}\n"
                f"- 범위: **전체 봇 (총합)**\n"
                f"- {detail}\n"
                f"- 청산 {trade_cnt}건\n"
                f"- 조치: **금일 신규 매수 중단** (보유 종목 매도·손절은 유지)"
            )
        else:
            body = (
                f"{title}\n"
                f"- 범위: **{scope}**\n"
                f"- {detail}\n"
                f"- 청산 {trade_cnt}건\n"
                f"- 조치: **{scope} 신규 매수만 중단** (다른 전략·보유 매도는 유지)"
            )
        try:
            self._notify_fn(body, scope if scope != "GLOBAL" else None)
        except Exception as ex:
            logger.debug("일일익절 MM 알림 실패: %s", ex)
