import requests
import logging
import urllib.parse as urllib_parse

from datetime import datetime
from psi_elt.abstract_elt_job import AbstractELTJob, EXTRACTOR_ELT_JOB_TYPE
from requests.auth import HTTPBasicAuth
from typeguard import typechecked
from typing import Any, Dict, List


@typechecked
class CustomExtractor(AbstractELTJob):
    '''A CustomExtractor is the first of the ELT jobs that is responsible for
    extracting data from an external API and returning the raw data without any
    transformation.

    This class should be treated as abstract and should be extended.
    Child classes must override the _extract function.
    This class includes utilties for running HTTP requests.
    '''

    def __init__(
        self,
        name: str,
        capture_time: datetime,
        aws_session: Any = None,
        logger: logging.Logger = None,
        api: Any = None,
        http_session: Any = None
    ):
        super().__init__(
            name=name,
            elt_job_type=EXTRACTOR_ELT_JOB_TYPE,
            capture_time=capture_time,
            aws_session=aws_session,
            logger=logger
        )
        self.api = api
        self.__http_session = http_session or requests.Session()

    @property
    def http_session(self) -> Any:
        return self.__http_session

    def extract(self, is_test: bool, extract_kwargs: dict = None) -> Any:
        self.logger.info(f'Beginning metrics extraction for {self.name}')
        extract_kwargs = extract_kwargs or {}
        extract_kwargs = {key: val for key, val in extract_kwargs.items() if val}
        data = self._extract(is_test, **extract_kwargs)
        return data

    def _extract(self, is_test: bool, **extract_kwargs) -> Any:
        raise Exception('CustomExtractor._extract must be overridden by the child class')

    def run_http_request(
        self,
        url: str,
        method: str = 'GET',
        path_params: List[str] = None,
        query_params: Dict[str, Any] = None,
        headers: Dict[str, str] = None,
        body: Any = None,
        json_body: dict = None,
        basic_auth_username: str = None,
        basic_auth_password: str = None,
        bearer_token: str = None,
        url_encode_query_params: bool = False
    ):
        assert url, 'A URL is required to run a HTTP request'
        assert method in ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']
        assert not (body and json_body), f'Must specify either body or json_body, not both'
        http_resp = None
        try:
            # Format path parameters
            if path_params:
                path_params_str = '/'.join(path_params)
                url += f'/{path_params_str}'

            # Format query parameters
            if query_params:
                query_params = self.__url_encode_query_params(query_params, url_encode_query_params)
                query_params_str = '&'.join([
                    f'{k}={v}'
                    for k, v in query_params.items()
                ])
                url += f'?{query_params_str}'

            # Format basic auth
            basic_auth = None
            if basic_auth_username and basic_auth_password:
                basic_auth = HTTPBasicAuth(basic_auth_username, basic_auth_password)

            # Format bearer token
            if bearer_token:
                headers = headers or {}
                headers['Authorization'] = f'Bearer {bearer_token}'

            self.http_session: requests.Session = self.http_session
            http_resp = self.http_session.request(
                method=method,
                url=url,
                headers=headers or None,
                data=body or None,
                json=json_body or None,
                auth=basic_auth or None
            )
            http_resp.raise_for_status()
        except Exception as e:
            self.logger.error(f'Exception occurred when running HTTP request: {e}')
            if http_resp:
                self.logger.error(f'Status code: {http_resp.status_code}')
                self.logger.error(f'Response text: {http_resp.text}')
            raise e

    def __url_encode_query_params(self, query_params: dict, url_encode: bool = True) -> dict:
        if url_encode:
            return {
                urllib_parse.quote(k): urllib_parse.quote(str(v))
                for k, v in query_params.items()
            }
        return query_params
