import asyncio
import logging
import requests
import argparse

from fastapi import FastAPI
from starlette.responses import StreamingResponse

import ray
from ray import serve

logger = logging.getLogger("ray.serve")

fastapi_app = FastAPI()


# Input: a prompt of words
# Output: each word reversed and produced N times.
@serve.deployment(
    num_replicas=6, ray_actor_options={"num_cpus": 0.01, "memory": 10 * 1024 * 1024}
)
class ReverseAndDupEachWord:
    def __init__(self, dup_times: int):
        self.dup_times = dup_times

    async def __call__(self, prompt: str):
        for word in prompt.split():
            rev = word[::-1]
            for _ in range(self.dup_times):
                await asyncio.sleep(0.001)
                yield rev


@serve.deployment(
    num_replicas=6, ray_actor_options={"num_cpus": 0.01, "memory": 10 * 1024 * 1024}
)
@serve.ingress(fastapi_app)
class Textbot:
    def __init__(self, llm):
        self.llm = llm.options(stream=True)

    @fastapi_app.post("/")
    async def handle_request(self, prompt: str) -> StreamingResponse:
        logger.info(f'Got prompt with size "{len(prompt)}"')
        remote_async_gen = await self.llm.remote(prompt)
        return StreamingResponse(
            self.local_async_gen(remote_async_gen), media_type="text/plain"
        )

    async def local_async_gen(self, iterable):
        async for i in iterable:
            yield await i
            # Ideally we want to do " ".join(words), but for the sake of simplicity we
            # also have an extra trailing space.
            yield " "


@ray.remote(num_cpus=0.1, memory=10 * 1024 * 1024)
def make_http_query(num_words, num_queries):
    for _ in range(num_queries):
        words = "Lorem ipsum dolor sit amet".split()
        prompt_words = [words[i % len(words)] for i in range(num_words)]
        prompt = " ".join(prompt_words)
        expected_words = [word[::-1] for word in prompt_words for _ in range(2)]

        response = requests.post(f"http://localhost:8000/?prompt={prompt}", stream=True)
        response.raise_for_status()
        content = response.content.decode()
        assert content == " ".join(expected_words) + " ", content


def main():
    parser = argparse.ArgumentParser(description="Generates HTTP workloads with Ray.")

    parser.add_argument("--num_tasks", type=int, required=True, help="Number of tasks.")
    parser.add_argument(
        "--num_queries_per_task",
        type=int,
        required=True,
        help="Number of queries per task.",
    )
    parser.add_argument(
        "--num_words_per_query",
        type=int,
        required=True,
        help="Number of words per query",
    )

    args = parser.parse_args()

    # Run the serve, run the client, then showdown serve.
    llm = ReverseAndDupEachWord.bind(2)
    app = Textbot.bind(llm)

    serve.run(app)

    objs = [
        make_http_query.remote(args.num_words_per_query, args.num_queries_per_task)
        for _ in range(args.num_tasks)
    ]
    ray.get(objs)

    serve.shutdown()


main()
