from __future__ import annotations

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


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


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


def _obj(v: Any) -> dict[str, Any]:
    if isinstance(v, dict):
        return v
    if isinstance(v, str):
        try:
            x = json.loads(v)
            return x if isinstance(x, dict) else {}
        except Exception:
            return {}
    return {}


def _rows(v: Any) -> list[dict[str, Any]]:
    if isinstance(v, list):
        return [x for x in v if isinstance(x, dict)]
    if isinstance(v, str):
        try:
            return _rows(json.loads(v))
        except Exception:
            return []
    return []


def _f(v: Any) -> float:
    try:
        return float(v)
    except Exception:
        return 0.0


def _cash_krw(row: dict[str, Any]) -> float:
    v = _f(row.get("expected_immediate_krw"))
    if v > 0:
        return v
    return _f(row.get("immediate_krw"))


def _weighted_damage(rows: list[dict[str, Any]]) -> float:
    total = sum(_cash_krw(r) for r in rows)
    if total <= 0:
        return 0.0
    num = sum(_cash_krw(r) * _f(r.get("value_damage_pct")) for r in rows)
    return round(num / total, 2)


def _best_combo(rows: list[dict[str, Any]], shortfall_min: float) -> list[dict[str, Any]]:
    if not rows:
        return []
    if shortfall_min <= 0:
        return []
    n = len(rows)
    if n > 14:
        # Keep deterministic bounded search.
        rows = sorted(rows, key=lambda r: (_f(r.get("value_damage_pct")), -_f(r.get("expected_immediate_krw"))))[:14]
        n = len(rows)
    best: list[dict[str, Any]] = []
    best_score = None
    for k in range(1, n + 1):
        for idx in combinations(range(n), k):
            cand = [rows[i] for i in idx]
            recovered = sum(_cash_krw(r) for r in cand)
            if recovered < shortfall_min:
                continue
            dmg = _weighted_damage(cand)
            oversell = recovered - shortfall_min
            score = (dmg, oversell, k)
            if best_score is None or score < best_score:
                best_score = score
                best = cand
        if best:
            # Already found feasible set with current k (minimum legs), keep best at this cardinality.
            break
    return best or rows


DEFAULT_FJ = ROOT / "Temp" / "final_judgment_gate_v1.json"


def _breach_tickers(payload: dict[str, Any]) -> dict[str, float]:
    """손절이탈(BREACH) 종목 ticker → 전체 시장가 매핑.
    BREACH 종목: 손절가 이미 이탈 → 부분매도 아닌 전량 시장가로 대체해야 함.
    """
    fj = _load(DEFAULT_FJ)
    if not fj:
        return {}
    data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    data_feed = data.get("data_feed") if isinstance(data.get("data_feed"), list) else []
    feed_map = {str(r.get("Ticker") or ""): r for r in data_feed if isinstance(r, dict)}

    result: dict[str, float] = {}
    for row in (fj.get("rows") or []):
        if not isinstance(row, dict):
            continue
        sell_signals = row.get("sell_signals") or {}
        if sell_signals.get("stop_breach_gate") != "BREACH":
            continue
        ticker = str(row.get("ticker") or "")
        feed = feed_map.get(ticker, {})
        market_val = _f(feed.get("Account_Market_Value"))
        if market_val <= 0.0:
            qty = _f(feed.get("Account_Holding_Qty"))
            close = _f(feed.get("Close"))
            if qty > 0 and close > 0:
                market_val = qty * close
        if market_val > 0.0:
            result[ticker] = market_val
    return result


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

    jp = Path(args.json)
    rp = Path(args.rebound)
    op = Path(args.out)
    if not jp.is_absolute():
        jp = ROOT / jp
    if not rp.is_absolute():
        rp = ROOT / rp
    if not op.is_absolute():
        op = ROOT / op

    payload = _load(jp)
    rebound = _load(rp)
    data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
    hctx = data.get("_harness_context") if isinstance(data.get("_harness_context"), dict) else {}
    if isinstance(payload.get("hApex"), dict):
        hctx = dict(hctx) | payload["hApex"]

    scrs = _obj(hctx.get("scrs_v2_json"))
    combo = _rows(scrs.get("selected_combo"))
    shortfall_min = _f(hctx.get("cash_shortfall_min_krw"))
    if shortfall_min <= 0.0:
        shortfall_min = _f(scrs.get("cash_shortfall_min_krw"))

    total_immediate = _f(scrs.get("total_immediate_sell_krw"))
    expected_rebound_gain = _f(scrs.get("expected_rebound_gain_krw"))
    value_damage_avg = _f((rebound.get("metrics") or {}).get("value_damage_pct_avg"))
    if value_damage_avg <= 0.0:
        value_damage_avg = _f(scrs.get("value_damage_pct_avg"))

    # [V4-BREACH-FIRST] 결정론 보장: BREACH 종목(손절이탈) 항상 우선 시도.
    # GAS combo가 breach 종목을 포함하지 않을 수 있으므로 교체(REPLACE) + 보충(ADD) 모두 수행.
    # 정공법: 현금부족 있을 때 항상 breach pool에서 먼저 최적 조합을 탐색. feasible이면 채택.
    breach_supplement_used = False
    breach_map = _breach_tickers(payload) if shortfall_min > 0.0 else {}
    if breach_map:
        # 기존 combo 중 breach인 것은 교체, 비breach는 유지
        breach_combo_candidates = []
        combo_tickers = {str(c.get("ticker") or "") for c in combo}
        for c in combo:
            ticker = str(c.get("ticker") or "")
            if ticker in breach_map:
                upgraded = dict(c)
                upgraded["immediate_krw"] = breach_map[ticker]
                upgraded["expected_immediate_krw"] = breach_map[ticker]
                upgraded["value_damage_pct"] = 0.0
                upgraded["source"] = "BREACH_FULL_LIQUIDATION"
                breach_combo_candidates.append(upgraded)
            else:
                breach_combo_candidates.append(c)
        # combo에 없는 breach 종목도 추가
        for ticker, market_val in breach_map.items():
            if ticker not in combo_tickers:
                breach_combo_candidates.append({
                    "ticker": ticker,
                    "immediate_krw": market_val,
                    "expected_immediate_krw": market_val,
                    "value_damage_pct": 0.0,
                    "source": "BREACH_FULL_LIQUIDATION",
                })
        breach_combo = _best_combo(breach_combo_candidates, shortfall_min)
        breach_damage = _weighted_damage(breach_combo) if breach_combo else 999.0
        breach_recovered = sum(_cash_krw(r) for r in breach_combo)
        breach_feasible = shortfall_min <= 0.0 or breach_recovered >= shortfall_min
        if breach_combo and breach_feasible:
            optimized_combo = breach_combo
            optimized_damage = breach_damage
            breach_supplement_used = True
        else:
            optimized_combo = _best_combo(combo, shortfall_min)
            optimized_damage = _weighted_damage(optimized_combo) if optimized_combo else value_damage_avg
    else:
        optimized_combo = _best_combo(combo, shortfall_min)
        optimized_damage = _weighted_damage(optimized_combo) if optimized_combo else value_damage_avg

    recovered = sum(_cash_krw(r) for r in optimized_combo)
    if recovered <= 0.0:
        recovered = total_immediate
    remaining = max(0.0, shortfall_min - recovered)
    optimized_damage = _weighted_damage(optimized_combo) if optimized_combo else value_damage_avg
    shortfall_covered = shortfall_min <= 0.0 or recovered >= shortfall_min
    execution_allowed = optimized_damage <= 10.0 and shortfall_covered
    emergency_full_sell = False
    if shortfall_min > 0.0 and recovered > 0.0 and recovered * 2 < shortfall_min:
        emergency_full_sell = True

    result = {
        "formula_id": "SMART_CASH_RECOVERY_V4",
        "status": "PASS" if execution_allowed else "CASH_RECOVERY_VALUE_DAMAGE_BLOCK",
        "execution_allowed": execution_allowed,
        "value_damage_block_threshold": 10.0,
        "selected_sell_combo": optimized_combo,
        "cash_recovered_krw": round(recovered),
        "cash_shortfall_min_krw": round(shortfall_min),
        "cash_shortfall_remaining_krw": round(remaining),
        "cash_shortfall_covered": shortfall_covered,
        "value_damage_pct_avg": value_damage_avg,          # P0-T1: 은폐 금지, 표시=원시값
        "value_damage_pct_avg_raw": value_damage_avg,      # raw 동일값 유지 (backward compat)
        "value_damage_pct_avg_optimized": optimized_damage, # 선택 조합의 최적화값 (참고용)
        "expected_rebound_gain_krw": round(expected_rebound_gain),
        "total_immediate_sell_krw": round(total_immediate),
        "emergency_full_sell": emergency_full_sell,
        "optimization": {
            "candidate_count": len(combo),
            "selected_count": len(optimized_combo),
            "method": "MIN_WEIGHTED_DAMAGE_SUBSET_WITH_SHORTFALL_COVER",
            "breach_supplement_used": breach_supplement_used,
        },
    }

    op.parent.mkdir(parents=True, exist_ok=True)
    op.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0


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