"""SELL_WATERFALL_ENGINE_V2 — 매도 폭포수 엔진 V2.

V1 4단계 유지 + 신규 추가:
  (a) 호가단위 기반 슬리피지 시뮬레이션 (est_slippage_bps)
  (b) 유동성 라벨(LIQUIDITY_FLOW) 기반 exec_mode 결정 (MARKET/TWAP/LIMIT)
  (c) 부분체결 시 잔량 자동 stage 승격 (escalation_rule)

Stage 정의:
  1 = EMERGENCY_FULL      — stop_breach or emergency_full_sell
  2 = URGENT_TRIM         — urgent_liquidity_trim / cash preservation ≥ 50%
  3 = DISTRIBUTION_EXIT   — 분산 매도 (여러 날 분할)
  4 = TRIM_ONLY           — 소규모 이익실현 / 리밸런싱

exec_mode:
  MARKET  — AvgTradeValue ≥ 500억: 시장가
  TWAP    — 50억 ≤ AvgTradeValue < 500억: 시간분할(TWAP)
  LIMIT   — AvgTradeValue < 50억: 지정가

escalation_rule: 부분체결 < 80% 시 자동 stage+1 승격 (최대 stage 1)
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_JSON = ROOT / "GatherTradingData.json"
DEFAULT_OUT = ROOT / "Temp" / "sell_waterfall_engine_v2.json"

# 유동성 임계값 (억 단위: AvgTradeValue_20D_M / 1e8)
_LIQUIDITY_HIGH_THRESHOLD = 500.0   # ≥ 500억 → MARKET
_LIQUIDITY_MED_THRESHOLD = 50.0     # 50~500억 → TWAP
# < 50억 → LIMIT

# 슬리피지: 호가단위(tick) × 배수
_SLIPPAGE_TICKS = 1  # 1 tick 슬리피지 가정

# 부분체결 에스컬레이션 임계
_PARTIAL_FILL_THRESHOLD = 0.80  # 80% 미만 체결 시 escalation

# 단계 skip 금지 — 1→2→3→4 순서 유지 (단, 현 단계에서 승격 허용)


def _load(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        d = json.loads(path.read_text(encoding="utf-8"))
        return d if isinstance(d, dict) else {}
    except Exception:
        return {}


def _rows(v: Any) -> list[dict[str, Any]]:
    if isinstance(v, list):
        return [x for x in v if isinstance(x, dict)]
    return []


def _f(v: Any, default: float = 0.0) -> float:
    try:
        return float(v) if v is not None else default
    except (TypeError, ValueError):
        return default


def _tick_size(price: float) -> float:
    """한국 주식 호가단위 (KRX 기준)."""
    if price < 1_000:
        return 1.0
    if price < 5_000:
        return 5.0
    if price < 10_000:
        return 10.0
    if price < 50_000:
        return 50.0
    if price < 100_000:
        return 100.0
    if price < 500_000:
        return 500.0
    return 1_000.0


def _slippage_bps(price: float, ticks: int = _SLIPPAGE_TICKS) -> float:
    """예상 슬리피지 (basis points = 0.01%)."""
    if price <= 0:
        return 0.0
    tick = _tick_size(price)
    return round((tick * ticks / price) * 10_000, 1)


def _exec_mode(avg_trade_value_m: float | None) -> str:
    """유동성 기반 체결 방식 결정.
    avg_trade_value_m: AvgTradeValue_20D_M (억 단위가 아닌 백만 단위임 주의)
    """
    if avg_trade_value_m is None:
        return "LIMIT"
    # AvgTradeValue_20D_M 은 백만원(M) 단위
    # 500억 = 50,000M, 50억 = 5,000M
    if avg_trade_value_m >= 50_000:   # ≥ 500억
        return "MARKET"
    if avg_trade_value_m >= 5_000:    # 50~500억
        return "TWAP"
    return "LIMIT"


def _stage_from_style(execution_style: str, emergency_full_sell: bool) -> int:
    """execution_style → stage 번호."""
    if emergency_full_sell:
        return 1
    s = (execution_style or "").upper()
    if "EMERGENCY" in s:
        return 1
    if "URGENT" in s:
        return 2
    if "DISTRIBUTION" in s or "OVERSOLD" in s:
        return 3
    if "TRIM" in s or "PROTECT" in s or "PROFIT" in s:
        return 4
    return 3  # default


def _split_count(exec_mode: str, stage: int, qty: int) -> int:
    """분할 횟수: TWAP/LIMIT는 분할, MARKET은 1회."""
    if exec_mode == "MARKET":
        return 1
    if exec_mode == "TWAP":
        # stage별 분할 수: 1→1, 2→2, 3→3, 4→2
        return {1: 1, 2: 2, 3: 3, 4: 2}.get(stage, 2)
    # LIMIT: 더 세분화
    return {1: 1, 2: 3, 3: 5, 4: 3}.get(stage, 3)


def _escalation_rule(stage: int, exec_mode: str) -> str:
    """부분체결 에스컬레이션 규칙 설명."""
    if stage <= 1:
        return f"partial<{int(_PARTIAL_FILL_THRESHOLD*100)}%→MARKET_OVERRIDE(단계 최상위이므로 mode만 변경)"
    return (
        f"partial<{int(_PARTIAL_FILL_THRESHOLD*100)}%→stage{stage-1}승격_"
        f"({_stage_name(stage)}→{_stage_name(stage-1)})"
        f"_단계건너뜀금지"
    )


def _stage_name(stage: int) -> str:
    return {1: "EMERGENCY_FULL", 2: "URGENT_TRIM", 3: "DISTRIBUTION_EXIT", 4: "TRIM_ONLY"}.get(stage, f"STAGE{stage}")


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--json", default=str(DEFAULT_JSON))
    ap.add_argument("--out", default=str(DEFAULT_OUT))
    args = ap.parse_args()

    json_path = Path(args.json) if Path(args.json).is_absolute() else ROOT / args.json
    out_path = Path(args.out) if Path(args.out).is_absolute() else ROOT / args.out

    payload = _load(json_path)
    data_block = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    h = data_block.get("_harness_context") if isinstance(data_block.get("_harness_context"), dict) else {}
    df_list = _rows(data_block.get("data_feed"))

    # 가격 + AvgTradeValue lookup
    price_lut: dict[str, float] = {}
    atv_lut: dict[str, float] = {}
    for r in df_list:
        t = str(r.get("Ticker") or "")
        if t:
            price_lut[t] = _f(r.get("Close") or r.get("close"), 0.0)
            atv_lut[t] = _f(r.get("AvgTradeValue_20D_M"), 0.0)

    # V1 waterfall plan
    wp_raw = h.get("waterfall_plan_json", [])
    if isinstance(wp_raw, str):
        try:
            wp_raw = json.loads(wp_raw)
        except Exception:
            wp_raw = []
    wp = _rows(wp_raw)

    # K2 rebound trigger lookup: ticker → rebound_trigger_price
    k2_raw = h.get("k2_staged_rebound_sell_json", [])
    if isinstance(k2_raw, str):
        try:
            k2_raw = json.loads(k2_raw)
        except Exception:
            k2_raw = []
    k2_lut: dict[str, dict] = {}
    for k2r in _rows(k2_raw):
        t = str(k2r.get("ticker") or "")
        if t:
            k2_lut[t] = k2r

    # deadline_date lookup from sell_candidates
    sc_raw = h.get("sell_candidates_json", [])
    if isinstance(sc_raw, str):
        try:
            sc_raw = json.loads(sc_raw)
        except Exception:
            sc_raw = []
    deadline_lut: dict[str, str] = {}
    for scr in _rows(sc_raw):
        t = str(scr.get("ticker") or "")
        dd = scr.get("deadline_date") or scr.get("target_date") or scr.get("rebound_deadline")
        if t and dd:
            deadline_lut[t] = str(dd)

    if not wp:
        result = {
            "formula_id": "SELL_WATERFALL_ENGINE_V2",
            "gate": "FAIL",
            "note": "waterfall_plan_json is empty or missing",
            "rows": [],
        }
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
        print("SELL_WATERFALL_ENGINE_V2 gate=FAIL rows=0")
        return 0

    rows_out = []
    skip_count = 0
    stage_counts: dict[int, int] = {}

    for item in wp:
        ticker = str(item.get("ticker") or "")
        name = str(item.get("name") or "")
        execution_style = str(item.get("execution_style") or "")
        emergency_full_sell = bool(item.get("emergency_full_sell"))
        immediate_qty = int(item.get("immediate_qty") or 0)
        max_daily_qty = int(item.get("max_daily_qty") or immediate_qty or 0)
        rank = int(item.get("rank") or 0)

        price = price_lut.get(ticker, 0.0)
        atv = atv_lut.get(ticker, None) if atv_lut.get(ticker, 0.0) > 0 else None

        stage = _stage_from_style(execution_style, emergency_full_sell)
        mode = _exec_mode(atv)
        slip_bps = _slippage_bps(price)
        n_splits = _split_count(mode, stage, immediate_qty)
        escal = _escalation_rule(stage, mode)

        # 최소 수량 검증 (qty 빈 경우)
        sell_qty = max(0, immediate_qty)
        qty_note = "OK" if sell_qty > 0 else "ZERO_QTY"

        stage_counts[stage] = stage_counts.get(stage, 0) + 1

        # rebound_trigger_price from K2 lookup; "N/A" for non-K2 stages
        k2_row = k2_lut.get(ticker, {})
        rebound_trigger = k2_row.get("rebound_trigger_price")
        if rebound_trigger is None:
            rebound_trigger = "N/A"

        # deadline_date from sell_candidates lookup; "N/A" when not set
        deadline = deadline_lut.get(ticker, "N/A")

        rows_out.append({
            "rank": rank,
            "ticker": ticker,
            "name": name,
            "stage": stage,
            "stage_label": _stage_name(stage),
            "execution_style": execution_style,
            "exec_mode": mode,
            "sell_qty": sell_qty,
            "max_daily_qty": max_daily_qty,
            "split_count": n_splits,
            "est_slippage_bps": slip_bps,
            "escalation_rule": escal,
            "qty_note": qty_note,
            "close_price": price,
            "avg_trade_value_m": atv,
            "rebound_trigger_price": rebound_trigger,
            "deadline_date": deadline,
        })

    # 단계 건너뜀 검증 (stage는 rank 순서대로 증가하거나 같아야 한다는 것은 아니지만
    # 한 ticker 안에서는 단계를 건너뛰지 않도록 — 이미 per-ticker 1행이므로 skip=0)
    has_all_stage_labels = all(r["stage"] in {1, 2, 3, 4} for r in rows_out)
    has_exec_mode = all(r["exec_mode"] in {"MARKET", "TWAP", "LIMIT"} for r in rows_out)
    has_split_count = all(r["split_count"] >= 1 for r in rows_out)

    gate = "PASS" if (rows_out and has_all_stage_labels and has_exec_mode and has_split_count and skip_count == 0) else "CAUTION"

    result = {
        "formula_id": "SELL_WATERFALL_ENGINE_V2",
        "gate": gate,
        "row_count": len(rows_out),
        "stage_counts": stage_counts,
        "escalation_skip_violations": skip_count,
        "partial_fill_threshold_pct": int(_PARTIAL_FILL_THRESHOLD * 100),
        "validation": {
            "all_stage_labels_valid": has_all_stage_labels,
            "all_exec_modes_valid": has_exec_mode,
            "all_split_counts_valid": has_split_count,
        },
        "rows": rows_out,
    }
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    print(
        f"SELL_WATERFALL_ENGINE_V2 gate={gate} rows={len(rows_out)} "
        f"stages={stage_counts} skip_violations={skip_count}"
    )
    return 0


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