#!/usr/bin/env python3

import json
import logging
import os
import re
from enum import Enum
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from urllib.error import HTTPError, URLError
from urllib.parse import parse_qsl, urlparse
from urllib.request import Request, urlopen

import gen_universe

# Binds to all available interfaces
HOST_NAME = ''
# Gets the port number from $PORT0 environment variable
PORT_NUMBER = int(os.environ['PORT_UNIVERSECONVERTER'])
MAX_REPO_SIZE = int(os.environ.get('MAX_REPO_SIZE', '20'))

# Constants
MAX_TIMEOUT = 60
MAX_BYTES = MAX_REPO_SIZE * 1024 * 1024

header_user_agent = 'User-Agent'
header_accept = 'Accept'
header_content_type = 'Content-Type'
header_content_length = 'Content-Length'
param_charset = 'charset'
default_charset = 'utf-8'

json_key_packages = 'packages'
param_url = 'url'
transform_url_path = '/transform'

logger = logging.getLogger(__name__)
logging.basicConfig(
    level=os.environ.get('LOGLEVEL', 'DEBUG'),
    format="[%(asctime)s|%(threadName)s-%(funcName)s(%(lineno)d)|%(levelname)s]: %(message)s",
)


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle each request in a separate thread"""


class Handler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        """Override the default behavior of writing to stderr with `logging`"""
        logger.info("[%s] %s", self.address_string(), format % args)

    def do_GET(self):
        logger.debug('\n{}\n{}'.format(self.requestline, self.headers).rstrip())
        url_path = urlparse(self.path).path
        try:
            if url_path == transform_url_path:
                self.handle_transform()
            else:
                raise ValueError(ErrorResponse.INVALID_PATH.to_msg(url_path))
        except Exception as e:
            self.send_error(
                HTTPStatus.BAD_REQUEST,
                explain=e.message if hasattr(e, 'message') else str(e)
            )

    def handle_transform(self):
        """
        Respond to the GET request. The expected format of this request is:
                http://<host>:<port>/transform?url=<url> with `User-Agent`
                and `Accept` headers
        """
        errors = _validate_request(self)
        if errors:
            self.send_error(HTTPStatus.BAD_REQUEST, explain=errors)
            return

        query = dict(parse_qsl(urlparse(self.path).query))
        if param_url not in query:
            self.send_error(
                HTTPStatus.BAD_REQUEST,
                explain=ErrorResponse.PARAM_NOT_PRESENT.to_msg(param_url)
            )
            return

        user_agent = self.headers.get(header_user_agent)
        accept = self.headers.get(header_accept)
        decoded_url = query.get(param_url)

        try:
            content_type, json_response = handle(decoded_url, user_agent, accept)
        except ValueError as e:
            self.send_error(HTTPStatus.BAD_REQUEST, explain=str(e))
            return
        except HTTPError as e:
            logger.info(
                'Upstream error :\nURL: [%s]\nReason: [%s %s]\nBody:\n[%s]',
                decoded_url,
                e.code,
                e.reason,
                e.read(),
                exc_info=True
            )
            self.send_error(HTTPStatus.BAD_GATEWAY, explain=str(e))
            return
        except URLError as e:
            logger.info(
                'Route error :\nURL: [%s]\nReason: [%s]',
                decoded_url,
                e.reason,
                exc_info=True
            )
            self.send_error(HTTPStatus.BAD_GATEWAY, explain=str(e))
            return
        except Exception as e:
            logger.exception(
                'Unhandled exception for [{}] UA [{}] Accept [{}]'.format(
                    query,
                    user_agent,
                    accept
                ),
                e
            )
            raise e

        self.send_response(HTTPStatus.OK)
        self.send_header(header_content_type, content_type)
        self.send_header(header_content_length, len(json_response))
        self.end_headers()
        self.wfile.write(json_response.encode())


def run_server():
    """Runs a builtin python server using the given server_class.

    :return: None
    """
    server_address = (HOST_NAME, PORT_NUMBER)
    httpd = ThreadedHTTPServer(server_address, Handler)
    logger.warning('Server Starts on port - %s', PORT_NUMBER)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        httpd.server_close()
        logger.warning('Server Stops on port - %s', PORT_NUMBER)


def handle(decoded_url, user_agent, accept) -> (str, str):
    """Returns the requested json data. May raise an error instead, if it fails.

    :param decoded_url: The url to be fetched from
    :type decoded_url: str
    :param user_agent: User-Agent header value
    :type user_agent: str
    :param accept: Accept header value
    :return Requested json data
    :rtype str (a valid json object)
    """
    req = Request(decoded_url)
    req.add_header(header_user_agent, user_agent)
    req.add_header(header_accept, accept)
    logger.debug('\n{}\n{}\n{}'.format(
        '<--- Upstream Request --->',
        req.full_url,
        _format_dict(req.headers)
    ))
    with urlopen(req, timeout=MAX_TIMEOUT) as res:
        charset = res.info().get_param(param_charset) or default_charset

        if header_content_length not in res.headers:
            raise ValueError(ErrorResponse.ENDPOINT_HEADER_MISS.to_msg())

        if int(res.headers.get(header_content_length)) > MAX_BYTES:
            raise ValueError(ErrorResponse.MAX_SIZE.to_msg())
        resp_content = res.read().decode(charset)
        logger.debug('\n{}\n{} {}\n{}\n{}'.format(
            '<--- Upstream Response --->',
            res.getcode(),
            res.reason,
            _format_dict(res.headers),
            resp_content if res.getcode() // 200 != 1 else ''
        ))
        content_type, repo_version = _get_repo_version(accept)
        dcos_version = _get_dcos_version(user_agent)
        logger.debug('Version [%s] DC/OS [%s]', repo_version, dcos_version)
        try:
            json_body = json.loads(resp_content)
        except ValueError as e:
            logger.exception(e)
            raise ValueError(ErrorResponse.INVALID_JSON_FROM_UPSTREAM.to_msg(decoded_url))
        assert json_key_packages in json_body, 'Expected key [{}] is not present in response'.format(json_key_packages)
        return content_type, render_json(
            json_body[json_key_packages],