"""
GT 공통 자본 배분·포트폴리오 시뮬 엔진.

ground_truth.allocate_gt_order_amounts · simulate_truth_portfolio ·
matching/portfolio_sim 이 동일 규칙을 공유합니다.
"""

from __future__ import annotations

from typing import Any, Callable

from config import (
    GT_INITIAL_CASH_KRW,
    GT_MIN_ORDER_KRW,
    TRADING_FEE_RATE,
)
from deepcoin.ground_truth.gt_model import remaining_weight_sum


def resolve_sell_qty(
    t: dict[str, Any],
    qty: float,
    price: float,
    sell_base_qty: float,
    weight: float,
) -> float:
    """
    매도 수량: amount_krw 우선, 없으면 sell_base_qty × weight.

    Args:
        t: trade dict.
        qty: 현재 보유 수량.
        price: 체결가.
        sell_base_qty: leg 첫 매도 시점 보유량.
        weight: 매도 비중.

    Returns:
        매도 수량.
    """
    if qty <= 0 or price <= 0:
        return 0.0
    ak = t.get("amount_krw")
    if ak is not None and float(ak) > 0:
        gross_cap = float(ak)
        if gross_cap >= qty * price * 0.999:
            return qty
        return min(qty, gross_cap / price)
    return min(sell_base_qty * weight, qty)


def allocate_order_amounts_chronological(
    trades: list[dict[str, Any]],
    *,
    initial_cash: float = GT_INITIAL_CASH_KRW,
    min_order_krw: float = GT_MIN_ORDER_KRW,
    fee_rate: float = TRADING_FEE_RATE,
    large_legs: set[int] | None = None,
    asset_pct_scale_fn: Callable[[dict[str, Any]], float] | None = None,
    causal_tier: bool = False,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """
    시각순·leg 비중·티어 스케일로 amount_krw를 배분합니다.

    causal_tier=True: 청산 완료 leg의 realized return 만으로 tier 산정 (인과적).

    Args:
        trades: trade dict (weight·leg_id·action·price).
        initial_cash: 초기 현금.
        min_order_krw: 최소 체결 원화.
        fee_rate: 수수료율.
        large_legs: 대형 leg. None이면 GT trades에서 산출(비인과).
        asset_pct_scale_fn: 매수 trade별 tier scale.
        causal_tier: 과거 청산 leg 수익률만으로 tier.

    Returns:
        (amount_krw 채워진 trades, alloc_stats).
    """
    from config import GT_BUY_PCT_LARGE_LEG, GT_LARGE_LEG_TOP_PCT

    from deepcoin.matching.position_sizing import (
        compute_buy_amount_krw,
        large_leg_ids_from_past_returns,
        leg_asset_pct_scale,
        top_leg_ids_by_forward_return,
    )

    chron = sorted(trades, key=lambda x: x["dt"])
    if large_legs is None and not causal_tier:
        large_legs = top_leg_ids_by_forward_return(chron)
    elif large_legs is None:
        large_legs = set()

    leg_buy_idxs: dict[int, list[int]] = {}
    leg_sell_idxs: dict[int, list[int]] = {}
    for i, t in enumerate(chron):
        lid = int(t.get("leg_id", 0))
        if t["action"] == "buy":
            leg_buy_idxs.setdefault(lid, []).append(i)
        elif t["action"] == "sell":
            leg_sell_idxs.setdefault(lid, []).append(i)

    cash = float(initial_cash)
    qty = 0.0
    qty_by_leg: dict[int, float] = {}
    sell_leg: int | None = None
    sell_base_qty = 0.0
    buy_executed = 0
    buy_skipped = 0
    sell_executed = 0
    sell_skipped = 0
    buy_amounts: list[float] = []
    large_tier_buys = 0
    completed_leg_ret: dict[int, float] = {}
    leg_cost_krw: dict[int, float] = {}
    leg_proceeds_krw: dict[int, float] = {}

    for i, t in enumerate(chron):
        price = float(t["price"])
        if price <= 0:
            continue
        leg_id = int(t.get("leg_id", 0))
        weight = float(t.get("weight", 1.0))

        if t["action"] == "buy":
            w_sum = remaining_weight_sum(chron, leg_id, i)
            if causal_tier:
                large_now = large_leg_ids_from_past_returns(
                    completed_leg_ret, GT_LARGE_LEG_TOP_PCT
                )
                scale = leg_asset_pct_scale(leg_id, large_now)
            elif asset_pct_scale_fn is not None:
                scale = asset_pct_scale_fn(t, completed_leg_ret)
            else:
                scale = leg_asset_pct_scale(leg_id, large_legs)
            amount = compute_buy_amount_krw(
                cash,
                qty,
                price,
                weight,
                w_sum,
                asset_pct_scale=scale,
                min_order_krw=min_order_krw,
                fee_rate=fee_rate,
                ignore_weight_split=bool(t.get("conviction_buy")),
            )
            if amount <= 0:
                t["amount_krw"] = 0
                buy_skipped += 1
                continue
            t["amount_krw"] = amount
            fee = amount * fee_rate
            cash -= amount + fee
            bought_qty = amount / price
            qty += bought_qty
            qty_by_leg[leg_id] = qty_by_leg.get(leg_id, 0.0) + bought_qty
            leg_cost_krw[leg_id] = leg_cost_krw.get(leg_id, 0.0) + amount + fee
            buy_executed += 1
            buy_amounts.append(amount)
            if scale >= float(GT_BUY_PCT_LARGE_LEG) * 0.99:
                large_tier_buys += 1
            sell_leg = None

        elif t["action"] == "sell":
            leg_qty = qty_by_leg.get(leg_id, 0.0)
            if leg_qty <= 1e-12:
                sell_skipped += 1
                continue
            if sell_leg != leg_id:
                sell_leg = leg_id
                sell_base_qty = leg_qty
            rem_sells = [j for j in leg_sell_idxs.get(leg_id, []) if j >= i]
            is_last_leg_sell = bool(rem_sells) and i == rem_sells[-1]
            if is_last_leg_sell:
                sell_qty = leg_qty
                gross = sell_qty * price
            else:
                gross = sell_base_qty * weight * price
                if gross >= min_order_krw:
                    gross = max(min_order_krw, gross)
                gross = min(gross, leg_qty * price)
            if gross <= 0:
                sell_skipped += 1
                continue
            sell_qty = leg_qty if is_last_leg_sell else gross / price
            t["amount_krw"] = round(gross, 0)
            fee = gross * fee_rate
            cash += gross - fee
            leg_proceeds_krw[leg_id] = leg_proceeds_krw.get(leg_id, 0.0) + (gross - fee)
            leg_qty -= sell_qty
            qty_by_leg[leg_id] = max(leg_qty, 0.0)
            qty = max(qty - sell_qty, 0.0)
            if qty < 1e-12:
                qty = 0.0
            sell_executed += 1
            if (causal_tier or asset_pct_scale_fn is not None) and leg_qty <= 1e-12:
                cost = leg_cost_krw.pop(leg_id, 0.0)
                proceeds = leg_proceeds_krw.pop(leg_id, 0.0)
                if cost > 0:
                    completed_leg_ret[leg_id] = (proceeds - cost) / cost * 100.0

    stats: dict[str, Any] = {
        "buy_executed": buy_executed,
        "buy_skipped": buy_skipped,
        "sell_executed": sell_executed,
        "sell_skipped": sell_skipped,
        "buy_total_krw": round(sum(buy_amounts), 0),
        "large_leg_count": large_tier_buys,
        "large_tier_buy_count": large_tier_buys,
    }
    if buy_amounts:
        stats["buy_amount_avg_krw"] = round(sum(buy_amounts) / len(buy_amounts), 0)
        stats["buy_amount_min_krw"] = round(min(buy_amounts), 0)
        stats["buy_amount_max_krw"] = round(max(buy_amounts), 0)
    return trades, stats


def simulate_portfolio_steps(
    trades: list[dict[str, Any]],
    *,
    initial_cash: float = GT_INITIAL_CASH_KRW,
    fee_rate: float = TRADING_FEE_RATE,
    use_amount_krw: bool = True,
) -> list[dict[str, Any]]:
    """
    체결마다 현금·보유·총평가 스냅샷.

    Args:
        trades: 시각순 trade dict (amount_krw·weight·leg_id).
        initial_cash: 시작 현금.
        fee_rate: 수수료율.
        use_amount_krw: True면 amount_krw 기준 체결.

    Returns:
        step dict 리스트.
    """
    rows = sorted(trades, key=lambda x: x["dt"])
    cash = float(initial_cash)
    qty = 0.0
    qty_by_leg: dict[int, float] = {}
    sell_leg: int | None = None
    sell_base_qty = 0.0
    leg_budget = 0.0
    current_leg: int | None = None
    steps: list[dict[str, Any]] = []

    for t in rows:
        action = t.get("action", t.get("side", ""))
        price = float(t["price"])
        if price <= 0:
            continue
        weight = float(t.get("weight", 1.0))
        leg_id = int(t.get("leg_id", 0))

        if action == "buy":
            if use_amount_krw and t.get("amount_krw") is not None and float(t["amount_krw"]) > 0:
                amount = min(float(t["amount_krw"]), max(cash / (1.0 + fee_rate), 0.0))
            else:
                if leg_id != current_leg:
                    current_leg = leg_id
                    leg_budget = cash
                amount = min(leg_budget * weight, max(cash / (1.0 + fee_rate), 0.0))
            if amount <= 0:
                continue
            fee = amount * fee_rate
            cash -= amount + fee
            bought = amount / price
            qty += bought
            qty_by_leg[leg_id] = qty_by_leg.get(leg_id, 0.0) + bought
            sell_leg = None

        elif action == "sell" and qty > 0:
            leg_qty = qty_by_leg.get(leg_id, qty)
            if sell_leg != leg_id:
                sell_leg = leg_id
                sell_base_qty = leg_qty
            sell_qty = resolve_sell_qty(t, leg_qty, price, sell_base_qty, weight)
            if sell_qty <= 0:
                continue
            gross = sell_qty * price
            fee = gross * fee_rate
            cash += gross - fee
            leg_qty -= sell_qty
            qty_by_leg[leg_id] = max(leg_qty, 0.0)
            qty -= sell_qty
            if qty < 1e-12:
                qty = 0.0

        steps.append(
            {
                "dt": t["dt"],
                "action": action,
                "price": price,
                "weight": weight,
                "leg_id": leg_id,
                "amount_krw": t.get("amount_krw"),
                "cash_krw": round(cash, 0),
                "holding_qty": round(qty, 6),
                "total_asset_krw": round(cash + qty * price, 0),
            }
        )
    return steps


def compute_drawdown_metrics(steps: list[dict[str, Any]]) -> dict[str, Any]:
    """
    equity curve 기준 최대 낙폭·고점 대비 하락.

    Args:
        steps: simulate_portfolio_steps 결과.

    Returns:
        max_drawdown_pct, peak_asset_krw, trough_after_peak_krw.
    """
    if not steps:
        return {
            "max_drawdown_pct": 0.0,
            "peak_asset_krw": 0.0,
            "trough_asset_krw": 0.0,
        }
    assets = [float(s["total_asset_krw"]) for s in steps]
    peak = assets[0]
    max_dd = 0.0
    peak_at = assets[0]
    trough_at = assets[0]
    for a in assets:
        if a > peak:
            peak = a
        dd = (peak - a) / peak * 100.0 if peak > 0 else 0.0
        if dd > max_dd:
            max_dd = dd
            peak_at = peak
            trough_at = a
    return {
        "max_drawdown_pct": round(max_dd, 2),
        "peak_asset_krw": round(peak_at, 0),
        "trough_asset_krw": round(trough_at, 0),
    }


def simulate_portfolio_summary(
    trades: list[dict[str, Any]],
    *,
    initial_cash: float = GT_INITIAL_CASH_KRW,
    fee_rate: float = TRADING_FEE_RATE,
    last_price: float | None = None,
    use_amount_krw: bool = True,
) -> dict[str, Any]:
    """
    포트폴리오 시뮬 요약 + MDD.

    Args:
        trades: trade dict 리스트.
        initial_cash: 시작 현금.
        fee_rate: 수수료율.
        last_price: 미청산 평가가.
        use_amount_krw: amount_krw 체결 사용.

    Returns:
        pnl·fee·MDD 포함 dict.
    """
    steps = simulate_portfolio_steps(
        trades,
        initial_cash=initial_cash,
        fee_rate=fee_rate,
        use_amount_krw=use_amount_krw,
    )
    if not steps:
        return {
            "initial_cash_krw": round(initial_cash, 0),
            "final_asset_krw": round(initial_cash, 0),
            "pnl_krw": 0.0,
            "pnl_pct": 0.0,
            "trade_count": len(trades),
            "max_drawdown_pct": 0.0,
        }

    last_step = steps[-1]
    cash = float(last_step["cash_krw"])
    qty = float(last_step["holding_qty"])
    mark = float(last_price if last_price is not None else last_step["price"])
    holding_value = qty * mark
    final_asset = cash + holding_value
    pnl = final_asset - initial_cash
    pnl_pct = pnl / initial_cash * 100.0 if initial_cash else 0.0

    fees = 0.0
    for t in sorted(trades, key=lambda x: x["dt"]):
        ak = float(t.get("amount_krw") or 0)
        if ak <= 0:
            continue
        fees += ak * fee_rate

    dd = compute_drawdown_metrics(steps)
    return {
        "initial_cash_krw": round(initial_cash, 0),
        "final_asset_krw": round(final_asset, 0),
        "pnl_krw": round(pnl, 0),
        "pnl_pct": round(pnl_pct, 2),
        "total_fees_krw": round(fees, 0),
        "cash_krw": round(cash, 0),
        "holding_qty": round(qty, 6),
        "holding_value_krw": round(holding_value, 0),
        "mark_price": round(mark, 2),
        "fee_rate": fee_rate,
        "trade_count": len(trades),
        **dd,
    }
