"""매수/매도 판단 — 결정론적 규칙. LLM 미경유.

방향(애널·펀더) 약한 게이트 + 가점 → 타이밍(수급·기술)으로 진입/청산.
각 판단은 dashboard 용 체크리스트(checks)를 함께 반환한다.
"""
from __future__ import annotations

import math

from . import config, indicators as ind_mod


def compute_indicators(series: list[dict]) -> dict | None:
    """series(어제까지+오늘) → 지표 묶음. 데이터 부족 시 None."""
    closes = [c['close'] for c in series]
    sma_long = ind_mod.sma(closes, config.SMA_LONG)
    sma_short = ind_mod.sma(closes, config.SMA_SHORT)
    atr = ind_mod.atr(series, config.ATR_PERIOD)
    if sma_long is None or sma_short is None or atr is None:
        return None
    # 중기선(60) — 데이터 부족 시 None
    sma_mid = ind_mod.sma(closes, config.SMA_MID)
    # 20일선 기울기(우상향?) — N일 전 20일선과 비교. 데이터 부족 시 None
    slope_days = config.TREND_SLOPE_DAYS
    sma_long_prev = (ind_mod.sma(closes[:-slope_days], config.SMA_LONG)
                     if len(closes) > config.SMA_LONG + slope_days else None)
    sma_long_slope_up = (sma_long > sma_long_prev) if sma_long_prev is not None else None

    # 회전율 — 오늘 봉(라이브 시세)엔 turnover_rate 없음 → 최근 봉의 거래량/회전율로 유통주식 추정해 환산
    hist = series[:-1]
    turns = [c['turnover_rate'] for c in hist[-config.VOLUME_AVG_PERIOD:]
             if c.get('turnover_rate')]
    turnover_avg = sum(turns) / len(turns) if turns else None
    est_float = None
    for c in reversed(hist):
        tr, v = c.get('turnover_rate'), c.get('volume')
        if tr and v:
            est_float = v / (tr / 100.0)
            break
    turnover_today = (series[-1]['volume'] / est_float * 100.0) if est_float else None

    return {
        'price': closes[-1],
        'prev_close': closes[-2] if len(closes) >= 2 else closes[-1],
        'today_volume': series[-1]['volume'],
        'sma_short': sma_short,
        'sma_long': sma_long,
        'sma_mid': sma_mid,
        'sma_long_slope_up': sma_long_slope_up,
        'rel_strength': None,   # 라이브 engine이 주입(종목−지수 N일 수익률). backtest는 None(중립)
        'atr': atr,
        'rsi': ind_mod.rsi(closes, config.RSI_PERIOD),
        'recent_high': ind_mod.recent_high(series, config.BREAKOUT_LOOKBACK),
        'recent_low': ind_mod.recent_low(series, config.SWING_LOW_LOOKBACK),
        'volume_avg': ind_mod.volume_avg(series, config.VOLUME_AVG_PERIOD),
        'turnover_today': turnover_today,
        'turnover_avg': turnover_avg,
    }


def target_value(equity: float, fill_price: float, stop: float) -> float:
    """종목당 목표 진입금액 — 리스크 기반(자산×RISK% ÷ 손절폭)을 균등비중(자산/MAX_POSITIONS) 상한으로 캡.
    RISK_PER_TRADE_PCT=0 또는 손절 정보 없으면 균등비중으로 폴백. 엔진·백테스트 공용(같은 두뇌)."""
    cap = equity / max(1, config.MAX_POSITIONS)
    risk_pct = getattr(config, 'RISK_PER_TRADE_PCT', 0) or 0
    if risk_pct > 0 and stop and fill_price > stop:
        risk_value = (equity * risk_pct) / (fill_price - stop) * fill_price
        return min(cap, risk_value)
    return cap


def trend_ok(ind: dict) -> bool:
    """추세 1차 스크린 — 비싼 수급·애널 호출 전 게이트.
    기본: 현재가>20일선 & 정배열(5>20). 보강: ①20일선 우상향 ②5>중기선(60) — 데이터 있을 때만."""
    if not (ind['price'] > ind['sma_long'] and ind['sma_short'] > ind['sma_long']):
        return False
    if getattr(config, 'TREND_SLOPE_GATE', 1) and ind.get('sma_long_slope_up') is False:
        return False                                     # ① 장기선이 꺾여 내려오면 제외 (게이트 ON일 때만)
    sm = ind.get('sma_mid')
    if getattr(config, 'MID_ARRAY_GATE', 1) and sm is not None and ind['sma_short'] <= sm:
        return False                                     # ② 단기선이 중기선 아래면(역배열) 제외 (게이트 ON일 때만)
    return True


def compute_stop_target(entry: float, atr: float, recent_low: float | None) -> tuple[int, int]:
    """손절 = max(진입−ATR×2, 스윙저점) → 둘 중 위쪽(덜 손해). 목표 = 진입 + (진입−손절)×RR."""
    atr_stop = entry - atr * config.STOP_ATR_MULT
    stop = max(atr_stop, recent_low) if recent_low else atr_stop
    stop = min(stop, entry - 1)          # 손절은 진입가보다 아래여야 함
    target = entry + (entry - stop) * config.RR_RATIO
    return int(round(stop)), int(round(target))


def _analyst_checks(analyst: dict | None, price: float) -> tuple[bool, list[dict], int]:
    """애널 약한 게이트 + 가점. 반환: (게이트 통과, checks, 가점)."""
    checks: list[dict] = []
    bonus = 0
    if not analyst:
        checks.append({'label': '애널', 'ok': None, 'detail': '데이터 없음(통과)'})
        return True, checks, 0

    gate_ok = True
    op = analyst.get('opinion')
    if op is not None:
        sell = op <= config.ANALYST_SELL_OPINION_MAX
        checks.append({'label': '투자의견', 'ok': not sell,
                       'detail': f'{op:.1f}/5' + (' 매도권' if sell else '')})
        if sell:
            gate_ok = False
    up = analyst.get('upside_pct')
    if up is not None:
        ok = up >= config.ANALYST_MIN_UPSIDE_PCT
        checks.append({'label': '상승여력', 'ok': ok, 'detail': f'{up:+.0f}%'})
        if not ok:
            gate_ok = False
    if analyst.get('revision_up'):
        checks.append({'label': '목표가 리비전', 'ok': True, 'detail': '상향 ➕'})
        bonus += 1
    if analyst.get('surprise_pos'):
        checks.append({'label': '어닝 서프라이즈', 'ok': True, 'detail': '+ ➕'})
        bonus += 1
    return gate_ok, checks, bonus


def trend_checks(ind: dict) -> list[dict]:
    """추세 게이트 체크리스트(trend_ok와 동일 4조건 + 상대강도) — 매수 판단·제외 카드 공용.
    라벨은 실제 적용된 이평 일수(config)로 표기 (변이마다 SMA_LONG 10/20/40 다름)."""
    price = ind['price']
    _S, _L, _M = config.SMA_SHORT, config.SMA_LONG, config.SMA_MID
    slope = ind.get('sma_long_slope_up')
    sm = ind.get('sma_mid')
    rel = ind.get('rel_strength')
    slope_gate = getattr(config, 'TREND_SLOPE_GATE', 1)
    mid_gate = getattr(config, 'MID_ARRAY_GATE', 1)
    # 게이트 꺼진 조건은 ok=None(참고) — 통과/탈락에 영향 없음을 표시
    if not slope_gate:
        slope_check = {'label': f'추세 기울기({_L}일선↑)', 'ok': None,
                       'detail': '게이트 끔(완화)' + ('' if slope is None else f" · 실제 {'우상향' if slope else '하향'}")}
    else:
        slope_check = {'label': f'추세 기울기({_L}일선↑)', 'ok': None if slope is None else slope,
                       'detail': '데이터 없음(통과)' if slope is None else ('우상향' if slope else '하향')}
    if not mid_gate:
        mid_check = {'label': f'중기 정배열({_S}>{_M})', 'ok': None,
                     'detail': '게이트 끔(완화)' + ('' if sm is None else f" · {ind['sma_short']:,.0f} / {sm:,.0f}")}
    else:
        mid_check = {'label': f'중기 정배열({_S}>{_M})', 'ok': None if sm is None else ind['sma_short'] > sm,
                     'detail': '데이터 없음(통과)' if sm is None else f"{ind['sma_short']:,.0f} / {sm:,.0f}"}
    return [
        {'label': f'추세({_L}일선 위)', 'ok': price > ind['sma_long'],
         'detail': f"{price:,} vs {ind['sma_long']:,.0f}"},
        {'label': f'정배열({_S}>{_L})', 'ok': ind['sma_short'] > ind['sma_long'],
         'detail': f"{ind['sma_short']:,.0f} / {ind['sma_long']:,.0f}"},
        slope_check, mid_check,
        {'label': '상대강도(시장대비)', 'ok': None if rel is None else rel > 0,
         'detail': '데이터 없음(통과)' if rel is None else f'{rel:+.1f}%p'},
    ]


def evaluate_candidate(code, name, sources, ind, flow, analyst, market_ok) -> dict:
    """미보유 종목 매수 판단. flow/analyst 는 engine 이 trend 통과 후 주입."""
    price = ind['price']
    checks: list[dict] = list(trend_checks(ind))   # 1. 추세 게이트 (제외 카드와 공유)

    # 2. 수급
    fnet = flow.get('foreign', 0) if flow else 0
    inet = flow.get('institution', 0) if flow else 0
    supply_ok = fnet > 0 or inet > 0
    checks.append({'label': f'수급({config.FLOW_DAYS}일 외/기)', 'ok': supply_ok,
                   'detail': f'외 {fnet:+,} · 기 {inet:+,} (천주)'})

    # 3. 애널 게이트+가점
    gate_ok, acks, bonus = _analyst_checks(analyst, price)
    checks.extend(acks)

    # 시장 강약 — 소프트 필터(차단 X). 약세장이면 눌림목 진입만 보류, 돌파 진입은 허용
    checks.append({'label': '시장 강세', 'ok': market_ok,
                   'detail': '강세' if market_ok else '약세 — 돌파만 허용'})

    base = {'code': code, 'name': name, 'sources': sources, 'price': price,
            'checks': checks, 'action': None, 'buy_path': None, 'buy_price': None,
            'plan_stop': None, 'plan_target': None, 'bonus': bonus}

    if not gate_ok:
        return {**base, 'state': 'SKIP', 'reason': '애널 게이트 제외'}
    if not supply_ok:
        return {**base, 'state': 'WAIT', 'reason': '수급 미충족'}

    # 4. 과열 보류 — 회전율이 평균 대비 과도하면 돌파·눌림목 불문 신규매수 보류
    # 장중엔 부분 누적 거래량을 경과시간 비례로 하루치 환산(vol_time_frac, engine 주입·백테스트는 1.0)
    vfrac = ind.get('vol_time_frac') or 1.0
    vol_today = ind['today_volume'] / vfrac
    t_today, t_avg = ind.get('turnover_today'), ind.get('turnover_avg')
    if t_today is not None:
        t_today = t_today / vfrac
    proj = ' (환산)' if vfrac < 1.0 else ''
    overheated = bool(t_today and t_avg and t_today >= t_avg * config.TURNOVER_OVERHEAT_MULT)
    if t_today is not None and t_avg:
        checks.append({'label': '과열 아님(회전율)', 'ok': not overheated,
                       'detail': f"{t_today:.2f}%{proj} vs 평균 {t_avg:.2f}% (보류 {config.TURNOVER_OVERHEAT_MULT:.0f}배↑)"})
    if overheated:
        return {**base, 'state': 'WAIT',
                'reason': f'과열(회전율 {t_today:.1f}% ≥ 평균×{config.TURNOVER_OVERHEAT_MULT:.0f}) — 신규매수 보류'}

    # 5. 매수 경로 — 돌파 우선 (전고점 + 거래량 급증 + 회전율 급증 모두 충족)
    price_break = bool(ind['recent_high'] and price > ind['recent_high'])
    vol_mult_ok = bool(ind['volume_avg'] and vol_today >= ind['volume_avg'] * config.VOLUME_BREAKOUT_MULT)
    turn_ok = (t_today is None or not t_avg) or (t_today >= t_avg * config.TURNOVER_BREAKOUT_MULT)
    breakout = price_break and vol_mult_ok and turn_ok

    checks.append({'label': '돌파(전고점)', 'ok': price_break,
                   'detail': f"고점 {ind['recent_high']:,.0f}" if ind['recent_high'] else '—'})
    checks.append({'label': '거래량 급증', 'ok': vol_mult_ok,
                   'detail': f"{vol_today:,.0f}{proj} vs 평균 {ind['volume_avg']:,.0f}" if ind['volume_avg'] else '—'})
    if t_today is not None and t_avg:
        checks.append({'label': '회전율 급증', 'ok': bool(turn_ok),
                       'detail': f"{t_today:.2f}%{proj} vs 평균 {t_avg:.2f}%"})
    else:
        checks.append({'label': '회전율', 'ok': None, 'detail': '데이터 없음(통과)'})

    # RSI — 돌파 과열 추격 / 눌림목 낙하 진입 방지 (데이터 없으면 통과)
    rsi = ind.get('rsi')
    rsi_overbought = rsi is not None and rsi >= config.RSI_OVERBOUGHT
    rsi_oversold = rsi is not None and rsi < config.RSI_OVERSOLD
    checks.append({'label': 'RSI',
                   'ok': None if rsi is None else not (rsi_overbought or rsi_oversold),
                   'detail': '데이터 없음(통과)' if rsi is None else f'{rsi:.0f}'})

    if breakout:
        if not market_ok and config.BEAR_ENTRY_MODE == 0:
            return {**base, 'state': 'WAIT',
                    'reason': '약세장 — 완전 현금화(돌파도 보류)'}
        if rsi_overbought:
            return {**base, 'state': 'WAIT',
                    'reason': f'돌파했지만 RSI 과열({rsi:.0f} ≥ {config.RSI_OVERBOUGHT:g}) — 추격 보류'}
        return {**base, 'state': 'BUY', 'action': 'buy', 'buy_path': 'breakout',
                'buy_price': price, 'reason': '거래량·회전율 동반 전고점 돌파'}

    # 6. 눌림목 지정가
    limit = max(ind['sma_long'], ind['prev_close'] - ind['atr'] * config.PULLBACK_ATR_MULT)
    limit = int(round(limit))
    pulled = price <= limit
    checks.append({'label': '눌림목 도달', 'ok': pulled,
                   'detail': f'지정가 {limit:,} (현재 {price:,})'})
    if pulled:
        if not market_ok and config.BEAR_ENTRY_MODE != 2:
            # 약세장에선 바닥 잡기식 눌림목 진입 보류 (모드 2=눌림목 허용이면 통과)
            mode_msg = '완전 현금화' if config.BEAR_ENTRY_MODE == 0 else '돌파만 허용'
            return {**base, 'state': 'WAIT', 'watch_price': limit,
                    'reason': f'약세장 — 눌림목({limit:,}) 보류 ({mode_msg})'}
        if rsi_oversold:
            # 과매도 = 아직 떨어지는 칼 — 반등 확인 전 진입 보류
            return {**base, 'state': 'WAIT', 'watch_price': limit,
                    'reason': f'눌림목이지만 RSI 과매도({rsi:.0f} < {config.RSI_OVERSOLD:g}) — 낙하 중 보류'}
        # 현재가가 이미 지정가 이하 → 시장에서 현재가(체결가)로 매수
        return {**base, 'state': 'BUY', 'action': 'buy', 'buy_path': 'pullback',
                'buy_price': price, 'watch_price': limit,
                'reason': f'눌림목 지정가 {limit:,} 도달'}

    # 6-b. 즉시매수 — 추세·정배열·수급·애널 다 통과했는데 돌파·눌림목 둘 다 아니면(=안 내려오는 상승 종목),
    #       강세장·RSI 정상이면 현재가에 바로 매수. 꾸준히 오르기만 하는 좋은 종목 놓침 방지 (실험 토글).
    if getattr(config, 'IMMEDIATE_ENTRY', 0) and market_ok and not rsi_overbought:
        return {**base, 'state': 'BUY', 'action': 'buy', 'buy_path': 'immediate',
                'buy_price': price, 'reason': '조건 충족 — 즉시매수(눌림 안 기다림)'}

    return {**base, 'state': 'WAIT', 'watch_price': limit,
            'reason': f'눌림목 대기 — 지정가 {limit:,}'}


def evaluate_holding(position, ind, flow, analyst, held_days=None, market_ok=True) -> dict:
    """보유 종목 판단. 우선순위: 손절/추세이탈(전량) → 시간손절 → 분할익절(scale_out) → 추격매수(add) → 유지.

    position 의 stop/peak/trailing/scaled_out 갱신값도 position_update 로 함께 반환.
    held_days: 보유 경과 일수(달력). None이면 시간손절 미적용(엔진·백테스트가 산정해 전달).
    market_ok: 시장 강세 여부(물타기 허용 판단용, 백테스트는 중립 True).
    """
    price = ind['price']
    entry = position['entry_price']
    stop = position['stop']
    target = position['target']
    peak = max(position.get('peak', entry), price)
    trailing_on = position.get('trailing_on', False)
    scaled_out = position.get('scaled_out', False)
    tranches = position.get('tranches', 1)
    last_add = position.get('last_add_price', entry)

    if trailing_on:
        atarget = (analyst or {}).get('target_price')
        mult = config.TRAIL_ATR_MULT_TIGHT if (atarget and price >= atarget) else config.TRAIL_ATR_MULT
        stop = max(stop, int(round(peak - ind['atr'] * mult)))

    fnet = flow.get('foreign', 0) if flow else 0
    inet = flow.get('institution', 0) if flow else 0
    supply_ok = fnet > 0 or inet > 0
    trend_up = price > ind['sma_long'] and ind['sma_short'] > ind['sma_long']

    add_trigger = int(round(last_add + ind['atr'] * config.ADD_ATR_MULT))
    can_add = tranches < config.ENTRY_TRANCHES and not trailing_on and not scaled_out

    checks = [
        {'label': '손절선', 'ok': price > stop, 'detail': f'{stop:,} (현재 {price:,})'},
        {'label': '목표/트레일링', 'ok': None,
         'detail': ('트레일링 ON' if trailing_on else
                    ('일부익절 완료' if scaled_out else f'목표 {target:,}'))},
        {'label': f'추세({config.SMA_LONG}일선)', 'ok': price >= ind['sma_long'],
         'detail': f"{ind['sma_long']:,.0f}"},
        {'label': '수급(외/기)', 'ok': supply_ok, 'detail': f'외 {fnet:+,} · 기 {inet:+,}'},
        {'label': f'분할({tranches}/{config.ENTRY_TRANCHES})', 'ok': None,
         'detail': (f'추가 트리거 {add_trigger:,}' if can_add else '추가 종료')},
    ]

    upd = {'stop': stop, 'peak': peak, 'trailing_on': trailing_on, 'scaled_out': scaled_out}
    pnl_pct = (price / entry - 1) * 100

    base = {'code': position['code'], 'name': position['name'], 'sources': position.get('sources', []),
            'price': price, 'checks': checks, 'action': None, 'state': 'HOLD',
            'position_update': upd, 'pnl_pct': pnl_pct}

    # 0. 물타기(애버리징) — 켜진 변이만. 하락 시 손절 대신 추가매수로 평단↓ (한도·강세장·비급락 조건).
    #    당일 단일 급락(-CRASH%↓)은 물타기 금지→손절 우선(그림 #3), 약세장도 금지(그림 #4).
    avg_on = getattr(config, 'AVERAGING_ENTRY', 0)
    if avg_on:
        prev_close = ind.get('prev_close') or price
        today_chg = (price / prev_close - 1) if prev_close else 0
        crash = today_chg <= -getattr(config, 'CRASH_DROP_PCT', 0.10)
        avg_trigger = last_add * (1 - getattr(config, 'AVERAGING_DROP_PCT', 0.10))
        budget_left = tranches < config.ENTRY_TRANCHES
        # 트레일링/익절 전 + 한도 남음 + 강세장 + 비급락 + 직전매수가 -X% 도달 → 물타기
        if (budget_left and market_ok and not crash and not trailing_on and not scaled_out
                and price <= avg_trigger):
            return {**base, 'state': 'ADD', 'action': 'add', 'add_kind': 'down',
                    'reason': f'물타기 — {tranches + 1}/{config.ENTRY_TRANCHES}차 (평단↓, -{config.AVERAGING_DROP_PCT*100:.0f}%)'}
        # 물타기 한도 소진(또는 급락/약세장)인데 손절선 깨짐 → 최종 손절
        # (한도 남았는데 급락이면 아래 일반 손절로 자연 처리)

    # 1. 손절·추세이탈 — 전량
    if price <= stop:
        reason = '트레일링 익절' if (trailing_on and price > entry) else '손절'
        return {**base, 'state': 'SELL', 'action': 'sell', 'sell_frac': 1.0, 'reason': reason}
    if price < ind['sma_long'] and fnet < 0 and inet < 0:
        return {**base, 'state': 'SELL', 'action': 'sell', 'sell_frac': 1.0, 'reason': '추세·수급 이탈'}

    # 1-b. 시간손절 — N일 보유했는데 트레일링 전이고 진입가도 못 넘으면(진전 없음) 청산해 자본 회전
    max_hold = getattr(config, 'MAX_HOLD_DAYS', 0) or 0
    if (held_days is not None and max_hold > 0 and held_days >= max_hold
            and not trailing_on and not scaled_out and price <= entry):
        return {**base, 'state': 'SELL', 'action': 'sell', 'sell_frac': 1.0,
                'reason': f'시간손절 — {held_days}일 보유·진전 없음(자본 회전)'}

    # 2. 분할매도 — 목표 도달 & 미익절. 일부 익절 후 잔량은 트레일링으로 전환
    if target and price >= target and not scaled_out:
        upd['trailing_on'] = True
        if config.SCALE_OUT_FRAC > 0:
            upd['scaled_out'] = True
            return {**base, 'state': 'SELL', 'action': 'scale_out',
                    'sell_frac': config.SCALE_OUT_FRAC,
                    'reason': f'목표 도달 — {config.SCALE_OUT_FRAC * 100:.0f}% 분할익절'}
        return {**base, 'reason': '목표 도달 — 트레일링 전환'}

    # 3. 추격매수 — 트랜치 여유 & 추세 지속 & 수급 & 직전 진입가 + ATR 돌파
    if can_add and trend_up and supply_ok and price >= add_trigger:
        return {**base, 'state': 'ADD', 'action': 'add',
                'reason': f'추격매수 — {tranches + 1}/{config.ENTRY_TRANCHES}차 (추세 지속)'}

    return {**base, 'reason': '보유 유지'}
