from typing import Generator, Optional, List

import requests
import json
from ratelimit import limits, sleep_and_retry
from multidict import MultiDict

from ..settings import config_app, logger
from ..utils import parse_cgi_params
from ..utils.exceptions import (
    InvalidResponseException,
    AnswerNotCompletedException,
    ErrorResponseException
)


class Search:
    def __init__(self,
                 upper_search_url: str,
                 int_search_url: str,
                 upper_cgi_params: str = '',
                 int_cgi_params: str = '',
                 base_cgi_params: str = ''):
        self.__upper_search_url = upper_search_url
        self.__int_search_url = int_search_url
        self.__upper_cgi_params: MultiDict = parse_cgi_params(upper_cgi_params)
        self.__int_cgi_params: MultiDict = parse_cgi_params(int_cgi_params)
        self.__base_cgi_params: MultiDict = parse_cgi_params(base_cgi_params)

    @sleep_and_retry
    @limits(calls=config_app.getint('search', 'max_upper_rps'), period=1)
    def request_to_upper(self,
                         query: str,
                         noreask: Optional[bool] = None,
                         params: Optional[MultiDict] = None) -> List[dict]:
        if params is None:
            params = self.__upper_cgi_params.copy()
        if noreask is not None:
            params.add('noreask', str(int(noreask)))
        params.add('text', query)

        response = requests.get(self.__upper_search_url, params=params)
        if response.status_code == 200:
            try:
                data = json.loads(response.text)
                return data['searchdata.docs']
            except json.JSONDecodeError:
                logger.error(f'JSON Decode Error for request "{response.url}"')
                raise InvalidResponseException
        else:
            logger.error(f'Request "{response.url}" returned an error code {response.status_code}')
            raise InvalidResponseException

    @sleep_and_retry
    @limits(calls=config_app.getint('search', 'max_int_rps'), period=1)
    def request_to_int(self,
                       query: str,
                       params: Optional[MultiDict] = None) -> Optional[dict]:
        if params is None:
            params = self.__int_cgi_params.copy()
        params.add('text', query)

        response = requests.get(self.__int_search_url, params=params)
        if response.status_code == 200:
            try:
                data = json.loads(response.text)
                if 'Grouping' not in data:
                    if not data['DebugInfo']['AnswerIsComplete']:
                        logger.error(f'Answer is not completed for request "{response.url}"')
                        raise AnswerNotCompletedException
                    if data['ErrorInfo']['GotError'] == 1:
                        logger.error(f'Error response for request "{response.url}"')
                        raise ErrorResponseException
                return data
            except json.JSONDecodeError:
                logger.error(f'JSON Decode Error for request "{response.url}"')
                raise InvalidResponseException
        else:
            logger.error(f'Request "{response.url}" returned an error code {response.status_code}')
            raise InvalidResponseException

    @sleep_and_retry
    @limits(calls=config_app.getint('search', 'max_base_rps'), period=1)
    def request_to_base(self,
                        searcher_hostname: str,
                        doc_id: str,
                        params: Optional[MultiDict] = None) -> List[dict]:
        base_url = f'http://{searcher_hostname}'

        if params is None:
            params = self.__base_cgi_params.copy()
        params.add('info', f'docinfo:docid:{doc_id}')

        response = requests.get(base_url, params=params)
        if response.status_code == 200:
            return json.loads(response.text)
        else:
            logger.error(f'Base search "{searcher_hostname}" for document "{doc_id}" returned an error code {response.status_code}')

    def get_documents_from_int(self,
                               query: str,
                               params: Optional[MultiDict] = None) -> Generator[dict, None, None]:
        int_data = self.request_to_int(query, params)
        if 'Grouping' in int_data:
            for grouping in int_data['Grouping']:
                for group in grouping['Group']:
                    for document in group['Document']:
                        yield document
