# 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 copy
import hashlib
# import inspect
import json
import logging
import pprint
#import sys

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'}
# CAN_TYPE_INSPECT = sys.version_info >= (3, 12, 0)

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)
    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, api3=False):
    error = ''
    result = {}
    status = web.HTTPOk.status_code
    # print("!!P", pos_args)
    # print("!!K", kw_args)
    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.HTTPBadRequest.status_code
    if pi:
        if 'command' in kw_args or path_list[2] == 'command':
            if len(path_list) > 3:
                command = path_list[3]
                if len(path_list) > 4:
                    if not pos_args and not kw_args:
                        pos_args = path_list[4:]
            else:
                command = kw_args.get('command')
            if command:
                command_args = {'command': command, 'number': -1}
                if 'payload' in kw_args:
                    payload = kw_args['payload']
                else:
                    payload = {}
                    payload.update(kw_args)
                    if 'command' in payload:
                        del payload['command']
                    if 'printer' in payload:
                        del payload['printer']
                    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) > 4:
                        payload = path_list[4:]
                if payload:
                    command_args['payload'] = payload
                if api3:
                    logger = logging.getLogger('api3')
                else:
                    logger = logging.getLogger('api2')
                logger.info('API command: %s', pprint.pformat(command_args))
                # method = pi._get_method_by_command_name(command)
                # if CAN_TYPE_INSPECT:
                #     type_annotation = inspect.get_annotations(method)
                #     if 'return' in type_annotation:
                #         del type_annotation['return']
                #     if len(payload) != len(type_annotation):
                #         body = {'error': {'message': 'Invalid arguments. Call signature: ' + repr(type_annotation)}}
                if not api3:
                    ack = pi.execute_server_command(command_args)
                    if not ack['result']:
                        result = {'error': {'message': 'Command execution failed',
                                          'code': web.HTTPServerError.status_code,
                                          'ack' : ack}}
                        status = web.HTTPInternalServerError.status_code
                    else:
                        result = ack
                else:
                    result = pi.execute_command(command_args)
            else:
                status = web.HTTPBadRequest.status_code
                error = 'No command'
        elif len(path_list) < 3 or 'report' in path_list or 'report' in kw_args or path_list[2] == '':
            result = printer_report(pi)
            if not result:
                status = web.HTTPServerError.status_code
                error = 'The printer has disconnected'
        else:
            status = web.HTTPBadRequest.status_code
            error = 'No such command'
    return error, result, 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 api3_handler(request):
    request['api3'] = True
    return await api2_handler(request)

async def api2_handler(request):
    api3 = request.get('api3', False)
    print('API3', api3)
    if api3:
        logger = logging.getLogger('api3')
    else:
        logger = logging.getLogger('api2')
    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:
            logger.info('API auth error!')
            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:
                settings = config.get_settings()
                if request.method == "GET":
                    resp_data = config.get_settings()
                elif request.method == "POST" or request.method == "PATCH":
                    logger.info('API settings post/patch args: ' + str(kw_args))
                    if isinstance(kw_args, dict):
                        settings = config.merge_dictionaries(settings, kw_args, True)
                        config.Config.instance().save_settings(settings)
                        resp_data = settings
                        logger.info('Settings updated')
                    else:
                        status = web.HTTPBadRequest.status_code
                        error = "Post to settings must be mappable type(dict)"
                elif request.method == "PUT":
                    logger.info('API settings put args: ' + str(kw_args))
                    if isinstance(kw_args, dict):
                        for key, value in kw_args.items():
                            settings[key] = value
                        config.Config.instance().save_settings(settings)
                        resp_data = settings
                        logger.info('Settings updated')
                    else:
                        status = web.HTTPBadRequest.status_code
                        error = "Put to settings must be mappable type(dict)"
                elif request.method == 'DELETE':
                    logger.info('API settings delete args: ' + str(kw_args))
                    try:
                        if isinstance(kw_args, str):
                            try:
                                del settings[kw_args]
                            except:
                                error = 'Unable to find a settings key ' + str(kw_args)
                                status = web.HTTPBadRequest.status_code
                        elif isinstance(kw_args, dict):
                            for key, value in kw_args.items():
                                if value is not None:
                                    try:
                                        del settings[key][value]
                                    except:
                                        error = f'Invalid setting change with {key} {value}'
                                        status = web.HTTPInternalServerError.status_code
                                else:
                                    try:
                                        del settings[key]
                                    except:
                                        error = f'Invalid setting change with {key} {value}'
                                        status = web.HTTPInternalServerError.status_code
                        else:
                            error = 'Unable to find setting for ' + str(kw_args)
                            status = web.HTTPBadRequest.status_code
                        if not error:
                            config.Config.instance().save_settings(settings)
                            resp_data = settings
                            logger.info('Settings updated')
                    except Exception as e:
                        error = 'Error modifying settings. Details: ' + str(e)
                        status = web.HTTPInternalServerError.status_code
            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, api3)
            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)
