#!/usr/bin/env python3
"""
kis_trader/backtest/param_apply_ai.py
=====================================
파라미터 탐색 결과 JSON을 Gemini에 넘겨, 수익 나는 것 중 승률 높은 조합을
골라 DB env_config 에 적용한다.

- 스캘핑 :  최근 search_*.json          (top 배열) 중 total_pnl > 0 만 AI 후보
- 꼬리잡기:  최신 search_tail_*.json (또는 구 tail_search_*.json · 레거시 tail_param_result.json)
- 승률만 높거나 손익만 큰 것은 제외하고 "수익이 나는 것 중 승률이 높은 것" 을 고르도록 프롬프트 구성
- [안전장치] 총손익(total_pnl)이 양수인 조합이 하나도 없으면 AI 적용을 즉시 취소하여 계좌를 보호

위치 이관(2026-04 기준):
    backtest_scalping/param_apply_ai.py → kis_trader/backtest/param_apply_ai.py

경로 정책:
- ROOT            = kis_bot 프로젝트 루트 (kis_trader/backtest/파일 기준 3단계 위)
- HERE            = 본 스크립트 폴더 (kis_trader/backtest/)
- 결과 디렉터리   = [HERE/results] · [~/.kis_bot_search_results] · 구 [ROOT/backtest_scalping/results]
- tail 결과 파일 = search_tail_{mode}_{ts}.json (매 실행 새 파일 · momentum/breakout 동일 패턴)

사용: param_search.py / tail_param_search.py 에서 --apply-ai 옵션으로 호출 (이 모듈을 임포트해 사용).
"""

import os
import re
import sys
import json
import logging
from typing import List, Dict, Any, Optional

# 프로젝트 루트 (kis_bot) — 3단계 위
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if ROOT not in sys.path:
    sys.path.insert(0, ROOT)

# 본 스크립트 디렉터리 (kis_trader/backtest/) — param_search / tail_param_search 동일 폴더 임포트용
HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
    sys.path.insert(0, HERE)

from kis_trader.backtest.backtest_portfolio_common import merge_param_search_apply_source  # noqa: E402

logger = logging.getLogger("param_apply_ai")

# Gemini 클라이언트 (지연 초기화)
_gemini_client = None
_gemini_model_id = "gemini-2.5-flash"


# ──────────────────────────────────────────────────────────────────────────────
# 결과 파일 탐색 유틸 (신규 → 구 경로 fallback)
# ──────────────────────────────────────────────────────────────────────────────

def _candidate_result_dirs() -> List[str]:
    """결과 디렉터리 후보 [신규 > 홈 폴백 > 구] 순서로 반환."""
    return [
        os.path.join(HERE, "results"),
        os.path.join(os.path.expanduser("~"), ".kis_bot_search_results"),
        os.path.join(ROOT, "backtest_scalping", "results"),
    ]


def _find_latest_json(prefix: str) -> Optional[str]:
    """prefix(예: 'search_tail_', 'tail_search_') 로 시작하는 가장 최근 JSON 경로를 반환."""
    best_path, best_mtime = None, -1.0
    for d in _candidate_result_dirs():
        if not os.path.isdir(d):
            continue
        for f in os.listdir(d):
            if not (f.startswith(prefix) and f.endswith(".json")):
                continue
            p = os.path.join(d, f)
            try:
                m = os.path.getmtime(p)
            except OSError:
                continue
            if m > best_mtime:
                best_mtime = m
                best_path = p
    return best_path




# ──────────────────────────────────────────────────────────────────────────────
# Gemini
# ──────────────────────────────────────────────────────────────────────────────

def _get_gemini_client():
    """DB env_config 에서 GEMINI_API_KEY 를 읽어 클라이언트 생성."""
    global _gemini_client
    if _gemini_client is not None:
        return _gemini_client
    try:
        from database import TradeDB
        db = TradeDB()
        try:
            latest = db.get_latest_env()
            snap = (latest or {}).get("snapshot") or {}
            key = (snap.get("GEMINI_API_KEY") or "").strip()
        finally:
            db.close()
        if not key:
            msg = "❌ GEMINI_API_KEY 가 DB env_config 에 없습니다."
            logger.error(msg)
            print(msg)
            return None
        import google.genai as genai
        _gemini_client = genai.Client(api_key=key)
        return _gemini_client
    except ImportError:
        msg = "❌ google-genai 미설치. pip install google-genai"
        logger.error(msg)
        print(msg)
        return None
    except Exception as e:
        msg = f"❌ Gemini 클라이언트 초기화 실패: {e}"
        logger.error(msg)
        print(msg)
        return None


def _build_prompt(results: List[Dict], strategy_name: str, max_rank: int) -> str:
    """Gemini에 넘길 프롬프트 문자열 생성. 수익 나는 것 중 승률 높은 것 우선 선택 요청."""
    lines = [
        f"아래는 {strategy_name} 백테스트 파라미터 탐색 결과 상위 {len(results)}개입니다.",
        "각 행: 순위, 승률(%), 총손익(원), 거래수, Profit Factor",
        "",
    ]
    for i, r in enumerate(results, 1):
        pnl = r.get("total_pnl", 0)
        wr = r.get("win_rate", 0)
        trades = r.get("total_trades", 0)
        pf = r.get("pf", 0)
        lines.append(f"  {i:2d}위 | 승률 {wr}% | 손익 {pnl:+,}원 | 거래 {trades}건 | PF {pf}")
    lines.extend([
        "",
        "조건: 반드시 **총손익이 플러스(total_pnl > 0)** 인 것만 고려하고,",
        "그 중에서 **승률이 높은 것**을 우선으로 하나만 골라주세요.",
        f"1~{max_rank} 중 하나의 순위만 숫자로 답해주세요. 예: 3",
    ])
    return "\n".join(lines)


def _parse_rank_from_response(text: str, max_rank: int) -> Optional[int]:
    """Gemini 응답에서 1~max_rank 사이 숫자 하나 추출."""
    if not text:
        return None
    for m in re.finditer(r"\b([1-9]|[1-9][0-9]?)\b", text):
        n = int(m.group(1))
        if 1 <= n <= max_rank:
            return n
    return None


def pick_rank_by_ai(results: List[Dict], strategy_name: str, max_candidates: int = 50) -> Optional[int]:
    """
    결과 리스트(상위 max_candidates개)를 Gemini에 넘겨, 수익 나는 것 중 승률 높은 순위 하나 선택.
    Returns: 1-based rank (1~len(candidates)), 또는 None (실패 시).
    """
    candidates = results[:max_candidates] if len(results) > max_candidates else results
    if not candidates:
        return None
    max_rank = len(candidates)
    prompt = _build_prompt(candidates, strategy_name, max_rank)
    client = _get_gemini_client()
    if not client:
        return None
    try:
        response = client.models.generate_content(model=_gemini_model_id, contents=prompt)
        text = (response.text or "").strip()
        rank = _parse_rank_from_response(text, max_rank)
        if rank is not None:
            logger.info(f"🤖 Gemini 선택: {rank}위 (응답 일부: {text[:200]}...)")
            print(f"🤖 Gemini 선택: {rank}위")
        return rank
    except Exception as e:
        msg = f"❌ Gemini 호출 실패: {e}"
        logger.error(msg)
        print(msg)
        return None


# ──────────────────────────────────────────────────────────────────────────────
# 스캘핑 / 꼬리잡기 AI 적용 엔트리
# ──────────────────────────────────────────────────────────────────────────────

def apply_ai_scalp() -> bool:
    """
    최근 search_*.json 을 찾아 상위 목록 로드 → 수익 나는 것 중 Gemini가 고른 순위를 DB에 적용.
    Returns: 성공 여부.
    """
    latest_path = _find_latest_json("search_")
    if not latest_path:
        msg = "❌ search_*.json 파일이 없습니다. 먼저 탐색을 실행하세요."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    print(f"📂 사용 파일 (최근 결과): {latest_path}")

    with open(latest_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    top = data.get("top") or []
    if not top:
        msg = "❌ JSON에 top 배열이 없습니다."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    # [안전장치] 양수(수익)인 조합만 철저히 필터링
    profitable_top = [t for t in top if t.get("total_pnl", 0) > 0]
    if not profitable_top:
        print("\n🚨 scalp [안전장치 작동] 수익(총손익 > 0)이 발생하는 파라미터 조합이 하나도 없습니다.")
        print("🚨 확정 손실 파라미터를 라이브 봇에 적용할 수 없으므로 AI 자동 적용을 취소합니다.")
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    results = [
        {"total_pnl": t.get("total_pnl"), "win_rate": t.get("win_rate"),
         "total_trades": t.get("total_trades"), "pf": t.get("pf")}
        for t in profitable_top
    ]

    rank = pick_rank_by_ai(results, "스캘핑")
    if rank is None:
        msg = "❌ Gemini가 순위를 선택하지 못했습니다. 수동으로 --apply N 을 사용하세요."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    # 같은 폴더의 param_search_scalping 모듈 사용 (HERE 가 sys.path 에 들어있음)
    # 2026-05: param_search.py → param_search_scalping.py 로 rename (전략별 파일 분리)
    import param_search_scalping as _ps

    chosen = profitable_top[rank - 1]
    merged = merge_param_search_apply_source(chosen, data)
    if not merged:
        msg = "❌ 해당 순위에 merged_params/params가 없습니다."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    _ps._apply_to_db(merged)
    logger.info(f"✅ 스캘핑 {rank}위 조합을 DB에 적용했습니다. (파일: {latest_path})")
    print(f"✅ 스캘핑 {rank}위 조합을 DB에 적용했습니다.")
    return True


def apply_ai_tail() -> bool:
    """
    최신 search_tail_*.json (또는 구 tail_search_* / 레거시 tail_param_result.json) 을 읽어
    results 중 Gemini가 고른 순위를 DB에 적용.
    Returns: 성공 여부.
    """
    import tail_param_search as _tps

    path = _tps._find_tail_result_json()
    if not path:
        msg = (
            "❌ search_tail_*.json / tail_search_*.json / tail_param_result.json 을 찾을 수 없습니다."
        )
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    print(f"📂 사용 파일: {path}")

    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)

    results = data.get("results") or []
    if not results:
        msg = "❌ JSON에 results 배열이 없습니다."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    # [안전장치] 양수(수익)인 조합만 철저히 필터링
    profitable_results = [r for r in results if r.get("total_pnl", 0) > 0]
    if not profitable_results:
        print("\n🚨 tail [안전장치 작동] 수익(총손익 > 0)이 발생하는 파라미터 조합이 하나도 없습니다.")
        print("🚨 확정 손실 파라미터를 라이브 봇에 적용할 수 없으므로 AI 자동 적용을 취소합니다.")
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    list_for_ai = [
        {"total_pnl": r.get("total_pnl"), "win_rate": r.get("win_rate"),
         "total_trades": r.get("total_trades"), "pf": r.get("pf", 0)}
        for r in profitable_results
    ]

    rank = pick_rank_by_ai(list_for_ai, "꼬리잡기")
    if rank is None:
        msg = "❌ Gemini가 순위를 선택하지 못했습니다. 수동으로 --apply N 을 사용하세요."
        logger.error(msg)
        print(msg)
        print("📌 DB 미적용. 기존 설정 유지.")
        return False

    chosen = profitable_results[rank - 1]
    merged = merge_param_search_apply_source(chosen, data)
    _tps.apply_params_to_db(merged)
    logger.info(f"✅ 꼬리잡기 {rank}위 조합을 DB에 적용했습니다. (파일: {path})")
    print(f"✅ 꼬리잡기 {rank}위 조합을 DB에 적용했습니다.")
    return True
