# -*- coding: utf8 -*-
from __future__ import print_function
from __future__ import absolute_import
from builtins import zip
from builtins import next
from builtins import range
import copy
import json
import os
import signal
import sys
from logging import StreamHandler
from uuid import uuid4

import click
import ethereum.slogging as slogging
import gevent
import rlp
from click import BadParameter
from devp2p.app import BaseApp
from devp2p.discovery import NodeDiscovery
from devp2p.peermanager import PeerManager
from devp2p.service import BaseService
from ethereum import config as eth_config
from ethereum.block import Block
from ethereum.snapshot import create_snapshot, load_snapshot as _load_snapshot
from ethereum.utils import (
    encode_hex,
    decode_hex,
    to_string,
)
from gevent.event import Event

from . import config as app_config
from . import eth_protocol
from . import utils
from .accounts import AccountsService, Account
from .console_service import Console
from .db_service import DBService
from .eth_service import ChainService
from .jsonrpc import JSONRPCServer, IPCRPCServer
from .pow_service import PoWService
from pyethapp import __version__
from pyethapp.profiles import PROFILES, DEFAULT_PROFILE
from pyethapp.utils import merge_dict, load_contrib_services, FallbackChoice, \
                           enable_greenlet_debugger


log = slogging.get_logger('app')

services = [DBService, AccountsService, NodeDiscovery, PeerManager, ChainService,
            PoWService, JSONRPCServer, IPCRPCServer, Console]


class EthApp(BaseApp):
    client_name = 'pyethapp'
    client_version = '%s/%s/%s' % (__version__, sys.platform,
                                   'py%d.%d.%d' % sys.version_info[:3])
    client_version_string = '%s/v%s' % (client_name, client_version)
    start_console = False
    default_config = dict(BaseApp.default_config)
    default_config['client_version_string'] = client_version_string
    default_config['post_app_start_callback'] = None
    script_globals = {}


# TODO: Remove `profile` fallbacks in 1.4 or so
# Separators should be underscore!
@click.group(help='Welcome to {} {}'.format(EthApp.client_name, EthApp.client_version))
@click.option('--profile', type=FallbackChoice(
                  list(PROFILES.keys()),
                  {'frontier': 'livenet', 'morden': 'testnet'},
                  "PyEthApp's configuration profiles have been renamed to "
                  "'livenet' and 'testnet'. The previous values 'frontier' and "
                  "'morden' will be removed in a future update."),
              default=DEFAULT_PROFILE, help="Configuration profile.", show_default=True)
@click.option('alt_config', '--Config', '-C', type=str, callback=app_config.validate_alt_config_file,
              help='Alternative config file')
@click.option('config_values', '-c', multiple=True, type=str,
              help='Single configuration parameters (<param>=<value>)')
@click.option('alt_data_dir', '-d', '--data-dir', multiple=False, type=str,
              help='data directory', default=app_config.default_data_dir, show_default=True)
@click.option('-l', '--log_config', multiple=False, type=str, default=":info",
              help='log_config string: e.g. ":info,eth:debug', show_default=True)
@click.option('--log-json/--log-no-json', default=False,
              help='log as structured json output')
@click.option('--log-file', type=click.Path(dir_okay=False, writable=True, resolve_path=True),
              help="Log to file instead of stderr.")
@click.option('-b', '--bootstrap_node', multiple=False, type=str,
              help='single bootstrap_node as enode://pubkey@host:port')
@click.option('-m', '--mining_pct', multiple=False, type=int, default=0,
              help='pct cpu used for mining')
@click.option('--unlock', multiple=True, type=str,
              help='Unlock an account (prompts for password)')
@click.option('--password', type=click.File(), help='path to a password file')
@click.pass_context
def app(ctx, profile, alt_config, config_values, alt_data_dir, log_config,
        bootstrap_node, log_json, mining_pct, unlock, password, log_file):
    # configure logging
    slogging.configure(log_config, log_json=log_json, log_file=log_file)

    # data dir default or from cli option
    alt_data_dir = os.path.expanduser(alt_data_dir)
    data_dir = alt_data_dir or app_config.default_data_dir
    app_config.setup_data_dir(data_dir)  # if not available, sets up data_dir and required config
    log.info('using data in', path=data_dir)

    # prepare configuration
    # config files only contain required config (privkeys) and config different from the default
    if alt_config:  # specified config file
        config = app_config.load_config(alt_config)
        if not config:
            log.warning('empty config given. default config values will be used')
    else:  # load config from default or set data_dir
        config = app_config.load_config(data_dir)

    config['data_dir'] = data_dir

    # Store custom genesis to restore if overridden by profile value
    genesis_from_config_file = config.get('eth', {}).get('genesis')

    # Store custom network_id to restore if overridden by profile value
    network_id_from_config_file = config.get('eth', {}).get('network_id')

    # Store custom bootstrap_nodes to restore them overridden by profile value
    bootstrap_nodes_from_config_file = config.get('discovery', {}).get('bootstrap_nodes')

    # add default config
    app_config.update_config_with_defaults(config, app_config.get_default_config([EthApp] + services))
    app_config.update_config_with_defaults(config, {'eth': {'block': eth_config.default_config}})

    # Set config values based on profile selection
    merge_dict(config, PROFILES[profile])

    if genesis_from_config_file:
        # Fixed genesis_hash taken from profile must be deleted as custom genesis loaded
        del config['eth']['genesis_hash']
        config['eth']['genesis'] = genesis_from_config_file

    if network_id_from_config_file:
        del config['eth']['network_id']
        config['eth']['network_id'] = network_id_from_config_file

    if bootstrap_nodes_from_config_file:
        # Fixed bootstrap_nodes taken from profile must be deleted as custom bootstrap_nodes loaded
        del config['discovery']['bootstrap_nodes']
        config['discovery']['bootstrap_nodes'] = bootstrap_nodes_from_config_file

    pre_cmd_line_config_genesis = config.get('eth', {}).get('genesis')
    # override values with values from cmd line
    for config_value in config_values:
        try:
            app_config.set_config_param(config, config_value)
        except ValueError:
            raise BadParameter('Config parameter must be of the form "a.b.c=d" whe
re "a.b.c" '
                               'specifies the parameter to set and d is a valid yaml value '
                               '(example: "-c jsonrpc.port=5000")')

    if pre_cmd_line_config_genesis != config.get('eth', {}).get('genesis'):
        # Fixed genesis_hash taked from profile must be d
eleted as custom genesis loaded
        if 'genesis_hash' in config['eth']:
            del config['eth']['genesis_hash']

    # Load genesis config
    app_config.update_config_from_genesis_json(config,
                                               genesis_json_filename_or_dict=config['eth']['genesis'])
    if bootstrap_node:
        # [NOTE]: check it
        config['discovery']['bootstrap_nodes'] = [to_string(bootstrap_node)]
    if mining_pct > 0:
        config['pow']['activated'] = True
        config['pow']['cpu_pct'] = int(min(100, mining_pct))
    if not config.get('pow', {}).get('activated'):
        config['deactivated_services'].append(PoWService.name)

    ctx.obj = {'config': config,
               'unlock': unlock,
               'password': password.read().rstrip() if password else None,
               'log_file': log_file}
    assert (password and ctx.obj['password'] is not None and len(
        ctx.obj['password'])) or not password, "empty password file"


@app.command()
@click.option('--dev/--nodev', default=False,
              help='Drop into interactive debugg