#!/usr/bin/env python3
from __future__ import annotations

"""
dex_platform/scripts/paper_live_virtual_lp_v1.py

Paper-live virtual LP runner.

This script does not sign transactions. It replays a live-like event stream from a
fee-replay NPZ, runs portfolio_router_v4 decisions, and writes a persistent
virtual LP state bundle:
- paper_live_summary.csv
- paper_live_decision_log.csv
- virtual_lp_state.json
- virtual_lp_events.jsonl

Use this before any small-cap signed live.
"""

import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List

# Allow running from dex_platform/scripts while importing sibling backtest module.
SCRIPT_DIR = Path(__file__).resolve().parent
BACKTEST_DIR = SCRIPT_DIR.parent / "backtest"
if str(BACKTEST_DIR) not in sys.path:
    sys.path.insert(0, str(BACKTEST_DIR))

import portfolio_router_v4 as router  # noqa: E402

SCRIPT_VERSION = "paper_live_virtual_lp_v1_2026_05_04"


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def read_state(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def write_json(path: Path, payload: Dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, ensure_ascii=False, sort_keys=True), encoding="utf-8")


def append_jsonl(path: Path, rows: List[Dict[str, Any]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as f:
        for r in rows:
            f.write(json.dumps(r, ensure_ascii=False, sort_keys=True) + "\n")


def build_router_namespace(args: argparse.Namespace) -> argparse.Namespace:
    """Build an argparse-like object for portfolio_router_v4.run_one_npz."""
    # Most defaults mirror portfolio_router_v4 CLI. Keep explicit fields so this script
    # does not depend on parsing private CLI internals.
    return argparse.Namespace(
        npz=args.npz,
        npzs=[],
        npz_glob=[],
        out_dir=str(args.out_dir),
        fee_rates=args.fee_rates,
        time_from=args.time_from,
        time_to=args.time_to,
        dec0=args.dec0,
        dec1=args.dec1,
        routing_mode=args.routing_mode,
        total_capital_usd=args.total_capital_usd,
        lookback_hours=args.lookback_hours,
        decision_hours=args.decision_hours,
        gas_usd=args.gas_usd,
        swap_cost_bps=args.swap_cost_bps,
        max_current_liquidity_share_pct=args.max_current_liquidity_share_pct,
        max_deploy_fraction=args.max_deploy_fraction,
        bio_micro_min_drift_pct=args.bio_micro_min_drift_pct,
        bio_micro_min_trend_ratio=args.bio_micro_min_trend_ratio,
        bio_macro_lookback_hours=args.bio_macro_lookback_hours,
        bio_macro_kill_drift_pct=args.bio_macro_kill_drift_pct,
        bio_bull_min_fee_budget_pct_day=args.bio_bull_min_fee_budget_pct_day,
        bio_bull_max_toxicity=args.bio_bull_max_toxicity,
        check_bear_max_drift_pct=args.check_bear_max_drift_pct,
        check_bear_min_fee_budget_pct_day=args.check_bear_min_fee_budget_pct_day,
        min_events_per_hour=args.min_events_per_hour,
        allow_synd=args.allow_synd,
        no_bootstrap=args.no_bootstrap,
        strict_quote_check=args.strict_quote_check,
        prewarm_hours=args.prewarm_hours,
        warmup_probe_hours=args.warmup_probe_hours,
        warmup_min_span_fraction=args.warmup_min_span_fraction,
        warmup_min_return_pct=args.warmup_min_return_pct,
        warmup_max_mdd_pct=args.warmup_max_mdd_pct,
        warmup_min_pnl_mdd=args.warmup_min_pnl_mdd,
        warmup_min_time_in_range_pct=args.warmup_min_time_in_range_pct,
        warmup_max_p99_share_pct=args.warmup_max_p99_share_pct,
        warmup_max_max_share_pct=args.warmup_max_max_share_pct,
        dema_enable=args.dema_enable,
        dema_fast_hours=args.dema_fast_hours,
        dema_slow_hours=args.dema_slow_hours,
        dema_slope_hours=args.dema_slope_hours,
        dema_min_slow_slope_pct=args.dema_min_slow_slope_pct,
        dema_no_price_above_slow=args.dema_no_price_above_slow,
        dema_no_fast_above_slow=args.dema_no_fast_above_slow,
        health_check_enable=args.health_check_enable,
        health_min_micro_drift_pct=args.health_min_micro_drift_pct,
        health_probe_enable=args.health_probe_enable,
        health_probe_hours=args.health_probe_hours,
        health_probe_min_return_pct=args.health_probe_min_return_pct,
        health_probe_max_mdd_pct=args.health_probe_max_mdd_pct,
        health_probe_min_pnl_mdd=args.health_probe_min_pnl_mdd,
        health_probe_min_time_in_range_pct=args.health_probe_min_time_in_range_pct,
        health_probe_max_p99_share_pct=args.health_probe_max_p99_share_pct,
        exit_on_route_mismatch=args.exit_on_route_mismatch,
        no_exit_on_route_mismatch=args.no_exit_on_route_mismatch,
        no_rebalance_when_due=args.no_rebalance_when_due,
        no_reentry_after_exit=args.no_reentry_after_exit,
        health_max_total_dd_pct=args.health_max_total_dd_pct,
        strict_mdd_pct=args.strict_mdd_pct,
        min_pnl_mdd=args.min_pnl_mdd,
        max_avg_liquidity_share_pct=args.max_avg_liquidity_share_pct,
        max_p95_liquidity_share_pct=args.max_p95_liquidity_share_pct,
        max_p99_liquidity_share_pct=args.max_p99_liquidity_share_pct,
        max_liquidity_share_pct=args.max_liquidity_share_pct,
        target_mdd_pct=args.target_mdd_pct,
        w_mdd=args.w_mdd,
        w_avg_share=args.w_avg_share,
        w_p95_share=args.w_p95_share,
        w_p99_share=args.w_p99_share,
        w_max_share=args.w_max_share,
        w_rebalance=args.w_rebalance,
    )


def add_args(ap: argparse.ArgumentParser) -> None:
    ap.add_argument("--npz", required=True, help="Fee-replay NPZ used as the paper-live event stream.")
    ap.add_argument("--out-dir", required=True)
    ap.add_argument("--state-json", default="", help="Optional path for persistent virtual state. Defaults to <out-dir>/virtual_lp_state.json")
    ap.add_argument("--append-events", action="store_true", help="Append JSONL events instead of overwriting by report files only.")
    ap.add_argument("--fee-rates", default="metadata")
    ap.add_argument("--time-from", default="")
    ap.add_argument("--time-to", default="")
    ap.add_argument("--dec0", type=int, default=0)
    ap.add_argument("--dec1", type=int, default=0)

    ap.add_argument("--routing-mode", choices=["locked_candidates", "rolling", "rolling_warmup", "rolling_v4"], default="rolling_v4")
    ap.add_argument("--total-capital-usd", type=float, default=600.0)
    ap.add_argument("--lookback-hours", type=float, default=48.0)
    ap.add_argument("--decision-hours", type=float, default=12.0)
    ap.add_argument("--gas-usd", type=float, default=0.0)
    ap.add_argument("--swap-cost-bps", type=float, default=0.0)
    ap.add_argument("--max-current-liquidity-share-pct", type=float, default=15.0)
    ap.add_argument("--max-deploy-fraction", type=float, default=1.0)

    ap.add_argument("--bio-micro-min-drift-pct", type=float, default=1.0)
    ap.add_argument("--bio-micro-min-trend-ratio", type=float, default=0.02)
    ap.add_argument("--bio-macro-lookback-hours", type=float, default=168.0)
    ap.add_argument("--bio-macro-kill-drift-pct", type=float, default=-5.0)
    ap.add_argument("--bio-bull-min-fee-budget-pct-day", type=float, default=0.15)
    ap.add_argument("--bio-bull-max-toxicity", type=float, default=0.85)
    ap.add_argument("--check-bear-max-drift-pct", type=float, default=-5.0)
    ap.add_argument("--check-bear-min-fee-budget-pct-day", type=float, default=0.15)
    ap.add_argument("--min-events-per-hour", type=float, default=1.0)
    ap.add_argument("--allow-synd", action="store_true")
    ap.add_argument("--no-bootstrap", action="store_true")
    ap.add_argument("--strict-quote-check", action="store_true")

    ap.add_argument("--prewarm-hours", type=float, default=168.0)
    ap.add_argument("--warmup-probe-hours", type=float, default=168.0)
    ap.add_argument("--warmup-min-span-fraction", type=float, default=0.80)
    ap.add_argument("--warmup-min-return-pct", type=float, default=0.0)
    ap.add_argument("--warmup-max-mdd-pct", type=float, default=12.0)
    ap.add_argument("--warmup-min-pnl-mdd", type=float, default=1.5)
    ap.add_argument("--warmup-min-time-in-range-pct", type=float, default=40.0)
    ap.add_argument("--warmup-max-p99-share-pct", type=float, default=10.0)
    ap.add_argument("--warmup-max-max-share-pct", type=float, default=25.0)

    ap.add_argument("--dema-enable", action="store_true")
    ap.add_argument("--dema-fast-hours", type=float, default=48.0)
    ap.add_argument("--dema-slow-hours", type=float, default=168.0)
    ap.add_argument("--dema-slope-hours", type=float, default=24.0)
    ap.add_argument("--dema-min-slow-slope-pct", type=float, default=0.0)
    ap.add_argument("--dema-no-price-above-slow", action="store_true")
    ap.add_argument("--dema-no-fast-above-slow", action="store_true")

    ap.add_argument("--health-check-enable", action="store_true")
    ap.add_argument("--health-min-micro-drift-pct", type=float, default=-5.0)
    ap.add_argument("--health-probe-enable", action="store_true")
    ap.add_argument("--health-probe-hours", type=float, default=72.0)
    ap.add_argument("--health-probe-min-return-pct", type=float, default=-2.0)
    ap.add_argument("--health-probe-max-mdd-pct", type=float, default=12.0)
    ap.add_argument("--health-probe-min-pnl-mdd", type=float, default=0.0)
    ap.add_argument("--health-probe-min-time-in-range-pct", type=float, default=25.0)
    ap.add_argument("--health-probe-max-p99-share-pct", type=float, default=10.0)
    ap.add_argument("--exit-on-route-mismatch", action="store_true")
    ap.add_argument("--no-exit-on-route-mismatch", action="store_true")
    ap.add_argument("--no-rebalance-when-due", action="store_true")
    ap.add_argument("--no-reentry-after-exit", action="store_true")
    ap.add_argument("--health-max-total-dd-pct", type=float, default=0.0)

    ap.add_argument("--strict-mdd-pct", type=float, default=20.0)
    ap.add_argument("--min-pnl-mdd", type=float, default=2.0)
    ap.add_argument("--max-avg-liquidity-share-pct", type=float, default=3.0)
    ap.add_argument("--max-p95-liquidity-share-pct", type=float, default=5.0)
    ap.add_argument("--max-p99-liquidity-share-pct", type=float, default=10.0)
    ap.add_argument("--max-liquidity-share-pct", type=float, default=25.0)

    ap.add_argument("--target-mdd-pct", type=float, default=20.0)
    ap.add_argument("--w-mdd", type=float, default=2.0)
    ap.add_argument("--w-avg-share", type=float, default=5.0)
    ap.add_argument("--w-p95-share", type=float, default=10.0)
    ap.add_argument("--w-p99-share", type=float, default=3.0)
    ap.add_argument("--w-max-share", type=float, default=0.5)
    ap.add_argument("--w-rebalance", type=float, default=0.02)


def main() -> None:
    ap = argparse.ArgumentParser(description="Paper-live virtual LP runner. No signed transactions.")
    add_args(ap)
    args = ap.parse_args()
    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    state_path = Path(args.state_json) if args.state_json else out_dir / "virtual_lp_state.json"
    previous_state = read_state(state_path)

    router_args = build_router_namespace(args)
    cfg = router.build_config(router_args)
    rows, logs = router.run_one_npz(args.npz, router_args, cfg)

    router.write_rows_csv(out_dir / "paper_live_summary.csv", rows)
    router.write_rows_csv(out_dir / "paper_live_decision_log.csv", logs)
    router.write_rows_csv(out_dir / "virtual_lp_events.csv", logs)

    if args.append_events:
        append_jsonl(out_dir / "virtual_lp_events.jsonl", logs)
    else:
        jsonl = out_dir / "virtual_lp_events.jsonl"
        if jsonl.exists():
            jsonl.unlink()
        append_jsonl(jsonl, logs)

    latest_row = rows[0] if rows else {}
    last_log = logs[-1] if logs else {}
    state = {
        "script_version": SCRIPT_VERSION,
        "router_version": router.SCRIPT_VERSION,
        "updated_at": now_iso(),
        "source_npz": str(args.npz),
        "out_dir": str(out_dir),
        "previous_last_ts": previous_state.get("last_ts"),
        "last_ts": last_log.get("ts"),
        "last_iso_time": last_log.get("iso_time"),
        "last_action": last_log.get("action_taken"),
        "last_active_route": last_log.get("active_route"),
        "last_candidate_route": last_log.get("candidate_route"),
        "last_exit_reason": last_log.get("exit_reason"),
        "summary": latest_row,
        "rows": len(rows),
        "decision_log_rows": len(logs),
        "signed_transactions": False,
        "live_ready_claim": False,
    }
    write_json(state_path, state)

    print(json.dumps({
        "script_version": SCRIPT_VERSION,
        "state_json": str(state_path),
        "out_dir": str(out_dir),
        "rows": len(rows),
        "decision_log_rows": len(logs),
        "last_action": state.get("last_action"),
        "last_active_route": state.get("last_active_route"),
        "return_total_pct": latest_row.get("return_total_pct"),
        "mdd_total_pct": latest_row.get("mdd_total_pct"),
        "strict_pass": latest_row.get("strict_pass"),
    }, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()
