# 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 importlib
import logging
import os

import asyncio

from aiohttp import web
from aiohttp import hdrs

import config
import paths
import web_interface.page_former
import web_interface.printers_html
from web_interface.auth import check_auth
import version

PATHS_WITH_FILE_UPLOAD = {'accept_update_from_file' : paths.UPDATE_FILE_PATH,\
                          'upload_plugin' :  paths.PLUGIN_INSTALL_FILE_PATH,\
                          'upload_to_storage' : None,
                          'print_files': None}
EXECUTOR_TIMEOUT = 5.8 # 0.2s less than WebInterface.SHUTDOWN_TIMEOUT
PRINT_START_EXECUTOR_TIMEOUT = 60
UPLOAD_CHUNK_SIZE = 1024*256 # greatly affects upload performance


async def reload_old_static(request):
    if request.remote == request.host.split(':')[0]:
        logging.getLogger('webui.reload_old_static').info("Reloading the web content folder")
        request.app['3dp_app'].web_interface.reload_web_content()
        raise web.HTTPNoContent()
    raise web.HTTPUnauthorized()


async def reload_page_former(request):
    if request.remote == request.host.split(':')[0]:
        logging.getLogger('webui.reload_page_former').info("Reloading the PageFormer")
        importlib.reload(web_interface.page_former)
        raise web.HTTPNoContent()
    raise web.HTTPUnauthorized()


async def legacy_handler(request):
    #  logger = logging.getLogger('webui.generic_web_handler')
    pf_args = []
    if request.method == hdrs.METH_GET:
        #  logger.debug("GET %s" % request.path)
        method_name = 'do_GET'
    elif request.method == hdrs.METH_POST:
        #  logger.debug("POST %s" % request.path)
        method_name = 'do_POST'
        post_data = await request.post()
        pf_args.append(post_data)
    else:
        raise web.HTTPMethodNotAllowed(method=request.method, allowed_methods=[hdrs.METH_GET, hdrs.METH_POST])
    return await handle_legacy_page(request, method_name, pf_args)


async def multipart_upload_handler(request, storage=paths.STORAGE_FOLDER, old_style_resp=False):
    # TODO validate and sanitize file_path before saving
    logger = logging.getLogger('webui.multipart_upload_handler')
    check_auth(request)
    print('Upload path:', request.path.strip("/"))
    if config.get_settings()['forbid_uploads']:
        raise web.HTTPForbidden()
    file_path = PATHS_WITH_FILE_UPLOAD.get(request.path.strip("/"))
    app = request.app['3dp_app']
    error = None
    got_file = None
    printer_id = request.query.get('printer_id')
    start_print = bool(request.query.get('start_print'))
    try:
        reader = await request.multipart()
        fields_left = 128 # max number of fields
        while True:
            if app.stop_flag:
                raise web.HTTPFound("/")
            field = await reader.next()
            if field and field.name == 'printer_id':
                printer_id = await field.read()
            elif field and field.name == 'start_print':
                start_print = await field.read()
            elif field and field.name == 'file':
                got_file = True
                if not file_path:
                    if field and field.filename:
                        file_path = os.path.join(storage, field.filename)
                    else:
                        error = "Please select a file"
                if not error:
                    size = 0
                    try:
                        with open(file_path, 'wb') as f:
                            while True:
                                if app.stop_flag:
                                    break
                                chunk = await field.read_chunk(UPLOAD_CHUNK_SIZE) 
                                if not chunk:
                                    break
                                size += len(chunk)
                                file_write_future = asyncio.get_event_loop().run_in_executor(
                                        request.app['threads_pool'],
                                        lambda f, chunk: f.write(chunk),
                                        f,
                                        chunk)
                                await asyncio.wait_for(file_write_future, EXECUTOR_TIMEOUT)
                                if request.app['3dp_app'].stop_flag:
                                    os.remove(file_path)
                                    raise web.HTTPFound("/")
                    except (OSError, IOError) as e:
                        error = "Error saving upload: " + str(e)
                        logger.error(error)
                    else:
                        logger.info('Received upload size: %dB' % size)
            elif not field:
                if fields_left:
                    fields_left -= 1
                else:
                    if not got_file: 
                        error = 'Error: no not file field found'
                    break
    except Exception as e:
        error = "Error while on upload: " + str(e)
        logger.error(error)
    if not file_path or file_path == storage:
        error = "Please select a file"
    if old_style_resp or error:
        return await handle_legacy_page(request, 'do_POST', [{"multipart_error" : error}])
    if error:
        raise web.HTTPInternalServerError(text=error) 
    path = "/"
    if start_print:
        if not printer_id:
            raise web.HTTPBadGateway(text='Invalid arguments: got start_print without printer_id')
        for pi in app.printer_interfaces:
            if not printer_id or pi.id_string == printer_id:
                if pi.sender:
                    break
        else:
            raise web.HTTPBadRequest(text='No printer with id: ' + printer_id)
        print_start_future = asyncio.get_event_loop().run_in_executor(
                request.app['threads_pool'],
                pi.sender.print_file,
                file_path)
        #  await asyncio.wait_for(print_start_future, EXECUTOR_TIMEOUT)
        asyncio.create_task(asyncio.wait_for(print_start_future, PRINT_START_EXECUTOR_TIMEOUT))
    else:
        if printer_id:
            path = "/local_file_print_or_delete?printer_id=" + printer_id
    raise web.HTTPTemporaryRedirect(path)


async def handle_legacy_page(request, method_name, pf_args=[]):
    logger = logging.getLogger('webui.generic_web_handler')
    pf = web_interface.page_former.PageFormer(request.app['3dp_app'],
                     request.app['3dp_content'],
                     request.host,
                     request.method,
                     request.raw_path,
                     request.secure,
                     request.remote,
                     request.cookies,
                     request.app['cookie_tokens'])
    try:
        if web_interface.page_former.PageFormer.print_from_file_or_delete.__name__ in request.raw_path:
            timeout = PRINT_START_EXECUTOR_TIMEOUT
        else:
            timeout = EXECUTOR_TIMEOUT
        executor_future = asyncio.get_event_loop().run_in_executor(request.app['threads_pool'], getattr(pf, method_name), *pf_args)
        await asyncio.wait_for(executor_future, timeout)
    except asyncio.TimeoutError:
        logger.warning(f'Executor timeout when handling {request.raw_path}')
        raise web.HTTPFound('/', text='Timeout while handing call')
    except Exception as e:
        logger.error(e) # check why some error are not logged by log.log_exception
    else:
        if pf.new_cookie_token:
            if pf.new_cookie_token not in request.app['cookie_tokens']:
                request.app['cookie_tokens'].append(pf.new_cookie_token)
                request.app.save_cookie_tokens()
    return web.Response(status=pf.status, headers=pf.headers, body=pf.body)


async def printers_html(request):
    check_auth(request)
    return web.Response(body=web_interface.printers_html.get_printers_html(request))


async def http_warning_html(request):
    body = ''
    if not request.secure:
        https_port = request.app['3dp_app'].settings['web_interface'].get('port_https')
        if https_port:
            http_port = https_port = request.app['3dp_app'].settings['web_interface']['port']
            host = request.host.strip(":" + str(http_port))
            if http_port != 443:
                host += ":" + str(https_port)
            body = '<div id="http_secure_message">For security reasons we strongly recommend to use HTTPS URL to access the cloud client interface.' \
                    f' <a href="https://{host}/">Click here to switch to HTTPS</a>.</div>'
    return web.Response(body=body)


async def host_version(request):
    url = request.app['3dp_app'].settings['URL'].replace('cli-', '')
    return web.Response(body=f"{url} v{version.version}.{version.build}_{version.branch}")


async def camera_modules_html(request):
    check_auth(request)
    module_selector_html = ''
    camera_controller = getattr(request.app['3dp_app'], 'camera_controller', None)
    if camera_controller:
        if camera_controller.current_camera_name in camera_controller.MODULES_FOR_UI:
            modules = camera_controller.MODULES_FOR_UI
        else:
            modules = camera_controller.CAMERA_MODULES
        for module in modules:
            if module == camera_controller.current_camera_name:
                module_selector_html += \
                    '<p><input type="radio" name="module" checked value="' + module + \
                    '"><font color="lightgrey" title="Current live view mode">'\
                    + module + '</font></p>'
            else:
                module_selector_html += '<p><input type="radio" name="module" value="' \
                                        + module + '"> ' + module + '</p>'
    return web.Response(body=module_selector_html)

async def main_page(request):
    html = request.app['3dp_content']['pages']['main.html']
    return web.Response(text=html, content_type="text/html")


async def pages(request):
    page_path = request.match_info.get('path')
    if page_path:
        item = request.app['3dp_content']['pages']
        for path in page_path.split('/'):
            page = item.get(path + ".html")
            if page:
                return web.Response(text=page, content_type="text/html")
            item = item.get(path)
            if not item:
                break
    raise web.HTTPNotFound()


async def telemetry(request):
    app = request.app['3dp_app']
    telemetry = {}
    telemetry['login'] = getattr(getattr(app, 'user_login', None), 'login', None)
    telemetry['version'] = version.version
    telemetry['update_available'] = app.updater.update_available
    telemetry['camera_type'] = getattr(getattr(app, 'camera_controller', None), 'current_camera_type', None)
    telemetry['printers'] = [{'usb_info': pi.id_string, 'report': pi.status_report()} for pi in app.printer_interfaces]
    return web.json_response(data=telemetry)


async def wizard_handler(request):
    check_auth(request)
    path = request.match_info.get('path')
    if not path:
        raise web.HTTPFound('/pages/wizard/greetings')
    elif 'close_wizard' in path:
        settings = request.app['3dp_app'].settings
        settings['wizard_on_start'] = False
        config.Config.instance().save_settings(settings)
        raise web.HTTPFound('/')
    else:
        raise web.HTTPFound('/pages/wizard/' + path)
        #return await pages(request)
