from __future__ import absolute_import
from .events import WebhookAction
from .parsers import get_service_handler


def WebhookRequestHandlerFactory(config, event_store, server_status, is_https=False):
    """Factory method for webhook request handler class"""
    try:
        from SimpleHTTPServer import SimpleHTTPRequestHandler
    except ImportError as e:
        from http.server import SimpleHTTPRequestHandler

    class WebhookRequestHandler(SimpleHTTPRequestHandler, object):
        """Extends the BaseHTTPRequestHandler class and handles the incoming
        HTTP requests."""

        def __init__(self, *args, **kwargs):
            self._config = config
            self._event_store = event_store
            self._server_status = server_status
            self._is_https = is_https
            super(WebhookRequestHandler, self).__init__(*args, **kwargs)

        def end_headers(self):
            self.send_header('Access-Control-Allow-Origin', '*')
            SimpleHTTPRequestHandler.end_headers(self)

        def do_HEAD(self):

            # Web UI needs to be enabled
            if not self.validate_web_ui_enabled():
                return

            # Web UI might require HTTPS
            if not self.validate_web_ui_https():
                return

            # Client needs to be whitelisted
            if not self.validate_web_ui_whitelist():
                return

            # Client needs to authenticate
            if not self.validate_web_ui_basic_auth():
                return

            return SimpleHTTPRequestHandler.do_HEAD(self)

        def do_GET(self):

            # Web UI needs to be enabled
            if not self.validate_web_ui_enabled():
                return

            # Web UI might require HTTPS
            if not self.validate_web_ui_https():
                return

            # Client needs to be whitelisted
            if not self.validate_web_ui_whitelist():
                return

            # Client needs to authenticate
            if not self.validate_web_ui_basic_auth():
                return

            # Handle status API call
            if self.path == "/api/status":
                self.handle_status_api()
                return

            # Serve static file
            return SimpleHTTPRequestHandler.do_GET(self)

        def handle_status_api(self):
            import json
            from os import urandom
            from base64 import b64encode

            data = {
                'events': self._event_store.dict_repr(),
                'auth-key': self._server_status['auth-key']
            }

            data.update(self.get_server_status())

            self.send_response(200, 'OK')
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps(data).encode('utf-8'))

        def do_POST(self):
            """Invoked on incoming POST requests"""
            from threading import Timer
            import logging
            import json
            import threading
            from urlparse import parse_qs

            logger = logging.getLogger()

            content_length = int(self.headers.get('content-length'))
            request_body = self.rfile.read(content_length).decode('utf-8')

            # Extract request headers and make all keys to lowercase (makes them easier to compare)
            request_headers = dict(self.headers)
            request_headers = dict((k.lower(), v) for k, v in request_headers.items())

            action = WebhookAction(self.client_address, request_headers, request_body)
            self._event_store.register_action(action)
            action.set_waiting(True)

            action.log_info('Incoming request from %s:%s' % (self.client_address[0], self.client_address[1]))

            # Payloads from GitHub can be delivered as form data. Test the request for this pattern and extract json payload
            if request_headers['content-type'] == 'application/x-www-form-urlencoded':
                res = parse_qs(request_body.decode('utf-8'))
                if 'payload' in res and len(res['payload']) == 1:
                    request_body = res['payload'][0]

            # Test case debug data
            test_case = {
                'headers': dict(self.headers),
                'payload': json.loads(request_body),
                'config': {},
                'expected': {'status': 200, 'data': [{'deploy': 0}]}
            }

            try:

                # Will raise a ValueError exception if it fails
                ServiceRequestHandler = get_service_handler(request_headers, request_body, action)

                # Unable to identify the source of the request
                if not ServiceRequestHandler:
                    self.send_error(400, 'Unrecognized service')
                    test_case['expected']['status'] = 400
                    action.log_error("Unable to find appropriate handler for request. The source service is not supported")
                    action.set_waiting(False)
                    action.set_success(False)
                    return

                service_handler = ServiceRequestHandler(self._config)

                action.log_info("Handling the request with %s" % ServiceRequestHandler.__name__)

                # Could be GitHubParser, GitLabParser or other
                projects = service_handler.get_matching_projects(request_headers, request_body, action)

                action.log_info("%s candidates matches the request" % len(projects))

                # request_filter = WebhookRequestFilter()

                if len(projects) == 0:
                    self.send_error(400, 'Bad request')
                    test_case['expected']['status'] = 400
                    action.log_error("No matching projects")
                    action.set_waiting(False)
                    action.set_success(False)
                    return

                # Apply filters
                matching_projects = []
                for project in projects:
                    if project.apply_filters(request_headers, request_body, action):
                        matching_projects.append(project)

                # Only keep projects that matches
                projects = matching_projects

                action.log_info("%s candidates matches after applying filters" % len(projects))

                if not service_handler.validate_request(request_headers, request_body, projects, action):
                    self.send_error(400, 'Bad request')
                    test_case['expected']['status'] = 400
                    action.log_warning("Request was rejected due to a secret token mismatch")
                    action.set_waiting(False)
                    action.set_success(False)
                    return

                test_case['expected']['status'] = 200

                self.send_response(200, 'OK')
                self.send_header('Content-type', 'text/plain')
                self.end_headers()

                if len(projects) == 0:
                    action.set_waiting(False)
                    action.set_success(False)
                    return

                action.log_info("Proceeding with %s candidates" % len(projects))
                action.set_waiting(False)
                action.set_success(True)

                for project in projects:

                    # Schedule the execution of the webhook (git pull and trigger deploy etc)
                    thread = threading.Thread(target=project.execute_webhook, args=[self._event_store])
                    thread.start()

                    # Add additional test case data
                    test_case['config'] = {
                        'url': 'url' in project and project['url'],
                        'branch': 'branch' in project and project['branch'],
                        'remote': 'remote' in project and project['remote'],
                        'deploy': 'echo test!'
                    }

            except ValueError as e:
                self.send_error(400, 'Unprocessable request')
                action.log_warning('Unable to process incoming request from %s:%s' % (self.client_address[0], self.client_address[1]))
                test_case['expected']['status'] = 400
                action.set_waiting(False)
                action.set_success(False)
                return

            except Exception as e:
                self.send_error(500, 'Unable to process request')
                test_case['expected']['status'] = 500
                action.log_warning("Unable to process request")
                action.set_waiting(False)
                action.set_success(False)

                raise e

            finally:

                # Save the request as a test case
                if 'log-test-case' in self._config and self._config['log-test-case']:
                    self.save_test_case(test_case)

        def log_message(self, format, *args):
            """Overloads the default message logging method to allow messages to
            go through our custom logger instead."""
            import logging
            logger = logging.getLogger()
            logger.info("%s - %s" % (self.client_address[0], format%args))

        def save_test_case(self, test_case):
            """Log request information in a way it can be used as a test case."""
            import time
            import json
            import os

            # Mask some header values
            masked_headers = ['x-github-delivery', 'x-hub-signature']
            for key in test_case['headers']:
                if key in masked_headers:
                    test_case['headers'][key] = 'xxx'

            target = '%s-%s.tc.json' % (self.client_address[0], time.strftime("%Y%m%d%H%M%S"))
            if 'log-test-case-dir' in self._config and self._config['log-test-case-dir']:
                target = os.path.join(self._config['log-test-case-dir'], target)

            file = open(target, 'w')
            file.write(json.dumps(test_case, sort_keys=True, indent=4))
            file.close()

        def get_server_status(self):
            """Generate a copy of the server status object that contains the public IP or hostname."""

            server_status = {}
            for item in self._server_status.items():
                key, value = item
                public_host = self.headers.get('host').split(':')[0]

                if key == 'http-uri':
                    server_status[key] = value.replace(self._config['http-host'], public_host)

                if key == 'https-uri':
                    server_status[key] = value.replace(self._config['https-host'], public_host)

                if key == 'wss-uri':
                    server_status[key] = value.replace(self._config['wss-host'], public_host)

            return server_status

        def validate_web_ui_enabled(self):
            """Verify that the Web UI is enabled"""

            if self._config['web-ui-enabled']:
                return True

            self.send_error(403, "Web UI is not enabled")
            return False

        def validate_web_ui_https(self):
            """Verify that the request is made over HTTPS"""

            if self._is_https:
                return True

            if not self._config['web-ui-require-https']:
                return True

            # Attempt to redirect the request to HTTPS
            server_status = self.get_server_status()
            if 'https-uri' in server_status:
                self.send_response(307)
                self.send_header('Location', '%s%s' % (server_status['https-uri'], self.path))
                self.end_headers()
                return False

            self.send_error(403, "Web UI is only accessible through HTTPS")
            return False

        def validate_web_ui_whitelist(self):
            """Verify that the client address is whitelisted"""

            # Allow all if whitelist is empty
            if len(self._config['web-ui-whitelist']) == 0:
                return True

            # Verify that client IP is whitelisted
            if self.client_address[0] in self._config['web-ui-whitelist']:
                return True

            self.send_error(403, "%s is not allowed access" % self.client_address[0])
            return False

        def validate_web_ui_basic_auth(self):
            """Authenticate the user"""
            import base64

            if not self._config['web-ui-auth-enabled']:
                return True

            # Verify that a username and password is specified in the config
            if self._config['web-ui-username'] is None or self._config['web-ui-password'] is None:
                self.send_error(403, "Authentication credentials missing in config")
                return False

            # Verify that the provided username and password matches the ones in the config
            key = base64.b64encode("%s:%s" % (self._config['web-ui-username'], self._config['web-ui-password']))
            if self.headers.getheader('Authorization') == 'Basic ' + key:
                return True

            # Let the client know that authentication is required
            self.send_response(401)
            self.send_header('WWW-Authenticate', 'Basic realm=\"GAD\"')
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write('Not authenticated')
            return False

    return WebhookRequestHandler
