#!/usr/bin/env python3
"""
kis_trader/strategies/dart_strategy.py — DART 수주 공시 실매 전략
================================================================
유니버스 = dart_watchlist (구독 스위치) 또는 당일 disclosures.
매매 = STRATEGY_DART_ENABLED + DART_TRADE_ENABLED.
"""
from __future__ import annotations

from datetime import datetime as dt
from typing import Dict, List, Optional

from ..engine import dart_engine as de
from ..scan.dart_watchlist import event_time_for_code, list_active_watch
from ..utils.env import get_env_bool, get_env_int
from .base import BaseStrategy


class DartStrategy(BaseStrategy):
    strategy_id = "DART"
    loop_min_sleep = 1.0
    loop_max_sleep = 2.5

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.candle_tf = 1
        self._engine_params: Optional[Dict] = None
        self.reload_config()

    def reload_config(self) -> None:
        try:
            base = de.get_dart_defaults_from_db()
            self._engine_params = base
            self.min_price = float(base.get("min_price", 1000.0))
            self.stop_loss_pct = -abs(float(base.get("sl_pct", 0.02)))
            self.take_profit_pct = abs(float(base.get("tp_pct", 0.04)))
            self.slot_money = int(base.get("slot_money", 300_000))
            self.time_start_hm = int(base.get("time_start_hm", 930))
            self.time_end_hm = int(base.get("time_end_hm", 1520))
        except Exception as e:
            self.logger.debug("dart defaults 실패: %s", e)
            self._engine_params = {}

    def _trade_on(self) -> bool:
        return get_env_bool("DART_TRADE_ENABLED", False)

    def _in_trade_window(self) -> bool:
        now = dt.now()
        hm = now.hour * 100 + now.minute
        return self.time_start_hm <= hm < self.time_end_hm

    def _db_raw(self):
        return self.db.raw if hasattr(self.db, "raw") else self.db

    def _load_candidates(self) -> List[Dict]:
        out: List[Dict] = []
        lim = get_env_int("DART_CAND_LIMIT", 20) or 20
        try:
            conn_db = self._db_raw()
            watches = list_active_watch(conn_db)
            for w in watches:
                code = (w.get("stock_code") or "").strip()
                if not code:
                    continue
                out.append({
                    "code": code.zfill(6) if code.isdigit() else code,
                    "name": w.get("corp_name") or code,
                    "score": 0.0,
                })
            if out:
                return out[:lim]
            today0 = dt.now().strftime("%Y-%m-%d") + " 00:00:00"
            rows = conn_db.conn.execute(
                """
                SELECT stock_code, corp_name FROM dart_disclosures
                WHERE first_seen_at >= %s AND stock_code <> ''
                ORDER BY first_seen_at DESC LIMIT %s
                """,
                (today0, lim),
            ).fetchall()
            for r in rows:
                code = (r.get("stock_code") or "").strip()
                if not code:
                    continue
                out.append({
                    "code": code.zfill(6) if code.isdigit() else code,
                    "name": r.get("corp_name") or code,
                    "score": 0.0,
                })
        except Exception as e:
            self.logger.debug("DART 후보 로드 실패: %s", e)
        return out

    def _candidate_filter(self, candidate: Dict) -> bool:
        if not self._trade_on():
            return False
        if not candidate.get("code"):
            return False
        if not self._in_trade_window():
            return False
        return True

    def _norm_candle(self, c: dict) -> dict:
        ct = c.get("candle_time") or c.get("candle_time_str", "")
        if isinstance(ct, str) and len(ct) == 19 and " " in ct:
            ct = ct.replace("-", "").replace(" ", "").replace(":", "")[:12]
        return {
            "candle_time": ct,
            "open": float(c.get("open", 0)),
            "high": float(c.get("high", 0)),
            "low": float(c.get("low", 0)),
            "close": float(c.get("close", 0)),
            "volume": float(c.get("volume", 0)),
            "is_confirmed": c.get("is_confirmed", 1),
        }

    def _force_buy_test(self, code: str, name: str) -> Optional[Dict]:
        wsd = self.ws.get_price(code)
        px = 0.0
        if wsd:
            try:
                px = abs(float(str(wsd.get("stck_prpr", 0)).replace(",", "")))
            except Exception:
                px = 0.0
        if px <= 0:
            return None
        qty, rej = self._resolve_buy_qty_live(px)
        if rej or qty < 1:
            return None
        return {
            "code": code,
            "name": name,
            "price": px,
            "qty": qty,
            "stop_price": px * (1 + self.stop_loss_pct),
            "target_price": px * (1 + self.take_profit_pct),
            "atr_entry": 0.0,
            "size_class": "",
            "entry_features": {"force_test": True},
        }

    def check_buy(self, code: str, name: str) -> Optional[Dict]:
        if not self._trade_on() or not self._in_trade_window():
            return None
        try:
            if get_env_bool("FORCE_BUY_TEST", False):
                return self._force_buy_test(code, name)

            params = self._engine_params or de.get_dart_defaults_from_db()
            ev = event_time_for_code(self._db_raw(), code)
            if not ev:
                return None

            need = de.dart_min_bars_required(params)
            candles_raw = self.ws.get_candles(code, self.candle_tf, n=need + 30)
            if len(candles_raw) < need:
                try:
                    self.ws.fill_gap([code], force=True)
                except Exception:
                    pass
                return None
            candles = [self._norm_candle(c) for c in candles_raw]
            reject, msg, sig = de.check_buy_signal_dart_live(
                candles, params, event_candle_time=ev,
            )
            if reject:
                self.logger.info("🔍 [%s] %s(%s) %s", reject, name, code, msg or "")
                return None
            if not sig:
                return None

            curr_price = float(sig.get("entry_price") or 0)
            if curr_price <= 0 or curr_price < self.min_price:
                return None
            hard_cap = get_env_int("DART_MAX_BUY_AMOUNT", 0) or get_env_int(
                "MAX_BUY_AMOUNT_PER_STOCK", 0,
            )
            qty, rej = self._resolve_buy_qty_live(curr_price, hard_cap=hard_cap)
            if rej or qty < 1:
                return None
            return {
                "code": code,
                "name": name,
                "price": curr_price,
                "qty": qty,
                "stop_price": curr_price * (1 + self.stop_loss_pct),
                "target_price": curr_price * (1 + self.take_profit_pct),
                "atr_entry": 0.0,
                "size_class": "",
                "entry_features": {
                    "rsi": sig.get("rsi"),
                    "event_bars": sig.get("event_bars"),
                    "strategy": "DART",
                },
            }
        except Exception as e:
            self.logger.info("🔍 [탈락-예외] %s %s: %s", name, code, e)
            return None

    def check_sell_signals(self) -> List[Dict]:
        if not self.holdings:
            return []
        signals: List[Dict] = []
        params = self._engine_params or de.get_dart_defaults_from_db()
        now = dt.now()
        hm = now.hour * 100 + now.minute
        for code, holding in list(self.holdings.items()):
            try:
                name = holding.get("name", code)
                buy_price = float(holding.get("buy_price", 0))
                qty = int(holding.get("qty", 0))
                max_price = float(holding.get("max_price", buy_price))
                if qty <= 0 or buy_price <= 0:
                    continue
                current_price = 0.0
                wsd = self.ws.get_price(code)
                if wsd:
                    try:
                        current_price = abs(
                            float(str(wsd.get("stck_prpr", 0)).replace(",", ""))
                        )
                    except Exception:
                        current_price = 0.0
                if current_price <= 0:
                    continue
                if current_price > max_price:
                    max_price = current_price
                    holding["max_price"] = max_price
                bars = int(holding.get("bars_held") or 0)
                sell, reason = de.check_sell_signal_dart_live(
                    buy_price=buy_price,
                    highest=max_price,
                    last_price=current_price,
                    bars_held=bars,
                    params=params,
                    now_hm=hm,
                )
                if sell:
                    signals.append({
                        "code": code,
                        "name": name,
                        "qty": qty,
                        "reason": reason,
                        "price": current_price,
                    })
            except Exception as e:
                self.logger.error("DART 매도 체크 %s: %s", code, e)
        return signals
