"""
kis_trader/strategies/dbband_stock_cfg.py
=========================================
더블 볼린저(DBBAND) 종목별 파라미터 — ``dbband_stock_config`` 단일 테이블.

- 종목 행이 있으면 그 값만 사용 (env ``DBBAND_*`` 로 덮지 않음).
- 행이 없을 때만 env 폴백 (그리드 끝값·전역 기본).
- QQQM(나스닥100)·069500(KOSPI) 등 **종목마다 BB/MA/손익비를 따로** 저장·탐색.

분봉(tf): ``dbband_stock_config.tf_min`` 양수 → 해당 분봉, 아니면 env ``DBBAND_TIMEFRAME``.
"""
from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional, Tuple

from ..engine.dbband_engine import CFG_ENGINE_KEYS, DEFAULT_DBBAND_CONFIG
from ..utils.env import get_env_int

logger = logging.getLogger("kis_trader.dbband_stock_cfg")

_STR_COLS = ("side_mode", "entry_mode", "stop_mode", "tp_mode", "exit_mode")

_DBBAND_STOCK_DDL = """
CREATE TABLE IF NOT EXISTS dbband_stock_config (
    id                      BIGINT AUTO_INCREMENT PRIMARY KEY,
    code                    VARCHAR(32)  NOT NULL,
    market_type             VARCHAR(8)   NOT NULL DEFAULT 'KR',
    exchange                VARCHAR(16)  NOT NULL DEFAULT 'KRX',
    symbol                  VARCHAR(32)  NOT NULL DEFAULT '',
    name                    VARCHAR(50)  DEFAULT '',
    created_at              DATETIME     DEFAULT CURRENT_TIMESTAMP,
    tf_min                  INT          NOT NULL DEFAULT 15,
    bb_period               INT          NOT NULL DEFAULT 20,
    bb_inner_std            DOUBLE       NOT NULL DEFAULT 2,
    bb_outer_std            DOUBLE       NOT NULL DEFAULT 3,
    trend_ma_period         INT          NOT NULL DEFAULT 200,
    use_trend_filter        DOUBLE       NOT NULL DEFAULT 1,
    side_mode               VARCHAR(16)  NOT NULL DEFAULT 'long_only',
    entry_valid_bars        INT          NOT NULL DEFAULT 3,
    entry_mode              VARCHAR(16)  NOT NULL DEFAULT 'break_high',
    stop_mode               VARCHAR(16)  NOT NULL DEFAULT 'signal_low',
    stop_buffer_pct         DOUBLE       NOT NULL DEFAULT 0.1,
    stop_loss_pct           DOUBLE       NOT NULL DEFAULT 2,
    tp_mode                 VARCHAR(16)  NOT NULL DEFAULT 'opposite_band',
    take_profit_pct         DOUBLE       NOT NULL DEFAULT 3,
    rr_ratio                DOUBLE       NOT NULL DEFAULT 2,
    exit_mode               VARCHAR(16)  NOT NULL DEFAULT 'classic',
    shoulder_min_high_pct   DOUBLE       NOT NULL DEFAULT 0.3,
    shoulder_cut_pct        DOUBLE       NOT NULL DEFAULT 0.2,
    trail_pct               DOUBLE       NOT NULL DEFAULT 0,
    trail_arm_pct           DOUBLE       NOT NULL DEFAULT 0,
    max_hold_bars           INT          NOT NULL DEFAULT 0,
    slot_money              DOUBLE       NOT NULL DEFAULT 3000000,
    cooldown_min            DOUBLE       NOT NULL DEFAULT 15,
    max_daily               INT          NOT NULL DEFAULT 3,
    KEY idx_dbband_stock_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DBBAND 더블볼린저 종목별 파라미터'
"""


def _trade_db_core(db: Any):
    return getattr(db, "raw", db)


def ensure_dbband_stock_config_table(db: Any) -> None:
    raw = _trade_db_core(db)
    raw.conn.execute(_DBBAND_STOCK_DDL.strip())
    try:
        raw.conn.execute(
            "ALTER TABLE dbband_stock_config "
            "ADD COLUMN exit_mode VARCHAR(16) NOT NULL DEFAULT 'classic'"
        )
        raw.conn.commit()
    except Exception:
        pass
    try:
        raw.conn.execute(
            "UPDATE dbband_stock_config SET symbol = code "
            "WHERE (symbol IS NULL OR symbol = '') AND code IS NOT NULL AND code != ''"
        )
        raw.conn.commit()
    except Exception:
        pass


def ensure_dbband_backtest_tables(db: Any) -> None:
    try:
        import holding_bot as hb
        hb.ensure_holding_tables(db)
    except Exception as e:
        logger.warning("holding 분봉 테이블 확인 경고: %s", e)
    ensure_dbband_stock_config_table(db)
    logger.info("✅ DBBAND DB 확인: holding_min_candles + dbband_stock_config")


def _pct_to_frac(v: float) -> float:
    """DB 퍼센트(2.0) → 엔진 비율(0.02). 1 미만이면 이미 비율."""
    try:
        x = float(v)
    except (TypeError, ValueError):
        return 0.0
    return x / 100.0 if x >= 1.0 else x


def _engine_cfg_from_row(row: Dict[str, Any]) -> Dict[str, Any]:
    out: Dict[str, Any] = {}
    for k in _STR_COLS:
        out[k] = str(row.get(k) or DEFAULT_DBBAND_CONFIG.get(k, "")).strip().lower()

    out["bb_period"] = int(float(row.get("bb_period") or 20))
    out["bb_inner_std"] = float(row.get("bb_inner_std") or 2.0)
    out["bb_outer_std"] = float(row.get("bb_outer_std") or 3.0)
    out["trend_ma_period"] = int(float(row.get("trend_ma_period") or 200))
    utf = row.get("use_trend_filter", 1)
    out["use_trend_filter"] = bool(utf) if isinstance(utf, bool) else float(utf) >= 0.5
    out["entry_valid_bars"] = int(float(row.get("entry_valid_bars") or 3))
    out["sl_pct"] = _pct_to_frac(row.get("stop_loss_pct", 2.0))
    out["tp_pct"] = _pct_to_frac(row.get("take_profit_pct", 3.0))
    out["stop_buffer_pct"] = _pct_to_frac(row.get("stop_buffer_pct", 0.1))
    out["rr_ratio"] = float(row.get("rr_ratio") or 2.0)
    out["exit_mode"] = str(row.get("exit_mode") or "classic").strip().lower()
    out["shoulder_min_high"] = _pct_to_frac(row.get("shoulder_min_high_pct", 0.3))
    out["shoulder_cut_pct"] = _pct_to_frac(row.get("shoulder_cut_pct", 0.2))
    out["trail_pct"] = _pct_to_frac(row.get("trail_pct", 0.0))
    out["trail_arm_pct"] = _pct_to_frac(row.get("trail_arm_pct", 0.0))
    out["max_hold_bars"] = int(float(row.get("max_hold_bars") or 0))
    out["slot_money"] = float(row.get("slot_money") or 3_000_000)
    out["cooldown_min"] = float(row.get("cooldown_min") or 15.0)
    out["max_daily"] = int(float(row.get("max_daily") or 3))
    out["tf_min"] = int(float(row.get("tf_min") or 15))
    out["timeframe"] = out["tf_min"]
    return out


def resolve_market_meta(
    code: str,
    market_type: Optional[str] = None,
    exchange: Optional[str] = None,
    symbol: Optional[str] = None,
) -> Tuple[str, str, str]:
    c = str(code or symbol or "").strip().upper()
    mt = str(market_type or "KR").strip().upper()
    if mt not in ("KR", "US"):
        mt = "KR"
    ex = str(exchange or ("KRX" if mt == "KR" else "NASD")).strip().upper()
    sym = str(symbol or c).strip().upper()
    if c.isdigit() and len(c) == 6:
        return "KR", ex if ex not in ("NASD", "NAS", "NYSE", "NYS") else "KRX", sym or c
    if c.isalpha() and 1 <= len(c) <= 8 and (mt == "KR" or ex in ("", "KRX")):
        return "US", "NASD", sym or c
    return mt, ex or ("KRX" if mt == "KR" else "NASD"), sym or c


def get_dbband_stock_config_row(db: Any, code: str) -> Optional[Dict[str, Any]]:
    ensure_dbband_stock_config_table(db)
    code = str(code or "").strip()
    if not code:
        return None
    raw = _trade_db_core(db)
    row = raw.conn.execute(
        "SELECT * FROM dbband_stock_config WHERE code=%s OR symbol=%s ORDER BY id DESC LIMIT 1",
        [code, code],
    ).fetchone()
    if not row:
        return None
    return _engine_cfg_from_row(dict(row))


def get_dbband_stock_meta(db: Any, code: str) -> Optional[Dict[str, Any]]:
    ensure_dbband_stock_config_table(db)
    code = str(code or "").strip()
    if not code:
        return None
    raw = _trade_db_core(db)
    row = raw.conn.execute(
        "SELECT code, market_type, exchange, symbol, name, tf_min, created_at "
        "FROM dbband_stock_config WHERE code=%s OR symbol=%s ORDER BY id DESC LIMIT 1",
        [code, code],
    ).fetchone()
    return dict(row) if row else None


def set_dbband_stock_config(
    db: Any,
    code: str,
    name: str,
    engine_cfg: Dict[str, Any],
    tf_min: Optional[int] = None,
    market_type: Optional[str] = None,
    exchange: Optional[str] = None,
    symbol: Optional[str] = None,
) -> None:
    ensure_dbband_stock_config_table(db)
    code = str(code or symbol or "").strip().upper()
    if not code:
        raise ValueError("code 필수")
    mt, ex, sym = resolve_market_meta(code, market_type, exchange, symbol)
    base = dict(DEFAULT_DBBAND_CONFIG)
    for k in CFG_ENGINE_KEYS:
        if k in engine_cfg and engine_cfg[k] is not None:
            base[k] = engine_cfg[k]
    try:
        tfv = int(float(tf_min if tf_min is not None else engine_cfg.get("tf_min", base.get("timeframe", 15))))
    except (TypeError, ValueError):
        tfv = int(get_env_int("DBBAND_TIMEFRAME", 15))
    if tfv < 1:
        tfv = int(get_env_int("DBBAND_TIMEFRAME", 15))

    num_cols = [
        "bb_period", "bb_inner_std", "bb_outer_std", "trend_ma_period", "use_trend_filter",
        "entry_valid_bars", "stop_buffer_pct", "stop_loss_pct", "take_profit_pct", "rr_ratio",
        "shoulder_min_high_pct", "shoulder_cut_pct", "trail_pct", "trail_arm_pct",
        "max_hold_bars", "slot_money", "cooldown_min", "max_daily",
    ]
    str_cols = list(_STR_COLS)
    cols = ["code", "market_type", "exchange", "symbol", "name", "tf_min"] + num_cols + str_cols
    vals: List[Any] = [code, mt, ex, sym, str(name or "").strip() or code, tfv]
    row_map = {
        "stop_loss_pct": abs(float(engine_cfg.get("stop_loss_pct") or engine_cfg.get("sl_pct") or base.get("stop_loss_pct", 2))),
        "take_profit_pct": abs(float(engine_cfg.get("take_profit_pct") or engine_cfg.get("tp_pct") or base.get("take_profit_pct", 3))),
        "shoulder_min_high_pct": float(engine_cfg.get("shoulder_min_high_pct") or engine_cfg.get("shoulder_min_high") or 0.3),
        "shoulder_cut_pct": float(engine_cfg.get("shoulder_cut_pct") or 0.2),
        "use_trend_filter": 1.0 if engine_cfg.get("use_trend_filter", True) in (True, 1, "1", "true") else 0.0,
    }
    for nc in num_cols:
        if nc in row_map:
            vals.append(row_map[nc])
        elif nc in engine_cfg:
            vals.append(float(engine_cfg[nc]))
        else:
            vals.append(float(base.get(nc, DEFAULT_DBBAND_CONFIG.get(nc, 0))))
    for sc in str_cols:
        vals.append(str(engine_cfg.get(sc) or base.get(sc) or DEFAULT_DBBAND_CONFIG.get(sc, "")).strip().lower())

    placeholders = ", ".join(["%s"] * len(cols))
    col_sql = ", ".join(cols)
    raw = _trade_db_core(db)
    raw.conn.execute(
        f"INSERT INTO dbband_stock_config ({col_sql}) VALUES ({placeholders})",
        vals,
    )
    raw.conn.commit()


def load_dbband_engine_cfg(db: Any, code: str, env_fallback: Dict[str, Any]) -> Dict[str, Any]:
    row_cfg = get_dbband_stock_config_row(db, code)
    if row_cfg:
        return row_cfg
    return dict(env_fallback)


def fetch_latest_dbband_stock_config_by_code(db: Any) -> Dict[str, Dict[str, Any]]:
    ensure_dbband_stock_config_table(db)
    raw = _trade_db_core(db)
    sql = """
        SELECT d.* FROM dbband_stock_config d
        INNER JOIN (
            SELECT code, MAX(id) AS mx FROM dbband_stock_config GROUP BY code
        ) t ON d.code = t.code AND d.id = t.mx
        ORDER BY d.code
    """
    rows = raw.conn.execute(sql).fetchall() or []
    out: Dict[str, Dict[str, Any]] = {}
    for row in rows:
        d = dict(row)
        code = str(d.get("code", "")).strip()
        if not code:
            continue
        eng = _engine_cfg_from_row(d)
        eng["name"] = str(d.get("name") or code).strip() or code
        eng["tf_min"] = int(float(d.get("tf_min") or get_env_int("DBBAND_TIMEFRAME", 15)))
        mt, ex, sym = resolve_market_meta(
            code, d.get("market_type"), d.get("exchange"), d.get("symbol"),
        )
        eng["market_type"] = mt
        eng["exchange"] = ex
        eng["symbol"] = sym
        out[code] = eng
    return out


def list_dbband_stock_codes(db: Any) -> List[Dict[str, Any]]:
    by_code = fetch_latest_dbband_stock_config_by_code(db)
    out = []
    for c in sorted(by_code.keys()):
        eng = by_code[c]
        out.append({
            "code": c,
            "name": eng.get("name", c),
            "market_type": eng.get("market_type", "KR"),
            "exchange": eng.get("exchange", "KRX"),
            "symbol": eng.get("symbol", c),
        })
    return out


def effective_dbband_tf_for_code(db: Any, code: str, env_tf: int) -> int:
    meta = get_dbband_stock_meta(db, code)
    if meta and meta.get("tf_min"):
        try:
            t = int(float(meta["tf_min"]))
            if t >= 1:
                return t
        except (TypeError, ValueError):
            pass
    return int(env_tf)


def engine_cfg_to_ui(cfg: Dict[str, Any]) -> Dict[str, Any]:
    """엔진 cfg → 웹 입력란 (퍼센트 표시)."""
    def pct(v: Any, default: float = 0.0) -> float:
        try:
            x = float(v)
            return round(x * 100, 3) if 0 < x < 1 else round(x, 3)
        except (TypeError, ValueError):
            return default
    return {
        "bb_period": int(cfg.get("bb_period") or 20),
        "bb_inner_std": float(cfg.get("bb_inner_std") or 2.0),
        "bb_outer_std": float(cfg.get("bb_outer_std") or 3.0),
        "trend_ma_period": int(cfg.get("trend_ma_period") or 200),
        "use_trend_filter": bool(cfg.get("use_trend_filter", True)),
        "side_mode": str(cfg.get("side_mode") or "long_only"),
        "entry_valid_bars": int(cfg.get("entry_valid_bars") or 3),
        "entry_mode": str(cfg.get("entry_mode") or "break_high"),
        "stop_mode": str(cfg.get("stop_mode") or "signal_low"),
        "stop_buffer_pct": pct(cfg.get("stop_buffer_pct"), 0.1),
        "sl_pct": pct(cfg.get("sl_pct") or cfg.get("stop_loss_pct"), 2.0),
        "tp_mode": str(cfg.get("tp_mode") or "opposite_band"),
        "tp_pct": pct(cfg.get("tp_pct") or cfg.get("take_profit_pct"), 3.0),
        "rr_ratio": float(cfg.get("rr_ratio") or 2.0),
        "exit_mode": str(cfg.get("exit_mode") or "classic"),
        "shoulder_min_high": pct(cfg.get("shoulder_min_high"), 0.3),
        "shoulder_cut_pct": pct(cfg.get("shoulder_cut_pct"), 0.2),
        "trail_pct": pct(cfg.get("trail_pct"), 0.0),
        "trail_arm_pct": pct(cfg.get("trail_arm_pct"), 0.0),
        "max_hold_bars": int(cfg.get("max_hold_bars") or 0),
        "slot_money": int(float(cfg.get("slot_money") or 3_000_000)),
        "cooldown_min": float(cfg.get("cooldown_min") or 15.0),
        "max_daily": int(cfg.get("max_daily") or 3),
        "timeframe": int(cfg.get("tf_min") or cfg.get("timeframe") or 15),
    }
