import asyncio
import aiohttp

import re
import urllib
import json
from urllib.error import HTTPError
from json.decoder import JSONDecodeError
from .cache.cache import EsiCache
from .auth import EsiAuth
from urllib.parse import urlencode
import logging

logger = logging.getLogger("EsiPysi")

class EsiOp(object):
    """ A class to handle operations of the ESI api """
    def __init__(self, operation, base_url, **kwargs):
        """
        Initialize the class - this should only be done through an EsiPysi class object

        Arguments:
            session -- A ClientSession from aiohttp used to make ESI calls
            operation -- A dict with the modified swagger structure that defines the operation
            base_url -- url to the base endpoint
        Keyword arguments:
            user_agent -- User agent to use when interacting with ESI
            cache -- an EsiCache object to use for caching queries
            auth -- an EsiAuth object to use for authorizing the ESI call
            loop -- A asyncio event loop to use for async calls
        """
        self.__operation = operation
        self.__path = operation.get("path")
        self.__verb = operation.get("verb")
        self.__parameters = operation.get("parameters")
        self.__operation_id = operation.get("operationId")
        self.__base_url = base_url
        self.__cached_seconds = operation.get("x-cached-seconds", -1)
        self.user_agent = kwargs.get("user_agent")
        self.cache = kwargs.get("cache")
        auth_args = kwargs.get("auth")
        self.__auth = None
        self.set_auth(auth_args)

        self.__loop = kwargs.get("loop")
        self.__use_loop = True
        if self.__loop is None:
            self.__use_loop = False
        elif not isinstance(self.__loop, asyncio.BaseEventLoop):
            raise TypeError("loop must be a asyncio event loop")

        self.use_cache = False
        if self.cache is not None:
            if not issubclass(EsiCache, type(self.cache)):
                ValueError("cache should be of the type EsiCache")
            self.use_cache = True

    def set_auth(self, esiauth):
        """
        Set the authorization for this operation

        Arguments:
            esiauth -- An EsiAuth object which contains the authorization info
        """
        if not issubclass(EsiAuth, type(esiauth)):
            ValueError("esiauth should be of the type EsiAuth")
        
        self.auth = esiauth

    async def json(self, **kwargs):
        """
        Call the ESI API and retrieve the json data and decode as a dict

        Keyword arguments:
            Arguments from the ESI call
        """
        return await self.__call_esi_async(False, **kwargs)

    async def raw(self, **kwargs):
        """
        Call the ESI API and retrieve the raw result

        Keyword arguments:
            Arguments from the ESI call
        """
        return await self.__call_esi_async(True, **kwargs)

    async def __call_esi_async(self, raw, **kwargs):
        if self.__loop is None:
            async with aiohttp.ClientSession() as session:
                return await self.__execute(session, raw, **kwargs)
        else:
            async with aiohttp.ClientSession(loop = self.__loop) as session:
                return await self.__execute(session, raw, **kwargs)
        

    async def __execute(self, session, raw, **kwargs):

        cache_value = self.__cache_read(raw, **kwargs)
        if cache_value is not None:
            return cache_value

        url = self.__base_url + self.__path
        #Handle parameters
        for name, param in self.__parameters.items():
            required = param.get("required", False)
            if required:
                if name not in kwargs:
                    raise ValueError("'{}' is a required parameter".format(name))

        body = None
        query_parameters = {}
        headers = {}

        for key, value in kwargs.items():
            if key not in self.__parameters:
                raise ValueError("'{}' is not a valid parameter".format(key))
            param_info = self.__parameters.get(key)
            #Handle path parameters
            param_type = param_info.get("in")
            if param_type == "path":
                escaped_value = urllib.parse.quote(str(value))
                url = url.replace("{" + param_info.get("name") + "}", escaped_value)
            elif param_type == "query":
                query_parameters[key] = value
            elif param_type == "header":
                headers[key] = value
            elif param_type == "body":
                body = json.dumps(value)

        if self.user_agent is not None:
            headers['User-Agent'] = self.user_agent
        if self.auth is not None:
            auth_code = await self.auth.authorize()
            headers["Authorization"] = "Bearer {}".format(auth_code)
        #Call operation
        logger.info("Calling '{}' with data '{}' using HTTP {}".format(url, body, self.__verb.upper()))

        if self.__verb.lower() == "get":
            async with session.get(url, params=query_parameters, headers=headers) as resp:
                return await self.__process_response(resp, raw, **kwargs)
        else:
            #After this I don't know WTF CCP was thinking
            if query_parameters:
                query_str = "?" + urlencode(query_parameters)
            else:
                query_str = ""
            full_url = url + query_str
            if self.__verb.lower() == "post":
                async with session.post(full_url, data=body, headers=headers) as resp:
                    return await self.__process_response(resp, raw, **kwargs)
            elif self.__verb.lower() == "put":
                async with session.put(full_url, data=body, headers=headers) as resp:
                    return await self.__process_response(resp, raw, **kwargs)
            elif self.__verb.lower() == "delete":
                async with session.delete(full_url, data=body, headers=headers) as resp:
                    return await self.__process_response(resp, raw, **kwargs)
        
    async def __process_response(self, resp, raw, **kwargs):
        text = await resp.text()
        if resp.status >= 400:
            exception = HTTPError(resp.url, resp.status, text, resp.headers, None)
            logger.exception("ESI HTTP error occured: {}".format(exception))
            raise exception

        if self.use_cache:
            self.cache.store(self.__operation_id, kwargs, text, self.__cached_seconds)

        if raw:
            return text

        try:
            return await resp.json()
        except Exception:
            logger.exception("Could not parse JSON")
            return None

    def __cache_read(self, raw, **kwargs):
        if not self.use_cache:
            return None
        
        value = self.cache.retrieve(self.__operation_id, kwargs)
        result = None
        if value is not None:
            if raw:
                result = value
            else:
                try:
                    result = json.loads(value)
                except Exception:
                    logger.exception("Could not parse JSON")
            if result is not None:
                logger.info("Result found in cache for {} : {}".format(self.__operation_id, kwargs))
        return result


    def __str__(self):
        return self.__operation_id
        