# Copyright (c) ModelScope Contributors. All rights reserved.
import aiohttp
import json
from copy import deepcopy
from dacite import from_dict
from dataclasses import asdict
from requests.exceptions import HTTPError
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union

from swift.metrics import Metric
from .infer_engine import InferEngine
from .protocol import (ChatCompletionRequest, ChatCompletionResponse, ChatCompletionStreamResponse, InferRequest,
                       ModelList, RequestConfig)


class InferClient(InferEngine):

    def __init__(self,
                 host: str = '127.0.0.1',
                 port: int = 8000,
                 api_key: str = 'EMPTY',
                 *,
                 base_url: Optional[str] = None,
                 timeout: Optional[int] = 86400) -> None:
        """
        Initialize the InferClient.

        Args:
            host (str): The hostname of the inference server. Defaults to '127.0.0.1'.
            port (str): The port of the inference server. Defaults to '8000'.
            api_key (str): The API key for authentication. Defaults to 'EMPTY'.
            timeout (Optional[int]): The timeout for requests in seconds. Defaults to None.
        """
        self.api_key = api_key
        self.host = host
        self.port = port
        self.timeout = timeout
        if base_url is None:
            base_url = f'http://{self.host}:{self.port}/v1'
        self.base_url = base_url
        self._models = None

    @property
    def models(self):
        if self._models is None:
            models = []
            for model in self.get_model_list().data:
                models.append(model.id)
            assert len(models) > 0, f'models: {models}'
            self._models = models
        return self._models

    def get_model_list(self) -> ModelList:
        """Get model list from the inference server.
        """
        coro = self.get_model_list_async()
        return self.safe_asyncio_run(coro)

    def _get_request_kwargs(self) -> Dict[str, Any]:
        request_kwargs = {}
        if isinstance(self.timeout, (int, float)) and self.timeout > 0:
            request_kwargs['timeout'] = self.timeout
        if self.api_key is not None:
            request_kwargs['headers'] = {'Authorization': f'Bearer {self.api_key}'}
        return request_kwargs

    async def get_model_list_async(self) -> ModelList:
        url = f"{self.base_url.rstrip('/')}/models"
        async with aiohttp.ClientSession() as session:
            async with session.get(url, **self._get_request_kwargs()) as resp:
                resp_obj = await resp.json()
        return from_dict(ModelList, resp_obj)

    def infer(
            self,
            infer_requests: List[InferRequest],
            request_config: Optional[RequestConfig] = None,
            metrics: Optional[List[Metric]] = None,
            *,
            model: Optional[str] = None,
            use_tqdm: Optional[bool] = None
    ) -> List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
        """
        Perform inference using the specified model.

        Args:
            infer_requests (List[InferRequest]): A list of inference requests.
            request_config (Optional[RequestConfig]): Configuration for the request. Defaults to None.
            metrics (Optional[List[Metric]]): The usage information to return. Defaults to None.
            model (Optional[str]): The model name to be used for inference. Defaults to None.
            use_tqdm (Optional[bool]): Whether to use tqdm for progress tracking. Defaults to None.

        Returns:
            List[Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]]:
            The inference responses or an iterator of streaming responses.
        """
        return super().infer(infer_requests, request_config, metrics, model=model, use_tqdm=use_tqdm)

    @staticmethod
    def _prepare_request_data(model: str, infer_request: InferRequest, request_config: RequestConfig) -> Dict[str, Any]:
        if not isinstance(infer_request, dict):
            infer_request = asdict(infer_request)
        res = asdict(ChatCompletionRequest(model, **infer_request, **asdict(request_config)))
        # ignore empty
        empty_request = ChatCompletionRequest('', [])
        for k in list(res.keys()):
            if res[k] == getattr(empty_request, k):
                res.pop(k)
        return res

    @staticmethod
    def _parse_stream_data(data: bytes) -> Optional[str]:
        data = data.decode(encoding='utf-8')
        data = data.strip()
        if len(data) == 0:
            return
        assert data.startswith('data:'), f'data: {data}'
        return data[5:].strip()

    async def infer_async(
        self,
        infer_request: InferRequest,
        request_config: Optional[RequestConfig] = None,
        *,
        model: Optional[str] = None,
    ) -> Union[ChatCompletionResponse, AsyncIterator[ChatCompletionStreamResponse]]:
        request_config = deepcopy(request_config or RequestConfig())
        if model is None:
            if len(self.models) == 1:
                model = self.models[0]
            else:
                raise ValueError(f'Please explicitly specify the model. Available models: {self.models}.')
        url = f"{self.base_url.rstrip('/')}/chat/completions"

        request_data = self._prepare_request_data(model, infer_request, request_config)
        if request_config.stream:

            async def _gen_stream() -> AsyncIterator[ChatCompletionStreamResponse]:
                async with aiohttp.ClientSession() as session:
                    async with session.post(url, json=request_data, **self._get_request_kwargs()) as resp:
                        async for data in resp.content:
                            data = self._parse_stream_data(data)
                            if data == '[DONE]':
                                break
                            if data is not None:
                                resp_obj = json.loads(data)
                                if resp_obj['object'] == 'error':
                                    raise HTTPError(resp_obj['message'])
                                yield from_dict(ChatCompletionStreamResponse, resp_obj)

            return _gen_stream()
        else:
            async with aiohttp.ClientSession() as session:
                async with session.post(url, json=request_data, **self._get_request_kwargs()) as resp:
                    resp_obj = await resp.json()
                    if resp_obj['object'] == 'error':
                        raise HTTPError(resp_obj['message'])
                    return from_dict(ChatCompletionResponse, resp_obj)
