"""
Ground Truth 타점·비중·자본 배분 모델 (일반화 명세).

타점 생성(ground_truth.py), 자본 배분(gt_allocation.py),
시뮬(position_sizing.py)의 공통 언어.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable

from config import (
    GT_BUY_BB_MAX,
    GT_BUY_MIN_BARS,
    GT_BUY_MIN_SWING_PCT,
    GT_BUY_PCT_LARGE_LEG,
    GT_BUY_PCT_SMALL_LEG,
    GT_BUY_WEIGHT_RULE,
    GT_LARGE_LEG_TOP_PCT,
    GT_MAX_BUYS_PER_LEG,
    GT_MAX_SELLS_PER_LEG,
    GT_MIN_ORDER_KRW,
    GT_SELL_SPLIT_GAP_PCT,
    GT_SELL_SPLIT_WEIGHTS,
    GT_SELECTION_MODE,
)

# --- 매수 비중 규칙 (확장 가능) ---

EntryWeightFn = Callable[[list[float]], list[float]]


def normalize_weights(scores: list[float]) -> list[float]:
    """
    비중 점수를 합 1로 정규화합니다.

    Args:
        scores: raw score 리스트.

    Returns:
        정규화 weight (합 ≈ 1).
    """
    if not scores:
        return []
    total = sum(scores)
    if total <= 0:
        n = len(scores)
        return [1.0 / n] * n
    return [s / total for s in scores]


def compute_buy_weights_inverse_price(prices: list[float]) -> list[float]:
    """
    저점 매수 비중: score_i = 1/price_i → 합=1 정규화.

    Args:
        prices: leg 내 매수 후보 가격.

    Returns:
        weight 리스트 (합 ≈ 1).
    """
    if not prices:
        return []
    scores = [1.0 / max(p, 1e-9) for p in prices]
    return normalize_weights(scores)


def compute_buy_weights_equal(_prices: list[float]) -> list[float]:
    """
    균등 분할 매수 비중.

    Args:
        _prices: leg 내 매수 후보 가격(미사용).

    Returns:
        weight 리스트 (합 = 1).
    """
    n = len(_prices)
    if n <= 0:
        return []
    return [1.0 / n] * n


ENTRY_WEIGHT_RULES: dict[str, EntryWeightFn] = {
    "inverse_price_normalized": compute_buy_weights_inverse_price,
    "equal": compute_buy_weights_equal,
}


def compute_entry_weights(
    prices: list[float],
    rule: str | None = None,
) -> list[float]:
    """
    매수 타점 비중을 규칙명으로 계산합니다.

    Args:
        prices: 체결 가격 리스트.
        rule: `inverse_price_normalized` | `equal`. None이면 config.

    Returns:
        leg 내 매수 weight (합 ≈ 1).
    """
    key = (rule or GT_BUY_WEIGHT_RULE).strip()
    fn = ENTRY_WEIGHT_RULES.get(key, compute_buy_weights_inverse_price)
    return fn(prices)


def leg_entry_weights(
    prices: list[float],
    rule: str | None = None,
) -> list[float]:
    """
    leg 내 매수 타점 비중 (compute_entry_weights 별칭).

    Args:
        prices: 체결 가격 리스트.
        rule: 비중 규칙 키.

    Returns:
        weight 리스트 (합 ≈ 1).
    """
    return compute_entry_weights(prices, rule)


def leg_exit_weights(
    n_sells: int,
    exit_spec: GtExitSpec | None = None,
) -> list[float]:
    """
    leg 매도 분할 비중.

    Args:
        n_sells: 매도 횟수.
        exit_spec: 매도 명세.

    Returns:
        weight 리스트 (합 ≈ 1).
    """
    return sell_split_weights(n_sells, exit_spec)


@dataclass(frozen=True)
class GtEntrySpec:
    """
    매수 타점(leg 내 분할) 규칙.

    Attributes:
        pivot_kind: ZigZag 저점(trough).
        price_field: 체결가 = 봉 Low.
        weight_rule: 매수 비중 규칙 키.
        max_per_leg: leg당 최대 매수 횟수.
        min_bars_gap: 분할 매수 최소 봉 간격.
    """

    pivot_kind: str = "trough"
    price_field: str = "Low"
    weight_rule: str = GT_BUY_WEIGHT_RULE
    max_per_leg: int = GT_MAX_BUYS_PER_LEG
    min_bars_gap: int = GT_BUY_MIN_BARS
    bb_filter: str = f"bb_pos <= {GT_BUY_BB_MAX}"


@dataclass(frozen=True)
class GtExitSpec:
    """
    매도 타점(leg 내 분할) 규칙.

    Attributes:
        pivot_kind: major swing 고점(peak).
        price_field: 체결가 = 봉 High.
        split_weights: N회 분할 시 각 매도 비중 (합=1).
        split_gap_pct: 2차 고점 인정 최소 괴리(%).
    """

    pivot_kind: str = "peak"
    price_field: str = "High"
    split_weights: tuple[float, ...] = GT_SELL_SPLIT_WEIGHTS
    split_gap_pct: float = GT_SELL_SPLIT_GAP_PCT
    max_per_leg: int = GT_MAX_SELLS_PER_LEG


@dataclass(frozen=True)
class GtCapitalSpec:
    """
    체결 원화(amount_krw) 배분 규칙.

    Attributes:
        buy_formula: 총자산 × 최적매수율, 상한=가용현금.
        optimal_buy_rate: leg 내 남은 weight 비중.
        large_leg_top_pct: 수익률 상위 leg 비율.
        pct_large: 상위 leg 총자산 배분 스케일.
        pct_small: 그 외 leg 스케일.
        min_order_krw: 최소 체결 원화.
    """

    buy_formula: str = (
        "target = total_asset * (weight/remaining_weights) * tier_scale; "
        "amount = min(target, available_cash/(1+fee))"
    )
    optimal_buy_rate: str = "weight / sum(remaining_buy_weights_in_leg)"
    large_leg_top_pct: float = GT_LARGE_LEG_TOP_PCT
    pct_large: float = GT_BUY_PCT_LARGE_LEG
    pct_small: float = GT_BUY_PCT_SMALL_LEG
    min_order_krw: float = float(GT_MIN_ORDER_KRW)
    sell_formula: str = "sell_base_qty * sell_weight * price (last sell = full leg_qty)"


@dataclass
class GroundTruthModel:
    """
    GT 전체 모델 (타점 + 비중 + 자본).

    Attributes:
        selection_mode: 타점 생성 모드.
        entry: 매수 명세.
        exit: 매도 명세.
        capital: 자본 배분 명세.
        leg: leg 정의.
    """

    selection_mode: str = GT_SELECTION_MODE
    entry: GtEntrySpec = field(default_factory=GtEntrySpec)
    exit: GtExitSpec = field(default_factory=GtExitSpec)
    capital: GtCapitalSpec = field(default_factory=GtCapitalSpec)
    leg_definition: str = (
        "이전 고점 매도 ~ 다음 고점 매도 구간 = leg_id; "
        "기간말 잔여 구간은 마지막 leg"
    )
    execution_order_chrono: str = (
        "amount_krw 배분·summary.pnl_pct = 시각순 체결(매도 후 현금 → 다음 매수 반영)"
    )
    execution_order_leg_block: str = (
        "JSON 저장 순서 = leg별 매수 전량 → 매도 전량 (차트·테이블 leg 정합)"
    )


def default_model() -> GroundTruthModel:
    """현재 config 기준 GT 모델."""
    return GroundTruthModel()


def sell_split_weights(
    n_sells: int,
    exit_spec: GtExitSpec | None = None,
) -> list[float]:
    """
    leg 매도 비중 (1회=100%, N회=split_weights 정규화).

    Args:
        n_sells: 매도 횟수(1 이상).
        exit_spec: None이면 default.

    Returns:
        weight 리스트 (합 ≈ 1).
    """
    spec = exit_spec or GtExitSpec()
    if n_sells <= 1:
        return [1.0]
    weights = list(spec.split_weights[:n_sells])
    if len(weights) < n_sells:
        weights.extend([weights[-1]] * (n_sells - len(weights)))
    return normalize_weights(weights)


def pair_peak_sell_weights(
    n_peaks: int,
    exit_spec: GtExitSpec | None = None,
) -> list[tuple[float, float]]:
    """
    고점 피벗 (피벗, weight) 쌍 — 1회 또는 분할.

    Args:
        n_peaks: 인정된 고점 수 (1 또는 2+).
        exit_spec: 매도 명세.

    Returns:
        (weight,) 또는 (w1, w2) 리스트. 호출측에서 피벗과 zip.
    """
    if n_peaks <= 1:
        return [(1.0,)]
    w = sell_split_weights(2, exit_spec)
    return [(w[0],), (w[1],)]


def remaining_weight_sum(
    trades: list[dict[str, Any]],
    leg_id: int,
    from_index: int,
) -> float:
    """
    leg 내 from_index 이후 남은 매수 weight 합.

    Args:
        trades: 시각순 trade dict.
        leg_id: leg 번호.
        from_index: chron 리스트 인덱스.

    Returns:
        남은 weight 합.
    """
    total = 0.0
    for j, t in enumerate(trades):
        if j < from_index:
            continue
        if int(t.get("leg_id", 0)) != leg_id:
            continue
        if t.get("action") == "buy":
            total += float(t.get("weight", 1.0))
    return total


def model_to_dict(model: GroundTruthModel | None = None) -> dict[str, Any]:
    """
    JSON·리포트용 모델 dict.

    Args:
        model: None이면 default.

    Returns:
        직렬화 dict.
    """
    m = model or default_model()
    w_rule = m.entry.weight_rule
    w_formula = (
        "w_i = (1/price_i) / sum(1/price_j)"
        if w_rule == "inverse_price_normalized"
        else "w_i = 1 / n"
    )
    return {
        "selection_mode": m.selection_mode,
        "leg_definition": m.leg_definition,
        "entry": {
            "pivot": m.entry.pivot_kind,
            "price": m.entry.price_field,
            "weight_rule": m.entry.weight_rule,
            "weight_formula": w_formula,
            "max_buys_per_leg": m.entry.max_per_leg,
            "min_bars_between_buys": m.entry.min_bars_gap,
            "bb_filter": m.entry.bb_filter,
        },
        "exit": {
            "pivot": m.exit.pivot_kind,
            "price": m.exit.price_field,
            "weight_rule": "fixed_split_or_full",
            "weights_split": list(m.exit.split_weights),
            "split_gap_pct": m.exit.split_gap_pct,
            "max_sells_per_leg": m.exit.max_per_leg,
        },
        "capital": {
            "buy": m.capital.buy_formula,
            "optimal_buy_rate": m.capital.optimal_buy_rate,
            "large_leg_top_pct": m.capital.large_leg_top_pct,
            "pct_large_leg": m.capital.pct_large,
            "pct_small_leg": m.capital.pct_small,
            "min_order_krw": m.capital.min_order_krw,
            "sell": m.capital.sell_formula,
        },
        "execution": {
            "chrono": m.execution_order_chrono,
            "leg_block_json": m.execution_order_leg_block,
        },
    }


def summarize_leg_weights(trades: list[dict[str, Any]]) -> dict[str, Any]:
    """
    leg별 매수·매도 비중 합 검증용 요약.

    Args:
        trades: GT trade dict.

    Returns:
        leg_id → {buy_sum, sell_sum, n_buy, n_sell, valid}.
    """
    legs: dict[int, dict[str, Any]] = {}
    for t in trades:
        lid = int(t.get("leg_id", 0))
        legs.setdefault(
            lid,
            {"buy_sum": 0.0, "sell_sum": 0.0, "n_buy": 0, "n_sell": 0},
        )
        w = float(t.get("weight", 0))
        if t.get("action") == "buy":
            legs[lid]["buy_sum"] += w
            legs[lid]["n_buy"] += 1
        elif t.get("action") == "sell":
            legs[lid]["sell_sum"] += w
            legs[lid]["n_sell"] += 1
    for lid, info in legs.items():
        buy_ok = abs(info["buy_sum"] - 1.0) < 0.02 or info["n_buy"] == 0
        sell_ok = abs(info["sell_sum"] - 1.0) < 0.02 or info["n_sell"] == 0
        info["valid"] = buy_ok and sell_ok
        info["buy_sum"] = round(info["buy_sum"], 4)
        info["sell_sum"] = round(info["sell_sum"], 4)
    return legs


def weight_policy_summary(model: GroundTruthModel | None = None) -> dict[str, Any]:
    """
    시뮬·리포트용 비중 정책 요약.

    Args:
        model: GT 모델.

    Returns:
        entry/exit/capital 요약 dict.
    """
    m = model or default_model()
    return {
        "entry_weight_rule": m.entry.weight_rule,
        "exit_split_weights": list(m.exit.split_weights),
        "capital_large_pct": m.capital.pct_large,
        "capital_small_pct": m.capital.pct_small,
        "large_leg_top_pct": m.capital.large_leg_top_pct,
    }
