from __future__ import annotations

import math
from datetime import date, timedelta
from typing import Any

# 매도 결정에 동원하는 5개 독립 팩터군. 단일 팩터의 임계값 돌파만으로는 행동을
# 트리거하지 않는다 — 최소 CONFLUENCE_MIN개 팩터군이 동일 방향으로 합의해야
# SELL/ADD 확신도가 성립한다. (기계적 단일 트리거 매도 금지 원칙)
FACTOR_FAMILIES: tuple[str, ...] = (
    "macro_pressure",
    "fundamental_trajectory",
    "short_interest_pressure",
    "microstructure_pressure",
    "liquidity_rotation_risk",
)
CONFLUENCE_MIN = 3
EVENT_PRE_GUARD_DAYS = 5   # macro_event_synchronizer_v2.event_hold_gate와 동일 — HIGH 이벤트 5일 전
EVENT_POST_GUARD_DAYS = 2  # 이벤트 후 2일 변동성 소화 구간

# 금리국면별 시장 성격: 금리 상승기=실적장세(펀더멘털/수출입 실적이 가격을 주도),
# 금리 보합·하락기=기술장세(수급·미시구조가 가격을 주도). 동일한 5팩터라도
# 국면에 따라 가중치를 달리 줘야 confluence가 의미를 갖는다.
REGIME_FLAT_WEIGHTS: dict[str, float] = {family: 1.0 for family in FACTOR_FAMILIES}
REGIME_WEIGHT_TABLE: dict[str, dict[str, float]] = {
    "PERFORMANCE_MARKET": {  # 금리 상승기 — 실적/수출입 펀더멘털 가중 상향
        "macro_pressure": 1.2,
        "fundamental_trajectory": 1.8,
        "short_interest_pressure": 1.0,
        "microstructure_pressure": 0.5,
        "liquidity_rotation_risk": 1.0,
    },
    "TECHNICAL_MARKET": {  # 금리 보합·하락기 — 수급/미시구조 가중 상향
        "macro_pressure": 0.8,
        "fundamental_trajectory": 0.8,
        "short_interest_pressure": 1.3,
        "microstructure_pressure": 1.6,
        "liquidity_rotation_risk": 1.3,
    },
    "NEUTRAL": REGIME_FLAT_WEIGHTS,
}


def classify_market_regime(rate_trend: str | None) -> str:
    """금리 추세 문자열(RISING/FLAT/FALLING)을 실적장세/기술장세로 분류.

    RISING → PERFORMANCE_MARKET(실적장세): 금리 상승기엔 유동성보다 실적/펀더멘털이
    가격을 결정. FLAT/FALLING → TECHNICAL_MARKET(기술장세): 유동성이 풍부해 수급·
    미시구조·테마성 모멘텀이 가격을 주도. 입력 결측 시 NEUTRAL(가중치 변화 없음).
    """
    trend = str(rate_trend or "").upper()
    if trend == "RISING":
        return "PERFORMANCE_MARKET"
    if trend in {"FLAT", "FALLING"}:
        return "TECHNICAL_MARKET"
    return "NEUTRAL"


def _finite(value: Any) -> bool:
    return isinstance(value, (int, float)) and math.isfinite(float(value))


def compute_short_interest_composite(ctx: dict[str, Any]) -> dict[str, Any]:
    """SHORT_INTEREST_RISK_GAUGE_V1.

    5요소: 공매도잔고율 변화, 공매도거래비중, 상대수익률(섹터/지수 대비),
    거래량 이상, 실적전망. 잔고율 단독으로는 매도 근거가 약함(현대로템형) —
    잔고율이 낮을 때는 거래비중·상대수익률 가중치를 자동 상향한다.
    """
    missing: list[str] = []

    short_balance_ratio = ctx.get("short_balance_ratio")          # %, 현재 잔고율
    short_balance_ratio_chg_20d = ctx.get("short_balance_ratio_chg_20d")  # %p, 20일 변화
    short_turnover_share = ctx.get("short_turnover_share")        # 당일 거래 중 공매도 비중 %
    relative_return_20d = ctx.get("relative_return_20d")          # 종목수익률 - 섹터(or지수)수익률, %p
    volume_ratio_5d = ctx.get("volume_ratio_5d")                  # 5일평균거래량 대비 비율
    earnings_outlook = str(ctx.get("earnings_outlook") or "").upper()  # IMPROVING|STABLE|DETERIORATING|UNKNOWN

    for name, value in (
        ("short_balance_ratio", short_balance_ratio),
        ("short_turnover_share", short_turnover_share),
        ("relative_return_20d", relative_return_20d),
    ):
        if not _finite(value):
            missing.append(name)

    if missing:
        return {
            "short_interest_pressure": None,
            "status": "DATA_MISSING",
            "missing_inputs": missing,
            "note": "잔고율/거래비중/상대수익률 중 결측 — 공매도 합성 점수를 산출하지 않음(추정 금지)",
        }

    low_balance_regime = float(short_balance_ratio) < 1.0  # 잔고율 1% 미만이면 '낮은 잔고율' 취급(현대로템형)

    # 잔고율 추세: 상승=매도근거 강화, 하락=매도근거 약화(혹은 매수근거)
    balance_trend_signal = 0.0
    if _finite(short_balance_ratio_chg_20d):
        balance_trend_signal = max(-1.0, min(1.0, float(short_balance_ratio_chg_20d) / 1.5))

    turnover_signal = max(-1.0, min(1.0, (float(short_turnover_share) - 8.0) / 12.0))  # 8% 기준선
    relative_return_signal = max(-1.0, min(1.0, -float(relative_return_20d) / 10.0))   # 상대 약세일수록 +
    volume_signal = 0.0
    if _finite(volume_ratio_5d):
        volume_signal = max(-1.0, min(1.0, (float(volume_ratio_5d) - 1.0)))

    outlook_signal = {
        "IMPROVING": -0.6,
        "STABLE": 0.0,
        "DETERIORATING": 0.7,
    }.get(earnings_outlook, 0.0)

    if low_balance_regime:
        # 잔고율 자체는 약한 근거 — 거래비중·상대수익률 가중치 상향, 잔고율추세 가중치 하향
        weights = {"balance": 0.10, "turnover": 0.30, "relative": 0.30, "volume": 0.10, "outlook": 0.20}
    else:
        weights = {"balance": 0.30, "turnover": 0.20, "relative": 0.20, "volume": 0.10, "outlook": 0.20}

    pressure = (
        balance_trend_signal * weights["balance"]
        + turnover_signal * weights["turnover"]
        + relative_return_signal * weights["relative"]
        + volume_signal * weights["volume"]
        + outlook_signal * weights["outlook"]
    )
    pressure = max(-1.0, min(1.0, pressure))

    label = "ELEVATED_SHORT_PRESSURE" if pressure >= 0.5 else "WATCH" if pressure >= 0.2 else \
        "SHORT_COVERING_SUPPORTIVE" if pressure <= -0.5 else "NEUTRAL"

    return {
        "short_interest_pressure": round(pressure, 4),
        "status": "OK",
        "low_balance_regime": low_balance_regime,
        "label": label,
        "components": {
            "balance_trend_signal": round(balance_trend_signal, 4),
            "turnover_signal": round(turnover_signal, 4),
            "relative_return_signal": round(relative_return_signal, 4),
            "volume_signal": round(volume_signal, 4),
            "outlook_signal": outlook_signal,
        },
        "weights_used": weights,
    }


def compute_microstructure_pressure_from_orderbook(orderbook_output1: dict[str, Any]) -> dict[str, Any]:
    """MICROSTRUCTURE_PRESSURE_FROM_ORDERBOOK_V1.

    KIS Open API FHKST01010200(주식현재가 호가/예상체결) output1의 10단계 호가 잔량을
    -1(매수우위/지지)~+1(매도우위/압력)로 계량화. 실측 확인된 필드명(2026-06-21,
    005930 라이브 호출): total_askp_rsqn, total_bidp_rsqn(10단계 합계 잔량).
    이 점수는 전략 방향 결정에는 쓰지 않고 confluence가 성립한 이후의 '집행 타이밍'
    보조로만 사용한다(spec/exit/qualitative_sell_strategy_v1.yaml:factor_families.
    microstructure_pressure 참조).
    """
    total_askp = orderbook_output1.get("total_askp_rsqn")
    total_bidp = orderbook_output1.get("total_bidp_rsqn")
    try:
        total_askp = float(total_askp)
        total_bidp = float(total_bidp)
    except (TypeError, ValueError):
        return {"microstructure_pressure": None, "status": "DATA_MISSING"}

    denom = total_askp + total_bidp
    if denom <= 0:
        return {"microstructure_pressure": None, "status": "DATA_MISSING"}

    pressure = max(-1.0, min(1.0, (total_askp - total_bidp) / denom))
    return {
        "microstructure_pressure": round(pressure, 4),
        "status": "OK",
        "total_askp_rsqn": total_askp,
        "total_bidp_rsqn": total_bidp,
    }


def _event_review_window(
    today: date,
    pressure_sign: int,
    next_earnings_date: date | None,
    next_macro_event_date: date | None,
    macro_event_impact: str | None,
    earnings_outlook: str,
) -> dict[str, Any]:
    """캘린더 기반 검토 구간 산출 — 임의 날짜 고정이 아니라 실제 이벤트 일정에서 역산."""
    candidates: list[tuple[date, str]] = []

    if next_earnings_date is not None:
        if pressure_sign < 0 and earnings_outlook == "DETERIORATING":
            # 실적 악화 전망 + 매도압력 → 실적발표 전 정리(서프라이즈 리스크 회피)
            candidates.append((next_earnings_date - timedelta(days=EVENT_PRE_GUARD_DAYS), "PRE_EARNINGS_EXIT_BEFORE_SURPRISE_RISK"))
        elif pressure_sign < 0 and earnings_outlook in {"IMPROVING", "STABLE"}:
            # 단기 기술적 매도압력이지만 실적전망은 양호 → 발표 직전 매도는 가치훼손, 발표 이후로 연기
            candidates.append((next_earnings_date + timedelta(days=EVENT_POST_GUARD_DAYS), "DEFER_TO_POST_EARNINGS_AVOID_PREMATURE_EXIT"))
        elif pressure_sign > 0:
            # 추가매수/보유 신호 — 발표 변동성 통과 후 확신 재평가
            candidates.append((next_earnings_date + timedelta(days=EVENT_POST_GUARD_DAYS), "REASSESS_AFTER_EARNINGS_CONFIRM"))

    if next_macro_event_date is not None and str(macro_event_impact or "").upper() in {"HIGH", "VERY_HIGH"}:
        if pressure_sign < 0:
            candidates.append((next_macro_event_date - timedelta(days=EVENT_PRE_GUARD_DAYS), "PRE_MACRO_EVENT_DERISK"))
        else:
            candidates.append((next_macro_event_date + timedelta(days=EVENT_POST_GUARD_DAYS), "POST_MACRO_EVENT_CONFIRM"))

    if not candidates:
        return {
            "review_window_start": today.isoformat(),
            "review_window_end": (today + timedelta(days=10)).isoformat(),
            "window_basis": "NO_SCHEDULED_EVENT_DEFAULT_10D_REVIEW",
        }

    earliest = min(candidates, key=lambda item: item[0])
    window_start = max(today, earliest[0] - timedelta(days=2))
    window_end = earliest[0] + timedelta(days=2)
    return {
        "review_window_start": window_start.isoformat(),
        "review_window_end": window_end.isoformat(),
        "window_basis": earliest[1],
    }


def compute_qualitative_sell_strategy(ctx: dict[str, Any]) -> dict[str, Any]:
    """QUALITATIVE_SELL_STRATEGY_V1.

    매크로/실적/펀더멘털/공매도수급/호가미시구조/대내외(IPO·로테이션) 5개
    독립 팩터군의 합의(confluence)로만 행동을 생성한다. 현금부족 사유는
    입력에서 의도적으로 배제(cash_shortfall_excluded=True) — 가치보존이
    유일한 목적 함수.
    """
    today_raw = ctx.get("today")
    today = today_raw if isinstance(today_raw, date) else date.today()

    factor_values: dict[str, float | None] = {}
    missing_factors: list[str] = []
    for family in FACTOR_FAMILIES:
        value = ctx.get(family)
        if _finite(value):
            factor_values[family] = max(-1.0, min(1.0, float(value)))
        else:
            factor_values[family] = None
            missing_factors.append(family)

    available = {k: v for k, v in factor_values.items() if v is not None}
    if len(available) < CONFLUENCE_MIN:
        return {
            "action": "INSUFFICIENT_DATA_NO_ACTION",
            "conviction": "NONE",
            "available_factors": list(available.keys()),
            "missing_factors": missing_factors,
            "rationale": "5개 팩터군 중 confluence 판정에 필요한 최소 데이터가 부족 — 추정으로 행동 생성 금지",
            "cash_shortfall_excluded": True,
            "mechanical_sell_prohibited": True,
        }

    # 부호 규약: 모든 팩터군은 +1(매도압력 최대) ~ -1(보유/추가 지지 최대) 동일 스케일.
    # short_interest_pressure도 동일 — ELEVATED_SHORT_PRESSURE(+) / SHORT_COVERING_SUPPORTIVE(-).
    # confluence 합의 카운트는 국면 가중치와 무관하게 원시 방향성으로만 판정한다
    # (가중치는 행동 '강도'에만 영향 — 합의 성립 여부 자체를 왜곡하지 않는다).
    sell_agree = [k for k, v in available.items() if v >= 0.30]
    hold_add_agree = [k for k, v in available.items() if v <= -0.30]

    market_regime = classify_market_regime(ctx.get("rate_trend")) if "market_regime" not in ctx else str(ctx.get("market_regime") or "NEUTRAL").upper()
    regime_weights = REGIME_WEIGHT_TABLE.get(market_regime, REGIME_FLAT_WEIGHTS)
    weighted_sum = sum(available[k] * regime_weights.get(k, 1.0) for k in available)
    weight_total = sum(regime_weights.get(k, 1.0) for k in available)
    composite_score = weighted_sum / weight_total if weight_total else 0.0

    earnings_outlook = str(ctx.get("earnings_outlook") or "STABLE").upper()
    next_earnings_date = ctx.get("next_earnings_date") if isinstance(ctx.get("next_earnings_date"), date) else None
    next_macro_event_date = ctx.get("next_macro_event_date") if isinstance(ctx.get("next_macro_event_date"), date) else None
    macro_event_impact = ctx.get("macro_event_impact")

    if len(sell_agree) >= CONFLUENCE_MIN:
        conviction = "HIGH" if len(sell_agree) >= 4 else "MEDIUM"
        action = "EXIT_REVIEW_FULL" if composite_score >= 0.6 else "TRIM_REVIEW_PARTIAL"
        pressure_sign = -1
        rationale = f"매도압력 합의({len(sell_agree)}/{len(available)} 팩터군 매도방향 합치): " + ", ".join(sell_agree)
    elif len(hold_add_agree) >= CONFLUENCE_MIN:
        conviction = "HIGH" if len(hold_add_agree) >= 4 else "MEDIUM"
        action = "HOLD_ADD_CONVICTION"
        pressure_sign = 1
        rationale = f"보유/추가 근거 합의({len(hold_add_agree)}/{len(available)} 팩터군 지지방향 합치): " + ", ".join(hold_add_agree)
    else:
        conviction = "LOW"
        action = "HOLD_NO_CONFLUENCE"
        pressure_sign = 0
        rationale = "팩터군 간 합의 미달 — 단일/소수 팩터의 임계값 돌파만으로는 매도 트리거 금지"

    window = _event_review_window(
        today=today,
        pressure_sign=pressure_sign,
        next_earnings_date=next_earnings_date,
        next_macro_event_date=next_macro_event_date,
        macro_event_impact=macro_event_impact,
        earnings_outlook=earnings_outlook,
    ) if pressure_sign != 0 else None

    return {
        "action": action,
        "conviction": conviction,
        "market_regime": market_regime,
        "composite_score": round(composite_score, 4),
        "sell_agreeing_factors": sell_agree,
        "hold_add_agreeing_factors": hold_add_agree,
        "missing_factors": missing_factors,
        "review_window": window,
        "rationale": rationale,
        "cash_shortfall_excluded": True,
        "mechanical_sell_prohibited": True,
    }


def compute_satellite_candidate_score(ctx: dict[str, Any]) -> dict[str, Any]:
    """SATELLITE_CANDIDATE_SCORE_V1.

    미보유 유니버스 종목을 섹터 수출입 전망(sector_export_trend) + 펀더멘털
    추세 + 국면적합도로 평가해 WATCH/BUY_CANDIDATE/AVOID를 산출한다. 보유종목
    매도판단(compute_qualitative_sell_strategy)과 동일한 부호 규약을 쓰지 않고
    별도 -1(약세)~+1(강세) 매력도 스케일을 쓴다 — 매수후보 평가와 매도판단은
    목적함수가 다르므로 동일 점수를 재사용하지 않는다.
    """
    sector_export_trend = ctx.get("sector_export_trend")          # %, 섹터 수출 YoY/MoM 추세
    fundamental_trajectory = ctx.get("fundamental_trajectory")    # -1(악화)~+1(개선), 매도엔진과 동일 정의역이나 부호 반대 해석 주의
    relative_return_20d = ctx.get("relative_return_20d")
    market_regime = str(ctx.get("market_regime") or classify_market_regime(ctx.get("rate_trend"))).upper()

    missing = [name for name, value in (
        ("sector_export_trend", sector_export_trend),
        ("fundamental_trajectory", fundamental_trajectory),
    ) if not _finite(value)]
    if missing:
        return {
            "satellite_action": "INSUFFICIENT_DATA_NO_ACTION",
            "missing_inputs": missing,
            "market_regime": market_regime,
        }

    export_signal = max(-1.0, min(1.0, float(sector_export_trend) / 10.0))
    fundamental_signal = max(-1.0, min(1.0, -float(fundamental_trajectory)))  # 매도엔진 부호(+)=악화 -> 매력도는 반전
    relative_signal = max(-1.0, min(1.0, float(relative_return_20d) / 10.0)) if _finite(relative_return_20d) else 0.0

    if market_regime == "PERFORMANCE_MARKET":
        weights = {"export": 0.45, "fundamental": 0.40, "relative": 0.15}
    elif market_regime == "TECHNICAL_MARKET":
        weights = {"export": 0.20, "fundamental": 0.25, "relative": 0.55}
    else:
        weights = {"export": 0.34, "fundamental": 0.33, "relative": 0.33}

    attractiveness = (
        export_signal * weights["export"]
        + fundamental_signal * weights["fundamental"]
        + relative_signal * weights["relative"]
    )
    attractiveness = max(-1.0, min(1.0, attractiveness))

    if attractiveness >= 0.5:
        satellite_action = "BUY_CANDIDATE"
    elif attractiveness >= 0.2:
        satellite_action = "WATCH"
    elif attractiveness <= -0.4:
        satellite_action = "AVOID"
    else:
        satellite_action = "NEUTRAL_NO_EDGE"

    return {
        "satellite_action": satellite_action,
        "attractiveness_score": round(attractiveness, 4),
        "market_regime": market_regime,
        "components": {
            "export_signal": round(export_signal, 4),
            "fundamental_signal": round(fundamental_signal, 4),
            "relative_signal": round(relative_signal, 4),
        },
        "weights_used": weights,
    }
