# 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 hashlib
import json
import logging
import secrets
import pprint


from aiohttp import web

import config
import user_login
import version
import web_interface.auth
import web_interface.page_former


SHOW_TOKENS_IN_API = False
SHOW_PRINTER_ATTRS = {'name': 'printer_name', 'reg_code': 'registration_code', 'conn_id': 'connection_id', 'type_selector': 'show_printer_type_selector'}


def find_printer_interface(app, printer_id):
    for pi in app.printer_interfaces:
        if not printer_id or pi.id_string == printer_id:
            return pi

def get_printer_attrs(pi):
    attrs = {}
    for name, attr_name in SHOW_PRINTER_ATTRS.items():
        value = getattr(pi, attr_name, None)
        if value is not None:
            attrs[name] = value
    return attrs

def printer_report(pi):
    report = {}
    report['report'] = pi.status_report()
    #TODO concatenate an IP to API2 id bellow
    report['errors'] = pi.get_errors_to_send('API2')
    report['ids'] = pi.usb_info
    report['attrs'] = get_printer_attrs(pi)
    reg_code = pi.registration_code
    if reg_code:
        report['reg_code'] = reg_code
    if SHOW_TOKENS_IN_API:
        report['token'] = pi.printer_token
    return report


def get_static_values(app):
    values = {'version' : f"v{version.version}.{version.build}_{version.branch}"}
    values['cloud_url'] = config.get_settings()['URL'].replace("cli-", "")
    values['email'] = app.user_login.login
    values['style_name'] = config.get_settings().get("web_interface", {}).get("style", "style")
    values['offline_mode'] = app.offline_mode
    values['https_port'] = config.get_settings()['web_interface']['port_https']
    return values


def printers_report(app):
    response_dict = {'printers': {}}
    for pi in app.printer_interfaces:
        response_dict['printers'][pi.id_string] = printer_report(pi)
    return response_dict


def full_report(app):
    response_dict = printers_report(app)
    response_dict['static'] = get_static_values(app)
    if getattr(app, "updater"):
        response_dict['update_available'] = app.updater.update_available
        response_dict['update_downloaded'] = app.updater.update_downloaded
    response_dict.update(get_static_values(app))
    return response_dict


def printer_command(path_list, pos_args, kw_args, app):
    error = ''
    body = {}
    status = web.HTTPOk.status_code
    if len(path_list) > 1:
        printer_id = path_list[1]
    else:
        printer_id = kw_args.get('printer_id')
    if not printer_id:
        try:
            pi = app.printer_interfaces[0]
        except IndexError:
            pi = app.host_commands_interface
    else:
        pi = find_printer_interface(app, printer_id)
        if not pi:
            error = 'No printer with id ' + str(printer_id)
            status = web.HTTPClientError.status_code
    if pi:
        if 'command' in kw_args or path_list[2] == 'command':
            if len(path_list) > 2:
                command = path_list[3]
            else:
                command = kw_args.get('command')
            if command:
                command_args = {'command': command, 'number': -1}
                payload = {}
                payload.update(kw_args)
                if 'command' in payload:
                    del payload['command']
                if 'printer_id' in payload:
                    del payload['printer_id']
                if 'auth_token' in payload:
                    del payload['auth_token']
                #  for path_part in path_list:
                #      if path_part in payload:
                #          del payload[path_part]
                if not payload:
                    if pos_args:
                        payload = pos_args
                    elif len(path_list) > 3:
                        payload = path_list[4:]
                if payload:
                    command_args['payload'] = payload
                logging.getLogger('api2').info('API2 command: %s', pprint.pformat(command_args))
                ack = pi.execute_server_command(command_args)
                if not ack['result']:
                    body = {'error': {'message': 'Command failure',
                                      'code': web.HTTPServerError.status_code,
                                      'ack' : ack}}
                    status = 500
                    #status = web.HTTPServerError.status_code
                else:
                    body = ack
            else:
                status = web.HTTPClientError.status_code
                error = 'No command'
        elif len(path_list) < 3 or 'report' in path_list or 'report' in kw_args or path_list[2] == '':
            body = printer_report(pi)
            if not body:
                status = web.HTTPServerError.status_code
                error = 'The printer has disconnected'
        else:
            status = web.HTTPBadRequest.status_code
            error = 'No such command'
    return error, body, status


def process_login(args, app):
    login = args.get('login', '').replace(' ', '+') # a hack to fix + in email.
    password = args.get('password', '')
    password_hash = args.get('password_hash')
    if not password_hash:
        password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
    auth_token = args.get('auth_token')
    if not login and not auth_token:
        return "No login"
    correct_login, correct_password_hash = user_login.UserLogin.load_login()
    if correct_login:
        if login != correct_login or password_hash != correct_password_hash:
            return "Incorrect credentials"
    elif not app.user_login.got_network_connection:
        return "No network to check login credentials on the server"
    else:
        error = app.user_login.login_as_user(login, password_hash, auth_token)
        if error:
            return error[1]


def formatted_json_dumps(*args, **kw_args):
    return json.dumps(*args, **kw_args, indent = 4, separators = (',', ': '))
            

async def api2_handler(request):
    resp_data = {}
    path_list = request.path.split('/')[2:]
    status = web.HTTPOk.status_code
    error = None
    pos_args = []
    kw_args = {}
    kw_args.update(request.query)
    formatting = 'pretty' in kw_args
    app = request.app['3dp_app']
    if request.method == 'POST':
        if request.content_type == 'application/json':
            json_args = await request.json()
            if isinstance(json_args, dict):
                kw_args.update(json_args)
            elif isinstance(json_args, list):
                pos_args.extend(json_args)
            else: #TODO create another args var and remake its code
                kw_args = json_args
        else:
            kw_args.update(await request.post())
    else:
        try:
            json_args = await request.json()
            if isinstance(json_args, dict):
                kw_args.update(json_args)
            elif isinstance(json_args, list):
                pos_args.extend(json_args)
            else: #TODO create another args var and remake its code
                kw_args = json_args
        except:
            pass
    if path_list and path_list[0] == 'login':
        error = process_login(kw_args, app)
        if error:
            status = web.HTTPNonAuthoritativeInformation.status_code
        else:
            resp = web_interface.auth.create_auth_response(request)
            return resp
    if 'static_values' in path_list:
        resp_data = get_static_values(app)
    elif path_list and path_list[0] == 'settings' and request.method == "GET":
        resp_data = config.get_settings()
    else:
        try:
            web_interface.auth.check_auth(request, kw_args.get('auth_token'))
        except:
            logging.getLogger('api2').info('API2 auth error: %s\t%s', pprint.pformat(pos_args), pprint.pformat(kw_args))
            status = web.HTTPUnauthorized.status_code
            error = 'Not authorized. Use route /auth to get a token. Then add the auth_token to one of: header Authorization, auth_token in json dict body, or just use a cookie from successful /auth request(automatic in sessions of most http clients)'
        else:
            if 'full_report' in path_list:
                resp_data = full_report(app)
            elif 'printers_report' in path_list:
                resp_data = printers_report(app)
            elif 'settings' in path_list:
                if request.method == "GET":
                    resp_data = config.get_settings()
                elif request.method == "POST" or \
                     request.method == "PUT" or \
                     request.method == "PATCH" or \
                     request.method == 'DELETE':
                    settings = config.get_settings()
                    try:
                        if request.method == "PUT":
                            settings = kw_args
                        elif request.method == "DELETE":
                            if type(kw_args) == str:
                                if kw_args not in settings:
                                    raise web.HTTPBadRequest(data={'error': {'message': 'Unable to find setting for' + str(kw_args), 'code': 400}}, content_type='application/json')
                                del settings[kw_args]
                            elif type(kw_args) == dict:
                                for key, value in kw_args:
                                    if not kw_args.get(key) or value not in kw_args[key]:
                                        raise web.HTTPBadRequest()
                                    del settings[key][value]
                            else:
                                raise web.HTTPBadRequest(data={'error': {'message': 'Unable to find setting for ' + str(kw_args), 'code': 400}}, content_type='application/json')
                        else:
                            settings = config.merge_dictionaries(settings, kw_args, True)
                        config.Config.instance().save_settings(settings)
                        resp_data = settings
                    except Exception as e:
                        raise web.HTTPBadRequest(data={'error': {'message': 'Error modifying settings. Details: ' + str(e), 'code': 500}}, content_type='application/json')
            elif 'printer_profiles' in path_list:
                resp_data = config.get_profiles()
                formatting = True
            elif 'printer_id' in kw_args or 'printer_id' in path_list:
                error, resp_data, status = printer_command(path_list, pos_args, kw_args, app)
            elif path_list:
                if path_list[0] == 'cloud_logout' or path_list[0] == 'logout':
                    app.user_login.logout()
                    app.restart_flag = True
                    app.stop_flag = True
                elif path_list[0] == 'quit':
                    app.restart_flag = False
                    app.stop_flag = True
                elif path_list[0] == 'restart':
                    app.restart_flag = True
                    app.stop_flag = True
    if error:
        resp_data = {'error': {'message': error, 'code': status}}
    if formatting:
        dumps = formatted_json_dumps
    else:
        dumps = json.dumps
    return web.json_response(status=status, data=resp_data, dumps=dumps)
