# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import asyncio
import collections
import concurrent.futures
import json
import logging
import os
import pprint
import ssl
import subprocess
import sys
import time
import threading

import aiohttp
from aiohttp import web

import config
import log
import paths
from awaitable import Awaitable

from . import routes
from . import ws_handler_bridge

class WebInterface(threading.Thread, Awaitable):

    PORT_SETTING_NAME = "port"
    NAME = "WebInterface"
    WRITE_CHUNK_SIZE = 16384
    SHUTDOWN_TIMEOUT = 6
    SHUTDOWN_WAITING_STEP = 0.01
    WEB_CONTENT_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_content_files')
    STATIC_FOLDER = os.path.join(WEB_CONTENT_FOLDER, 'static')
    COOKIE_TOKENS_FILE = os.path.join(paths.CURRENT_SETTINGS_FOLDER, "cookie_tokens.json")
    MAX_COOKIE_TOKENS = 1024

    @classmethod
    def load_web_content(cls, path=WEB_CONTENT_FOLDER):
        binary_files_extensions = ('jpg', 'png', 'ico')
        web_content = {}
        for name in os.listdir(path):
            file_path = os.path.join(path, name)
            if os.path.isdir(file_path):
                web_content[name] = cls.load_web_content(file_path)
            else:
                read_mode = 'r'
                if name.split(".")[-1] in binary_files_extensions:
                    read_mode += 'b'
                try:
                    with open(file_path, read_mode) as f:
                        web_content[name] = f.read()
                except Exception as e:
                    logger = logging.getLogger('WebInterface.load_web_content')
                    logger.error('Exception while reading file ' + os.path.join(path, name) + ' : ' + str(e))
        return web_content

    def __init__(self, app):
        self.logger = logging.getLogger("app." + __name__)
        self.logger.info("Aiohttp version:" + aiohttp.__version__)
        self.app = app
        self.loop = None
        self.http_site = None
        self.https_site = None
        self.ready = False
        self.loop_lock = threading.Lock()
        self.port = config.get_settings()['web_interface']['port']
        self.https_port = config.get_settings()['web_interface']['port_https']
        if config.get_settings()['remote_control']['web_server']:
            self.ip = config.REMOTE_IP
        else:
            self.ip = config.LOCAL_IP
        threading.Thread.__init__(self, name=self.NAME, daemon=True)
        Awaitable.__init__(self, app)

    @log.log_exception
    def run(self):
        if self.https_port:
            self.ssl_context = self.create_ssl_context()
        else:
            self.ssl_context = None
        self.logger.info("Starting web server...")
        with self.loop_lock:
            aiohttp_logger = logging.getLogger("app.aiohttp")
            aiohttp_logger.setLevel(logging.ERROR)
            self.loop = asyncio.new_event_loop()
            asyncio.set_event_loop(self.loop)
            self.server = web.Application()
            self.server['3dp_app'] = self.app
            self.server['3dp_content'] = WebInterface.load_web_content()
            self.server['cookie_tokens'] = self.load_cookie_tokens()
            self.server['threads_pool'] = concurrent.futures.ThreadPoolExecutor()
            self.server['wizard_on_start'] = config.get_settings()['wizard_on_start']
            self.ws_handler_bridge = ws_handler_bridge.WSHandlerBridge(self.app, self.loop)
            self.server.router.add_get('/ws', self.ws_handler_bridge.websock_handler)
            self.server.add_routes([web.static('/static', self.STATIC_FOLDER)])
            routes.set_routes(self.server)
            if config.get_settings()['logging'].get('http_access_log'):
                self.runner = web.AppRunner(self.server, handle_signals=False)
            else:
                self.runner = web.AppRunner(self.server, handle_signals=False, access_log=None)
        try:
            asyncio.run_coroutine_threadsafe(self.init_server(), self.loop)
            self.loop.run_forever()
            self.logger.info("Asyncio loop exit")
        except Exception:
            self.logger.exception("Error: unable to start Web Interface Server")

    def check_function(self):
        return self.ready

    def generate_ssl_certificates(self, keyfile, certfile):
        cmd = ["openssl", "req", "-x509", "-newkey", "rsa:2048", "-keyout", keyfile, "-out", certfile,
               "-days", "10000", "-nodes", "-subj",
               "/C=US/ST=California/L=San Francisco/O=3D Control Systems/OU=3DPrinterOS/CN=3dprinteros.com"]
        capture_output_support = sys.version_info > (3,6)
        self.logger.info("Generation HTTPS server certificates...")
        if capture_output_support:
            call = subprocess.run(cmd, capture_output=True)
        else:
            call = subprocess.run(cmd)
        if call.returncode:
            self.logger.error('Some problem with auto certificates generating. '
                              'Please refer to how-to-generate-certificates.txt and generate certs manually.')
            if capture_output_support:
                self.logger.info('OpenSSL stderr: %s' % call.stderr)
            raise RuntimeError("OpenSSL generation failed with code: %d" % call.returncode)

    def create_ssl_context(self):
        try:
            sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS)
            keyfile = os.path.join(paths.CURRENT_SETTINGS_FOLDER, 'private_key.pem')
            certfile = os.path.join(paths.CURRENT_SETTINGS_FOLDER, 'certificate.pem')
            if not os.path.isfile(keyfile) or not os.path.isfile(certfile):
                self.generate_ssl_certificates(keyfile, certfile)
            sslcontext.load_cert_chain(certfile, keyfile)
            return sslcontext
        except:
            self.logger.exception("Exception while creating ssl context:")

    def load_cookie_tokens(self):
        try:
            with open(self.COOKIE_TOKENS_FILE) as f:
                text = f.read()
                cookie_tokens = json.loads(text)
                if type(cookie_tokens) != list:
                    raise ValueError
        except (OSError, IOError, ValueError):
            cookie_tokens = []
        finally:
            return collections.deque(cookie_tokens, self.MAX_COOKIE_TOKENS)

    def save_cookie_tokens(self):
        try:
            with open(self.COOKIE_TOKENS_FILE, "w") as f:
                try:
                    cookie_tokens = self.server['cookie_tokens']
                except TypeError:
                    pass
                else:
                    if cookie_tokens:
                        json_tokens = json.dumps(list(cookie_tokens))
                        f.write(json_tokens)
        except Exception as e:
            self.logger.error("Error saving cookie tokens:" + str(e))

    def reload_web_content(self):
        self.server['3dp_content'] = WebInterface.load_web_content()

    def close(self):
        self.save_cookie_tokens()
        with self.loop_lock:
            if self.loop and (self.http_site or self.https_site):
                if self.ws_handler_bridge:
                    self.ws_handler_bridge.close()
                shutdown_coroutine = asyncio.run_coroutine_threadsafe(self.shutdown_server(), self.loop)
                start_time = time.monotonic()
                while time.monotonic() < start_time + self.SHUTDOWN_TIMEOUT + 1:
                    try:
                        if shutdown_coroutine.done():
                            break
                    except asyncio.CancelledError:
                        pass
                    time.sleep(self.SHUTDOWN_WAITING_STEP)
                self.logger.info("Sending loop stop")
                self.loop.call_soon_threadsafe(self.loop.stop)
                while self.loop.is_running():
                    time.sleep(self.SHUTDOWN_WAITING_STEP)
                self.loop.close()

    async def init_server(self):
        await self.runner.setup()
        self.http_site = web.TCPSite(self.runner, self.ip, self.port, shutdown_timeout=self.SHUTDOWN_TIMEOUT)
        await self.http_site.start()
        if self.ssl_context:
            #TODO add 12 seconds read and write timeouts
            self.https_site = web.TCPSite(self.runner, self.ip, self.https_port, shutdown_timeout=self.SHUTDOWN_TIMEOUT, ssl_context=self.ssl_context)
            await self.https_site.start()
        self.ready = True

    async def shutdown_server(self):
        self.logger.info("Shutdown coroutine started")
        if self.ws_handler_bridge:
            self.ws_handler_bridge.close()
        await self.runner.shutdown()
        try:
            await self.http_site.stop()
            self.http_site = None
        except AttributeError:
            pass
        try:
            await self.https_site.stop()
            self.https_site = None
        except AttributeError:
            pass
        try:
            await self.server['threads_pool'].shutdown(wait=False, cancel_futures=True)
        except:
            try:
                await self.server['threads_pool'].shutdown()
            except:
                pass
        await self.runner.cleanup()
        await asyncio.sleep(0)
        for task in asyncio.all_tasks():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
        self.logger.info("Shutdown coroutine finished")
