"""
APEX V16 — Entry point.

CLI argument parsing, environment loading, config validation,
and bot lifecycle handoff to orchestrator.

Usage:
    python main.py --mode dry
    python main.py --mode paper --asset MES,MNQ
    python main.py --mode live --account ineligible
    python main.py --mode live --account express   # requires confirmation
    python main.py --mode paper --fresh-start

For full options, run: python main.py --help
"""

from __future__ import annotations

import argparse
import asyncio
import os
import signal
import sys
from pathlib import Path
from typing import Optional


# ============================================================
# .env LOADING
# ============================================================

def load_env_file(env_path: Path) -> int:
    """
    Load KEY=VALUE pairs from a .env file into os.environ.
    Existing env vars are NOT overridden (they win).

    Returns the number of variables loaded.
    """
    if not env_path.exists():
        return 0

    loaded = 0
    for line in env_path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            continue
        key, _, value = line.partition("=")
        key = key.strip()
        value = value.strip().strip('"').strip("'")
        if key and key not in os.environ:
            os.environ[key] = value
            loaded += 1
    return loaded


# ============================================================
# CLI PARSING
# ============================================================

def build_parser() -> argparse.ArgumentParser:
    """Build the argparse parser with all V16 CLI options."""
    p = argparse.ArgumentParser(
        prog="apex_v16",
        description="APEX V16 — Trading bot for CME futures via TopstepX/ProjectX",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  python main.py --mode dry\n"
            "  python main.py --mode paper --asset MES,MNQ\n"
            "  python main.py --mode live --account ineligible\n"
            "  python main.py --mode live --account express   # requires confirmation"
        ),
    )

    # Mode (required)
    p.add_argument(
        "--mode",
        choices=["dry", "paper", "live"],
        required=True,
        help="Run mode: dry (connected, no trades), paper (simulated), live (real trades)",
    )

    # Account (only meaningful in live mode, but we accept for paper too for state separation)
    p.add_argument(
        "--account",
        choices=["ineligible", "express"],
        default="ineligible",
        help="Topstep account: ineligible (burned) or express (FUNDED). Default: ineligible.",
    )

    # Fresh start
    p.add_argument(
        "--fresh-start",
        action="store_true",
        help="Ignore previous state file and start fresh (deletes state and backup).",
    )

    # Asset filter
    p.add_argument(
        "--asset",
        type=str,
        default=None,
        help="Comma-separated asset filter (e.g. 'MES,MNQ'). Default: all from config_futures.",
    )

    # Env file
    p.add_argument(
        "--env-file",
        type=Path,
        default=Path.home() / "apex_v15" / ".env",
        help="Path to .env file. Default: ~/apex_v15/.env (V15 reuse).",
    )

    # Cap session trades (dev safety)
    p.add_argument(
        "--max-trades-session",
        type=int,
        default=None,
        help="Hard cap on trades for this session (dev/testing safety).",
    )

    return p


# ============================================================
# CONFIG BUILD
# ============================================================

def build_config(args: argparse.Namespace):
    """
    Build RuntimeConfig from CLI args.
    Applies EXPRESS profile (tightened risk) automatically when account=express.
    """
    # Lazy imports so --help works even if a downstream module has issues
    from core.config import RuntimeConfig, RunMode, AccountKind, apply_express_profile

    cfg = RuntimeConfig(
        mode=RunMode(args.mode),
        account=AccountKind(args.account),
        fresh_start=args.fresh_start,
        asset_filter=[a.strip() for a in args.asset.split(",")] if args.asset else None,
    )

    if args.max_trades_session is not None:
        cfg.max_trades_session = args.max_trades_session

    # If running live on Express, apply tightened profile
    if cfg.is_live and cfg.is_express:
        cfg = apply_express_profile(cfg)

    return cfg


def resolve_asset_list(cfg) -> list[str]:
    """
    Resolve the effective tradable asset list.

      --asset MES,MNQ  -> intersect with ASSETS_MAP, warn on unknowns.
      (no --asset)     -> all keys of ASSETS_MAP.

    Returns [] if every requested asset was unknown — caller exits.
    Lazy import keeps --help fast.
    """
    from core.config_futures import ASSETS_MAP

    if not cfg.asset_filter:
        return list(ASSETS_MAP.keys())

    requested = list(cfg.asset_filter)
    known = [a for a in requested if a in ASSETS_MAP]
    unknown = [a for a in requested if a not in ASSETS_MAP]
    if unknown:
        print(
            f"WARNING: --asset contained unknown symbols (ignored): "
            f"{', '.join(unknown)}",
            file=sys.stderr,
        )
        print(
            f"  Known symbols: {', '.join(sorted(ASSETS_MAP.keys()))}",
            file=sys.stderr,
        )
    return known


# ============================================================
# BANNER
# ============================================================

def print_banner(cfg, asset_list: list[str]) -> None:
    """Print configuration summary at startup."""
    bar = "=" * 60
    mode_label = {
        "dry":   "DRY RUN (no trades placed)",
        "paper": "PAPER (simulated)",
        "live":  "LIVE (real trades)",
    }[cfg.mode.value]

    account_label = {
        "ineligible": "INELIGIBLE (burned $100K combine)",
        "express":    "EXPRESS (FUNDED $100K)  *** REAL CAPITAL ***",
    }[cfg.account.value]

    print(bar)
    print("APEX PREDATOR V16")
    print(bar)
    print(f"Mode:      {mode_label}")
    print(f"Account:   {account_label}")
    print(f"Account #: {cfg.account_id}")
    print(f"State:     {cfg.state_file}")
    print(f"Logs:      {cfg.log_dir}")
    print(f"Fresh:     {cfg.fresh_start}")
    print(f"Risk:      {cfg.risk_per_trade*100:.2f}% per trade")
    print(f"Daily:     target ${cfg.daily_profit_target:.0f} / hard stop ${cfg.daily_loss_hard_stop:.0f}")
    print(f"Correlat.: {'ON' if cfg.enable_correlation else 'OFF'}")
    suffix = " (filter: --asset)" if cfg.asset_filter else ""
    print(f"Assets:    {', '.join(asset_list)} ({len(asset_list)}){suffix}")
    print(bar)


# ============================================================
# MAIN
# ============================================================

async def _connect_with_retry(
    broker,
    *,
    logger,
    max_attempts: int = 3,
    delays_seconds: tuple[int, ...] = (5, 10, 20),
) -> bool:
    """
    Attempt broker.connect() up to max_attempts times with exponential-ish
    backoff. Returns True on success, False after the last failed attempt.
    Used for startup robustness against transient Topstep / network issues.
    BACKLOG: longer maintenance windows -> calibrate delays.
    """
    for attempt in range(1, max_attempts + 1):
        try:
            ok = await broker.connect()
            if ok:
                if attempt > 1:
                    logger.system.info("broker.connect succeeded on attempt %d", attempt)
                return True
            logger.system.warning(
                "broker.connect attempt %d/%d returned False",
                attempt, max_attempts,
            )
        except Exception as e:
            logger.system.warning(
                "broker.connect attempt %d/%d raised: %s",
                attempt, max_attempts, e,
            )
        if attempt < max_attempts:
            delay = delays_seconds[min(attempt - 1, len(delays_seconds) - 1)]
            await asyncio.sleep(delay)
    return False


async def async_main(cfg, logger, state_store, asset_list: list[str]) -> int:
    """
    Async entry point: connect AI client, build market data provider,
    instantiate brains, then hand off to Orchestrator.run().
    Phase C1 wires broker.connect (LIVE/DRY) + signal handlers.
    """
    from core.config import RunMode
    logger.system.info(f"V16 starting in {cfg.mode.value} mode on {cfg.account.value} account")
    logger.log_session_event(
        "session_started",
        mode=cfg.mode.value,
        account=cfg.account.value,
        fresh_start=cfg.fresh_start,
        asset_filter=cfg.asset_filter,
    )

    # Load or initialize state (state v2 schema, with auto daily reset on LIVE)
    from persistence.state_store import StateLoadMode, load_state
    if cfg.fresh_start:
        state_store.reset()
        logger.system.info("State reset (--fresh-start)")
    state = load_state(
        state_store,
        mode=StateLoadMode.RESUME,
        auto_daily_reset=cfg.is_live,
    )
    logger.system.info(
        f"State loaded: active_trades={len(state.active_trades)}, "
        f"daily_pnl=${state.daily.daily_pnl:.2f}, halted={state.halted}"
    )

    # Connect AI client
    from brain.ai_client import AIClient
    ai_client = AIClient(model=cfg.ai_model if hasattr(cfg, "ai_model") else None,
                         logger=logger)
    if not await ai_client.connect():
        logger.system.error("AI client failed to connect; cannot proceed")
        return 1

    # ========================================================
    # BROKER + PROVIDER wireup (Phase C1)
    # ========================================================
    # LIVE  -> TopstepXBroker connected, place/close real
    # DRY   -> DryRunBroker(TopstepXBroker connected): real reads, fake writes
    # PAPER -> no broker, FakeMarketDataProvider({}) (paper-on-real-data BACKLOG)
    from analysis.market_data import (
        BrokerMarketDataProvider, FakeMarketDataProvider,
    )
    # V18 SDK patch: idempotente, applica anche se topstepx_adapter
    # non viene importato (es. PAPER mode con USE_TV_FEED=1).
    from broker._sdk_patches import apply_sdk_patches
    apply_sdk_patches()
    # V18: opt-in TradingView feed. USE_TV_FEED=1 + TV_USERNAME/TV_PASSWORD
    # in env -> TVDataFeedProvider replaces both BrokerMarketDataProvider
    # (LIVE/DRY) and FakeMarketDataProvider (PAPER). Anonymous fallback if
    # creds missing — limited history depth but still functional.
    use_tv_feed = os.getenv("USE_TV_FEED", "0") == "1"
    broker = None
    if cfg.mode == RunMode.LIVE or cfg.mode == RunMode.DRY:
        from broker.topstepx_v16 import TopstepXBroker
        real_broker = TopstepXBroker(instruments=asset_list)
        ok = await _connect_with_retry(real_broker, logger=logger)
        if not ok:
            logger.system.error("broker.connect failed after retries; aborting")
            logger.log_session_event("session_failed", reason="broker_connect")
            return 3
        if cfg.mode == RunMode.DRY:
            from broker.dry_run_broker import DryRunBroker
            broker = DryRunBroker(real_broker, logger=logger)
            logger.system.info("DRY mode: broker reads real, writes intercepted")
        else:
            broker = real_broker
        if use_tv_feed:
            from analysis.tv_data_provider import TVDataFeedProvider
            provider = TVDataFeedProvider(
                username=os.getenv("TV_USERNAME"),
                password=os.getenv("TV_PASSWORD"),
                token=os.getenv("TV_TOKEN"),
            )
        else:
            provider = BrokerMarketDataProvider(broker)
        is_paper = False
    else:
        # PAPER mode: simulated broker layer end-to-end
        if use_tv_feed:
            from analysis.tv_data_provider import TVDataFeedProvider
            provider = TVDataFeedProvider(
                username=os.getenv("TV_USERNAME"),
                password=os.getenv("TV_PASSWORD"),
                token=os.getenv("TV_TOKEN"),
            )
        else:
            provider = FakeMarketDataProvider({})
        is_paper = True

    # Single startup log: which provider is actually active. TV gets an
    # auth-mode suffix (anon/token/creds) so an operator can tell at a
    # glance whether TV_TOKEN was picked up.
    provider_name = type(provider).__name__
    if provider_name == "TVDataFeedProvider":
        if provider.token:
            tv_auth = "token"
        elif provider.username and provider.password:
            tv_auth = "creds"
        else:
            tv_auth = "anon"
        logger.system.info("MarketDataProvider: %s (%s)", provider_name, tv_auth)
    else:
        logger.system.info("MarketDataProvider: %s", provider_name)

    # Instantiate brains
    from brain.brain_tf import BrainTF
    from brain.brain_mr import BrainMR
    brains = {
        "TF": BrainTF(cfg, ai_client, logger),
        "MR": BrainMR(cfg, ai_client, logger),
    }

    # Wire trade lifecycle.
    # In DRY: opener/closer talk to DryRunBroker which intercepts the writes.
    # is_paper=False -> live code path is exercised end-to-end against the
    # DryRunBroker (so anything that would have happened on TopstepX is logged).
    from trading.trade_opener import TradeOpener
    from trading.trade_closer import TradeCloser
    from trading.risk_manager import RiskManager
    opener = TradeOpener(broker=broker, is_paper=is_paper, logger=logger)
    closer = TradeCloser(broker=broker, is_paper=is_paper, logger=logger)
    risk_manager = RiskManager(cfg, state, logger=logger)

    # ========================================================
    # RECONCILIATION (Phase C2a) — V15-BUG-9 root-cause fix
    # ========================================================
    # Run once at startup to align state.active_trades with broker reality.
    # Drops case (ii) ghosts and recovers their P&L from broker history when
    # available. Logs cases (iii)/(iv)/(v) for operator review (conservative
    # policy — V16 does not auto-adopt orphans).
    # PAPER has no broker, so reconciliation is skipped there.
    reconciler = None
    if broker is not None:
        from broker.reconciliation import Reconciler
        reconciler = Reconciler(
            broker=broker, state=state,
            risk_manager=risk_manager, logger=logger,
        )
        try:
            report = await reconciler.reconcile_startup()
            logger.system.info(
                "Reconcile: case_i=%d case_ii=%d (recovered %d, $%.2f) "
                "case_iii=%d case_iv=%d case_v=%d",
                len(report.case_i_ok),
                len(report.case_ii_state_open_broker_flat),
                len(report.case_ii_recovered_via_history),
                report.pnl_recovered_usd,
                len(report.case_iii_state_flat_broker_open),
                len(report.case_iv_size_mismatch),
                len(report.case_v_naked_orders),
            )
        except Exception as e:
            logger.system.warning("Reconcile startup failed (non-fatal): %s", e)

    # Hand off to Orchestrator
    from orchestrator import Orchestrator
    orchestrator = Orchestrator(
        config=cfg, ai_client=ai_client, market_data_provider=provider,
        state=state, store=state_store, logger=logger,
        brain_dispatch=brains,
        opener=opener, closer=closer, risk_manager=risk_manager,
        broker=broker, reconciler=reconciler,
    )

    # C2b — wire mid-loop reconnect to _connect_with_retry with config-driven
    # parameters (10 attempts / cumulative ~10min). PAPER skips this (no
    # broker). The orchestrator calls this lambda with no args from its
    # _do_reconnect path; we supply the broker-instance closure here so
    # the orchestrator stays broker-agnostic.
    if broker is not None:
        async def _mid_loop_reconnect(b):
            try:
                await b.disconnect()
            except Exception:
                pass
            return await _connect_with_retry(
                b, logger=logger,
                max_attempts=int(cfg.mid_loop_reconnect_max_attempts),
                delays_seconds=tuple(cfg.mid_loop_reconnect_delays_seconds),
            )
        orchestrator._reconnect_fn = _mid_loop_reconnect

    # ========================================================
    # Signal handlers — graceful stop (V15 had none; V15-BUG-10 fixed)
    # ========================================================
    import signal
    loop = asyncio.get_running_loop()
    signal_reasons: dict[str, str] = {}
    def _make_handler(name: str):
        def _h():
            signal_reasons["last"] = name
            logger.system.warning("Received %s; requesting graceful stop", name)
            orchestrator.stop()
        return _h
    for sig in (signal.SIGTERM, signal.SIGINT):
        try:
            loop.add_signal_handler(sig, _make_handler(sig.name))
        except (NotImplementedError, ValueError):
            # add_signal_handler is not supported on Windows; KeyboardInterrupt
            # path in orchestrator.run() handles SIGINT there.
            pass

    try:
        rc = await orchestrator.run()
    finally:
        # On signal-driven shutdown: close broker (if any). PAPER has no broker.
        if broker is not None:
            try:
                await broker.disconnect()
            except Exception as e:
                logger.system.warning("broker.disconnect failed: %s", e)
        logger.log_session_event(
            "session_ended",
            reason=signal_reasons.get("last", "orchestrator_returned"),
        )

    return rc


def main() -> int:
    """Synchronous entry point. Parses CLI, builds config, runs async main."""
    parser = build_parser()
    args = parser.parse_args()

    # Load .env (must happen before instantiating anything that reads env vars)
    n_loaded = load_env_file(args.env_file)

    # Sanity: ANTHROPIC_API_KEY required
    if not os.environ.get("ANTHROPIC_API_KEY"):
        print(f"ERROR: ANTHROPIC_API_KEY not set.", file=sys.stderr)
        print(f"  Tried .env file: {args.env_file} ({n_loaded} vars loaded)", file=sys.stderr)
        print(f"  Set it in .env or export it before running.", file=sys.stderr)
        return 2

    # Build config (creates state/ and logs/<scope>/ directories)
    cfg = build_config(args)

    # Express safety confirmation (interactive)
    cfg.require_express_confirmation()

    # Resolve tradable asset list (default = all of ASSETS_MAP).
    # Empty result = bot has nothing to trade -> abort before banner.
    asset_list = resolve_asset_list(cfg)
    if not asset_list:
        print(
            "ERROR: no tradable assets after resolving --asset. "
            "Check the symbols against core/config_futures.ASSETS_MAP.",
            file=sys.stderr,
        )
        return 2

    # Banner
    print_banner(cfg, asset_list)

    # Logger and state (lazy imports to keep --help fast)
    from persistence.logging_setup import build_logger_bundle
    from persistence.state_store import StateStore

    logger = build_logger_bundle(cfg.log_dir)
    state_store = StateStore(cfg.state_file)

    # Signal handlers for clean shutdown (SIGTERM, SIGINT)
    shutdown_event = asyncio.Event() if False else None  # placeholder for orchestrator

    # Run async main
    try:
        return asyncio.run(async_main(cfg, logger, state_store, asset_list))
    except KeyboardInterrupt:
        logger.system.warning("Interrupted by user (Ctrl+C)")
        logger.log_session_event("session_ended", reason="keyboard_interrupt")
        return 130
    except Exception as e:
        logger.system.exception(f"Unhandled exception: {e}")
        logger.log_error(where="main", error=str(e))
        return 1


if __name__ == "__main__":
    sys.exit(main())
