#!/usr/bin/env python3
"""Apply / revert step-timing additions to vLLM's existing agentic step log.

vLLM already has a per-step JSONL emitter triggered by AGENTIC_STEP_LOG_PATH.
This patch enriches it with three duration fields:

  step_duration_us   : full schedule() wall time
  build_meta_us      : duration of self.connector.build_connector_meta(...)
  emit_overhead_us   : duration of _agentic_emit_step_log itself
                       (lets us subtract the patch's own cost)

We also add a worker-side patch in kv_connector_model_runner_mixin.py to
record start_load_kv() duration into a per-process JSONL file pointed to
by VLLM_PD_PROFILE_LOG=$RUN_DIR/worker_step.jsonl.

All inserts are marked with `# CONNECTOR_TAX_PATCH` so revert is just
"delete those lines".

Usage:
  python apply_step_timing.py --apply  [--vllm-root PATH]
  python apply_step_timing.py --revert [--vllm-root PATH]
"""

import argparse
import re
import sys
from pathlib import Path

MARKER = "# CONNECTOR_TAX_PATCH"
DEFAULT_VLLM_ROOT = (
    Path.home()
    / "agentic-kv/.venv/lib/python3.12/site-packages/vllm"
)


def already_patched(text: str) -> bool:
    return MARKER in text


def revert_text(text: str) -> str:
    out = [l for l in text.splitlines() if MARKER not in l]
    return "\n".join(out) + ("\n" if text.endswith("\n") else "")


# ── scheduler.py patches ────────────────────────────────────────────────────
def patch_scheduler(text: str) -> str:
    if already_patched(text):
        print("  scheduler.py already patched, skipping")
        return text

    # 1. At top of schedule(): record _step_t0
    pat = (
        r"(    def schedule\(self\) -> SchedulerOutput:\n"
        r"        # NOTE\(woosuk\) on the scheduling algorithm:)"
    )
    repl = (
        r"    def schedule(self) -> SchedulerOutput:\n"
        r"        import time as _t  " + MARKER + "\n"
        r"        _step_t0 = _t.perf_counter_ns()  " + MARKER + "\n"
        r"        self._ct_build_meta_ns = 0  " + MARKER + "\n"
        r"        # NOTE(woosuk) on the scheduling algorithm:"
    )
    text, n = re.subn(pat, repl, text, count=1)
    if n == 0:
        raise RuntimeError("Failed to patch schedule() entry")

    # 2. Time the build_connector_meta call
    pat = (
        r"        if self\.connector is not None:\n"
        r"            meta: KVConnectorMetadata = self\.connector\.build_connector_meta\(\n"
        r"                scheduler_output\n"
        r"            \)\n"
        r"            scheduler_output\.kv_connector_metadata = meta"
    )
    repl = (
        "        if self.connector is not None:\n"
        f"            _bm_t0 = _t.perf_counter_ns()  {MARKER}\n"
        "            meta: KVConnectorMetadata = self.connector.build_connector_meta(\n"
        "                scheduler_output\n"
        "            )\n"
        f"            self._ct_build_meta_ns = _t.perf_counter_ns() - _bm_t0  {MARKER}\n"
        "            scheduler_output.kv_connector_metadata = meta"
    )
    text, n = re.subn(pat, repl, text, count=1)
    if n == 0:
        raise RuntimeError("Failed to patch build_connector_meta")

    # 3. Pass step duration into _agentic_emit_step_log via attribute
    #    (cleaner than threading kwargs through). Then in the emit
    #    function inject the new fields into `record`.
    pat = (
        r"        if self\._agentic_step_log_fh is not None:\n"
        r"            self\._agentic_emit_step_log\("
    )
    repl = (
        f"        if self._agentic_step_log_fh is not None:\n"
        f"            self._ct_step_duration_ns = _t.perf_counter_ns() - _step_t0  {MARKER}\n"
        f"            self._agentic_emit_step_log("
    )
    text, n = re.subn(pat, repl, text, count=1)
    if n == 0:
        raise RuntimeError("Failed to patch step_duration insertion")

    # 4. Inject extra fields into the `record` dict in _agentic_emit_step_log
    pat = (
        r"        record = \{\n"
        r"            \"t_unix\": _time\.time\(\),"
    )
    repl = (
        "        record = {\n"
        f"            \"step_duration_us\": getattr(self, '_ct_step_duration_ns', 0) // 1000,  {MARKER}\n"
        f"            \"build_meta_us\":    getattr(self, '_ct_build_meta_ns', 0) // 1000,  {MARKER}\n"
        "            \"t_unix\": _time.time(),"
    )
    text, n = re.subn(pat, repl, text, count=1)
    if n == 0:
        raise RuntimeError("Failed to patch record dict")

    return text


# ── apply / revert driver ───────────────────────────────────────────────────
def apply_to_file(path: Path, fn) -> bool:
    if not path.exists():
        print(f"  SKIP {path} (not found)")
        return False
    orig = path.read_text()
    new = fn(orig)
    if new == orig:
        print(f"  unchanged: {path}")
        return False
    path.write_text(new)
    print(f"  patched ({new.count(MARKER)} marks): {path}")
    return True


def revert_file(path: Path) -> bool:
    if not path.exists():
        return False
    orig = path.read_text()
    new = revert_text(orig)
    if new == orig:
        print(f"  no marks: {path}")
        return False
    path.write_text(new)
    print(f"  reverted: {path}")
    return True


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--apply", action="store_true")
    ap.add_argument("--revert", action="store_true")
    ap.add_argument("--vllm-root", type=Path, default=DEFAULT_VLLM_ROOT)
    args = ap.parse_args()

    if not (args.apply ^ args.revert):
        ap.error("Specify exactly one of --apply / --revert")

    sched = args.vllm_root / "v1/core/sched/scheduler.py"

    if args.apply:
        print(f"Applying connector-tax step-timing patch to {args.vllm_root}")
        apply_to_file(sched, patch_scheduler)
    else:
        print(f"Reverting connector-tax step-timing patch from {args.vllm_root}")
        revert_file(sched)


if __name__ == "__main__":
    main()
