#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import copy
from datetime import datetime as dt
import http.server
import inspect
import json
import os
import ssl
import threading
import time
from urllib.parse import urlparse

from oslo_log import log as logging

LOG = logging.getLogger(__name__)


def PrepareRequestHandler(manager):
    class DummyRequestHandler(http.server.CGIHTTPRequestHandler):
        """HTTP request handler for dummy server."""

        def __init__(self, request, client_address, server):
            super().__init__(request, client_address, server)
            return

        def _is_match_with_list(self):
            """Return given path is listed in dictionary or not.

            Return:
                True/False
            """
            func_uri_list = manager._methods[self.command]
            for objChkUrl in func_uri_list:
                # Check which requested path is in our list.
                LOG.debug('path for check:%s' % objChkUrl)
                if (self.path.startswith(objChkUrl)):
                    return True
            return False

        def _returned_callback(self, path, mock_info):
            """Send responses to client. Called in do_* methods.

            This method do not handle message when error is occured.

            Args:
                path (str): URI path
                mock_info (tuple): callback informations from caller.
            """
            request_headers = dict(self.headers._headers)
            request_body = self._parse_request_body()
            response_body_str = b''

            (status_code, mock_headers, mock_body) = self._get_mock_info(
                mock_info, request_headers, request_body)
            self.send_response(status_code)

            # Check what I should return to client ?
            if mock_info.get('content') is not None:
                response_body_str = open(mock_info.get('content'), 'rb').read()
            elif len(mock_body) > 0:
                response_body_str = json.dumps(mock_body).encode('utf-8')
                if '/token' in self.path or 'v2' in self.path or (
                        isinstance(mock_body, dict) and
                        'v2' in mock_body.get('_links', {}).get(
                        'vnfLcmOpOcc', {}).get('href')):
                    pass
                else:
                    mock_headers['Content-Length'] = str(len(
                        response_body_str))

            # Send custom header if exist
            for key, val in mock_headers.items():
                self.send_header(key, val)
                self.end_headers()

            if len(response_body_str) > 0:
                self.wfile.write(response_body_str)

            manager.add_history(path, RequestHistory(
                status_code=status_code,
                request_headers=request_headers,
                request_body=request_body,
                response_headers=copy.deepcopy(mock_headers),
                response_body=copy.deepcopy(mock_body)))

            if mock_info.get('content') is None and 'v2/grants' not in path:
                self.end_headers()

        def _parse_request_body(self):
            if '/token' in self.path:
                return {}

            if 'content-length' not in self.headers:
                return {}

            request_content_len = int(self.headers.get('content-length'))
            if request_content_len == 0:
                return {}

            decode_request_body = self.rfile.read(
                request_content_len).decode('utf-8')

            return json.loads(decode_request_body)

        def _get_mock_info(self, mock_info, request_headers, request_body):
            """Call mock(callback) and get responses

            This method is called from _returned_callback().

            Args:
                mock_info (tuple): callback informations from caller.
                request_headers (dict): Request headers
                request_body (dict):  Request Bodies

            Returns:
                (tuple): status_code, response headers, response bodies.
                         response body will be converted into JSON string
                         with json.dumps().
            """
            # Prepare response contents
            func = mock_info.get('callback')
            status_code = mock_info.get('status_code')
            mock_headers = mock_info.get('response_headers')
            mock_body = mock_info.get('response_body')

            # Call function if callable.
            if callable(func):
                mock_body = func(request_headers, request_body)

            return (status_code, mock_headers, mock_body)

        def do_DELETE(self):
            self.send_response(http.HTTPStatus.NO_CONTENT)
            self.end_headers()

        def do_GET(self):
            """Process GET request"""
            LOG.debug('[Start] %s.%s()' % (self.__class__.__name__,
                inspect.currentframe().f_code.co_name))

            # Check URI in request.
            if self._is_match_with_list():
                # Request is registered in our list.
                tplUri = urlparse(self.path)
                self._returned_callback(tplUri.path,
                    manager._funcs_gets[tplUri.path])
            else:
                # Unregistered URI is requested
                LOG.debug('GET Recv. Unknown URL: "%s"' % self.path)
                self.send_response(http.HTTPStatus.BAD_REQUEST)
                self.end_headers()

            LOG.debug('[ End ] %s.%s()' %
                      (self.__class__.__name__,
                          inspect.currentframe().f_code.co_name))

        def do_POST(self):
            """Process POST request"""
            LOG.debug(
                '[Start] %s.%s()' %
                (self.__class__.__name__,
                 inspect.currentframe().f_code.co_name))

            # URI might have trailing uuid or not.
            if self._is_match_with_list():
                # Request is registered in our list.
                tplUri = urlparse(self.path)
                if self.path.startswith('/server_notification'):
                    for key in manager._funcs_posts.keys():
                        if self.path.startswith(key):
                            self._returned_callback(tplUri.path,
                                manager._funcs_posts[key])
                else:
                    self._returned_callback(tplUri.path,
                        manager._funcs_posts[tplUri.path])
            else:
                # Unregistered URI is requested
                LOG.debug('POST Recv. Unknown URL: "%s"' % self.path)
                self.send_response(http.HTTPStatus.BAD_REQUEST)
                self.end_headers()

            LOG.debug(
                '[ End ] %s.%s()' %
                (self.__class__.__name__,
                 inspect.currentframe().f_code.co_name))

        def do_PUT(self):
            """Process PUT request"""
            LOG.debug(
                '[Start] %s.%s()' %
                (self.__class__.__name__,
                 inspect.currentframe().f_code.co_name))

            # URI might have trailing uuid or not.
            if self._is_match_with_list():
                # Request is registered in our list.
                tplUri = urlparse(self.path)
                self._returned_callback(tplUri.path,
                                        manager._funcs_puts[tplUri.path])
            else:
                # Unregistered URI is requested
                LOG.debug('PUT Recv. Unknown URL: "%s"' % self.path)
                self.send_response(http.HTTPStatus.BAD_REQUEST)
                self.end_headers()

            LOG.debug(
                '[ End ] %s.%s()' %
                (self.__class__.__name__,
                 inspect.currentframe().f_code.co_name))

    return DummyRequestHandler


class RequestHistory:
    """Storage class for storing requested data(Maybe POSTed datas)."""

    def __init__(
            self,
            status_code,
            request_headers=None,
            request_body=None,
            response_headers=None,
            response_body=None):
        self.timestamp = dt.now()
        self.status_code = status_code
        self.request_headers = request_headers
        self.request_body = request_body
        self.response_headers = response_headers
        self.response_body = response_body


class FakeServerManager(object):
    """Manager class to manage dummy server setting and control"""

    SERVER_PORT = 9990
    SERVER_PORT_T1 = 9995
    SERVER_PORT_T2 = 9996
    SERVER_INVOKE_CHECK_INTERVAL = 10

    def __init__(self):
        # Initialize class-specific variables.

        # Storage for request header/body and response header/body
        # history (dict) is updated using RequestHistory class.
        self._history = {}

        # Initialize function list for each request method.
        # DELETE/PUT method is listed but not supported currently.
        self._funcs_deletes = {}
        self._funcs_gets = {}
        self._funcs_posts = {}
        self._funcs_puts = {}
        self._methods = {
            'DELETE': self._funcs_deletes,
            'GET': self._funcs_gets,
            'POST': self._funcs_posts,
            'PUT': self._funcs_puts}

    def set_callback(
            self,
            method,
            uri,
            status_code=None,
            response_headers=None,
            response_body=None,
            content=None,
            callback=None):
        """Set callback function and some stuff for specified URI.

        ALL additional parameter is set default to None, so you have to
        specify what your callback-function need. response_header and
        response_body will be passed to callback.

        Args:
            method (str): Reqested method
            uri (str): Requested URI
            status_code (http.HTTPStatus): HTTP status code and
                                           reason phrase.
            response_headers (dict): Addtional response header.
            response_body (dict): Response body. Must be Jason Bourne
            content (str): File path that you want client to download.
            callback (callable): Callback function. Must return serializable
                                 object json.dumps() can handle.
        """
        callbacks = self._methods[method]
        callbacks[uri] = {
            'status_code': status_code or http.HTTPStatus.OK,
            'response_headers': response_headers or {},
            'response_body': response_body or {},
            'content': content,
            'callback': callback,
        }
        self._methods[method].update(callbacks)

        # Check file existence for content
        if content is not None:
            if not os.path.isfile(content):
                raise FileNotFoundError

        LOG.debug('Set callback for %s(%s): %s' %
                  (method, uri, callback))

    def add_history(self, path, history):
        """Add Request/Response header/body to history.

        This method maybe called in DummyRequestHandler._returned_callback()
        only. This method should not be called from outside of This class.

        Args:
            path (str): URI path
            history (RequestHistory): Storage container for each request.
        """
        if path in self._history:
            self._history[path].append(history)
        else:
            self._history[path] = [history]

    def clear_history(self, path=None):
        """Clear Request/Response header/body of history.

        Args:
            path (str): URI path
        """
        if not path:
            self._history = {}
            return

        if path in self._history:
            self._history.pop(path)

    def get_history(self, path=None):
        """Get Request/Response header/body from history.

        Args:
            path (str): URI path

        Returns:
            history list(RequestHistory): Storage container for each request.
        """
        history = copy.deepcopy(self._history)

        if not path:
            return history

        return history.get(path) or []

    def prepare_http_server(
            self,
            address="localhost",
            port=SERVER_PORT):
        """Set up HTTPd server your behalf.

        Args:
            address (str): bind address for listen
            port (int): por number for listen
        """
        LOG.debug(
            '[Start] %s.%s()' %
            (self.__class__.__name__,
             inspect.currentframe().f_code.co_name))
        while True:
            try:
                RequestHandler = PrepareRequestHandler(self)
                self.objHttpd = http.server.HTTPServer(
                    (address, port), RequestHandler)
            except OSError:
                time.sleep(self.SERVER_INVOKE_CHECK_INTERVAL)
                continue
            else:
                break
        self.thread_server = threading.Thread(None, self.run)
        LOG.debug(
            '[ End ] %s.%s()' %
            (self.__class__.__name__,
             inspect.currentframe().f_code.co_name))

    def set_https_server(self):
        CERTFILE = "/etc/https_server/ssl/https_server.pem"
        context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        context.load_cert_chain(CERTFILE)
        self.objHttpd.socket = context.wrap_socket(
            self.objHttpd.socket, server_side=True)

    def start_server(self):
        """Start server in thread."""
        LOG.debug('[START] %s()' % inspect.currentframe().f_code.co_name)
        self.thread_server.start()
        LOG.debug('[ END ] %s()' % inspect.currentframe().f_code.co_name)

    def run(self):
        """HTTPd server runner"""
        LOG.debug('[START] %s()' % inspect.currentframe().f_code.co_name)
        try:
            self.objHttpd.serve_forever()
        except KeyboardInterrupt:
            self.stop_server()
        finally:
            self.objHttpd.server_close()
        LOG.debug('[ END ] %s()' % inspect.currentframe().f_code.co_name)

    def stop_server(self):
        """Stop HTTP Server"""
        LOG.debug('[START] %s()' % inspect.currentframe().f_code.co_name)
        thread_shutdown = threading.Thread(None, self.objHttpd.shutdown)
        thread_shutdown.daemon = True
        thread_shutdown.start()
        self.thread_server.join()
        LOG.debug('[ END ] %s()' % inspect.currentframe().f_code.co_name)
