#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.

import os
import sys
import json
import shutil
import signal
import logging
import traceback
import subprocess

import basicswap.config as cfg
from basicswap import __version__
from basicswap.ui.util import getCoinName
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer


logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
    logger.addHandler(logging.StreamHandler(sys.stdout))

swap_client = None


class Daemon:
    __slots__ = ('handle', 'files')

    def __init__(self, handle, files):
        self.handle = handle
        self.files = files


def is_known_coin(coin_name: str) -> bool:
    for k, v in chainparams:
        if coin_name == v['name']:
            return True
    return False


def signal_handler(sig, frame):
    global swap_client
    logger.info('Signal %d detected, ending program.' % (sig))
    if swap_client is not None:
        swap_client.stopRunning()


def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
    daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))

    datadir_path = os.path.expanduser(node_dir)

    # Rewrite litecoin.conf for 0.21.3
    ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf')
    if os.path.exists(ltc_conf_path):
        config_to_add = ['blockfilterindex=0', 'peerblockfilters=0']
        with open(ltc_conf_path) as fp:
            for line in fp:
                line = line.strip()
                if line in config_to_add:
                    config_to_add.remove(line)

        if len(config_to_add) > 0:
            logging.info('Rewriting litecoin.conf')
            shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last')
            with open(ltc_conf_path, 'a') as fp:
                for line in config_to_add:
                    fp.write(line + '\n')

    args = [daemon_bin, ]
    add_datadir: bool = extra_config.get('add_datadir', True)
    if add_datadir:
        args.append('-datadir=' + datadir_path)
    args += opts
    logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else ''))

    opened_files = []
    if extra_config.get('stdout_to_file', False):
        stdout_dest = open(os.path.join(datadir_path, extra_config.get('stdout_filename', 'core_stdout.log')), 'w')
        opened_files.append(stdout_dest)
        stderr_dest = stdout_dest
    else:
        stdout_dest = subprocess.PIPE
        stderr_dest = subprocess.PIPE

    return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)


def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
    daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))

    datadir_path = os.path.expanduser(node_dir)
    args = [daemon_bin, '--non-interactive', '--config-file=' + os.path.join(datadir_path, 'monerod.conf')] + opts
    logging.info('Starting node {} --data-dir={}'.format(daemon_bin, node_dir))

    # return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    file_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w')
    file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w')
    return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=file_stdout, stderr=file_stderr, cwd=datadir_path), [file_stdout, file_stderr])


def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
    daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
    args = [daemon_bin, '--non-interactive']

    needs_rewrite: bool = False
    config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy=']

    data_dir = os.path.expanduser(node_dir)
    config_path = os.path.join(data_dir, 'monero_wallet.conf')
    if os.path.exists(config_path):
        args += ['--config-file=' + config_path]
        with open(config_path) as fp:
            for line in fp:
                if any(line.startswith(config_line) for config_line in config_to_remove):
                    logging.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
                    needs_rewrite = True
    args += opts

    if needs_rewrite:
        logging.info('Rewriting monero_wallet.conf')
        shutil.copyfile(config_path, config_path + '.last')
        with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to:
            for line in fp_from:
                if not any(line.startswith(config_line) for config_line in config_to_remove):
                    fp_to.write(line)

    logging.info('Starting wallet daemon {} --wallet-dir={}'.format(daemon_bin, node_dir))

    # TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
    wallet_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w')
    wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w')
    return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir), [wallet_stdout, wallet_stderr])


def ws_new_client(client, server):
    if swap_client:
        swap_client.log.debug(f'ws_new_client {client["id"]}')


def ws_client_left(client, server):
    if client is None:
        return
    if swap_client:
        swap_client.log.debug(f'ws_client_left {client["id"]}')


def ws_message_received(client, server, message):
    if len(message) > 200:
        message = message[:200] + '..'
    if swap_client:
        swap_client.log.debug(f'ws_message_received {client["id"]} {message}')


def runClient(fp, data_dir, chain, start_only_coins):
    global swap_client
    daemons = []
    pids = []
    threads = []
    settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
    pids_path = os.path.join(data_dir, '.pids')

    if os.getenv('WALLET_ENCRYPTION_PWD', '') != '':
        raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.')

    if not os.path.exists(settings_path):
        raise ValueError('Settings file not found: ' + str(settings_path))

    with open(settings_path) as fs:
        settings = json.load(fs)

    swap_client = BasicSwap(fp, data_dir, settings, chain)

    if os.path.exists(pids_path):
        with open(pids_path) as fd:
            for ln in fd:
                # TODO: try close
                logger.warning('Found pid for daemon {} '.format(ln.strip()))

    # Ensure daemons are stopped
    swap_client.stopDaemons()

    # Settings may have been modified
    settings = swap_client.settings
    try:
        # Try start daemons
        for c, v in settings['chainclients'].items():

            if len(start_only_coins) > 0 and c not in start_only_coins:
                continue
            try:
                coin_id = swap_client.getCoinIdFromName(c)
                display_name = getCoinName(coin_id)
            except Exception as e:
                logger.warning('Error getting coin display name for {}: {}'.format(c, str(e)))
                display_name = 'Unknown'
            if c == 'monero':
                if v['manage_daemon'] is True:
                    swap_client.log.info(f'Starting {display_name} daemon')
                    filename = 'monerod' + ('.exe' if os.name == 'nt' else '')
                    daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
                    pid = daemons[-1].handle.pid
                    swap_client.log.info('Started {} {}'.format(filename, pid))

                if v['manage_wallet_daemon'] is True:
                    swap_client.log.info(f'Starting {display_name} wallet daemon')
                    daemon_addr = '{}:{}'.format(v['rpchost'], v['rpcport'])
                    trusted_daemon: bool = swap_client.getXMRTrustedDaemon(coin_id, v['rpchost'])
                    opts = ['--daemon-address', daemon_addr, ]

                    proxy_log_str = ''
                    proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost'])
                    if proxy_host:
                        proxy_log_str = ' through proxy'
                        opts += ['--proxy', f'{proxy_host}:{proxy_port}', ]

                    swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str))

                    daemon_rpcuser = v.get('rpcuser', '')
                    daemon_rpcpass = v.get('rpcpassword', '')
                    if daemon_rpcuser != '':
                        opts.append('--daemon-login')
                        opts.append(daemon_rpcuser + ':' + daemon_rpcpass)

                    opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
                    filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
                    daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
                    pid = daemons[-1].handle.pid
                    swap_client.log.info('Started {} {}'.format(filename, pid))

                continue
            if v['manage_daemon'] is True:
                swap_client.log.info(f'Starting {display_name} daemon')

                filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
                daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
                pid = daemons[-1].handle.pid
                pids.append((c, pid))
                swap_client.setDaemonPID(c, pid)
                swap_client.log.info('Started {} {}'.format(filename, pid))
        if len(pids) > 0:
            with open(pids_path, 'w') as fd:
                for p in pids:
                    fd.write('{}:{}\n'.format(*p))

        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)

        if len(start_only_coins) > 0:
            logger.info(f'Only running {start_only_coins}. Manually exit with Ctrl + c when ready.')
            while not swap_client.delay_event.wait(0.5):
                pass
        else:
            swap_client.start()
            if 'htmlhost' in settings:
                swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
                allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
                thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
                threads.append(thread_http)
                thread_http.start()

            if 'wshost' in settings:
                ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport'])
                swap_client.log.info(f'Starting ws server at {ws_url}.')

                swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport'])
                swap_client.ws_server.set_fn_new_client(ws_new_client)
                swap_client.ws_server.set_fn_client_left(ws_client_left)
                swap_client.ws_server.set_fn_message_received(ws_message_received)
                swap_client.ws_server.run_forever(threaded=True)

            logger.info('Exit with Ctrl + c.')
            while not swap_client.delay_event.wait(0.5):
                swap_client.update()

    except Exception as ex:
        traceback.print_exc()

    if swap_client.ws_server:
        try:
            swap_client.log.info('Stopping websocket server.')
            swap_client.ws_server.shutdown_gracefully()
        except Exception as ex:
            traceback.print_exc()

    swap_client.finalise()
    swap_client.log.info('Stopping HTTP threads.')
    for t in threads:
        try:
            t.stop()
            t.join()
        except Exception as ex:
            traceback.print_exc()

    closed_pids = []
    for d in daemons:
        swap_client.log.info('Interrupting {}'.format(d.handle.pid))
        try:
            d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT)
        except Exception as e:
            swap_client.log.info('Interrupting %d, error %s', d.handle.pid, str(e))
    for d in daemons:
        try:
            d.handle.wait(timeout=120)
            for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files:
                if fp:
                    fp.close()
            closed_pids.append(d.handle.pid)
        except Exception as ex:
            swap_client.log.error('Error: {}'.format(ex))

    if os.path.exists(pids_path):
        with open(pids_path) as fd:
            lines = fd.read().split('\n')
        still_running = ''
        for ln in lines:
            try:
                if not int(ln.split(':')[1]) in closed_pids:
                    still_running += ln + '\n'
            except Exception:
                pass
        with open(pids_path, 'w') as fd:
            fd.write(still_running)


def printVersion():
    logger.info('Basicswap version: %s', __version__)


def printHelp():
    print('Usage: basicswap-run ')
    print('\n--help, -h               Print help.')
    print('--version, -v            Print version.')
    print('--datadir=PATH           Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR))
    print('--mainnet                Run in mainnet mode.')
    print('--testnet                Run in testnet mode.')
    print('--regtest                Run in regtest mode.')
    print('--startonlycoin          Only start the provides coin daemon/s, use this if a chain requires extra processing.')


def main():
    data_dir = None
    chain = 'mainnet'
    start_only_coins = set()

    for v in sys.argv[1:]:
        if len(v) < 2 or v[0] != '-':
            logger.warning('Unknown argument %s', v)
            continue

        s = v.split('=')
        name = s[0].strip()

        for i in range(2):
            if name[0] == '-':
                name = name[1:]

        if name == 'v' or name == 'version':
            printVersion()
            return 0
        if name == 'h' or name == 'help':
            printHelp()
            return 0

        if name in ('mainnet', 'testnet', 'regtest'):
            chain = name
            continue

        if len(s) == 2:
            if name == 'datadir':
                data_dir = os.path.expanduser(s[1])
                continue
        if name == 'startonlycoin':
            for coin in [s.lower() for s in s[1].split(',')]:
                if is_known_coin(coin) is False:
                    raise ValueError(f'Unknown coin: {coin}')
                start_only_coins.add(coin)
            continue

        logger.warning('Unknown argument %s', v)

    if data_dir is None:
        data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
    logger.info('Using datadir: %s', data_dir)
    logger.info('Chain: %s', chain)

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
        logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n')
        runClient(fp, data_dir, chain, start_only_coins)

    logger.info('Done.')
    return swap_client.fail_code if swap_client is not None else 0


if __name__ == '__main__':
    main()