/*
 * Authors:
 * Dmitriy A. Gerasimov <kahovski@gmail.com>
 * DeM Labs Ltd.   https://demlabs.net
 * CellFrame         https://cellframe.net
 * Copyright  (c) 2017-2020
 * All rights reserved.

 This file is part of DAP (Distributed Applications Platform) the open source project

    DAP (Distributed Applications Platform) is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    DAP is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with any DAP based project.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "dap_strfuncs.h"
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <getopt.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>

#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#include <io.h>
#include <pthread.h>
#include "userenv.h"

#endif

#define LOG_TAG "main"

#ifndef _WIN32
  #include "sig_unix_handler.h"
#else
    #include "sig_win32_handler.h"
    #include "registry.h"
    void  S_SetExceptionFilter( void );
#endif
#include "dap_common.h"
#include "dap_config.h"
#include "dap_server.h"
#include "dap_notify_srv.h"
#include "dap_http_server.h"
#include "dap_http_folder.h"
#include "dap_chain_node_dns_client.h"
#include "dap_chain_node_dns_server.h"
#include "dap_chain_net_balancer.h"
#include "dap_chain_net_node_list.h"

#ifdef DAP_MODULES_DYNAMIC
#include "dap_modules_dynamic_cdb.h"
#endif

#include "dap_events.h"
#include "dap_enc.h"
#include "dap_enc_ks.h"
#include "dap_enc_http.h"

#include "dap_chain.h"
#include "dap_chain_wallet.h"

#include "dap_chain_cs_blocks.h"
#include "dap_chain_cs_dag.h"
#include "dap_chain_cs_dag_poa.h"
#include "dap_chain_cs_none.h"
#include "dap_chain_cs_esbocs.h"

//#include "dap_chain_bridge.h"
//#include "dap_chain_bridge_btc.h"

#include "dap_chain_net.h"
#include "dap_chain_net_srv.h"
#include "dap_chain_net_srv_geoip.h"

#if defined(DAP_OS_DARWIN) || ( defined(DAP_OS_LINUX) && ! defined (DAP_OS_ANDROID))
#include "dap_chain_net_srv_vpn.h"
#include "dap_chain_net_vpn_client.h"
#endif

#include "dap_global_db.h"
#include "dap_chain_mempool.h"
#include "dap_chain_node.h"
#include "dap_chain_node_cli.h"
#include "dap_json_rpc.h"

#include "dap_stream.h"
#include "dap_stream_ctl.h"
#include "dap_chain_net_srv_order.h"
#include "dap_chain_net_srv_xchange.h"
#include "dap_chain_net_srv_voting.h"
#include "dap_chain_net_srv_bridge.h"
#include "dap_chain_net_srv_stake_pos_delegate.h"
#include "dap_chain_net_srv_stake_lock.h"
#include "dap_chain_net_srv_emit_delegate.h"

#include "dap_chain_wallet_cache.h"
#include "dap_chain_policy.h"

#include "dap_events_socket.h"
#include "dap_client.h"
#include "dap_http_simple.h"
#include "dap_process_manager.h"

#include "dap_file_utils.h"
#include "dap_plugin.h"

#ifdef DAP_SUPPORT_PYTHON_PLUGINS
    #include "dap_chain_plugins.h"
    #include "dap_plugins_python_app_context.h"
#endif

#define MEMPOOL_URL "/mempool"
#define MAIN_URL "/"
const char *dap_node_version();
static int s_proc_running_check(const char *a_path);

#ifdef DAP_OS_ANDROID
#include "dap_app_cli.h"
#include <android/log.h>
#include <jni.h>
#endif

#ifndef BUILD_HASH
#define BUILD_HASH "0000000" // 0000000 means uninitialized
#endif

#ifndef BUILD_TS
#define BUILD_TS "undefined"
#endif

#define NODE_NAME "cellframe-node"

const char *dap_node_version() {
    return "CellframeNode, " DAP_VERSION ", " BUILD_TS ", " BUILD_HASH;
}

void set_global_sys_dir(const char *dir)
{
    g_sys_dir_path = dap_strdup(dir);
}

int main( int argc, const char **argv )
{
    if ( argv[1] && !dap_strcmp("-version", argv[1]) )
        return printf("%s\n", dap_node_version()), 0;
        
    dap_server_t *l_server = NULL; // DAP Server instance
    bool l_debug_mode = true;
    bool bServerEnabled = false;
    int rc = 0;

    dap_set_appname(NODE_NAME);
#if defined(_WIN32) && defined(NDEBUG)
    S_SetExceptionFilter( );
#endif

    // get relative path to config
#if !DAP_OS_ANDROID
    if (argc > 2 && !dap_strcmp("-B" , argv[1]))
        g_sys_dir_path = dap_strdup(argv[2]);
#endif

    if (!g_sys_dir_path) {
#ifdef DAP_OS_WINDOWS
        g_sys_dir_path = dap_strdup_printf("%s/%s", regGetUsrPath(), dap_get_appname());
#elif DAP_OS_MAC
        g_sys_dir_path = dap_strdup_printf("/Library/Application Support/CellframeNode/");
#elif DAP_OS_UNIX
        g_sys_dir_path = dap_strdup_printf("/opt/%s", dap_get_appname());
#endif
    }
    if ( !dap_dir_test(g_sys_dir_path) ) {
        printf("Invalid path \"%s\"", g_sys_dir_path);
        rc = -1;
    } else {
        char l_path[MAX_PATH + 1];
        int pos = snprintf(l_path, sizeof(l_path), "%s/var/log", g_sys_dir_path);
        if ( dap_mkdir_with_parents(l_path) ) {
            printf("Can't create directory %s, error %d", l_path, errno);
            rc = -2;
        } else {
            snprintf(l_path + pos, sizeof(l_path) - pos, "/%s.log", dap_get_appname());
            if ( dap_common_init(dap_get_appname(), l_path) ) {
                printf("Fatal Error: Can't init common functions module");
                rc = -3;
            } else {
#if defined (DAP_DEBUG) || !defined(DAP_OS_WINDOWS)
                dap_log_set_external_output(LOGGER_OUTPUT_STDOUT, NULL);
#else
                dap_log_set_external_output(LOGGER_OUTPUT_NONE, NULL);
#endif
#ifdef DAP_OS_ANDROID
                dap_log_set_external_output(LOGGER_OUTPUT_ALOG, "NativeCellframeNode");
#endif
                log_it(L_DEBUG, "Use main path: %s", g_sys_dir_path);
                snprintf(l_path, sizeof(l_path), "%s/etc", g_sys_dir_path);
                if ( dap_config_init(l_path) ) {
                    log_it( L_CRITICAL,"Can't init general config \"%s/%s.cfg\"\n", l_path, NODE_NAME );
                    rc = -4;
                }
            }
        }
    }
    if ( rc )
        return DAP_DELETE(g_sys_dir_path), rc;

    if (!( g_config = dap_config_open(dap_get_appname()) ))
        return log_it( L_CRITICAL,"Can't open general config %s.cfg", dap_get_appname() ), DAP_DELETE(g_sys_dir_path), -5;
#ifndef DAP_OS_WINDOWS
    char l_default_dir[MAX_PATH + 1];
    snprintf(l_default_dir, MAX_PATH + 1, "%s/tmp", g_sys_dir_path);
    char *l_pid_file_path = dap_config_get_item_str_path_default(g_config,  "resources", "pid_path", l_default_dir);
    int l_pid_check = s_proc_running_check(l_pid_file_path);
    DAP_DELETE(l_pid_file_path);
    if (l_pid_check)
        return 2;
#else
    if ( s_proc_running_check("DAP_CELLFRAME_NODE_74E9201D33F7F7F684D2FEF1982799A79B6BF94"
                              "B568446A8D1DE947B00E3C75060F3FD5BF277592D02F77D7E50935E56") )
        return DAP_DELETE(g_sys_dir_path), 2;
#endif

    log_it(L_DEBUG, "Parsing command line args");

    l_debug_mode = dap_config_get_item_bool_default( g_config,"general","debug_mode", false );

    if ( l_debug_mode )
        log_it( L_ATT, "*** DEBUG MODE ***" );
    else
       log_it( L_ATT, "*** NORMAL MODE ***" );

    dap_log_level_set( l_debug_mode ? L_DEBUG : L_NOTICE );

    log_it( L_DAP, "*** CellFrame Node version: %s ***", DAP_VERSION );
    
    if ( dap_config_get_item_bool_default(g_config, "log", "rotate_enabled", false) ) {
        size_t  l_timeout_minutes   = dap_config_get_item_int64(g_config, "log", "rotate_timeout"),
                l_max_file_size     = dap_config_get_item_int64(g_config, "log", "rotate_size");
        log_it(L_NOTICE, "Log rotation every %lu min enabled, max log file size %lu MB",
                         l_timeout_minutes, l_max_file_size);
        dap_common_enable_cleaner_log(l_timeout_minutes * 60000, l_max_file_size);
    }

    if ( dap_enc_init() != 0 ){
        log_it( L_CRITICAL, "Can't init encryption module" );
        return -56;
    }
    // change to dap_config_get_item_int_default when it's will be possible
    uint32_t l_thread_cnt = dap_config_get_item_int32_default(g_config, "resources", "threads_cnt", 0);
    // New event loop init
    dap_events_init(l_thread_cnt, 0);
    dap_events_start();

    bServerEnabled = dap_config_get_item_bool_default( g_config, "server", "enabled", false );

    if ( bServerEnabled && dap_server_init() != 0 ) {
        log_it( L_CRITICAL, "Can't init socket server module" );
        return -4;
    }

    if ( dap_http_init() != 0 ) {
        log_it( L_CRITICAL, "Can't init http server module" );
        return -5;
    }

#if !DAP_OS_ANDROID
    if ( dap_http_folder_init() != 0 ){
        log_it( L_CRITICAL, "Can't init http server module" );
        return -55;
    }
#endif
    
    if ( dap_http_simple_module_init() != 0 ) {
        log_it(L_CRITICAL,"Can't init http simple module");
        return -9;
    }

    if ( enc_http_init() != 0 ) {
        log_it( L_CRITICAL, "Can't init encryption http session storage module" );
        return -81;
    }

    if ( dap_stream_init(g_config) != 0 ) {
        log_it( L_CRITICAL, "Can't init stream server module" );
        return -82;
    }

    if ( dap_stream_ctl_init() != 0 ){
        log_it( L_CRITICAL, "Can't init stream control module" );
        return -83;
    }

    dap_client_init();

    // Create and init notify server
    if ( dap_notify_server_init() != 0 ){
        log_it( L_ERROR, "Can't init notify server module" );
    }

    if ( dap_global_db_init() != 0 ) {
        log_it( L_CRITICAL, "Can't init global db module" );
        return -58;
    }

    if ( dap_datum_mempool_init() ) {
        log_it( L_CRITICAL, "Can't init mempool module" );
        return -59;
    }

    if( dap_chain_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain modules");
        return -60;
    }

    if (dap_chain_net_srv_stake_pos_delegate_init()) {
        log_it(L_ERROR, "Can't start delegated PoS stake service");
    }

    if( dap_chain_cs_dag_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain dag consensus module");
        return -62;
    }

    if( dap_chain_cs_dag_poa_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain dag consensus PoA module");
        return -63;
    }

    if( dap_chain_cs_blocks_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain blocks consensus module");
        return -62;
    }

    if( dap_chain_cs_esbocs_init() ){
        log_it(L_CRITICAL,"Can't init enhanced stake-based blocks operating consensus module");
        return -69;
    }

    if( dap_nonconsensus_init() ) {
        log_it(L_CRITICAL, "Can't init nonconsensus chain module");
        return -71;
    }

    if( dap_chain_net_init() ){
        log_it(L_CRITICAL,"Can't init dap chain network module");
        return -65;
    }

    if( dap_chain_policy_init() ){
        log_it(L_CRITICAL,"Can't init dap chain policy module");
        return -66;
    }

    if( dap_chain_wallet_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain wallet module");
        return -61;
    }

    if( dap_chain_net_srv_init() ){
        log_it(L_CRITICAL,"Can't init dap chain network service module");
        return -66;
    }

    if( dap_chain_net_srv_order_init() )
        return -67;

    if (dap_chain_net_srv_xchange_init()) {
        log_it(L_ERROR, "Can't provide exchange capability");
    }

    if (dap_chain_net_srv_voting_init()) {
        log_it(L_ERROR, "Can't provide voting capability");
    }
    
    if (dap_chain_net_srv_bridge_init()) {
        log_it(L_ERROR, "Can't provide bridge capability");
    }
    
    if (dap_chain_net_srv_stake_lock_init()) {
        log_it(L_ERROR, "Can't start stake lock service");
    }

    if (dap_chain_net_srv_emit_delegate_init()) {
        log_it(L_ERROR, "Can't start stake lock service");
    }
#ifndef _WIN32
#   if !DAP_OS_ANDROID
    if( dap_chain_net_srv_vpn_pre_init() ){
        log_it(L_ERROR, "Can't pre-init vpn service");
    }
#   endif
    if (sig_unix_handler_init(dap_config_get_item_str_default(g_config,
                                                              "resources",
                                                              "pid_path",
                                                              "/tmp")) != 0) {
        log_it(L_CRITICAL,"Can't init sig unix handler module");
        return -12;
    }
#else
    if ( sig_win32_handler_init( NULL ) ) {
        log_it( L_CRITICAL,"Can't init sig win32 handler module" );
        return -12;
    }
#endif

    if ( dap_chain_node_cli_init(g_config) ) {
        log_it( L_CRITICAL, "Can't init server for console" );
        return -11;
    }

    
    if( dap_chain_wallet_cache_init() ) {
        log_it(L_CRITICAL,"Can't init dap chain wallet module");
        return -61;
    }
    
    dap_chain_net_load_all();

    log_it(L_INFO, "Automatic mempool processing %s",
           dap_chain_node_mempool_autoproc_init() ? "enabled" : "disabled");
    
    uint16_t l_listen_addrs_count = 0;
    if ( bServerEnabled )
        l_server = dap_http_server_new("server", dap_get_appname());

    if ( l_server ) { // If listener server is initialized
        // Handshake URL
        enc_http_add_proc( DAP_HTTP_SERVER(l_server), "/"DAP_UPLINK_PATH_ENC_INIT );

        // Streaming URLs
        dap_stream_add_proc_http( DAP_HTTP_SERVER(l_server), "/"DAP_UPLINK_PATH_STREAM );
        dap_stream_ctl_add_proc( DAP_HTTP_SERVER(l_server), "/"DAP_UPLINK_PATH_STREAM_CTL );

        const char *str_start_mempool = dap_config_get_item_str( g_config, "mempool", "accept" );
        if ( str_start_mempool && !strcmp(str_start_mempool, "true")) {
            dap_chain_mempool_add_proc(DAP_HTTP_SERVER(l_server), MEMPOOL_URL);
        }

        if (dap_json_rpc_init(l_server, g_config)) {
            log_it( L_CRITICAL, "Can't init json-rpc" );
            return -12;
        } 


        // Built in WWW server
#if !DAP_OS_ANDROID
        if (  dap_config_get_item_bool_default(g_config,"www","enabled",false)  ){
                dap_http_folder_add( DAP_HTTP_SERVER(l_server), "/",
                                dap_config_get_item_str(g_config,
                                                            "resources",
                                                            "www_root") );
        }
#endif
        dap_server_set_default(l_server);
        dap_http_simple_proc_add(DAP_HTTP_SERVER(l_server), "/"DAP_UPLINK_PATH_NODE_LIST, 2048, dap_chain_net_node_check_http_issue_link);
        if ( dap_config_get_item_bool_default(g_config, "bootstrap_balancer", "http_server", false) ) {
            log_it(L_DEBUG, "HTTP balancer enabled");
            dap_http_simple_proc_add(DAP_HTTP_SERVER(l_server), "/"DAP_UPLINK_PATH_BALANCER,
                                     DAP_BALANCER_MAX_REPLY_SIZE, dap_chain_net_balancer_http_issue_link);
        }
        if ( dap_config_get_item_bool_default(g_config, "bootstrap_balancer", "dns_server", false) ) {
            log_it(L_DEBUG, "DNS balancer enabled");
            dap_dns_server_start("bootstrap_balancer");
        }
    } else
        log_it( L_INFO, "No enabled server, working in client mode only" );

#if defined(DAP_OS_DARWIN) || ( defined(DAP_OS_LINUX) && ! defined (DAP_OS_ANDROID))
    // vpn server
    if(dap_config_get_item_bool_default(g_config, "srv_vpn", "enabled", false)) {
        if(dap_chain_net_srv_vpn_init(g_config) != 0) {
            log_it(L_ERROR, "Can't init dap chain network service vpn module");
            return -70;
        }
    }
    // vpn client
    if(dap_chain_net_vpn_client_init(g_config) != 0) {
        log_it(L_ERROR, "Can't init dap chain network service vpn client");
        return -72;
    }

    if(dap_config_get_item_bool_default(g_config, "srv_vpn", "geoip_enabled", false)) {
        if(chain_net_geoip_init(g_config) != 0) {
            log_it(L_CRITICAL, "Can't init geoip module");
            return -73;
        }
    }
#endif

    if(dap_config_get_item_bool_default(g_config,"plugins","enabled",false)){
#ifdef DAP_OS_WINDOWS
        char * l_plugins_path_default = dap_strdup_printf("%s/var/lib/plugins/", g_sys_dir_path);
#else
        char * l_plugins_path_default = dap_strdup_printf("%s/var/lib/plugins", g_sys_dir_path);
#endif
        int rc_plugin_init = 0;
        rc_plugin_init = dap_plugin_init( dap_config_get_item_str_default(g_config, "plugins", "path", l_plugins_path_default) );
        if (rc_plugin_init) {
            log_it(L_ERROR, "The initial initialization for working with manifests and binary plugins failed. Error code %d", rc_plugin_init);    
            DAP_DELETE(l_plugins_path_default);
        } else {
            DAP_DELETE(l_plugins_path_default);
#ifdef DAP_SUPPORT_PYTHON_PLUGINS
            //Init python plugins
            log_it(L_NOTICE, "Loading python plugins");
            dap_plugins_python_app_content_init(l_server);
            rc_plugin_init = dap_chain_plugins_init(g_config);
#endif
            dap_plugin_start_all();

#ifdef DAP_SUPPORT_PYTHON_PLUGINS
            if (!rc_plugin_init) {
                dap_chain_plugins_save_thread(g_config);
            } else {
                log_it(L_ERROR, "Failed to initialize python-cellframe plugins. Error code %d", rc_plugin_init);
            }
#endif
        }
    }
    dap_chain_net_try_online_all();
    dap_chain_net_announce_addr_all();
    rc = dap_events_wait();
    log_it( rc ? L_CRITICAL : L_NOTICE, "Server loop stopped with return code %d", rc );
    // Deinit modules

//failure:
    if(dap_config_get_item_bool_default(g_config,"plugins","enabled",false)){
        dap_plugin_stop_all();
        dap_plugin_deinit();
    }

    dap_dns_server_stop();
    dap_stream_deinit();
    dap_stream_ctl_deinit();
#if !DAP_OS_ANDROID
    dap_http_folder_deinit();
#endif
    dap_http_deinit();
    if (bServerEnabled) dap_server_deinit();
    dap_enc_ks_deinit();
    dap_chain_node_mempool_autoproc_deinit();
    dap_chain_net_srv_xchange_deinit();
    dap_chain_net_srv_stake_pos_delegate_deinit();
    dap_chain_net_srv_stake_lock_deinit();
    dap_chain_net_srv_bridge_deinit();
    dap_chain_net_srv_voting_deinit();
    dap_chain_net_deinit();
    dap_global_db_deinit();
    dap_chain_deinit();
    dap_config_close( g_config );
    dap_interval_timer_deinit();
    dap_common_deinit();

    return rc * 10;
}

static struct option long_options[] = {

    { "stop", 0, NULL, 0 },
    { NULL,   0, NULL, 0 } // must be a last element
};

/*void parse_args( int argc, const char **argv ) {

    int opt, option_index = 0, is_daemon = 0;

    while ( (opt = getopt_long(argc, (char *const *)argv, "D0",
                              long_options, &option_index)) != -1) {
        switch ( opt ) {

        case 0: // --stop
        {
#ifndef DAP_OS_WINDOWS
            pid_t pid = get_pid_from_file(s_pid_file_path);

            if ( pid == 0 ) {
                log_it( L_ERROR, "Can't read pid from file" );
                exit( -20 );
            }

            if ( kill_process(pid) ) {
                log_it( L_INFO, "Server successfully stopped" );
                exit( 0 );
            }

            log_it( L_WARNING, "Server not stopped. Maybe he is not running now?" );
            exit( -21 );
#else
    // TODO OpenEvent + SetEvent
                exit (-22);
#endif

        }

        case 'D':
        {
            log_it( L_INFO, "Daemonize server starting..." );
            //exit_if_server_already_running( );
            is_daemon = 1;
            daemonize_process( );
            break;
        }

        default:
        log_it( L_WARNING, "Unknown option from command line" );
        }
    }

    //if( !is_daemon )
    //    exit_if_server_already_running( );
}*/

int s_proc_running_check(const char *a_path) {
#ifdef DAP_OS_WINDOWS
    CreateEvent(0, TRUE, FALSE, a_path);
    return GetLastError() == ERROR_ALREADY_EXISTS ? ( log_it(L_ERROR, "dap_server is already running"), 1 ) : 0;
#else
    FILE *l_pidfile = fopen(a_path, "r");
    if (l_pidfile) {
        pid_t f_pid = 0;
        if ( fscanf( l_pidfile, "%d", &f_pid ) && lockf(fileno(l_pidfile), F_TEST, 0) == -1 ) {
            return log_it(L_ERROR, "Error %d: \"%s\", dap_server is already running with PID %d",
                           errno, dap_strerror(errno), f_pid), 1;
        }
        else
            l_pidfile = freopen(a_path, "w", l_pidfile);
    } else
        l_pidfile = fopen(a_path, "w");
    
    if (!l_pidfile)
        return log_it(L_ERROR, "Can't open file %s for writing, error %d: %s", 
                                a_path, errno, dap_strerror(errno)), 2;
    fprintf(l_pidfile, "%d", getpid());
    fflush(l_pidfile);
    return lockf(fileno(l_pidfile), F_TLOCK, sizeof(pid_t));
#endif
}