"""Convert Inferact codex_swebenchpro_traces (ShareGPT) to agentic-pd-hybrid trace JSONL.

Output schema (one JSON object per line, matching src/agentic_pd_hybrid/trace.py):
  chat_id, parent_chat_id, timestamp, input_length, output_length, type, turn, hash_ids

Each trial in the input becomes one session. Each (human, gpt) pair within a trial
becomes one turn. The prefix at turn N is the concatenation of all (human, gpt) pairs
from turns 0..N-1 plus the current human message — this mirrors how agentic coding
agents grow context across calls.

hash_ids are derived per 24-token block via sha256 of the block's text + previous hash,
which gives stable, deterministic, prefix-shared hashes across turns of the same session.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
import time
from pathlib import Path

BLOCK_TOKEN_BUDGET = 24


def _block_hash(text: str, prev_hash: int) -> int:
    h = hashlib.sha256(text.encode("utf-8") + prev_hash.to_bytes(8, "big")).digest()
    return int.from_bytes(h[:8], "big") & 0x7FFFFFFFFFFFFFFF


def _build_hash_ids(token_ids: list[int]) -> list[int]:
    out: list[int] = []
    prev = 0
    for start in range(0, len(token_ids), BLOCK_TOKEN_BUDGET):
        block = token_ids[start : start + BLOCK_TOKEN_BUDGET]
        block_repr = ",".join(str(t) for t in block)
        prev = _block_hash(block_repr, prev)
        out.append(prev)
    return out


def _pair_turns(conv: list[dict]) -> list[tuple[str, str]]:
    """Pair consecutive (human, gpt) messages. Skip malformed."""
    pairs: list[tuple[str, str]] = []
    i = 0
    while i + 1 < len(conv):
        a, b = conv[i], conv[i + 1]
        if (
            isinstance(a, dict)
            and isinstance(b, dict)
            and a.get("from") == "human"
            and b.get("from") == "gpt"
        ):
            pairs.append((str(a.get("value", "")), str(b.get("value", ""))))
            i += 2
        else:
            i += 1
    return pairs


def convert(
    input_path: Path,
    output_path: Path,
    *,
    tokenizer_path: str,
    max_trials: int | None,
    inter_turn_gap_s: float,
    session_stagger_s: float,
    request_type: str,
) -> None:
    from transformers import AutoTokenizer

    print(f"loading tokenizer from {tokenizer_path}", file=sys.stderr)
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)

    print(f"loading {input_path}", file=sys.stderr)
    data = json.loads(input_path.read_text())
    if max_trials is not None:
        data = data[:max_trials]
    print(f"{len(data)} trials to process", file=sys.stderr)

    next_chat_id = 1_000_000
    written = 0
    skipped_trials = 0
    t0 = time.time()

    with output_path.open("w", encoding="utf-8") as out_f:
        for trial_idx, trial in enumerate(data):
            conv = trial.get("conversations") or []
            turns = _pair_turns(conv)
            if not turns:
                skipped_trials += 1
                continue

            base_ts = trial_idx * session_stagger_s
            ts = base_ts
            parent_chat_id = -1
            prefix_text = ""

            for turn_idx, (human, assistant) in enumerate(turns):
                # Input at this turn = full prior context + current human message.
                current_text = (
                    prefix_text + ("\n\n[USER]\n" if prefix_text else "[USER]\n") + human
                )
                input_ids = tokenizer.encode(current_text, add_special_tokens=False)
                input_length = len(input_ids)

                output_ids = tokenizer.encode(assistant, add_special_tokens=False)
                output_length = max(1, len(output_ids))

                hash_ids = _build_hash_ids(input_ids)

                chat_id = next_chat_id
                next_chat_id += 1
                record = {
                    "chat_id": chat_id,
                    "parent_chat_id": parent_chat_id,
                    "timestamp": round(ts, 6),
                    "input_length": input_length,
                    "output_length": output_length,
                    "type": request_type,
                    "turn": turn_idx,
                    "hash_ids": hash_ids,
                }
                out_f.write(json.dumps(record) + "\n")
                written += 1

                parent_chat_id = chat_id
                ts += inter_turn_gap_s
                prefix_text = current_text + "\n\n[ASSISTANT]\n" + assistant

            if (trial_idx + 1) % 20 == 0:
                elapsed = time.time() - t0
                rate = (trial_idx + 1) / elapsed if elapsed > 0 else 0
                eta = (len(data) - trial_idx - 1) / rate if rate > 0 else 0
                print(
                    f"  trial {trial_idx + 1}/{len(data)} reqs={written} "
                    f"rate={rate:.1f} trial/s eta={eta:.0f}s",
                    file=sys.stderr,
                )

    elapsed = time.time() - t0
    print(
        f"done: wrote {written} requests across {len(data) - skipped_trials} sessions "
        f"({skipped_trials} trials skipped, empty conversations) in {elapsed:.1f}s "
        f"to {output_path}",
        file=sys.stderr,
    )


def main() -> None:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument(
        "--input",
        type=Path,
        default=Path("third_party/codex_swebenchpro_traces/codex_swebenchpro.json"),
    )
    p.add_argument("--output", type=Path, required=True)
    p.add_argument(
        "--tokenizer",
        default="/mnt/models/Qwen/Qwen3-30B-A3B-Instruct-2507",
        help="Path or HF id for the tokenizer. Default matches v2 sweep model.",
    )
    p.add_argument(
        "--max-trials",
        type=int,
        default=None,
        help="Cap number of trials processed (useful for smoke / quick tests).",
    )
    p.add_argument("--inter-turn-gap-s", type=float, default=2.5)
    p.add_argument("--session-stagger-s", type=float, default=1.0)
    p.add_argument("--request-type", default="chat")
    args = p.parse_args()

    args.output.parent.mkdir(parents=True, exist_ok=True)
    convert(
        input_path=args.input,
        output_path=args.output,
        tokenizer_path=args.tokenizer,
        max_trials=args.max_trials,
        inter_turn_gap_s=args.inter_turn_gap_s,
        session_stagger_s=args.session_stagger_s,
        request_type=args.request_type,
    )


if __name__ == "__main__":
    main()
