# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

from fastapi.responses import JSONResponse, Response

from vllm import PoolingParams
from vllm.engine.protocol import EngineClient
from vllm.entrypoints.openai.engine.protocol import UsageInfo
from vllm.logger import init_logger
from vllm.outputs import PoolingRequestOutput, ScoringRequestOutput
from vllm.tasks import SCORE_TYPE_MAP, SupportedTask
from vllm.v1.pool.late_interaction import (
    build_late_interaction_doc_params,
    build_late_interaction_query_params,
)

from ..base.io_processor import PoolingIOProcessor
from ..base.serving import PoolingServing
from .io_processor import ScoringIOProcessors, ScoringServeContext
from .protocol import (
    RerankDocument,
    RerankRequest,
    RerankResponse,
    RerankResult,
    RerankUsage,
    ScoreRequest,
    ScoreResponse,
    ScoreResponseData,
)
from .typing import ScoreInput

logger = init_logger(__name__)


class ServingScores(PoolingServing):
    request_id_prefix = "score"

    def __init__(
        self,
        engine_client: EngineClient,
        *args,
        supported_tasks: tuple[SupportedTask, ...],
        enable_flash_late_interaction: bool = True,
        **kwargs,
    ):
        pooling_task = engine_client.model_config.get_pooling_task(supported_tasks)
        score_type = SCORE_TYPE_MAP.get(pooling_task, None)  # type: ignore[arg-type]
        assert score_type is not None

        self.io_processor_name: str = score_type
        self.enable_flash_late_interaction = (
            self.io_processor_name == "late-interaction"
            and enable_flash_late_interaction
        )

        if self.enable_flash_late_interaction:
            self.io_processor_name = "flash-late-interaction"

        if engine_client.model_config.architecture == "JinaForRanking":
            self.io_processor_name = "jina-reranking-scoring"
            self.enable_flash_late_interaction = False

        super().__init__(engine_client, *args, **kwargs)

    def init_io_processor(self, *args, **kwargs) -> PoolingIOProcessor:
        return ScoringIOProcessors[self.io_processor_name](*args, **kwargs)

    async def __call__(self, *args, **kwargs) -> Response:
        if not self.enable_flash_late_interaction:
            return await super().__call__(*args, **kwargs)

        return await self.flash_late_interaction(*args, **kwargs)

    def _build_response(
        self,
        ctx: ScoringServeContext,
    ) -> JSONResponse:
        final_res_batch = ctx.final_res_batch
        request_id = ctx.request_id
        created_time = ctx.created_time
        model_name = self.models.model_name()

        if isinstance(ctx.request, ScoreRequest):
            return self._request_output_to_score_response(
                final_res_batch,
                request_id,
                created_time,
                model_name,
            )
        elif isinstance(ctx.request, RerankRequest):
            return self._request_output_to_rerank_response(
                final_res_batch,
                request_id,
                model_name,
                ctx.request.documents,
                ctx.request.top_n if ctx.request.top_n > 0 else len(final_res_batch),
            )
        else:
            raise ValueError(f"Invalid {self.request_id_prefix} request type")

    def _request_output_to_score_response(
        self,
        final_res_batch: list[PoolingRequestOutput],
        request_id: str,
        created_time: int,
        model_name: str,
    ) -> JSONResponse:
        items: list[ScoreResponseData] = []
        num_prompt_tokens = 0

        for idx, final_res in enumerate(final_res_batch):
            classify_res = ScoringRequestOutput.from_base(final_res)

            item = ScoreResponseData(
                index=idx,
                score=classify_res.outputs.score,
            )
            prompt_token_ids = final_res.prompt_token_ids

            items.append(item)
            num_prompt_tokens += len(prompt_token_ids)

        usage = UsageInfo(
            prompt_tokens=num_prompt_tokens,
            total_tokens=num_prompt_tokens,
        )

        response = ScoreResponse(
            id=request_id,
            created=created_time,
            model=model_name,
            data=items,
            usage=usage,
        )

        return JSONResponse(content=response.model_dump())

    def _request_output_to_rerank_response(
        self,
        final_res_batch: list[PoolingRequestOutput],
        request_id: str,
        model_name: str,
        documents: ScoreInput | list[ScoreInput],
        top_n: int,
    ) -> JSONResponse:
        if not isinstance(documents, list):
            documents = [documents]

        results: list[RerankResult] = []
        num_prompt_tokens = 0
        for idx, final_res in enumerate(final_res_batch):
            classify_res = ScoringRequestOutput.from_base(final_res)

            document = documents[idx]
            if isinstance(document, str):
                rerank_document = RerankDocument(text=document)
            else:
                rerank_document = RerankDocument(
                    multi_modal=document.get("content", [])
                )

            result = RerankResult(
                index=idx,
                document=rerank_document,
                relevance_score=classify_res.outputs.score,
            )
            results.append(result)
            prompt_token_ids = final_res.prompt_token_ids
            num_prompt_tokens += len(prompt_token_ids)

        # sort by relevance, then return the top n if set
        results.sort(key=lambda x: x.relevance_score, reverse=True)
        if top_n < len(documents):
            results = results[:top_n]

        response = RerankResponse(
            id=request_id,
            model=model_name,
            results=results,
            usage=RerankUsage(
                total_tokens=num_prompt_tokens, prompt_tokens=num_prompt_tokens
            ),
        )

        return JSONResponse(content=response.model_dump())

    ###################################################################################
    ### Run pooling score MaxSim on worker side (GPU) in the API server process
    ### Can significantly improve late-interaction scoring performance.

    async def flash_late_interaction(self, *args, **kwargs) -> Response:
        ctx = await self._init_ctx(self.io_processor, *args, **kwargs)
        await self._preprocessing_async(self.io_processor, ctx)

        # stage 1: encode queries and cache token embeddings on workers.
        await self._flash_late_interaction_encode_queries(ctx)
        # stage 2: encode docs and return scalar scores from workers.
        await self._flash_late_interaction_encode_docs(ctx)

        return await self._postprocessing_async(self.io_processor, ctx)

    async def _flash_late_interaction_encode_queries(self, ctx: ScoringServeContext):
        assert ctx.n_queries is not None
        assert ctx.engine_inputs is not None
        assert isinstance(ctx.pooling_params, PoolingParams)

        n_queries = ctx.n_queries
        n_docs = len(ctx.engine_inputs) - n_queries
        query_engine_inputs = ctx.engine_inputs[:n_queries]

        query_keys = [f"{ctx.request_id}-query-{i}" for i in range(n_queries)]
        query_uses = [n_docs if n_queries == 1 else 1] * n_queries

        query_pooling_params_list = []
        for i in range(n_queries):
            pooling_params = ctx.pooling_params.clone()
            pooling_params.late_interaction_params = (
                build_late_interaction_query_params(
                    query_key=query_keys[i],
                    query_uses=query_uses[i],
                )
            )
            query_pooling_params_list.append(pooling_params)

        assert (
            n_queries
            == len(query_pooling_params_list)
            == len(query_engine_inputs)
            == len(query_keys)
        )

        query_ctx = ScoringServeContext(
            request=ctx.request,
            raw_request=ctx.raw_request,
            model_name=ctx.model_name,
            request_id=ctx.request_id,
            pooling_params=query_pooling_params_list,
            prompt_request_ids=query_keys,
            engine_inputs=query_engine_inputs,
        )

        await self._prepare_generators(query_ctx)
        await self._collect_batch(query_ctx)
        ctx.query_final_res_batch = query_ctx.final_res_batch

    async def _flash_late_interaction_encode_docs(self, ctx: ScoringServeContext):
        assert ctx.n_queries is not None
        assert ctx.engine_inputs is not None
        assert isinstance(ctx.pooling_params, PoolingParams)

        n_queries = ctx.n_queries
        n_docs = len(ctx.engine_inputs) - n_queries
        doc_engine_inputs = ctx.engine_inputs[n_queries:]

        query_keys = [f"{ctx.request_id}-query-{i}" for i in range(n_queries)]
        doc_keys = [f"{ctx.request_id}-doc-{i}" for i in range(n_docs)]

        doc_pooling_params_list = []
        for i in range(n_docs):
            query_idx = 0 if n_queries == 1 else i
            pooling_params = ctx.pooling_params.clone()
            pooling_params.late_interaction_params = build_late_interaction_doc_params(
                query_key=query_keys[query_idx]
            )
            doc_pooling_params_list.append(pooling_params)

        assert (
            n_docs
            == len(doc_pooling_params_list)
            == len(doc_engine_inputs)
            == len(doc_keys)
        )

        doc_ctx = ScoringServeContext(
            request=ctx.request,
            raw_request=ctx.raw_request,
            model_name=ctx.model_name,
            request_id=ctx.request_id,
            pooling_params=doc_pooling_params_list,
            prompt_request_ids=doc_keys,
            engine_inputs=doc_engine_inputs,
        )

        await self._prepare_generators(doc_ctx)
        await self._collect_batch(doc_ctx)

        ctx.final_res_batch = doc_ctx.final_res_batch
