import asyncio
import aiohttp
import re
import urllib
import json
from enum import Enum 
from urllib.parse import urlencode
from urllib.error import HTTPError
from json.decoder import JSONDecodeError

from .cache.cache import EsiCache
from .auth import EsiAuth

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(ResponseType.Json, **kwargs)

    async def text(self, **kwargs):
        """
        Call the ESI API and retrieve the result as plain text (string)

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

    async def raw(self, **kwargs):
        """
        Call the ESI API and retrieve the result as plain text (string)
        This is a legacy call to not break applications that used EsiPysi pre 0.6, use "text" instead

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

    async def response(self, **kwargs):
        """
        Call the ESI API and retrieve the result as an AioHttp ClientResponse object

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

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

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

        cache_value = await self.__cache_read(resp_type, **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, resp_type, **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, resp_type, **kwargs)
            elif self.__verb.lower() == "put":
                async with session.put(full_url, data=body, headers=headers) as resp:
                    return await self.__process_response(resp, resp_type, **kwargs)
            elif self.__verb.lower() == "delete":
                async with session.delete(full_url, data=body, headers=headers) as resp:
                    return await self.__process_response(resp, resp_type, **kwargs)
        
    async def __process_response(self, resp, resp_type, **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, resp, self.__cached_seconds)

        if resp_type == ResponseType.AiohttpResponse:
            return resp

        if resp_type == ResponseType.Text:
            return text

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

    async def __cache_read(self, resp_type, **kwargs):
        if not self.use_cache:
            return None
        
        value = self.cache.retrieve(self.__operation_id, kwargs) #TODO: Make this support async too
        result = None
        if value is not None:
            if resp_type == ResponseType.AiohttpResponse:
                result = value
            elif resp_type == ResponseType.Text:
                result = await value.text()
            elif resp_type == ResponseType.Json:
                try:
                    result = await value.json()
                except Exception:
                    logger.exception("Could not parse JSON")
            else:
                logger.warn("Invalid response type: {}".format(resp_type))
            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

class ResponseType(Enum):
    Json = 1
    Text = 2
    AiohttpResponse = 3
        