"""P0 scheduler: inject a pre-fixed per-request verification horizon."""

from __future__ import annotations

from typing import Any

from vllm.v1.core.sched.scheduler import Scheduler
from vllm.v1.outputs import DraftTokenIds

from .common import digest, histogram, log_event, plan_summary, requested_k


class P0Scheduler(Scheduler):
    """A Scheduler that changes only post-drafter candidate visibility.

    EAGLE has already produced its Kmax candidates when this method runs.  The
    P0 experiment therefore tests verifier-side ragged execution and collective
    phase behavior, not a drafter speedup.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self._p0_schedule_epoch = 0
        self._p0_candidate_epoch = 0

    def _p0_rank_fields(self) -> dict[str, Any]:
        config = self.vllm_config.parallel_config
        return {
            "data_parallel_rank": getattr(config, "data_parallel_rank", None),
            "data_parallel_size": getattr(config, "data_parallel_size", None),
            "tensor_parallel_size": getattr(config, "tensor_parallel_size", None),
            "expert_parallel_size": getattr(config, "expert_parallel_size", None),
        }

    def schedule(self, *args: Any, **kwargs: Any):  # type: ignore[no-untyped-def]
        output = super().schedule(*args, **kwargs)
        self._p0_schedule_epoch += 1
        log_event(
            "scheduler",
            "schedule",
            core_local_epoch=self._p0_schedule_epoch,
            **self._p0_rank_fields(),
            **plan_summary(output),
        )
        return output

    def update_draft_token_ids(self, draft_token_ids: DraftTokenIds) -> None:
        self._p0_candidate_epoch += 1
        before = [list(ids) for ids in draft_token_ids.draft_token_ids]
        chosen: list[int] = []
        after: list[list[int]] = []
        structured_output_request_ids: list[str] = []
        for request_id, ids in zip(draft_token_ids.req_ids, before):
            request = self.requests.get(request_id)
            if request is not None and getattr(request, "structured_output_request", None) is not None:
                # Do not make grammar behavior part of this premise experiment.
                chosen.append(len(ids))
                after.append(ids)
                structured_output_request_ids.append(str(request_id))
                continue
            k = requested_k(str(request_id), len(ids))
            chosen.append(k)
            after.append(ids[:k])
        pairs = [
            {"request_id": str(request_id), "before_k": len(ids), "after_k": after_k}
            for request_id, ids, after_k in zip(draft_token_ids.req_ids, before, chosen)
        ]
        log_event(
            "scheduler",
            "candidate_truncate",
            candidate_epoch=self._p0_candidate_epoch,
            **self._p0_rank_fields(),
            candidate_count=len(pairs),
            candidate_digest=digest(pairs),
            before_k_histogram=histogram([item["before_k"] for item in pairs]),
            after_k_histogram=histogram(chosen),
            structured_output_request_ids=structured_output_request_ids,
        )
        super().update_draft_token_ids(
            DraftTokenIds(req_ids=draft_token_ids.req_ids, draft_token_ids=after)
        )
