"""CLI for paper trading with Alpaca API.

Usage:
    fithia2 paper <command> [options]

Commands:
    auto        자동 데몬 — 매일 파이프라인 + 매매를 스케줄에 맞게 자동 실행
    start       Create a new paper trading session
    run         Run daily processing for a session
    run-close   장 마감 직전: same-day 이벤트 → MOC 매수
    run-open    장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수
    run-all     Run daily processing for all active sessions
    monitor     장중 실시간 stop/target 모니터링
    status      Show session status
    positions   Show current positions (live from Alpaca)
    trades      Show trade history
    equity      Show equity curve
    sessions    List all sessions
    pause       Pause a session
    resume      Resume a paused session
    close       Close session (liquidates all positions)
"""
from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path

from rich.console import Console

_console = Console(width=140)

# Default path for the paper trading SQLite DB
_DEFAULT_DB = os.environ.get("PAPER_TRADER_DB", "paper_trading.db")


def _get_state_manager(db_path: str):
    from apps.paper_trader.state import StateManager
    return StateManager(db_path)


def _get_broker():
    from apps.paper_trader.alpaca_broker import AlpacaBroker
    try:
        return AlpacaBroker.from_env()
    except (ValueError, ImportError) as exc:
        _console.print(f"[red]ERROR: {exc}[/]")
        sys.exit(1)


def _resolve_session(state, name_or_id: str):
    session = state.get_session(name_or_id)
    if session is None:
        _console.print(f"[red]ERROR: Session not found: '{name_or_id}'[/]")
        sys.exit(1)
    return session


def _resolve_config_path(id_or_path: str) -> str:
    """Resolve an experiment ID or name to a config file path.

    Accepts:
      - Numeric ID: '228'  → looks up name from index → 'configs/experiments/NAME.json'
      - Experiment name: 'return_max_long_v6new.362' → 'configs/experiments/NAME.json'
      - Existing file path: 'configs/experiments/foo.json' → unchanged
    """
    if Path(id_or_path).exists():
        return id_or_path
    try:
        from libs.backtest.experiments import resolve_experiment_name
        name = resolve_experiment_name(id_or_path)
        return f"configs/experiments/{name}.json"
    except (KeyError, Exception):
        # Fall back to treating as-is (original behaviour)
        return id_or_path


# ------------------------------------------------------------------ #
# Commands
# ------------------------------------------------------------------ #

def cmd_start(args: argparse.Namespace) -> None:
    """Create a new paper trading session."""
    config_path = _resolve_config_path(args.config)
    if not Path(config_path).exists():
        _console.print(f"[red]ERROR: Config not found: {config_path}[/]")
        sys.exit(1)

    if (
        getattr(args, "parking", None)
        or getattr(args, "idle_alpha", None)
        or getattr(args, "form4_sleeve", None)
        or getattr(args, "ownership_sleeve", None)
    ):
        from libs.backtest.domain import (
            FORM4_CAPTURE_SLEEVE_PRESETS,
            IDLE_ALPHA_SLEEVE_PRESETS,
            OWNERSHIP_CAPTURE_SLEEVE_PRESETS,
            PARKING_PRESETS,
        )
        if getattr(args, "parking", None) and args.parking not in PARKING_PRESETS:
            _console.print(f"[red]ERROR: Unknown parking preset: {args.parking}[/]")
            sys.exit(1)
        if getattr(args, "idle_alpha", None) and args.idle_alpha not in IDLE_ALPHA_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown idle alpha preset: {args.idle_alpha}[/]")
            sys.exit(1)
        if getattr(args, "form4_sleeve", None) and args.form4_sleeve not in FORM4_CAPTURE_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown Form 4 sleeve preset: {args.form4_sleeve}[/]")
            sys.exit(1)
        if getattr(args, "ownership_sleeve", None) and args.ownership_sleeve not in OWNERSHIP_CAPTURE_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown ownership sleeve preset: {args.ownership_sleeve}[/]")
            sys.exit(1)

    state = _get_state_manager(args.db)

    # Check for duplicate name
    existing = state.get_session(args.name)
    if existing is not None:
        _console.print(f"[red]ERROR: Session '{args.name}' already exists (id={existing.session_id}).[/]")
        _console.print("Use a different name or close the existing session first.")
        sys.exit(1)

    session_id = state.create_session(
        session_name=args.name,
        config_path=config_path,
        initial_equity=args.capital,
        parking_preset=getattr(args, "parking", None),
        idle_alpha_preset=getattr(args, "idle_alpha", None),
        form4_sleeve_preset=getattr(args, "form4_sleeve", None),
        ownership_sleeve_preset=getattr(args, "ownership_sleeve", None),
    )
    _console.print(f"[green]Session created:[/] {args.name}  id={session_id}  capital=${args.capital:,.0f}")
    _console.print(f"[dim]Config: {config_path}[/]")
    if getattr(args, "parking", None):
        _console.print(f"[dim]Cash parking: {args.parking}[/]")
    if getattr(args, "idle_alpha", None):
        _console.print(f"[dim]Idle alpha: {args.idle_alpha}[/]")
    if getattr(args, "form4_sleeve", None):
        _console.print(f"[dim]Form 4 sleeve: {args.form4_sleeve}[/]")
    if getattr(args, "ownership_sleeve", None):
        _console.print(f"[dim]Ownership sleeve: {args.ownership_sleeve}[/]")
    _console.print(f"\nRun daily processing with:\n  fithia2 paper run --session {args.name}")


def cmd_run(args: argparse.Namespace) -> None:
    """Run daily processing for a specific session."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)

    if session.status == "paused":
        _console.print(f"[yellow]Session '{session.session_name}' is paused. Use 'resume' first.[/]")
        sys.exit(1)
    if session.status == "closed":
        _console.print(f"[red]Session '{session.session_name}' is closed.[/]")
        sys.exit(1)

    broker = _get_broker()

    from apps.paper_trader.event_detector import EventDetector
    from apps.paper_trader.engine import PaperTradingEngine
    from apps.paper_trader.reporter import print_run_summary

    oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
    db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")

    detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url)
    engine = PaperTradingEngine(session=session, broker=broker, state=state, event_detector=detector)

    target_date = None
    if args.date:
        import datetime as dt
        try:
            target_date = dt.date.fromisoformat(args.date)
        except ValueError:
            _console.print(f"[red]ERROR: Invalid date format: {args.date}. Use YYYY-MM-DD.[/]")
            sys.exit(1)

    summary = asyncio.run(engine.run_daily(target_date=target_date, force=getattr(args, "force", False)))
    print_run_summary(summary)


def _make_engine(session, args):
    broker = _get_broker()
    from apps.paper_trader.event_detector import EventDetector
    from apps.paper_trader.engine import PaperTradingEngine
    from apps.backtester.run import load_manifest, resolve_config
    from apps.paper_trader.backtest_sim import load_snapshot_store_for_session
    oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
    db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")
    detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url)
    state = _get_state_manager(args.db)
    manifest = load_manifest(session.config_path)
    config = resolve_config(manifest)
    snapshot_store = load_snapshot_store_for_session(config, oracle_url, db_dsn)
    return PaperTradingEngine(
        session=session, broker=broker, state=state, event_detector=detector,
        snapshot_store=snapshot_store,
    )


def cmd_run_close(args: argparse.Namespace) -> None:
    """장 마감 직전: same-day 이벤트 → MOC 매수."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)

    if session.status != "active":
        _console.print(f"[red]Session '{session.session_name}' is not active.[/]")
        sys.exit(1)

    import datetime as dt
    target_date = None
    if args.date:
        try:
            target_date = dt.date.fromisoformat(args.date)
        except ValueError:
            _console.print(f"[red]ERROR: Invalid date: {args.date}[/]")
            sys.exit(1)

    engine = _make_engine(session, args)
    from apps.paper_trader.reporter import print_run_summary
    summary = asyncio.run(engine.run_reaction_close(target_date=target_date, force=getattr(args, "force", False)))
    print_run_summary(summary)


def cmd_run_open(args: argparse.Namespace) -> None:
    """장 시작 직후: 전날 exit 판단 + after-close 이벤트 → 시장가 매수."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)

    if session.status != "active":
        _console.print(f"[red]Session '{session.session_name}' is not active.[/]")
        sys.exit(1)

    import datetime as dt
    target_date = None
    if args.date:
        try:
            target_date = dt.date.fromisoformat(args.date)
        except ValueError:
            _console.print(f"[red]ERROR: Invalid date: {args.date}[/]")
            sys.exit(1)

    engine = _make_engine(session, args)
    from apps.paper_trader.reporter import print_run_summary
    summary = asyncio.run(engine.run_next_open(target_date=target_date, force=getattr(args, "force", False)))
    print_run_summary(summary)


def cmd_monitor(args: argparse.Namespace) -> None:
    """장중 실시간 stop/target 모니터링 (Ctrl+C로 종료)."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)

    if session.status != "active":
        _console.print(f"[red]Session '{session.session_name}' is not active.[/]")
        sys.exit(1)

    _console.print(f"[cyan]Monitoring session '{session.session_name}' every {args.interval}s (Ctrl+C to stop)...[/]")
    engine = _make_engine(session, args)
    try:
        asyncio.run(engine.run_monitor(interval_sec=args.interval))
    except KeyboardInterrupt:
        _console.print("\n[yellow]Monitor stopped.[/]")


def cmd_run_all(args: argparse.Namespace) -> None:
    """Run daily processing for all active sessions."""
    state = _get_state_manager(args.db)
    sessions = [s for s in state.list_sessions() if s.status == "active"]

    if not sessions:
        _console.print("[dim]No active sessions found.[/]")
        return

    broker = _get_broker()

    from apps.paper_trader.event_detector import EventDetector
    from apps.paper_trader.engine import PaperTradingEngine
    from apps.paper_trader.reporter import print_run_summary

    oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
    db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")

    for session in sessions:
        _console.print(f"\n[bold]Running session: {session.session_name}[/]")
        detector = EventDetector(db_dsn=db_dsn, oracle_url=oracle_url)
        engine = PaperTradingEngine(session=session, broker=broker, state=state, event_detector=detector)
        summary = asyncio.run(engine.run_daily())
        print_run_summary(summary)


def _resolve_sessions(state, name_or_id: str | None) -> list:
    """Return a single session if specified, or all sessions if None."""
    if name_or_id:
        session = state.get_session(name_or_id)
        if session is None:
            _console.print(f"[red]ERROR: Session not found: '{name_or_id}'[/]")
            sys.exit(1)
        return [session]
    sessions = state.list_sessions()
    if not sessions:
        _console.print("[dim]No sessions found.[/]")
        sys.exit(0)
    return sessions


def cmd_status(args: argparse.Namespace) -> None:
    """Show session status."""
    state = _get_state_manager(args.db)
    broker = _get_broker()
    from apps.paper_trader.reporter import print_status
    for session in _resolve_sessions(state, args.session):
        print_status(session, broker, state)


def cmd_positions(args: argparse.Namespace) -> None:
    """Show current positions (live from Alpaca)."""
    state = _get_state_manager(args.db)
    broker = _get_broker()
    from apps.paper_trader.reporter import print_positions
    for session in _resolve_sessions(state, args.session):
        print_positions(session, broker, state)


def cmd_trades(args: argparse.Namespace) -> None:
    """Show trade history."""
    state = _get_state_manager(args.db)
    from apps.paper_trader.reporter import print_trades
    for session in _resolve_sessions(state, args.session):
        print_trades(session, state, last=args.last)


def cmd_equity(args: argparse.Namespace) -> None:
    """Show equity curve."""
    state = _get_state_manager(args.db)
    from apps.paper_trader.reporter import print_equity
    for session in _resolve_sessions(state, args.session):
        print_equity(session, state)


def cmd_sessions(args: argparse.Namespace) -> None:
    """List all sessions."""
    state = _get_state_manager(args.db)
    sessions = state.list_sessions()

    from apps.paper_trader.reporter import print_sessions
    print_sessions(sessions)


def cmd_pause(args: argparse.Namespace) -> None:
    """Pause a session (stops daily processing)."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)
    if session.status != "active":
        _console.print(f"[yellow]Session '{session.session_name}' is already {session.status}.[/]")
        return
    state.set_session_status(session.session_id, "paused")
    _console.print(f"[yellow]Session '{session.session_name}' paused.[/]")


def cmd_resume(args: argparse.Namespace) -> None:
    """Resume a paused session."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)
    if session.status != "paused":
        _console.print(f"[yellow]Session '{session.session_name}' is not paused (status={session.status}).[/]")
        return
    state.set_session_status(session.session_id, "active")
    _console.print(f"[green]Session '{session.session_name}' resumed.[/]")


def _resolve_rank_configs(start: int, end: int) -> list[str]:
    """Load strategies ranked start..end from leaderboard by SQS score.

    start/end are 1-based inclusive. e.g. (1, 5) = top 5, (20, 40) = rank 20-40.
    """
    import json

    registry_path = Path("journal/experiment_registry.json")
    if not registry_path.exists():
        _console.print("[red]ERROR: journal/experiment_registry.json not found. Run `fithia2 lb` first.[/]")
        sys.exit(1)

    registry = json.loads(registry_path.read_text())
    ranked: list[str] = []
    entries = sorted(
        (
            e for e in registry.get("entries", [])
            if e.get("sqs_score") is not None
            and not e.get("is_retired", False)
        ),
        key=lambda e: e["sqs_score"],
        reverse=True,
    )

    for e in entries:
        if e.get("trade_count", 0) <= 0 or e.get("valid_trade_count", 0) <= 0:
            continue
        name = e["experiment_name"]
        cfg_path = e.get("config_path") or f"configs/experiments/{name}.json"
        if Path(cfg_path).exists():
            ranked.append(cfg_path)

    # 1-based inclusive slice
    return ranked[start - 1 : end]


def cmd_backtest(args: argparse.Namespace) -> None:
    """Run historical backtest simulation using paper trading engine."""
    import datetime as dt

    configs = [_resolve_config_path(c) for c in (args.configs or [])]
    if args.top:
        configs = _resolve_rank_configs(1, args.top) + configs
    if args.rank:
        parts = args.rank.split("-")
        if len(parts) == 1 and parts[0].isdigit():
            n = int(parts[0])
            configs = _resolve_rank_configs(n, n) + configs
        elif len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
            configs = _resolve_rank_configs(int(parts[0]), int(parts[1])) + configs
        else:
            _console.print("[red]ERROR: --rank format: N or START-END (e.g. 5 or 20-40)[/]")
            sys.exit(1)
    if not configs:
        _console.print("[red]ERROR: Specify --config, --top, or --rank[/]")
        sys.exit(1)

    for cfg in configs:
        if not Path(cfg).exists():
            _console.print(f"[red]ERROR: Config not found: {cfg}[/]")
            sys.exit(1)

    if (
        getattr(args, "parking", None)
        or getattr(args, "idle_alpha", None)
        or getattr(args, "form4_sleeve", None)
        or getattr(args, "ownership_sleeve", None)
    ):
        from libs.backtest.domain import (
            FORM4_CAPTURE_SLEEVE_PRESETS,
            IDLE_ALPHA_SLEEVE_PRESETS,
            OWNERSHIP_CAPTURE_SLEEVE_PRESETS,
            PARKING_PRESETS,
        )
        if getattr(args, "parking", None) and args.parking not in PARKING_PRESETS:
            _console.print(f"[red]ERROR: Unknown parking preset: {args.parking}[/]")
            sys.exit(1)
        if getattr(args, "idle_alpha", None) and args.idle_alpha not in IDLE_ALPHA_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown idle alpha preset: {args.idle_alpha}[/]")
            sys.exit(1)
        if getattr(args, "form4_sleeve", None) and args.form4_sleeve not in FORM4_CAPTURE_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown Form 4 sleeve preset: {args.form4_sleeve}[/]")
            sys.exit(1)
        if getattr(args, "ownership_sleeve", None) and args.ownership_sleeve not in OWNERSHIP_CAPTURE_SLEEVE_PRESETS:
            _console.print(f"[red]ERROR: Unknown ownership sleeve preset: {args.ownership_sleeve}[/]")
            sys.exit(1)

    import calendar

    def _latest_backtest_date() -> dt.date:
        """Return today if market is closed (after 4 PM ET or non-trading day), else yesterday."""
        from libs.common.time_utils import is_trading_day, to_eastern, utc_now
        now_et = to_eastern(utc_now())
        today = now_et.date()
        if not is_trading_day(today) or now_et.hour >= 16:
            return today
        return today - dt.timedelta(days=1)

    def _parse_date(val: str, is_end: bool = False) -> dt.date:
        """Parse YYYY-MM-DD, YYYY-MM, or YYYY. Clamp end dates to latest available."""
        latest = _latest_backtest_date()
        parts = val.split("-")
        if len(parts) == 1 and len(val) == 4 and val.isdigit():
            # YYYY
            year = int(val)
            if is_end:
                return min(dt.date(year, 12, 31), latest)
            return dt.date(year, 1, 1)
        if len(parts) == 2:
            # YYYY-MM
            year, month = int(parts[0]), int(parts[1])
            if is_end:
                last_day = calendar.monthrange(year, month)[1]
                return min(dt.date(year, month, last_day), latest)
            return dt.date(year, month, 1)
        return dt.date.fromisoformat(val)

    # Resolve --year shorthand
    if args.year:
        if args.start or args.end:
            _console.print("[red]ERROR: --year cannot be combined with --start/--end[/]")
            sys.exit(1)
        args.start = args.year
        args.end = args.year

    if not args.start:
        _console.print("[red]ERROR: Specify --start (and optionally --end), or --year[/]")
        sys.exit(1)

    try:
        start_date = _parse_date(args.start)
        end_date = _parse_date(args.end, is_end=True) if args.end else _latest_backtest_date()
    except ValueError as exc:
        _console.print(f"[red]ERROR: Invalid date: {exc}[/]")
        sys.exit(1)
    if getattr(args, "non_core_allocator_v2", False) and not getattr(args, "non_core_allocator_v2_mode", None):
        args.non_core_allocator_v2_mode = "shadow"

    oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
    db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")

    if not db_dsn:
        _console.print("[red]ERROR: POSTGRES_DSN (or DB_DSN) env var not set.[/]")
        sys.exit(1)

    _console.print(f"[bold cyan]Backtest:[/] {start_date} → {end_date}  capital=${args.capital:,.0f}")
    names = [Path(c).stem for c in configs]
    _console.print(f"Strategies ({len(configs)}): {', '.join(names)}")

    from apps.paper_trader.backtest_sim import run_backtest
    from apps.paper_trader.reporter import print_backtest_results

    results = run_backtest(
        configs=configs,
        capital=args.capital,
        start_date=start_date,
        end_date=end_date,
        db_dsn=db_dsn,
        oracle_url=oracle_url,
        console=_console,
        parking_preset=getattr(args, 'parking', None),
        idle_alpha_preset=getattr(args, 'idle_alpha', None),
        form4_sleeve_preset=getattr(args, 'form4_sleeve', None),
        ownership_sleeve_preset=getattr(args, 'ownership_sleeve', None),
        risk_off_alpha_sleeve_preset=getattr(args, 'risk_off_sleeve', None),
        non_core_allocator_v2_mode=getattr(args, 'non_core_allocator_v2_mode', None),
        snapshot_id_override=getattr(args, 'snapshot_id', None),
        auto_refresh=not getattr(args, 'no_refresh', False),
    )
    print_backtest_results(results, output_dir=args.output, show_trades=not args.no_trades)


def cmd_auto(args: argparse.Namespace) -> None:
    """자동 데몬 — Phoenix(MST) 기준 ET 장 스케줄에 맞게 파이프라인 + 매매 자동 실행."""
    from apps.paper_trader.auto import run_auto
    run_auto(
        sessions=args.session or [],
        db=args.db,
        dry_run=args.dry_run,
    )


def cmd_close(args: argparse.Namespace) -> None:
    """Close a session and liquidate all positions."""
    state = _get_state_manager(args.db)
    session = _resolve_session(state, args.session)

    if session.status == "closed":
        _console.print(f"[dim]Session '{session.session_name}' is already closed.[/]")
        return

    broker = _get_broker()

    # Confirm
    if not args.yes:
        positions = broker.list_positions()
        if positions:
            _console.print(f"[yellow]This will liquidate {len(positions)} open position(s):[/]")
            for p in positions:
                _console.print(f"  {p.symbol}: {p.qty} shares")
        confirm = input(f"Close session '{session.session_name}'? [y/N] ")
        if confirm.lower() != "y":
            _console.print("Cancelled.")
            return

    # Liquidate
    try:
        orders = broker.close_all_positions()
        _console.print(f"[yellow]Liquidated {len(orders)} position(s).[/]")
    except Exception as exc:
        _console.print(f"[red]WARNING: Failed to close positions: {exc}[/]")

    # Mark all open strategy states as closed
    for ss in state.get_open_strategy_states(session.session_id):
        state.close_strategy_state(session.session_id, ss.symbol)

    # Delete session and all related data
    state.delete_session(session.session_id)
    _console.print(f"[red]Session '{session.session_name}' closed and deleted.[/]")


# ------------------------------------------------------------------ #
# Main
# ------------------------------------------------------------------ #

def _print_help() -> None:
    from rich import box as rbox
    from rich.panel import Panel
    from rich.table import Table

    _console.print()
    _console.print(Panel(
        "[bold cyan]fithia2 paper-trader[/] — Alpaca Paper Trading CLI\n"
        "[dim]전략 백테스트 로직으로 판단, Alpaca Paper Trading API로 주문 실행[/]",
        border_style="cyan",
        padding=(0, 2),
    ))

    tbl = Table(box=rbox.SIMPLE, show_header=True, header_style="bold yellow", padding=(0, 2))
    tbl.add_column("Command", style="bold green", no_wrap=True)
    tbl.add_column("Description")
    tbl.add_column("Key Options", style="dim")

    tbl.add_row("[bold cyan]backtest[/]", "과거 기간 시뮬레이션 (복수 전략 비교)", "--config PATH | --top N  --year YYYY | --start DATE --end DATE  [--capital N] [--parking P] [--idle-alpha P] [--form4-sleeve P] [--ownership-sleeve P]")
    tbl.add_row("[bold cyan]auto[/]",     "자동 데몬 — 스케줄에 맞게 파이프라인+매매 자동 실행", "[--session NAME] [--dry-run]")
    tbl.add_row("", "", "")
    tbl.add_row("start",     "새 세션 생성",                           "--config PATH --capital FLOAT --name STR [--parking P] [--idle-alpha P] [--form4-sleeve P] [--ownership-sleeve P]")
    tbl.add_row("run",       "일일 처리 실행 (특정 세션)",              "--session NAME [--date YYYY-MM-DD]")
    tbl.add_row("run-close", "장 마감 직전: same-day 이벤트 → MOC 매수",  "--session NAME [--date]")
    tbl.add_row("run-open",  "장 시작 직후: exit + after-close 신규진입", "--session NAME [--date]")
    tbl.add_row("run-all",   "전체 활성 세션 일일 처리",                "")
    tbl.add_row("monitor",   "장중 실시간 stop/target 모니터링",         "--session NAME [--interval N]")
    tbl.add_row("", "", "")
    tbl.add_row("status",    "세션 상태 조회",                          "--session NAME")
    tbl.add_row("positions", "보유 포지션 조회 (Alpaca 실시간)",         "--session NAME")
    tbl.add_row("trades",    "거래 내역",                               "--session NAME [--last N]")
    tbl.add_row("equity",    "Equity curve",                            "--session NAME")
    tbl.add_row("sessions",  "세션 목록",                               "")
    tbl.add_row("pause",     "세션 일시정지",                            "--session NAME")
    tbl.add_row("resume",    "세션 재개",                               "--session NAME")
    tbl.add_row("close",     "세션 종료 (전 포지션 청산)",               "--session NAME [--yes]")

    _console.print(tbl)
    _console.print(
        "  [dim]공통 옵션:[/]  [bold]--db[/] [dim](기본: paper_trading.db)[/]\n"
        "  [dim]환경변수:[/]  ALPACA_API_KEY  ALPACA_SECRET_KEY  ALPACA_PAPER=true  "
        "ORACLE_URL  DB_DSN\n"
    )
    _console.print("  [bold]예시[/]")
    _console.print("    [green]fithia2 paper auto --session my_session[/]  [dim]← 추천: 데몬으로 모든 것을 자동 처리[/]")
    _console.print("    [green]fithia2 paper start --config configs/experiments/return_max_long_v504.json --name my_session[/]")
    _console.print("    [green]fithia2 paper positions --session my_session[/]")
    _console.print("    [green]fithia2 paper trades --session my_session --last 20[/]")
    _console.print()


def main() -> None:
    # Auto-load .env file (python-dotenv)
    try:
        from dotenv import load_dotenv
        load_dotenv()
    except ImportError:
        pass

    if len(sys.argv) == 1:
        _print_help()
        sys.exit(0)

    parser = argparse.ArgumentParser(
        description="fithia2 Paper Trader — Alpaca Paper Trading CLI",
        add_help=True,
    )
    sub = parser.add_subparsers(dest="command", required=True)

    db_kwargs = {"default": _DEFAULT_DB, "help": f"SQLite DB path (default: {_DEFAULT_DB})"}

    # backtest
    p = sub.add_parser("backtest", help="Run historical backtest simulation using paper trading engine")
    p.add_argument("--config", "-c", action="append",
                   dest="configs", metavar="PATH",
                   help="Config path (repeat for multiple strategies)")
    p.add_argument("--top", "-t", type=int, default=None, metavar="N",
                   help="Use top N strategies from leaderboard (by SQS score)")
    p.add_argument("--rank", default=None, metavar="START-END",
                   help="Use strategies ranked START to END (e.g. 20-40)")
    p.add_argument("--capital", "-k", type=float, default=10000.0,
                   help="Per-session capital (default: 10000)")
    p.add_argument("--start", default=None, metavar="YYYY[-MM-DD]",
                   help="Backtest start date (YYYY-MM-DD or YYYY)")
    p.add_argument("--end", default=None, metavar="YYYY[-MM-DD]",
                   help="Backtest end date (YYYY-MM-DD or YYYY)")
    p.add_argument("--year", "-y", default=None, metavar="YYYY",
                   help="Shorthand for --start YYYY --end YYYY")
    p.add_argument("--output", "-o", default=None,
                   help="Directory to save results CSV (optional)")
    p.add_argument("--no-trades", action="store_true", default=False,
                   help="Hide per-strategy trade log")
    p.add_argument("--parking", default=None, metavar="PRESET",
                   help="Apply cash parking preset (e.g. vm_24_m20, vol_20_24, sgov). Overrides config.")
    p.add_argument("--idle-alpha", default=None, metavar="PRESET",
                   help="Apply idle alpha sleeve preset (e.g. micro_event_alpha). Overrides config.")
    p.add_argument("--form4-sleeve", default=None, metavar="PRESET",
                   help="Apply Form 4 sleeve preset (e.g. reserve_form4_cluster_plus_fresh_same_day). Overrides config.")
    p.add_argument("--ownership-sleeve", default=None, metavar="PRESET",
                   help="Apply 13D/13G ownership sleeve preset (e.g. ownership_13d_raise_reserve_plus_strict). Overrides config.")
    p.add_argument("--risk-off-sleeve", default=None, metavar="PRESET",
                   help="Apply risk-off alpha sleeve preset. Overrides config.")
    p.add_argument("--non-core-allocator-v2", action="store_true", default=False,
                   help="Enable non-core allocator v2.")
    p.add_argument("--non-core-allocator-v2-mode", default=None, choices=["shadow", "live"],
                   help="Run non-core allocator v2 in shadow or live mode.")
    p.add_argument("--snapshot-id", default=None, metavar="SNAPSHOT_ID",
                   help="Override dataset_snapshot_id (e.g. midlarge-liquid-long-v1-oot-2020-2021)")
    p.add_argument("--no-refresh", action="store_true", default=False,
                   help="Skip automatic snapshot refresh (use existing data as-is)")

    # auto
    p = sub.add_parser("auto", help="자동 데몬 — ET 장 스케줄에 맞게 파이프라인+매매 자동 실행")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", nargs="*", default=[], metavar="NAME",
                   help="Session name(s). Default: all active sessions.")
    p.add_argument("--dry-run", action="store_true",
                   help="Print commands without executing.")

    # start
    p = sub.add_parser("start", help="Create a new paper trading session")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--config", "-c", required=True, help="Path to experiment manifest or config JSON")
    p.add_argument("--capital", "-k", type=float, default=10000.0, help="Initial capital (default: 10000)")
    p.add_argument("--name", "-n", required=True, help="Session name (unique)")
    p.add_argument("--parking", default=None, metavar="PRESET", help="Cash parking preset name")
    p.add_argument("--idle-alpha", default=None, metavar="PRESET", help="Idle alpha sleeve preset name")
    p.add_argument("--form4-sleeve", default=None, metavar="PRESET", help="Form 4 sleeve preset name")
    p.add_argument("--ownership-sleeve", default=None, metavar="PRESET", help="13D/13G ownership sleeve preset name")

    # run
    p = sub.add_parser("run", help="Run daily processing for a session")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")
    p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)")
    p.add_argument("--force", "-f", action="store_true", help="Re-run even if date already processed")

    # run-close
    p = sub.add_parser("run-close", help="장 마감 직전: same-day 이벤트 → MOC 매수")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")
    p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)")
    p.add_argument("--force", "-f", action="store_true", help="Re-run even if already processed")

    # run-open
    p = sub.add_parser("run-open", help="장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")
    p.add_argument("--date", "-d", help="Target date YYYY-MM-DD (default: today)")
    p.add_argument("--force", "-f", action="store_true", help="Re-run even if already processed")

    # monitor
    p = sub.add_parser("monitor", help="장중 실시간 stop/target 모니터링")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")
    p.add_argument("--interval", "-i", type=int, default=60, help="Check interval in seconds (default: 60)")

    # run-all
    p = sub.add_parser("run-all", help="Run daily processing for all active sessions")
    p.add_argument("--db", **db_kwargs)

    # status
    p = sub.add_parser("status", help="Show session status")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)")

    # positions
    p = sub.add_parser("positions", help="Show current positions")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)")

    # trades
    p = sub.add_parser("trades", help="Show trade history")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)")
    p.add_argument("--last", "-n", type=int, default=None, help="Show last N trades")

    # equity
    p = sub.add_parser("equity", help="Show equity curve")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", default=None, help="Session name or ID (omit for all)")

    # sessions
    p = sub.add_parser("sessions", help="List all sessions")
    p.add_argument("--db", **db_kwargs)

    # pause
    p = sub.add_parser("pause", help="Pause a session")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")

    # resume
    p = sub.add_parser("resume", help="Resume a paused session")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")

    # close
    p = sub.add_parser("close", help="Close session (liquidates all positions)")
    p.add_argument("--db", **db_kwargs)
    p.add_argument("--session", "-s", required=True, help="Session name or ID")
    p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")

    args = parser.parse_args()

    dispatch = {
        "backtest": cmd_backtest,
        "auto": cmd_auto,
        "start": cmd_start,
        "run": cmd_run,
        "run-close": cmd_run_close,
        "run-open": cmd_run_open,
        "monitor": cmd_monitor,
        "run-all": cmd_run_all,
        "status": cmd_status,
        "positions": cmd_positions,
        "trades": cmd_trades,
        "equity": cmd_equity,
        "sessions": cmd_sessions,
        "pause": cmd_pause,
        "resume": cmd_resume,
        "close": cmd_close,
    }
    dispatch[args.command](args)


if __name__ == "__main__":
    main()
