"""병렬 페이퍼 변이(variant) 관리 — 각 변이는 {이름, 파라미터} + 자체 가상계좌.

정의: state/sim/variants.json = [{"id":"v1","name":"...","params":{KEY:VAL,...}}]
계좌: state/sim/variants/<id>/{portfolio.json, trades.jsonl}
비교: state/sim/variants_compare.json (웹 비교 탭)
"""
from __future__ import annotations

import json

from . import config

VARIANTS_PATH = config.STATE_DIR / 'variants.json'
VARIANTS_DIR = config.STATE_DIR / 'variants'
COMPARE_PATH = config.STATE_DIR / 'variants_compare.json'


def load_variants() -> list[dict]:
    try:
        return json.loads(VARIANTS_PATH.read_text())
    except Exception:
        return []


def save_variants(variants: list[dict]):
    config.STATE_DIR.mkdir(parents=True, exist_ok=True)
    VARIANTS_PATH.write_text(json.dumps(variants, ensure_ascii=False, indent=2))


def variant_paths(vid: str):
    d = VARIANTS_DIR / vid
    return d / 'portfolio.json', d / 'trades.jsonl'


def _label(params: dict) -> str:
    short = {'RR_RATIO': 'RR', 'STOP_ATR_MULT': 'S', 'SMA_LONG': 'SMA',
             'PULLBACK_ATR_MULT': 'P', 'VOLUME_BREAKOUT_MULT': 'V',
             'TURNOVER_OVERHEAT_MULT': 'OH', 'RSI_OVERBOUGHT': 'RSI↑',
             'RSI_OVERSOLD': 'RSI↓', 'MAX_HOLD_DAYS': 'HOLD', 'RISK_PER_TRADE_PCT': 'RISK'}
    return '·'.join(f'{short.get(k, k)}{v:g}' for k, v in params.items())


def _philosophy_presets() -> list[dict]:
    """철학별 라이브 비교군 프리셋.

    기존 sweep은 같은 규칙의 강약 조절이 많아 보유 종목이 겹치기 쉬웠다. 이 프리셋은
    수급 기간, 애널 게이트, 시장 방어, 보유 종목 수, 진입 방식까지 같이 바꿔 라이브 행동을
    더 벌리는 용도다. 주문 모듈과 무관한 페이퍼 시뮬 전용 정의다.
    """
    base = {
        'RSI_OVERBOUGHT': 75,
        'RSI_OVERSOLD': 20,
        'MAX_HOLD_DAYS': 20,
        'BEAR_ENTRY_MODE': 1,
        'TREND_SLOPE_GATE': 1,
        'MID_ARRAY_GATE': 1,
        'IMMEDIATE_ENTRY': 0,
        'AVERAGING_ENTRY': 0,
        'ENTRY_TRANCHES': 2,
        'SCALE_OUT_FRAC': 0.0,
    }

    def p(name: str, strength: str, **params):
        merged = dict(base)
        merged.update(params)
        return {'name': f'{name}·{strength}', 'params': merged}

    return [
        p('대형주 안정형', '보수', SMA_LONG=60, RR_RATIO=1.5, STOP_ATR_MULT=2.5,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
          TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=3.0,
          MAX_POSITIONS=12, RISK_PER_TRADE_PCT=0.005, BEAR_ENTRY_MODE=0),
        p('대형주 안정형', '기본', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.3,
          TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=7, ANALYST_MIN_UPSIDE_PCT=5.0,
          MAX_POSITIONS=10, RISK_PER_TRADE_PCT=0.0075, BEAR_ENTRY_MODE=1),
        p('대형주 안정형', '공격', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.5,
          TURNOVER_OVERHEAT_MULT=4.5, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=5.0,
          MAX_POSITIONS=8, RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=1),

        p('수급 추종형', '보수', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
          PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
          FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=8,
          RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=0),
        p('수급 추종형', '기본', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=1.8,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.3,
          FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=8,
          RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=1),
        p('수급 추종형', '공격', SMA_LONG=10, RR_RATIO=2.5, STOP_ATR_MULT=1.5,
          PULLBACK_ATR_MULT=0.8, VOLUME_BREAKOUT_MULT=1.2, TURNOVER_BREAKOUT_MULT=1.2,
          FLOW_DAYS=3, ANALYST_MIN_UPSIDE_PCT=-5.0, MAX_POSITIONS=6,
          RISK_PER_TRADE_PCT=0.0125, BEAR_ENTRY_MODE=1, IMMEDIATE_ENTRY=1),

        p('돌파 모멘텀형', '보수', SMA_LONG=60, BREAKOUT_LOOKBACK=40, RR_RATIO=3.0,
          STOP_ATR_MULT=1.7, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=2.5,
          TURNOVER_BREAKOUT_MULT=2.0, TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5,
          ANALYST_MIN_UPSIDE_PCT=5.0, MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.006,
          BEAR_ENTRY_MODE=0),
        p('돌파 모멘텀형', '기본', SMA_LONG=20, BREAKOUT_LOOKBACK=20, RR_RATIO=3.0,
          STOP_ATR_MULT=1.5, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=2.0,
          TURNOVER_BREAKOUT_MULT=1.8, TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5,
          ANALYST_MIN_UPSIDE_PCT=5.0, MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.01,
          BEAR_ENTRY_MODE=1),
        p('돌파 모멘텀형', '공격', SMA_LONG=10, BREAKOUT_LOOKBACK=10, RR_RATIO=3.0,
          STOP_ATR_MULT=1.3, PULLBACK_ATR_MULT=0.0, VOLUME_BREAKOUT_MULT=1.7,
          TURNOVER_BREAKOUT_MULT=1.5, TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=3,
          ANALYST_MIN_UPSIDE_PCT=0.0, MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.0125,
          BEAR_ENTRY_MODE=1, IMMEDIATE_ENTRY=1),

        p('눌림 반등형', '보수', SMA_LONG=60, RR_RATIO=1.8, STOP_ATR_MULT=2.5,
          PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.5, TURNOVER_BREAKOUT_MULT=1.2,
          FLOW_DAYS=10, RSI_OVERSOLD=25, ANALYST_MIN_UPSIDE_PCT=5.0,
          MAX_POSITIONS=10, RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=0),
        p('눌림 반등형', '기본', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
          PULLBACK_ATR_MULT=1.5, VOLUME_BREAKOUT_MULT=1.3, TURNOVER_BREAKOUT_MULT=1.2,
          FLOW_DAYS=5, RSI_OVERSOLD=30, ANALYST_MIN_UPSIDE_PCT=5.0,
          MAX_POSITIONS=8, RISK_PER_TRADE_PCT=0.01, BEAR_ENTRY_MODE=0),
        p('눌림 반등형', '공격', SMA_LONG=20, RR_RATIO=2.5, STOP_ATR_MULT=2.0,
          PULLBACK_ATR_MULT=2.0, VOLUME_BREAKOUT_MULT=1.2, TURNOVER_BREAKOUT_MULT=1.1,
          FLOW_DAYS=3, RSI_OVERSOLD=35, ANALYST_MIN_UPSIDE_PCT=0.0,
          MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.0125, BEAR_ENTRY_MODE=1),

        p('소형 성장형', '보수', SMA_LONG=60, RR_RATIO=3.0, STOP_ATR_MULT=1.8,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.5, TURNOVER_BREAKOUT_MULT=2.0,
          TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=10.0,
          MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.004, BEAR_ENTRY_MODE=0),
        p('소형 성장형', '기본', SMA_LONG=20, RR_RATIO=3.0, STOP_ATR_MULT=1.5,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.0, TURNOVER_BREAKOUT_MULT=1.8,
          TURNOVER_OVERHEAT_MULT=6.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=10.0,
          MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.006, BEAR_ENTRY_MODE=1),
        p('소형 성장형', '공격', SMA_LONG=10, RR_RATIO=3.5, STOP_ATR_MULT=1.3,
          PULLBACK_ATR_MULT=0.8, VOLUME_BREAKOUT_MULT=1.7, TURNOVER_BREAKOUT_MULT=1.5,
          TURNOVER_OVERHEAT_MULT=7.0, FLOW_DAYS=3, ANALYST_MIN_UPSIDE_PCT=5.0,
          MAX_POSITIONS=4, RISK_PER_TRADE_PCT=0.008, BEAR_ENTRY_MODE=1,
          IMMEDIATE_ENTRY=1),

        p('방어 현금형', '보수', SMA_LONG=60, RR_RATIO=1.5, STOP_ATR_MULT=2.5,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.5, TURNOVER_BREAKOUT_MULT=2.0,
          TURNOVER_OVERHEAT_MULT=4.0, FLOW_DAYS=10, ANALYST_MIN_UPSIDE_PCT=12.0,
          MAX_POSITIONS=4, RISK_PER_TRADE_PCT=0.003, BEAR_ENTRY_MODE=0),
        p('방어 현금형', '기본', SMA_LONG=60, RR_RATIO=2.0, STOP_ATR_MULT=2.2,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=2.0, TURNOVER_BREAKOUT_MULT=1.8,
          TURNOVER_OVERHEAT_MULT=4.5, FLOW_DAYS=7, ANALYST_MIN_UPSIDE_PCT=10.0,
          MAX_POSITIONS=5, RISK_PER_TRADE_PCT=0.005, BEAR_ENTRY_MODE=0),
        p('방어 현금형', '공격', SMA_LONG=20, RR_RATIO=2.0, STOP_ATR_MULT=2.0,
          PULLBACK_ATR_MULT=1.0, VOLUME_BREAKOUT_MULT=1.8, TURNOVER_BREAKOUT_MULT=1.5,
          TURNOVER_OVERHEAT_MULT=5.0, FLOW_DAYS=5, ANALYST_MIN_UPSIDE_PCT=8.0,
          MAX_POSITIONS=6, RISK_PER_TRADE_PCT=0.0075, BEAR_ENTRY_MODE=1),
    ]


def seed_from_sweep(n: int = 3) -> list[dict]:
    """backtest_results.json 검증 상위 n개 → 변이로 등록 (기존 계좌는 유지)."""
    try:
        res = json.loads((config.STATE_DIR / 'backtest_results.json').read_text())
    except Exception:
        return []
    existing = {v['id']: v for v in load_variants()}
    variants = list(existing.values())
    seen_params = {json.dumps(v['params'], sort_keys=True) for v in variants}
    rank = 0
    for r in res.get('results', []):
        if rank >= n:
            break
        params = r['params']
        key = json.dumps(params, sort_keys=True)
        if key in seen_params:
            continue
        rank += 1
        vid = f'v{len(variants)+1}'
        variants.append({'id': vid, 'name': f'#{rank} {_label(params)}', 'params': params})
        seen_params.add(key)
    save_variants(variants)
    return variants


def sync_label_tops(results: dict) -> dict:
    """sweep 결과에서 세부 유형(시장적합 라벨)별 1위를 비교군과 동기화 — 수동 추가/제거 불필요.

    파라미터가 같은 변이는 계좌 유지, 빠진 유형 대표는 추가, 더는 대표가 아닌 변이는 삭제.
    (2026-06-10 기준전략 폐지 — 메인 제외 규칙 삭제, 유형별 1위 전부 변이로.)"""

    def _k(p):
        return json.dumps({k: float(v) for k, v in (p or {}).items()}, sort_keys=True)

    def _sk(r):
        w = r.get('worst_ret')
        return (w if w is not None else -999,
                r['test'].get('total_return_pct', -999) or -999)

    from .sim_web import _market_fit_summary   # 함수 레벨 import (순환 회피)
    uniq = {}
    for r in results.get('results', []):
        uniq.setdefault(_k(r['params']), r)
    by_label = {}
    for r in uniq.values():
        lbl = _market_fit_summary(r['params'])
        if lbl not in by_label or _sk(r) > _sk(by_label[lbl]):
            by_label[lbl] = r
    desired = {_k(r['params']): r['params'] for r in by_label.values()}
    current = {_k(v['params']): v for v in load_variants()}
    removed = added = 0
    for k, v in current.items():
        if k not in desired:
            remove_variant(v['id'])
            removed += 1
    for k, p in desired.items():
        if k not in current:
            if add_variant(p):
                added += 1
    return {'labels': len(by_label), 'added': added, 'removed': removed,
            'kept': len(current) - removed}


def add_variant(params: dict, name: str | None = None) -> dict | None:
    """단일 params 조합을 변이로 추가 (백테스트 행 → 비교군). 이미 있으면 None."""
    variants = load_variants()
    seen = {json.dumps(v['params'], sort_keys=True) for v in variants}
    if json.dumps(params, sort_keys=True) in seen:
        return None
    nums = [int(v['id'][1:]) for v in variants if v['id'][1:].isdigit()]
    vid = f'v{(max(nums) + 1) if nums else 1}'
    v = {'id': vid, 'name': name or _label(params), 'params': params}
    variants.append(v)
    save_variants(variants)
    return v


def add_philosophy_presets() -> dict:
    """철학별 프리셋 18개를 비교군에 추가. 기존 계좌와 중복 조합은 유지."""
    added = []
    skipped = 0
    for preset in _philosophy_presets():
        v = add_variant(preset['params'], preset['name'])
        if v:
            added.append(v)
        else:
            skipped += 1
    return {'added': added, 'skipped': skipped, 'total': len(load_variants())}


def remove_variant(vid: str):
    """변이 1개 삭제 (정의 + 계좌)."""
    import shutil
    save_variants([v for v in load_variants() if v['id'] != vid])
    d = VARIANTS_DIR / vid
    if d.exists():
        shutil.rmtree(d)


def reset_variant(vid: str):
    """변이 1개 성적만 초기화 (정의 유지 — 계좌 디렉터리 삭제 후 다음 스캔에 재생성)."""
    import shutil
    d = VARIANTS_DIR / vid
    if d.exists():
        shutil.rmtree(d)


def reset_all():
    """모든 변이 계좌 초기화 (variants.json 정의는 유지)."""
    import shutil
    if VARIANTS_DIR.exists():
        shutil.rmtree(VARIANTS_DIR)


def clear():
    """변이 정의·계좌 전부 삭제."""
    reset_all()
    if VARIANTS_PATH.exists():
        VARIANTS_PATH.unlink()
