#!/usr/bin/env python3

"""
Module: deepnox.clients.http_client

This file is a part of (deepnox.clients) project.

(c) 2021, Deepnox SAS.
"""

import asyncio
import logging
import time
import urllib
from types import FunctionType

from deepnox import loggers
from deepnox.network.http import HttpRequest, HttpResponse, HttpHit, HttpMethod, HttpRequestPayload
from deepnox.third import aiohttp

LOGGER = loggers.factory(__name__)
""" The module LOGGER. """


class HttpClient(object):
    """
    The HTTP client.

    """

    LOG = LOGGER.getChild("HttpClient")
    """ The LOGGER. """

    def __init__(self,
                 loop: asyncio.AbstractEventLoop = None,
                 auditor_logger=None,
                 timeout: int = 30,
                 verify_ssl: bool = False
                 ):
        self.loop = loop or asyncio.get_event_loop()
        self.AUDITOR = auditor_logger or loggers.auditor(f'auditor')

    @property
    def get_session(self):
        def _wrap():
            cookie_jar = aiohttp.CookieJar()
            timeout = aiohttp.ClientTimeout(total=None, sock_connect=30, sock_read=30)
            connector = aiohttp.TCPConnector(ssl=False)
            return aiohttp.ClientSession(cookie_jar=cookie_jar, timeout=timeout, connector=connector)
        return _wrap

    async def _parse_response(self, req: HttpRequest, resp: HttpResponse):
        """
        Wait the HTTP response.

        :param req: The HTTP request.
        :param resp: The HTTP response.
        :return:
        """
        logging.info(f"resp {resp}")

        text = await resp.text()
        status_code = resp.status
        end_at = time.time()
        response = HttpResponse(status_code=status_code,
                                headers=resp.headers,
                                text=text,
                                end_at=end_at,
                                elapsed_time=end_at - req.start_at)
        return response

    def _build_request_args(self, req: HttpRequest):
        data = {"url": str(req.url)}
        if isinstance(req.headers, dict):
            data.update({"headers": req.headers})
        if isinstance(req.payload, HttpRequestPayload):
            if isinstance(req.payload.params, dict):
                data.update({"params": req.payload.params})
            if isinstance(req.payload.data, str):
                data.update({"data": req.payload.data})
        return data

    async def request(self, req: HttpRequest):
        """
        Send a HTTP request.
        :param req: The request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        :return:
        """
        LOGGER.debug(f'get(req:{req})', extra={"request": req.dict()})
        method = str(getattr(req, "method"))
        req.start_at = time.time()
        async with self.get_session() as session:
            try:
                self.LOG.debug(f"req.url.to_python() {req.url}")
                session_method_fn: FunctionType = getattr(session, method)
                async with session_method_fn(**self._build_request_args(req)) as resp:
                    res = await self._parse_response(req, resp)
                    self._trace_audit(req, res)
                    return res
            except Exception as e:
                self.LOG.error(f'Response error: (req:{req.url})', exc_info=e)

    async def get(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: GET.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.GET
        return self.request(req)

    async def post(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: POST.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.POST
        return self.request(req)

    async def put(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: PUT.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.PUT
        return self.request(req)

    async def patch(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: PATCH.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.POST
        return self.request(req)

    async def options(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: OPTIONS.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.OPTIONS
        return self.request(req)

    async def head(self, req: HttpRequest):
        """
        Send a HTTP request using the verb: HEAD.
        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        """
        req.method = HttpMethod.HEAD
        return self.request(req)

    def _trace_audit(self, req: HttpRequest, res: HttpResponse):
        """
        Add trace.

        :param req: The HTTP request.
        :type req: :class:`deepnox.network.http.HttpRequest`
        :param res: The HTTP response.
        :type res: :class:`deepnox.network.http.HttpResponse`
        :return:
        """
        self.LOG.debug("_trace_audit(req, res)", extra={"req": req.dict(), "res": res.dict()})
        http_hit = HttpHit(start_at=req.start_at, end_at=res.end_at,
                           url=req.url, method=req.method,
                           request=req, response=res)
        if 200 < res.status_code >= 400:
            self.LOG.error(f"Failed: ({str(req.method)} at url:{req.url})", extra=http_hit.dict())
            print(http_hit.json())
        else:
            self.LOG.info(f"Success: ({str(req.method)} at url:{req.url})", extra=http_hit.dict())

    # def __del__(self):
    #     try:
    #         asyncio.create_task(self._close_session())
    #     except RuntimeError:
    #         self.loop.run_until_complete(self._close_session())
    #
    # async def _close_session(self):
    #     if not self._session.closed:
    #         self._session.close()

