#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
내일(다음 거래일) 장 시작(09:05)을 가정한 격리 스모크.
- 실매매 서비스(WS/주문)는 건드리지 않음
- FORCE_MARKET_OPEN 을 DB/실매에 쓰지 않음 (프로세스 내 datetime 패치만)
- 계좌 조회(REST) + 엔진/DB 기본값 + 장시작 리포트 문자열 + 직전 거래일 백테 스모크

로그: logs/market_open_sim_smoke.log
"""
from __future__ import annotations

import logging
import os
import sys
import traceback
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, List, Tuple
from unittest.mock import patch

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

LOG_PATH = ROOT / "logs" / "market_open_sim_smoke.log"
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] %(message)s",
    datefmt="%H:%M:%S",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler(LOG_PATH, encoding="utf-8"),
    ],
)
log = logging.getLogger("market_open_sim")

errors: List[str] = []
oks: List[str] = []


def _ok(msg: str) -> None:
    oks.append(msg)
    log.info("✅ %s", msg)


def _err(msg: str) -> None:
    errors.append(msg)
    log.error("❌ %s", msg)


def _next_trading_day(from_d: date) -> date:
    from kis_trader.utils.kr_trading_day import is_kr_trading_day

    d = from_d + timedelta(days=1)
    for _ in range(21):
        if is_kr_trading_day(d):
            return d
        d += timedelta(days=1)
    raise RuntimeError("next trading day not found")


def step_calendar() -> datetime:
    from kis_trader.utils.kr_trading_day import (
        clamp_to_prev_kr_trading_day,
        is_kr_trading_day,
        trading_dates_payload,
    )

    today = date.today()
    tom = _next_trading_day(today)
    assert is_kr_trading_day(tom), tom
    prev = clamp_to_prev_kr_trading_day(today)
    payload = trading_dates_payload(7)
    _ok(
        f"달력: today={today} next_open={tom} prev_td={prev} "
        f"web_defaults={payload.get('start')}~{payload.get('end')}"
    )
    # 장시작 09:05 가정
    return datetime(tom.year, tom.month, tom.day, 9, 5, 0)


def step_strategy_flags() -> None:
    from kis_trader.utils.env import get_env_bool

    flags = {
        "SCALP": get_env_bool("STRATEGY_SCALP_ENABLED", True),
        "SHORT": get_env_bool("STRATEGY_SHORT_ENABLED", True),
        "MOMENTUM": get_env_bool("STRATEGY_MOMENTUM_ENABLED", False),
        "BREAKOUT": get_env_bool("STRATEGY_BREAKOUT_ENABLED", False),
        "RANGE_BREAK": get_env_bool("STRATEGY_RANGE_BREAK_ENABLED", False),
        "UPDOW": get_env_bool("STRATEGY_UPDOW_ENABLED", False),
        "DBBAND": get_env_bool("STRATEGY_DBBAND_ENABLED", False),
    }
    on = [k for k, v in flags.items() if v]
    off = [k for k, v in flags.items() if not v]
    _ok(f"전략 ON={on} OFF={off}")
    # HTS 스킵은 false 유지 규칙
    for key in (
        "TAIL_SKIP_HTS_SCAN_DUPES",
        "SHORT_SKIP_HTS_SCAN_DUPES",
        "MOMENTUM_SKIP_HTS_SCAN_DUPES",
        "BREAKOUT_SKIP_HTS_SCAN_DUPES",
        "SCALP_SKIP_HTS_SCAN_DUPES",
    ):
        if get_env_bool(key, False):
            _err(f"{key}=true (기본 false 유지 규칙 위반)")
        else:
            _ok(f"{key}=false")


def step_market_hours(fake_now: datetime) -> None:
    from kis_trader.strategies.base import BaseStrategy
    from kis_trader.network.market_guard import MarketGuard

    class _Dummy(BaseStrategy):
        strategy_id = "SHORT"

        def __init__(self):
            # Thread/풀 초기화 우회: 최소 속성만
            self.strategy_id = "SHORT"

        def check_buy(self, *a, **k):  # pragma: no cover
            return False

        def check_sell_signals(self, *a, **k):  # pragma: no cover
            return None

        def run(self):  # pragma: no cover
            return None

    with patch("kis_trader.strategies.base.dt") as mock_dt, patch(
        "kis_trader.network.market_guard.dt"
    ) as mock_dt2:
        mock_dt.now.return_value = fake_now
        mock_dt2.now.return_value = fake_now
        d = _Dummy()
        # BaseStrategy.check_market_status 는 self 만 필요
        open_ok = BaseStrategy.check_market_status(d)
        buy_ok = BaseStrategy.check_buy_allowed(d)
        mg_ok = MarketGuard._is_market_hours()
    if open_ok and buy_ok and mg_ok:
        _ok(f"장시간 판정 (fake {fake_now}): market=True buy=True guard=True")
    else:
        _err(
            f"장시간 판정 실패: market={open_ok} buy={buy_ok} guard={mg_ok} "
            f"fake={fake_now}"
        )


def step_engine_defaults() -> None:
    try:
        from kis_trader.engine import momentum_engine as me
        from kis_trader.engine import scalping_engine as se
        from kis_trader.engine import tail_engine as te
        from kis_trader.strategies import breakout as bo

        te_d = te.get_tail_defaults_from_db() if hasattr(te, "get_tail_defaults_from_db") else None
        me_d = me.get_momentum_defaults_from_db()
        # scalping / breakout
        if hasattr(se, "get_scalping_defaults_from_db"):
            se_d = se.get_scalping_defaults_from_db()
        else:
            se_d = {"ok": True}
        from kis_trader.backtest import breakout_backtest_common as bbc
        from kis_trader.utils.env import get_merged_env_dict

        env_row = get_merged_env_dict() or {}
        if hasattr(bbc, "get_breakout_defaults_from_env_row"):
            bo_d = bbc.get_breakout_defaults_from_env_row(env_row)
        else:
            bo_d = {}
        _ok(
            f"엔진 DB 기본값 로드: tail_keys={len(te_d or {})} "
            f"mom={len(me_d or {})} scalp={len(se_d or {})} bo={len(bo_d or {})}"
        )
        # 손절 키 존재 스모크
        for name, d in (("mom", me_d),):
            if d and "sl_pct" in d and float(d["sl_pct"]) <= 0:
                _err(f"{name} sl_pct 비정상: {d.get('sl_pct')}")
    except Exception as e:
        _err(f"엔진 기본값 로드 실패: {e}")
        log.error(traceback.format_exc())


def step_account_and_open_report() -> None:
    """REST 잔고만 — WS/주문 없음. 장시작 리포트 문자열 생성."""
    try:
        from kis_trader.execution.kis_client import KISClient

        client = KISClient()
        # TradingBot._fetch_asset_snapshot 과 유사하게 잔고 조회
        bal = None
        for meth in ("get_balance", "inquire_balance", "account_balance"):
            fn = getattr(client, meth, None)
            if callable(fn):
                try:
                    bal = fn()
                    break
                except Exception:
                    continue
        if bal is None:
            # inquire-balance 계열 탐색
            for name in dir(client):
                if "balance" in name.lower() and callable(getattr(client, name)):
                    try:
                        bal = getattr(client, name)()
                        if bal:
                            break
                    except Exception:
                        continue
        mock = getattr(client, "mock", None)
        acct = getattr(client, "account_no", "") or ""
        _ok(f"KISClient 생성 ok mock={mock} acct=***{str(acct)[-4:]}")
        if bal is not None:
            _ok(f"잔고 조회 응답 type={type(bal).__name__}")
        else:
            # 주말이면 모의/실전 REST 가 비정상일 수 있음 — 치명으로 안 봄
            log.warning("⚠️ 잔고 조회 메서드 미확인/실패 (장외 REST 가능) — 계속")

        # 장시작 리포트 포맷만 검증 (MM 미전송)
        lines = [
            "🌅 **[장 시작 알림 - 09:00]** (SIM)",
            f"- 🤖 활성 전략: (smoke)",
            f"- 계좌: {'모의' if mock else '실전'}",
            "📈 오늘도 안전 매매! 손절 라인 준수.",
        ]
        body = "\n".join(lines)
        assert "장 시작" in body
        _ok("장시작 리포트 문자열 생성 OK (미전송)")
    except Exception as e:
        _err(f"계좌/장시작 리포트 실패: {e}")
        log.error(traceback.format_exc())


def step_verify_three_paths() -> None:
    try:
        from kis_trader.scripts import verify_three_paths as v3

        bad = 0
        for fn_name in ("verify_momentum", "verify_breakout", "verify_scalping", "verify_tail"):
            fn = getattr(v3, fn_name, None)
            if not callable(fn):
                continue
            n = int(fn() or 0)
            bad += n
            if n:
                _err(f"{fn_name} mismatch={n}")
            else:
                _ok(f"{fn_name} parity OK")
        if bad == 0:
            _ok("실매↔웹↔파람 변환 정합 스모크 통과")
    except Exception as e:
        _err(f"verify_three_paths 실패: {e}")
        log.error(traceback.format_exc())


def step_prev_day_backtest_smoke() -> None:
    """직전 거래일 1일 · 꼬리+돌파 웹 API 경로 (Flask test_client, 서버 불필요)."""
    from kis_trader.utils.kr_trading_day import clamp_to_prev_kr_trading_day

    day = clamp_to_prev_kr_trading_day(date.today())
    try:
        from backtest_web import app

        with app.test_client() as c:
            for label, path in (
                ("꼬리", "/api/backtest/tail"),
                ("돌파", "/api/backtest/breakout"),
            ):
                r = c.get(
                    path,
                    query_string={
                        "start": day,
                        "end": day,
                        "universe": "history",
                    },
                )
                if r.status_code != 200:
                    _err(f"{label} 백테 HTTP {r.status_code}")
                    continue
                d = r.get_json(silent=True) or {}
                if d.get("error"):
                    _err(f"{label} 백테 error: {d.get('error')}")
                    continue
                s = d.get("summary") or {}
                _ok(
                    f"{label} 백테 {day}: trades={s.get('total_trades', '?')} "
                    f"pnl={s.get('total_pnl', '?')}"
                )
    except Exception as e:
        _err(f"웹 백테 스모크 실패: {e}")
        log.error(traceback.format_exc())


def step_holdings_db() -> None:
    try:
        from kis_trader.utils.env import _get_db

        db = _get_db()
        if not db:
            _err("TradeDB 연결 실패")
            return
        cols = db.conn.execute("SHOW COLUMNS FROM active_trades").fetchall()
        col_names = [
            (c["Field"] if isinstance(c, dict) else c[0]) for c in (cols or [])
        ]
        if "status" in col_names:
            rows = db.conn.execute(
                "SELECT strategy, COUNT(*) AS n FROM active_trades "
                "WHERE status=%s GROUP BY strategy",
                ("HOLDING",),
            ).fetchall()
        else:
            rows = db.conn.execute(
                "SELECT strategy, COUNT(*) AS n FROM active_trades GROUP BY strategy"
            ).fetchall()
        summary = []
        for r in rows or []:
            if isinstance(r, dict):
                summary.append(f"{r.get('strategy')}={r.get('n')}")
            else:
                summary.append(f"{r[0]}={r[1]}")
        _ok(f"active_trades HOLDING: {', '.join(summary) or '(없음)'}")
    except Exception as e:
        _err(f"active_trades 조회 실패: {e}")
        log.error(traceback.format_exc())


def main() -> int:
    log.info("=== market_open_sim_smoke START ===")
    log.info("log=%s", LOG_PATH)
    # 실매 FORCE 오염 방지
    os.environ.pop("FORCE_MARKET_OPEN", None)
    os.environ.pop("FORCE_BUY_TEST", None)

    try:
        fake_now = step_calendar()
        step_strategy_flags()
        step_market_hours(fake_now)
        step_engine_defaults()
        step_holdings_db()
        step_account_and_open_report()
        step_verify_three_paths()
        step_prev_day_backtest_smoke()
    except Exception as e:
        _err(f"치명: {e}")
        log.error(traceback.format_exc())

    log.info("=== SUMMARY ok=%d err=%d ===", len(oks), len(errors))
    for e in errors:
        log.info("ERR: %s", e)
    if errors:
        log.info("RESULT: FAIL")
        return 1
    log.info("RESULT: PASS")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
