"""
kis_trader/strategies/updow_holding_cfg.py
==========================================
UPDOW(하락매수) 종목별 파라미터 — **``updow_stock_config`` 단일 테이블** (웹·백테·실매·JSON 적용 동일).

- 종목 행이 있으면 그 값만 사용 (``env_config`` 의 ``UPDOW_*`` 로 덮지 않음).
- 행이 없을 때만 env ``UPDOW_*`` 폴백 (신규 종목·그리드 끝값 등 전역 설정용).
- ``holding_stock_config`` 의 ``updow_*`` 는 **최초 1회 마이그레이션** 용도만 (이후 미사용).

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

import logging
from typing import Any, Dict, Optional

from ..engine.limit_entry_common import ENTRY_ALIGN, ENTRY_LIMIT_ATR
from ..utils.env import get_env_int
from .updow_buy import CFG_ENGINE_KEYS, DEFAULT_UPDOW_CONFIG

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

_UPDOW_STOCK_DDL = """
CREATE TABLE IF NOT EXISTS updow_stock_config (
    id                  BIGINT AUTO_INCREMENT PRIMARY KEY,
    code                VARCHAR(10)  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,
    body_drop_min_pct   DOUBLE       NOT NULL,
    body_drop_max_pct   DOUBLE       NOT NULL DEFAULT 0,
    tp_pct              DOUBLE       NOT NULL,
    stop_loss_pct       DOUBLE       NOT NULL,
    max_hold_bars       INT          NOT NULL,
    exit_on_green       DOUBLE       NOT NULL DEFAULT 0,
    tf_min              INT          NOT NULL DEFAULT 60,
    slot_money          DOUBLE       NOT NULL DEFAULT 3000000,
    regime_ma_bars      INT          NOT NULL DEFAULT 0,
    regime_ma_ease_pct  DOUBLE       NOT NULL DEFAULT 0,
    atr_use_dynamic     DOUBLE       NOT NULL DEFAULT 0,
    atr_period          INT          NOT NULL DEFAULT 14,
    atr_sl_mult         DOUBLE       NOT NULL DEFAULT 2,
    atr_tp_mult         DOUBLE       NOT NULL DEFAULT 4,
    atr_sl_min_pct      DOUBLE       NOT NULL DEFAULT 0.8,
    atr_sl_max_pct      DOUBLE       NOT NULL DEFAULT 6,
    atr_tp_min_pct      DOUBLE       NOT NULL DEFAULT 1.5,
    atr_tp_max_pct      DOUBLE       NOT NULL DEFAULT 12,
    shoulder_min_high_pct DOUBLE     NOT NULL DEFAULT 0.3,
    shoulder_cut_pct    DOUBLE       NOT NULL DEFAULT 0.2,
    limit_atr_mult      DOUBLE       NOT NULL DEFAULT 1.5,
    entry_mode          VARCHAR(16)  NOT NULL DEFAULT '',
    KEY idx_updow_stock_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='UPDOW 하락매수 종목별 파라미터'
"""


def _trade_db_core(db: Any):
    """TradeDBExt 이면 ``.raw``, 아니면 그대로."""
    return getattr(db, "raw", db)


def ensure_updow_stock_config_table(db: Any) -> None:
    """``updow_stock_config`` 테이블 생성."""
    raw = _trade_db_core(db)
    raw.conn.execute(_UPDOW_STOCK_DDL.strip())
    # 구버전 테이블(시장 컬럼 없음) 호환 마이그레이션
    alters = (
        "ADD COLUMN market_type VARCHAR(8) NOT NULL DEFAULT 'KR' AFTER code",
        "ADD COLUMN exchange VARCHAR(16) NOT NULL DEFAULT 'KRX' AFTER market_type",
        "ADD COLUMN symbol VARCHAR(32) NOT NULL DEFAULT '' AFTER exchange",
        "ADD COLUMN shoulder_min_high_pct DOUBLE NOT NULL DEFAULT 0.3 AFTER atr_tp_max_pct",
        "ADD COLUMN shoulder_cut_pct DOUBLE NOT NULL DEFAULT 0.2 AFTER shoulder_min_high_pct",
        "ADD COLUMN body_drop_max_pct DOUBLE NOT NULL DEFAULT 0 AFTER body_drop_min_pct",
        "ADD COLUMN limit_atr_mult DOUBLE NOT NULL DEFAULT 1.5 AFTER shoulder_cut_pct",
        # 종목별 진입모드(align/limit_atr) — '' 이면 글로벌 env UPDOW_ENTRY_MODE 사용
        "ADD COLUMN entry_mode VARCHAR(16) NOT NULL DEFAULT '' AFTER limit_atr_mult",
    )
    for frag in alters:
        try:
            raw.conn.execute(f"ALTER TABLE updow_stock_config {frag}")
        except Exception:
            pass
    try:
        raw.conn.execute(
            "UPDATE updow_stock_config "
            "SET symbol = code "
            "WHERE (symbol IS NULL OR symbol = '') AND code IS NOT NULL AND code != ''"
        )
    except Exception:
        pass
    # 영문 티커(QQQM 등)가 KR/KRX 로 저장된 구버전 행 보정
    try:
        raw.conn.execute(
            "UPDATE updow_stock_config "
            "SET market_type='US', exchange='NASD', symbol=UPPER(code) "
            "WHERE code REGEXP '^[A-Za-z]{1,8}$' "
            "AND (market_type='KR' OR exchange='KRX' OR exchange='')"
        )
    except Exception:
        pass
    raw.conn.commit()


def ensure_updow_backtest_tables(db: Any) -> None:
    """
    UPDOW 백테·탐색·웹용 DB 준비.

    - ``holding_min_candles`` 등 분봉 저장 테이블 (``holding_bot.ensure_holding_tables``)
    - ``updow_stock_config`` 종목별 파라미터 (하락매수 전용, ``holding_stock_config`` 와 분리)
    """
    try:
        import holding_bot as hb

        hb.ensure_holding_tables(db)
    except Exception as e:
        logger.warning("holding 분봉 테이블 확인 경고: %s", e)
    ensure_updow_stock_config_table(db)
    logger.info("✅ UPDOW DB 확인: holding_min_candles(분봉) + updow_stock_config(종목 파라미터)")


def _engine_cfg_from_row(row: Dict[str, Any]) -> Dict[str, Any]:
    out: Dict[str, Any] = {}
    for k in CFG_ENGINE_KEYS:
        if k in row and row[k] is not None:
            out[k] = float(row[k])
        else:
            # 구버전 행·신규 추가 키 누락 시 엔진 기본값으로 보강 (KeyError 방지)
            out[k] = float(DEFAULT_UPDOW_CONFIG.get(k, 0.0))
    # 종목별 진입모드(문자열) — 유효값일 때만 주입.
    # 빈 문자열을 out["entry_mode"]에 넣으면 updow_entry_mode()가 align 으로 오인하므로
    # 반드시 valid 일 때만 키를 세팅하고, 아니면 키 자체를 넣지 않아 글로벌 env 로 폴백한다.
    em = str(row.get("entry_mode") or "").strip().lower()
    if em in (ENTRY_ALIGN, ENTRY_LIMIT_ATR):
        out["entry_mode"] = em
    return out


def _normalize_market_meta(
    market_type: Optional[str],
    exchange: Optional[str],
    symbol: Optional[str],
    code: str,
) -> tuple[str, str, str]:
    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 code or "").strip().upper()
    if not sym:
        sym = str(code or "").strip().upper()
    if not ex:
        ex = "KRX" if mt == "KR" else "NASD"
    return mt, ex, sym


def resolve_market_meta(
    code: str,
    market_type: Optional[str] = None,
    exchange: Optional[str] = None,
    symbol: Optional[str] = None,
) -> tuple[str, str, str]:
    """
    시장 메타 정규화 + 티커 형태 보정.

    DB에 QQQM 등이 market_type=KR 로 잘못 들어가도,
    6자리 숫자가 아닌 영문 티커는 해외(US)로 간주한다.
    """
    c = str(code or symbol or "").strip().upper()
    mt, ex, sym = _normalize_market_meta(market_type, exchange, symbol, c)
    if not c:
        return mt, ex, sym
    if c.isdigit() and len(c) == 6:
        return "KR", ex if ex not in ("NASD", "NAS", "NYSE", "NYS", "AMEX", "AMS") else "KRX", sym or c
    if c.isalpha() and 1 <= len(c) <= 8:
        if mt == "KR" or ex in ("", "KRX"):
            return "US", "NASD", sym or c
    return mt, ex, sym


def get_updow_stock_config_row(db: Any, code: str) -> Optional[Dict[str, Any]]:
    """``updow_stock_config`` 최신 1행 → 엔진 cfg dict (없으면 None)."""
    ensure_updow_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 updow_stock_config "
        "WHERE code=%s OR symbol=%s "
        "ORDER BY id DESC LIMIT 1",
        [code, code],
    ).fetchone()
    if not row:
        return None
    d = dict(row) if not isinstance(row, dict) else dict(row)
    return _engine_cfg_from_row(d)


def get_updow_stock_meta(db: Any, code: str) -> Optional[Dict[str, Any]]:
    """종목명·tf 포함 메타 (최신 행)."""
    ensure_updow_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 updow_stock_config "
        "WHERE code=%s OR symbol=%s ORDER BY id DESC LIMIT 1",
        [code, code],
    ).fetchone()
    if not row:
        return None
    return dict(row) if not isinstance(row, dict) else dict(row)


def set_updow_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:
    """UPDOW 종목 파라미터 INSERT (최신 row 방식)."""
    ensure_updow_stock_config_table(db)
    code = str(code or symbol or "").strip().upper()
    if not code:
        raise ValueError("code 필수")
    mt, ex, sym = _normalize_market_meta(market_type, exchange, symbol, code)
    base = dict(DEFAULT_UPDOW_CONFIG)
    for k in CFG_ENGINE_KEYS:
        if k in engine_cfg and engine_cfg[k] is not None:
            base[k] = float(engine_cfg[k])
    try:
        tfv = int(float(tf_min if tf_min is not None else engine_cfg.get("tf_min", base.get("tf_min", 60))))
    except (TypeError, ValueError):
        tfv = int(get_env_int("UPDOW_TF_MIN", 60))
    if tfv < 1:
        tfv = int(get_env_int("UPDOW_TF_MIN", 60))
    # 진입모드(문자열) — align/limit_atr 만 허용, 그 외는 '' (글로벌 env 폴백)
    em = str(engine_cfg.get("entry_mode") or "").strip().lower()
    if em not in (ENTRY_ALIGN, ENTRY_LIMIT_ATR):
        em = ""
    cols = ["code", "market_type", "exchange", "symbol", "name", "tf_min"] + list(CFG_ENGINE_KEYS) + ["entry_mode"]
    vals = [code, mt, ex, sym, str(name or "").strip() or code, tfv]
    for k in CFG_ENGINE_KEYS:
        vals.append(base[k])
    vals.append(em)
    placeholders = ", ".join(["%s"] * len(cols))
    col_sql = ", ".join(cols)
    raw = _trade_db_core(db)
    raw.conn.execute(
        f"INSERT INTO updow_stock_config ({col_sql}) VALUES ({placeholders})",
        vals,
    )
    raw.conn.commit()


def load_updow_engine_cfg(db: Any, code: str, env_fallback: Dict[str, Any]) -> Dict[str, Any]:
    """
    UPDOW 종목 cfg — ``updow_stock_config`` 우선, 없으면 ``env_fallback`` 만.
    (holding_stock_config·env 이중 병합 없음)
    """
    row_cfg = get_updow_stock_config_row(db, code)
    if row_cfg:
        return row_cfg
    return dict(env_fallback)


def fetch_latest_updow_stock_config_by_code(db: Any) -> Dict[str, Dict[str, Any]]:
    """종목코드별 ``updow_stock_config`` 최신 1행 (실매 유니버스·웹 목록)."""
    ensure_updow_stock_config_table(db)
    raw = _trade_db_core(db)
    sql = """
        SELECT u.* FROM updow_stock_config u
        INNER JOIN (
            SELECT code, MAX(id) AS mx FROM updow_stock_config GROUP BY code
        ) t ON u.code = t.code AND u.id = t.mx
        ORDER BY u.code
    """
    cur = raw.conn.execute(sql)
    rows = cur.fetchall() if cur else []
    out: Dict[str, Dict[str, Any]] = {}
    for row in rows:
        d = dict(row) if not isinstance(row, dict) else 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("UPDOW_TF_MIN", 60)))
        eng["market_type"] = str(d.get("market_type") or "KR").strip().upper() or "KR"
        eng["exchange"] = str(d.get("exchange") or ("KRX" if eng["market_type"] == "KR" else "NASD")).strip().upper()
        eng["symbol"] = str(d.get("symbol") or code).strip().upper() or code
        out[code] = eng
    return out


def list_updow_stock_codes(db: Any) -> list:
    """웹 종목 셀렉트용 [{code, name}, ...]."""
    by_code = fetch_latest_updow_stock_config_by_code(db)
    out = []
    for c in sorted(by_code.keys()):
        mt, ex, sym = resolve_market_meta(
            c,
            by_code[c].get("market_type"),
            by_code[c].get("exchange"),
            by_code[c].get("symbol"),
        )
        out.append({
            "code": c,
            "name": by_code[c].get("name", c),
            "market_type": mt,
            "exchange": ex,
            "symbol": sym,
        })
    return out


def migrate_updow_from_holding(db: Any, code: str, env_fallback: Dict[str, Any]) -> bool:
    """
    ``holding_stock_config`` → ``updow_stock_config`` 1회 복사 (이미 있으면 False).
    구버전 holding ``updow_*`` + 익절/손절 병합값을 그대로 옮김.
    """
    if get_updow_stock_config_row(db, code):
        return False
    try:
        import holding_bot as hb
    except ImportError:
        return False
    hold_row = hb.get_stock_config_row(db, code)
    if not hold_row:
        return False
    merged = merge_holding_row_into_updow_cfg(hold_row, env_fallback)
    tf = effective_updow_tf_min(hold_row, int(env_fallback.get("tf_min", get_env_int("UPDOW_TF_MIN", 60))))
    name = str(hold_row.get("name") or code).strip() or code
    set_updow_stock_config(db, code, name, merged, tf_min=tf)
    logger.info("updow_stock_config 마이그레이션: %s ← holding_stock_config", code)
    return True


def migrate_all_updow_from_holding(db: Any, env_fallback: Dict[str, Any]) -> int:
    """holding 에 등록된 모든 코드를 updow_stock_config 로 (없는 것만)."""
    try:
        import holding_bot as hb
    except ImportError:
        return 0
    n = 0
    try:
        items = hb.load_watchlist()
    except Exception:
        items = []
    codes = {str(it.get("code", "")).strip() for it in items if it.get("code")}
    raw = _trade_db_core(db)
    cur = raw.conn.execute("SELECT DISTINCT code FROM holding_stock_config")
    for row in (cur.fetchall() if cur else []):
        c = str(dict(row).get("code", "")).strip()
        if c:
            codes.add(c)
    for code in sorted(codes):
        if migrate_updow_from_holding(db, code, env_fallback):
            n += 1
    return n


def ensure_holding_updow_columns(db: Any) -> None:
    """holding_stock_config 에 UPDOW 전용 선택 컬럼 추가(이미 있으면 무시)."""
    raw = _trade_db_core(db)
    alters = (
        "ADD COLUMN updow_body_drop_min_pct DOUBLE NULL "
        "COMMENT 'UPDOW 직전봉 몸통하락%% 하한, NULL=env UPDOW_BODY_DROP_MIN_PCT'",
        "ADD COLUMN updow_max_hold_bars INT NULL "
        "COMMENT 'UPDOW 최대 보유 봉 수, NULL=env UPDOW_MAX_HOLD_BARS'",
        "ADD COLUMN updow_exit_on_green DOUBLE NULL "
        "COMMENT 'UPDOW 양봉청산 1=ON 0=OFF, NULL=env UPDOW_EXIT_ON_GREEN'",
        "ADD COLUMN updow_tf_min INT NULL "
        "COMMENT 'UPDOW 분봉 tf, NULL 또는 1미만=env UPDOW_TF_MIN'",
        "ADD COLUMN updow_regime_ma_bars INT NULL "
        "COMMENT 'UPDOW KOSPI프록시 1분 MA봉수 0=OFF NULL=env UPDOW_REGIME_MA_BARS'",
        "ADD COLUMN updow_atr_use_dynamic DOUBLE NULL "
        "COMMENT 'UPDOW ATR 동적 청산 1=ON 0=OFF NULL=env UPDOW_ATR_USE_DYNAMIC'",
        "ADD COLUMN updow_atr_period INT NULL "
        "COMMENT 'UPDOW ATR 기간(봉), NULL=env UPDOW_ATR_PERIOD'",
        "ADD COLUMN updow_atr_sl_mult DOUBLE NULL "
        "COMMENT 'UPDOW ATR 손절 배수, NULL=env UPDOW_ATR_SL_MULT'",
        "ADD COLUMN updow_atr_tp_mult DOUBLE NULL "
        "COMMENT 'UPDOW ATR 익절 배수, NULL=env UPDOW_ATR_TP_MULT'",
        "ADD COLUMN updow_atr_sl_min_pct DOUBLE NULL "
        "COMMENT 'UPDOW ATR 손절 하한(%) NULL=env UPDOW_ATR_SL_MIN_PCT'",
        "ADD COLUMN updow_atr_sl_max_pct DOUBLE NULL "
        "COMMENT 'UPDOW ATR 손절 상한(%) NULL=env UPDOW_ATR_SL_MAX_PCT'",
        "ADD COLUMN updow_atr_tp_min_pct DOUBLE NULL "
        "COMMENT 'UPDOW ATR 익절 하한(%) NULL=env UPDOW_ATR_TP_MIN_PCT'",
        "ADD COLUMN updow_atr_tp_max_pct DOUBLE NULL "
        "COMMENT 'UPDOW ATR 익절 상한(%) NULL=env UPDOW_ATR_TP_MAX_PCT'",
        "ADD COLUMN updow_shoulder_min_high_pct DOUBLE NULL "
        "COMMENT 'UPDOW 어깨 발동(%%) NULL=env UPDOW_SHOULDER_MIN_HIGH_PCT'",
        "ADD COLUMN updow_shoulder_cut_pct DOUBLE NULL "
        "COMMENT 'UPDOW 어깨 폭(%%) NULL=env UPDOW_SHOULDER_CUT_PCT'",
    )
    for frag in alters:
        try:
            raw.conn.execute(f"ALTER TABLE holding_stock_config {frag}")
            raw.conn.commit()
        except Exception:
            pass


def fetch_latest_holding_stock_config_by_code(db: Any) -> Dict[str, Dict[str, Any]]:
    """종목코드별 ``holding_stock_config`` 최신 1행 (id DESC)."""
    raw = _trade_db_core(db)
    sql = """
        SELECT h.* FROM holding_stock_config h
        INNER JOIN (
            SELECT code, MAX(id) AS mx FROM holding_stock_config GROUP BY code
        ) t ON h.code = t.code AND h.id = t.mx
        ORDER BY h.code
    """
    cur = raw.conn.execute(sql)
    rows = cur.fetchall() if cur else []
    out: Dict[str, Dict[str, Any]] = {}
    for row in rows:
        d = dict(row) if not isinstance(row, dict) else dict(row)
        code = str(d.get("code", "")).strip()
        if not code:
            continue
        d.pop("id", None)
        d.pop("created_at", None)
        out[code] = d
    return out


def merge_holding_row_into_updow_cfg(
    row: Optional[Dict[str, Any]],
    env_fallback: Dict[str, Any],
) -> Dict[str, Any]:
    """
    ``holding_stock_config`` 한 행을 ``updow_buy`` 엔진 cfg 형식으로 병합.

    - ``take_profit_pct``  → ``tp_pct``
    - ``stop_loss_pct``   → ``stop_loss_pct`` (%% 양수, 백테와 동일)
    - ``slot_money``      → ``slot_money``
    - ``updow_body_drop_min_pct`` / ``updow_max_hold_bars`` / ``updow_exit_on_green`` /
      ``updow_regime_ma_bars``
      가 NULL 이 아니고 유효하면 env 를 덮어씀.
    """
    out: Dict[str, Any] = dict(env_fallback)
    if not row:
        return out

    def _f(key: str) -> Optional[float]:
        v = row.get(key)
        if v is None or (isinstance(v, str) and not str(v).strip()):
            return None
        try:
            return float(v)
        except (TypeError, ValueError):
            return None

    try:
        tp = _f("take_profit_pct")
        if tp is not None and tp > 0:
            out["tp_pct"] = tp
        sl = _f("stop_loss_pct")
        if sl is not None and sl > 0:
            out["stop_loss_pct"] = sl
        sm = _f("slot_money")
        if sm is not None and sm > 0:
            out["slot_money"] = sm

        bd = _f("updow_body_drop_min_pct")
        if bd is not None and bd >= 0.0:
            out["body_drop_min_pct"] = bd

        mh = _f("updow_max_hold_bars")
        if mh is not None and mh >= 1.0:
            out["max_hold_bars"] = float(int(mh))

        eg = _f("updow_exit_on_green")
        # -1 = 미설정(장기홀딩 기본값) → env 유지. 0~1 은 UPDOW 양봉청산 on/off.
        if eg is not None and eg >= 0.0:
            out["exit_on_green"] = 1.0 if eg >= 0.5 else 0.0

        rm = _f("updow_regime_ma_bars")
        if rm is not None and rm >= 0.0:
            out["regime_ma_bars"] = float(int(rm))

        aud = _f("updow_atr_use_dynamic")
        if aud is not None and aud >= 0.0:
            out["atr_use_dynamic"] = 1.0 if aud >= 0.5 else 0.0
        ap = _f("updow_atr_period")
        if ap is not None and ap >= 1.0:
            out["atr_period"] = float(int(ap))
        asm = _f("updow_atr_sl_mult")
        if asm is not None and asm > 0.0:
            out["atr_sl_mult"] = asm
        atm = _f("updow_atr_tp_mult")
        if atm is not None and atm > 0.0:
            out["atr_tp_mult"] = atm
        asmin = _f("updow_atr_sl_min_pct")
        if asmin is not None and asmin > 0.0:
            out["atr_sl_min_pct"] = asmin
        asmax = _f("updow_atr_sl_max_pct")
        if asmax is not None and asmax > 0.0:
            out["atr_sl_max_pct"] = asmax
        atmin = _f("updow_atr_tp_min_pct")
        if atmin is not None and atmin > 0.0:
            out["atr_tp_min_pct"] = atmin
        atmax = _f("updow_atr_tp_max_pct")
        if atmax is not None and atmax > 0.0:
            out["atr_tp_max_pct"] = atmax
        usmh = _f("updow_shoulder_min_high_pct")
        if usmh is not None and usmh > 0.0:
            out["shoulder_min_high_pct"] = usmh
        usc = _f("updow_shoulder_cut_pct")
        if usc is not None and usc > 0.0:
            out["shoulder_cut_pct"] = usc
    except Exception as e:
        logger.debug("merge_holding_row_into_updow_cfg 경고: %s", e)

    return out


def updow_param_sources_for_code(
    db: Any,
    code: str,
    env_fallback: Dict[str, Any],
) -> Dict[str, Dict[str, Any]]:
    """웹 표시: ``updow_stock_config`` 행 있으면 전부 종목, 없으면 env 폴백."""
    stock_cfg = get_updow_stock_config_row(db, code)
    applied = load_updow_engine_cfg(db, code, env_fallback)
    src = "updow_stock" if stock_cfg else "env"
    out: Dict[str, Dict[str, Any]] = {}
    for k in CFG_ENGINE_KEYS:
        out[k] = {
            "from": src,
            "applied": applied.get(k),
            "updow_stock": stock_cfg.get(k) if stock_cfg else None,
            "env": env_fallback.get(k),
        }
    return out


def effective_updow_tf_for_code(db: Any, code: str, env_tf: int) -> int:
    """``updow_stock_config.tf_min`` 우선."""
    meta = get_updow_stock_meta(db, code)
    if meta:
        try:
            iv = int(float(meta.get("tf_min", 0)))
            if iv >= 1:
                return iv
        except (TypeError, ValueError):
            pass
    try:
        et = int(env_tf)
    except (TypeError, ValueError):
        et = 0
    if et < 1:
        et = get_env_int("UPDOW_TF_MIN", 60)
    return et


def effective_updow_tf_min(row: Optional[Dict[str, Any]], env_tf: int) -> int:
    """
    종목별 ``holding_stock_config.updow_tf_min`` — 양의 정수면 해당 분봉,
    NULL·1미만·변환 실패 시 ``env_tf`` (보통 env ``UPDOW_TF_MIN`` 스냅샷 값).
    """
    try:
        et = int(env_tf)
    except (TypeError, ValueError):
        et = 0
    if et < 1:
        et = get_env_int("UPDOW_TF_MIN", 60)
    if not row:
        return et
    raw = row.get("updow_tf_min")
    if raw is None:
        return et
    try:
        iv = int(float(raw))
    except (TypeError, ValueError):
        return et
    if iv < 1:
        return et
    return iv
