import json
import os
from argparse import Namespace
from dataclasses import dataclass
from typing import List, Optional

import numpy as np
from transformers import PreTrainedTokenizerBase

from sglang.benchmark.datasets.common import BaseDataset, DatasetRow

# Per-turn output length when --sharegpt-output-len is not given; matches the
# ~220-token average assistant reply of OpenHands-style agentic traces.
DEFAULT_AGENTIC_OUTPUT_LEN = 220


@dataclass
class AgenticTraceDataset(BaseDataset):
    """Multi-turn agentic trace loader (e.g. OpenHands / SWE-smith traces).

    Expects a trace JSON of the shape::

        {
          "metadata": {...},
          "conversations": [
            [   # one conversation == a list of turns
              {"messages": [{"role": "system", ...}, {"role": "user", ...}],
               "prompt_tokens": 73821},
              {"messages": [{"role": "user", ...}], "prompt_tokens": 74894},
              ...
            ],
            ...
          ]
        }

    Each turn's ``messages`` holds only the new non-assistant messages for that
    turn. One conversation becomes one :class:`DatasetRow` whose ``prompt`` is
    the list of per-turn message deltas; ``bench_serving`` detects this shape as
    multi-turn and replays each conversation round by round, feeding the
    server's real assistant reply back into the next round's history.

    Use with a chat backend (``--backend sglang-oai-chat``).
    """

    dataset_path: str
    num_requests: int
    fixed_output_len: Optional[int]
    offset: int
    max_turns: Optional[int]

    @classmethod
    def from_args(cls, args: Namespace) -> "AgenticTraceDataset":
        return cls(
            dataset_path=args.dataset_path,
            num_requests=args.num_prompts,
            fixed_output_len=args.sharegpt_output_len,
            offset=args.dataset_offset,
            max_turns=args.agentic_max_turns,
        )

    def load(
        self, tokenizer: PreTrainedTokenizerBase, model_id=None
    ) -> List[DatasetRow]:
        if not os.path.isfile(self.dataset_path):
            raise FileNotFoundError(f"Dataset not found at {self.dataset_path}")

        with open(self.dataset_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        conversations = data.get("conversations", [])
        if not conversations:
            raise ValueError(f"No 'conversations' found in {self.dataset_path}.")

        offset = self.offset % len(conversations)
        if offset:
            conversations = conversations[offset:] + conversations[:offset]

        output_len = self.fixed_output_len or DEFAULT_AGENTIC_OUTPUT_LEN

        filtered_dataset: List[DatasetRow] = []
        for conversation in conversations:
            if self.num_requests > 0 and len(filtered_dataset) >= self.num_requests:
                break

            prompt = [turn["messages"] for turn in conversation if turn.get("messages")]
            if self.max_turns:
                prompt = prompt[: self.max_turns]
            if not prompt:
                continue

            # Informational only: multi-turn replay ignores per-row prompt_len.
            prompt_len = int(conversation[0].get("prompt_tokens", 0))

            filtered_dataset.append(
                DatasetRow(
                    prompt=prompt,
                    prompt_len=prompt_len,
                    output_len=output_len,
                )
            )

        if not filtered_dataset:
            raise ValueError(
                f"No usable conversations loaded from {self.dataset_path}."
            )

        num_turns = [len(row.prompt) for row in filtered_dataset]
        print(
            f"#Conversations: {len(filtered_dataset)} "
            f"(offset={offset}, turns/conv min={min(num_turns)} "
            f"max={max(num_turns)} avg={np.mean(num_turns):.1f})"
        )
        print(f"#Output tokens per turn: {output_len}")
        return filtered_dataset
