"""
kis_trader/scripts/backfill_trigger_eval_from_log.py
====================================================
journalctl 로그에서 호가필터 판정 라인을 파싱 → ws_orderbook(source='log_backfill') 적재.

⚠️ 한계 (정직):
  - 로그엔 호가 "본체"(10호가/bid3/ask3 절대값)가 없다.
  - 매 판정마다 "처음 걸린 검사 1개"의 수치만 reject_msg 텍스트로 남는다.
  - 따라서 본체 재계산 백테엔 못 쓰고, "판정 재생 + 완화방향 근사 파람서치"용이다.

저장 정책:
  - source='log_backfill' → 본체 기반 백테(source='filter_eval')와 분리.
  - reject_code/reject_msg(원문 보존)/strategy/snap_time 저장.
  - 매도벽은 ask3 절대값을 ask_qty_l3 에 best-effort 채움 (있는 정보만).

사용:
  # ① 파일 1개 → 하루
  python -m kis_trader.scripts.backfill_trigger_eval_from_log /tmp/mom_today.txt --ymd 20260626

  # ② journalctl 에서 하루 자동 추출 (파일 불필요)
  python -m kis_trader.scripts.backfill_trigger_eval_from_log --journalctl --ymd 20260626

  # ③ journalctl 에서 이번주 5일치 (월~금) 한 방에
  python -m kis_trader.scripts.backfill_trigger_eval_from_log --journalctl --from 20260622 --to 20260626
"""
from __future__ import annotations

import argparse
import datetime
import re
import subprocess
import sys
from typing import Any, Dict, Iterable, List, Optional

from kis_trader.database.db_manager import get_db

LOG_BACKFILL_SOURCE = "log_backfill"
# 모든 전략(MOMENTUM·BREAKOUT·SHORT) 로그가 적재되는 systemd 유닛 (heartbeat 로그 동일 출처)
DEFAULT_JOURNAL_UNIT = "kis_trader_main.service"

# [09:02:22] [kis_trader.strategy.MOMENTUM] 🔍 [탈락-호가수급] 세미티에스 0017J0: 매수/매도잔량(3호가) 0.13 < 0.85
_REJECT_RE = re.compile(
    r"\[(?P<hms>\d{2}:\d{2}:\d{2})\]\s+"
    r"\[kis_trader\.strategy\.(?P<strategy>[A-Z]+)\]\s+"
    r"🔍\s+\[(?P<reject>탈락-[^\]]+)\]\s+"
    r".*?\s(?P<code>[0-9A-Z]{6}):\s+(?P<msg>.+?)\s*$"
)
# [10:47:18] [kis_trader.strategy.BREAKOUT] 🔍 [탈락-호가수급] 원익IPS(240810) 매수/매도잔량(3호가) 0.83 < 0.85
_REJECT_RE_PAREN = re.compile(
    r"\[(?P<hms>\d{2}:\d{2}:\d{2})\]\s+"
    r"\[kis_trader\.strategy\.(?P<strategy>[A-Z]+)\]\s+"
    r"🔍\s+\[(?P<reject>탈락-[^\]]+)\]\s+"
    r".*?\((?P<code>[0-9A-Z]{6})\)\s+(?P<msg>.+?)\s*$"
)
# [09:31:16] [kis_trader.strategy.MOMENTUM] 🎯 [MOMENTUM 시그널] 모헨즈(006920) price=4255 ...
_SIGNAL_RE = re.compile(
    r"\[(?P<hms>\d{2}:\d{2}:\d{2})\]\s+"
    r"\[kis_trader\.strategy\.(?P<strategy>[A-Z]+)\]\s+"
    r"🎯\s+\[[A-Z]+ 시그널\]\s+.*?\((?P<code>[0-9A-Z]{6})\)"
)

# 호가필터 단계에서만 발생하는 사유 (백필 대상)
_ORDERBOOK_REJECTS = {
    "탈락-호가수급",
    "탈락-매도벽",
    "탈락-호가스프레드",
    "탈락-매수호가얇음",
    "탈락-돌파매도벽",
    "탈락-꼬리지지부족",
}
# 매도벽: "매도3호가 합 7762주 > 허용 1380주" → ask3 절대값 추출
_ASK3_RE = re.compile(r"합\s+(\d+)주\s+>\s+허용\s+(\d+)주")


def _parse_line(line: str, ymd: str) -> Optional[Dict[str, Any]]:
    m = _REJECT_RE.search(line)
    if not m:
        m = _REJECT_RE_PAREN.search(line)
    if m and m.group("reject") in _ORDERBOOK_REJECTS:
        hms = m.group("hms").replace(":", "")
        msg = m.group("msg").strip()
        ask3 = 0
        am = _ASK3_RE.search(msg)
        if am:
            ask3 = int(am.group(1))
        return {
            "market": "KR",
            "code": m.group("code"),
            "snap_time": ymd + hms,
            "best_bid": 0, "best_ask": 0,
            "total_bid_qty": 0, "total_ask_qty": 0,
            "bid_qty_l3": 0, "ask_qty_l3": ask3,
            "levels_json": "{}",
            "source": LOG_BACKFILL_SOURCE,
            "strategy": m.group("strategy"),
            "reject_code": m.group("reject"),
            "reject_msg": msg[:255],
            "eval_price": 0,
        }
    s = _SIGNAL_RE.search(line)
    if s:
        hms = s.group("hms").replace(":", "")
        return {
            "market": "KR",
            "code": s.group("code"),
            "snap_time": ymd + hms,
            "best_bid": 0, "best_ask": 0,
            "total_bid_qty": 0, "total_ask_qty": 0,
            "bid_qty_l3": 0, "ask_qty_l3": 0,
            "levels_json": "{}",
            "source": LOG_BACKFILL_SOURCE,
            "strategy": s.group("strategy"),
            "reject_code": None,  # PASS (호가필터 통과)
            "reject_msg": "PASS-호가필터통과",
            "eval_price": 0,
        }
    return None


def _iter_journalctl_lines(ymd: str, unit: str) -> Iterable[str]:
    """journalctl 에서 해당 거래일(ymd) 하루치 로그 라인 스트리밍.

    로그 메시지엔 날짜가 없고 ``[HH:MM:SS]`` 만 있으므로,
    journalctl ``--since/--until`` 로 하루 범위를 잘라 ymd 를 확정한다.
    (호스트 TZ=KST 기준 — 거래일과 동일)
    """
    since = f"{ymd[0:4]}-{ymd[4:6]}-{ymd[6:8]} 00:00:00"
    until = f"{ymd[0:4]}-{ymd[4:6]}-{ymd[6:8]} 23:59:59"
    cmd = [
        "journalctl", "-u", unit, "--no-pager", "-o", "cat",
        "--since", since, "--until", until,
    ]
    proc = subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
        text=True, encoding="utf-8", errors="ignore", bufsize=1,
    )
    assert proc.stdout is not None
    try:
        for line in proc.stdout:
            yield line
    finally:
        proc.stdout.close()
        proc.wait()


def _parse_lines(lines: Iterable[str], ymd: str, *, strategy_only: str = "") -> tuple:
    """라인 이터러블 → (items, stats). 파일/journalctl 공통 코어."""
    items: List[Dict[str, Any]] = []
    stats: Dict[str, int] = {"파싱": 0, "거절": 0, "통과": 0, "스킵전략": 0}
    for line in lines:
        row = _parse_line(line, ymd)
        if row is None:
            continue
        if strategy_only and row["strategy"] != strategy_only.upper():
            stats["스킵전략"] += 1
            continue
        stats["파싱"] += 1
        if row["reject_code"]:
            stats["거절"] += 1
        else:
            stats["통과"] += 1
        items.append(row)
    return items, stats


def _insert_items(db, items: List[Dict[str, Any]], ymd: str) -> int:
    """동일 ymd+source 기존 백필 삭제 후 재적재 (idempotent)."""
    try:
        db.conn.execute(
            "DELETE FROM ws_orderbook WHERE source=%s AND snap_time LIKE %s",
            (LOG_BACKFILL_SOURCE, ymd + "%"),
        )
    except Exception as e:
        print("기존 백필 삭제 경고:", e)

    inserted = 0
    BATCH = 200
    for i in range(0, len(items), BATCH):
        chunk = items[i:i + BATCH]
        if hasattr(db.raw, "insert_ws_orderbook_eval_batch"):
            inserted += db.raw.insert_ws_orderbook_eval_batch(chunk)
        else:
            inserted += db.raw.insert_ws_orderbook_batch(chunk)
    return inserted


def backfill_one_day(
    ymd: str,
    *,
    log_path: str = "",
    use_journalctl: bool = False,
    unit: str = DEFAULT_JOURNAL_UNIT,
    strategy_only: str = "",
    dry_run: bool = False,
    db=None,
) -> Dict[str, int]:
    """하루치 백필 (파일 또는 journalctl 소스)."""
    if db is None:
        db = get_db()
        if hasattr(db.raw, "migrate_trigger_eval_columns"):
            db.raw.migrate_trigger_eval_columns()

    if use_journalctl:
        lines: Iterable[str] = _iter_journalctl_lines(ymd, unit)
        src_label = f"journalctl:{unit}"
        items, stats = _parse_lines(lines, ymd, strategy_only=strategy_only)
    else:
        src_label = log_path
        with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
            items, stats = _parse_lines(f, ymd, strategy_only=strategy_only)

    if dry_run:
        print(f"[DRY-RUN] {ymd} ({src_label}) 파싱 {stats['파싱']}건 "
              f"(거절 {stats['거절']}, 통과 {stats['통과']}) — INSERT 안 함")
        for r in items[:5]:
            print("  샘플:", r["snap_time"], r["strategy"], r["code"],
                  r["reject_code"] or "PASS", "|", r["reject_msg"])
        return stats

    inserted = _insert_items(db, items, ymd)
    stats["적재"] = inserted
    print(f"✅ 백필 완료: {ymd} ({src_label}) → ws_orderbook(log_backfill) {inserted}건 "
          f"(거절 {stats['거절']}, 통과 {stats['통과']})")
    return stats


# 하위호환 — 기존 호출부(파일+ymd) 유지
def backfill(log_path: str, ymd: str, *, strategy_only: str = "", dry_run: bool = False) -> Dict[str, int]:
    return backfill_one_day(
        ymd, log_path=log_path, use_journalctl=False,
        strategy_only=strategy_only, dry_run=dry_run,
    )


def _ymd_range(start_ymd: str, end_ymd: str) -> List[str]:
    """start~end (포함) 평일(월~금)만 YYYYMMDD 리스트. (주말 자동 스킵)"""
    d0 = datetime.datetime.strptime(start_ymd, "%Y%m%d").date()
    d1 = datetime.datetime.strptime(end_ymd, "%Y%m%d").date()
    out: List[str] = []
    cur = d0
    one = datetime.timedelta(days=1)
    while cur <= d1:
        if cur.weekday() < 5:  # 0=월 ~ 4=금 (주말 제외)
            out.append(cur.strftime("%Y%m%d"))
        cur += one
    return out


def main(argv: Optional[List[str]] = None) -> int:
    ap = argparse.ArgumentParser(description="호가필터 판정 로그 → ws_orderbook(log_backfill) 백필")
    ap.add_argument("log_path", nargs="?", default="",
                    help="로그 파일 경로 (단일일·파일소스 시). --journalctl 사용 시 생략")
    ap.add_argument("--ymd", help="거래일 YYYYMMDD (단일일)")
    ap.add_argument("--from", dest="ymd_from", help="시작일 YYYYMMDD (범위, journalctl 권장)")
    ap.add_argument("--to", dest="ymd_to", help="종료일 YYYYMMDD (범위)")
    ap.add_argument("--journalctl", action="store_true",
                    help="파일 대신 journalctl 에서 추출 (날짜별 자동 범위)")
    ap.add_argument("--unit", default=DEFAULT_JOURNAL_UNIT, help="journalctl 유닛명")
    ap.add_argument("--strategy", default="", help="특정 전략만 (예: MOMENTUM). 미지정=전체")
    ap.add_argument("--dry-run", action="store_true", help="INSERT 없이 파싱 통계만")
    args = ap.parse_args(argv)

    # 범위 모드 (--from/--to) — journalctl 강제
    if args.ymd_from or args.ymd_to:
        if not (args.ymd_from and args.ymd_to):
            print("--from 과 --to 는 함께 지정", file=sys.stderr)
            return 2
        for d in (args.ymd_from, args.ymd_to):
            if len(d) != 8 or not d.isdigit():
                print("--from/--to 는 YYYYMMDD 8자리", file=sys.stderr)
                return 2
        days = _ymd_range(args.ymd_from, args.ymd_to)
        if not days:
            print("범위 내 평일이 없습니다.", file=sys.stderr)
            return 2
        db = get_db()
        if hasattr(db.raw, "migrate_trigger_eval_columns"):
            db.raw.migrate_trigger_eval_columns()
        total = {"파싱": 0, "거절": 0, "통과": 0, "적재": 0}
        print(f"📅 범위 백필 {args.ymd_from}~{args.ymd_to} | 평일 {len(days)}일 "
              f"| 소스={'journalctl:' + args.unit if not args.log_path else args.log_path}")
        for d in days:
            st = backfill_one_day(
                d,
                log_path=args.log_path,
                use_journalctl=(args.journalctl or not args.log_path),
                unit=args.unit,
                strategy_only=args.strategy,
                dry_run=args.dry_run,
                db=db,
            )
            for k in ("파싱", "거절", "통과", "적재"):
                total[k] += int(st.get(k, 0))
        print(f"🧮 합계: 파싱 {total['파싱']}건 (거절 {total['거절']}, 통과 {total['통과']}) "
              f"| 적재 {total['적재']}건")
        return 0

    # 단일일 모드
    if not args.ymd or len(args.ymd) != 8 or not args.ymd.isdigit():
        print("--ymd 는 YYYYMMDD 8자리 (또는 --from/--to 범위 사용)", file=sys.stderr)
        return 2
    use_jc = args.journalctl or not args.log_path
    if not use_jc and not args.log_path:
        print("파일 경로 또는 --journalctl 중 하나 필요", file=sys.stderr)
        return 2
    backfill_one_day(
        args.ymd,
        log_path=args.log_path,
        use_journalctl=use_jc,
        unit=args.unit,
        strategy_only=args.strategy,
        dry_run=args.dry_run,
    )
    return 0


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