import logging

from zope.interface import implements
from lbrynet.core.PaymentRateManager import PaymentRateManager
from lbrynet.lbryfilemanager.EncryptedFileCreator import create_lbry_file
from lbrynet.lbryfilemanager.EncryptedFileDownloader import ManagedEncryptedFileDownloader
from lbrynet.lbryfile.StreamDescriptor import publish_sd_blob, create_plain_sd
from lbrynet.lbrynet_console.interfaces import ICommandHandler, ICommandHandlerFactory
from lbrynet.core.StreamDescriptor import download_sd_blob
from lbrynet.core.Error import UnknownNameError, InvalidBlobHashError, InsufficientFundsError
from lbrynet.core.Error import InvalidStreamInfoError
from lbrynet.core.utils import is_valid_blobhash
from twisted.internet import defer, threads
import datetime
import os


log = logging.getLogger(__name__)


class InvalidChoiceError(Exception):
    pass


class InvalidValueError(Exception):
    pass


#class ControlHandlerFactory(object):
#    implements(IControlHandlerFactory)

#    control_handler_class = None

#    def get_prompt_description(self):
#        return self.control_handler_class.prompt_description

#    def __init__(self, *args):
#        self.args = args

#    def get_handler(self):
#        args = self.args
#        return self.control_handler_class(*args)


#class ControlHandler(object):
#    implements(IControlHandler)

#    prompt_description = None


def get_log_file():
    log_file = "console.log"
    logging_handlers = logging.getLogger().handlers
    if len(logging_handlers):
        log_file = logging_handlers[0].baseFilename
    return log_file


class RoundedTime(object):
    SECOND = 0
    MINUTE = 1
    HOUR = 2
    DAY = 3
    WEEK = 4
    units = ['second', 'minute', 'hour', 'day', 'week']

    def __init__(self, unit, val):
        assert unit < len(self.units)
        self.unit = unit
        self.val = val

    def __str__(self):
        assert self.unit < len(self.units)
        unit_str = self.units[self.unit]
        if self.val != 1:
            unit_str += "s"
        return "%d %s" % (self.val, unit_str)


def get_time_behind_blockchain(best_block_time):
    best_time = datetime.datetime.utcfromtimestamp(best_block_time)
    diff = datetime.datetime.utcnow() - best_time
    if diff.days > 0:
        if diff.days >= 7:
            val = diff.days // 7
            unit = RoundedTime.WEEK
        else:
            val = diff.days
            unit = RoundedTime.DAY
    elif diff.seconds >= 60 * 60:
        val = diff.seconds // (60 * 60)
        unit = RoundedTime.HOUR
    elif diff.seconds >= 60:
        val = diff.seconds // 60
        unit = RoundedTime.MINUTE
    else:
        val = diff.seconds
        unit = RoundedTime.SECOND
    return RoundedTime(unit, val)


class CommandHandlerFactory(object):
    implements(ICommandHandlerFactory)
    priority = 0
    short_help = "This should be overridden"
    full_help = "This should really be overridden"
    command = "this-must-be-overridden"
    control_handler_class = None

    def __init__(self, *args):
        self.args = args

    def get_prompt_description(self):
        return self.control_handler_class.prompt_description

    def get_handler(self, console):
        return self.control_handler_class(console, *self.args) # pylint: disable=not-callable


class CommandHandler(object):
    implements(ICommandHandler)

    prompt_description = None

    def __init__(self, console):
        self.console = console
        self.finished_deferred = defer.Deferred()

    def start(self):
        pass

    def handle_line(self, line):
        pass


def get_shortcuts_for_options(option_names):
    shortcut_keys = []
    names_with_shortcuts = []
    for option_name in option_names:
        name_with_shortcut = ''
        found_shortcut = False
        for c in option_name:
            if not found_shortcut and not c.lower() in shortcut_keys:
                name_with_shortcut += '[' + c.lower() + ']'
                shortcut_keys.append(c.lower())
                found_shortcut = True
            else:
                name_with_shortcut += c
        if found_shortcut is False:
            shortcut_keys.append("")
        names_with_shortcuts.append(name_with_shortcut)
    return shortcut_keys, names_with_shortcuts


class RecursiveCommandHandler(CommandHandler):

    def __init__(self, console, exit_after_one_done=False, reset_after_each_done=False):
        CommandHandler.__init__(self, console)
        self.current_handler = None
        self.exit_after_one_done = exit_after_one_done
        self.reset_after_each_done = reset_after_each_done
        self._set_control_handlers()

    def _get_control_handler_factories(self):
        raise NotImplementedError()

    def _set_control_handlers(self):
        self.control_handlers = {i + 1: handler for i, handler in enumerate(self._get_control_handler_factories())}

    def start(self):
        self._show_prompt()

    def handler_finished(self):
        self.current_handler = None
        if self.exit_after_one_done is True:
            self.finished_deferred.callback(None)
        else:
            if self.reset_after_each_done:
                self._set_control_handlers()
            self._show_prompt()

    def handler_failed(self, err):
        log.error("An error occurred in some handler: %s", err.getTraceback())
        self.finished_deferred.callback(None)

    def handle_line(self, line):
        if self.current_handler is None:
            if line is None:
                num = None
            else:
                try:
                    num = int(line)
                except ValueError:
                    num = None
            if num == 0:
                self.finished_deferred.callback(None)
                return
            if num in self.control_handlers:
                self.current_handler = self.control_handlers[num].get_handler(self.console)
                self.current_handler.finished_deferred.addCallbacks(lambda _: self.handler_finished(),
                                                                    self.handler_failed)
                self.current_handler.start()
                return
        if self.current_handler is not None:
            self.current_handler.handle_line(line)
            return
        if self.current_handler is None:
            self._show_prompt()

    def _show_prompt(self):
        prompt_string = "Options:\n"
        prompt_string += "[0] Exit this menu\n"
        for num, handler in self.control_handlers.iteritems():
            prompt_string += "[" + str(num) + "] " + handler.get_prompt_description() + "\n"
        self.console.sendLine(prompt_string)


class ModifyPaymentRate(CommandHandler):

    def __init__(self, console):
        CommandHandler.__init__(self, console)
        self._prompt_choices = {'cancel': (self._cancel, "Don't change anything")}
        self.got_input = False

    def start(self):
        self._show_prompt_string()

    def handle_line(self, line):
        if self.got_input is False:
            self.got_input = True
            if line.lower() in self._prompt_choices:
                d = self._prompt_choices[line.lower()][0]()
                d.addCallback(self._choice_made)
            else:
                try:
                    rate = float(line)
                except ValueError:
                    self.console.sendLine("Rate must be a number")
                    self.finished_deferred.callback(None)
                    return
                d = self._set_rate(rate)

                d.addCallback(lambda _: self._choice_made("Successfully set the rate"))

    @staticmethod
    def _cancel():
        return defer.succeed("No change was made")

    def _set_rate(self, rate):
        pass

    def _get_current_status(self):
        pass

    def _choice_made(self, result=None):
        if result is not None:
            self.console.sendLine(result)
        self.finished_deferred.callback(None)

    def _show_prompt_string(self):
        prompt_string = self._get_current_status() + "\n"
        for prompt_choice, (func, help_string) in self._prompt_choices.iteritems():
            prompt_string += prompt_choice + ": " + help_string + "\n"
        prompt_string += "To change the current rate, enter the desired rate\n"
        prompt_string += "Then hit enter\n"
        self.console.sendLine(prompt_string)


class ApplicationStatus(CommandHandler):
    #prompt_description = "Application Status"

    def __init__(self, console, rate_limiter, dht_node):
        CommandHandler.__init__(self, console)
        self.rate_limiter = rate_limiter
        self.dht_node = dht_node

    def start(self):
        d = self._show_status()
        d.chainDeferred(self.finished_deferred)
        return d

    def _show_status(self):
        status = "Total bytes uploaded: " + str(self.rate_limiter.total_ul_bytes) + "\n"
        status += "Total bytes downloaded: " + str(self.rate_limiter.total_dl_bytes) + "\n"
        if self.dht_node is not None:
            status += "Approximate number of nodes in DHT: " + str(self.dht_node.getApproximateTotalDHTNodes()) + "\n"
            status += "Approximate number of blobs in DHT: " + str(self.dht_node.getApproximateTotalHashes()) + "\n"
        self.console.sendLine(status)
        return defer.succeed(None)


class ApplicationStatusFactory(CommandHandlerFactory):
    control_handler_class = ApplicationStatus
    command = "application-status"
    short_help = "Show application status"
    full_help = "Show total bytes uploaded to other peers, total bytes downloaded from peers," \
                " approximate number of nodes in the DHT, and approximate number of hashes" \
                " in the DHT"


class GetWalletBalances(CommandHandler):
    #prompt_description = "Show wallet point balances"

    def __init__(self, console, wallet):
        CommandHandler.__init__(self, console)
        self.wallet = wallet

    def start(self):
        d = self._get_wallet_balances()
        d.chainDeferred(self.finished_deferred)
        return d

    #def handle_line(self, line):
    #    assert line is None, "Show wallet balances should not be passed any arguments"
    #    return True, self._get_wallet_balances()

    def _show_time_behind_blockchain(self, rounded_time):
        if rounded_time.unit >= RoundedTime.HOUR:
            self.console.sendLine("\n\nYour balance may be out of date. This application\n"
                                  "is %s behind the LBC blockchain. It may take a few minutes to\n"
                                  "catch up the first time you run this early version of LBRY.\n"
                                  "Please be patient =).\n\n" % str(rounded_time))
        else:
            self.console.sendLine("")

    def _log_recent_blocktime_error(self, err):
        log.error("An error occurred looking up the most recent blocktime: %s", err.getTraceback())
        self.console.sendLine("")

    def _get_wallet_balances(self):
        d = self.wallet.get_balance()

        def format_balance(balance):
            if balance == 0:
                balance = 0
            balance_string = "balance: " + str(balance) + " LBC"
            self.console.sendLine(balance_string)
            d = self.wallet.get_most_recent_blocktime()
            d.addCallback(get_time_behind_blockchain)
            d.addCallback(self._show_time_behind_blockchain)
            d.addErrback(self._log_recent_blocktime_error)
            return d

        d.addCallback(format_balance)
        return d


class GetWalletBalancesFactory(CommandHandlerFactory):
    control_handler_class = GetWalletBalances
    priority = 10
    command = "balance"
    short_help = "Show LBRYcrd balance"
    full_help = "Show the LBRYcrd balance of the wallet to which this application is connected"


class GetNewWalletAddress(CommandHandler):
    #prompt_description = "Get a new LBRYcrd address"

    def __init__(self, console, wallet):
        CommandHandler.__init__(self, console)
        self.wallet = wallet

    def start(self):
        d = self._get_new_address()
        d.chainDeferred(self.finished_deferred)
        return d

    def _get_new_address(self):
        #assert line is None, "Get new LBRYcrd address should not be passed any arguments"
        d = self.wallet.get_new_address()

        def show_address(address):
            self.console.sendLine(str(address))

        d.addCallback(show_address)
        return d


class GetNewWalletAddressFactory(CommandHandlerFactory):
    control_handler_class = GetNewWalletAddress
    command = "get-new-address"
    short_help = "Get a new LBRYcrd address"
    full_help = "Get a new LBRYcrd address from the wallet to which this application is connected"


class ShutDown(CommandHandler):
    #prompt_description = "Shut down"

    def __init__(self, console, lbry_service):
        CommandHandler.__init__(self, console)
        self.lbry_service = lbry_service

    def start(self):
        d = self._shut_down()
        return d

    #def handle_line(self, line):
    #    assert line is None, "Shut down should not be passed any arguments"
    #    return True, self._shut_down()

    def _shut_down(self):
        #d = self.lbry_service.shut_down()

        #def stop_reactor():
        from twisted.internet import reactor
        self.console.sendLine("Shutting down.")
        reactor.stop()

        #d.addBoth(lambda _: stop_reactor())
        return defer.succeed(True)


class ShutDownFactory(CommandHandlerFactory):
    control_handler_class = ShutDown
    priority = 5
    command = "exit"
    short_help = "Shut down"
    full_help = "Shut down"


class EncryptedFileStatus(CommandHandler):
    #prompt_description = "Print status information for all LBRY Files"

    def __init__(self, console, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file_manager = lbry_file_manager

    def start(self):
        d = self.lbry_file_manager.get_lbry_file_status_reports()
        d.addCallback(self._show_statuses)
        d.chainDeferred(self.finished_deferred)
        return d

    #def handle_line(self, line):
    #    assert line is None, "print status should not be passed any arguments"
    #    d = self.lbry_file_manager.get_lbry_file_status_reports()
    #    d.addCallback(self.format_statuses)
    #    return True, d

    def _show_statuses(self, status_reports):
        status_strings = []
        for status_report in status_reports:
            s = status_report.name + " status: " + status_report.running_status + "\n"
            s += str(status_report.num_completed) + " completed out of " + str(status_report.num_known) + "\n"
            status_strings.append(s)
        self.console.sendLine(''.join(status_strings))


class EncryptedFileStatusFactory(CommandHandlerFactory):
    control_handler_class = EncryptedFileStatus
    command = "lbryfile-status"
    short_help = "Print status information for LBRY files"
    full_help = "Print the status information for all streams that are being saved to disk." \
                "This includes whether the stream is currently downloading and the progress" \
                "of the download."


class AddStream(CommandHandler):
    #prompt_description = None
    #line_prompt = None
    cancel_prompt = "Trying to locate the stream's metadata. Type \"cancel\" to cancel..."
    canceled_message = "Canceled downloading."

    def __init__(self, console, sd_identifier, base_payment_rate_manager, wallet):
        CommandHandler.__init__(self, console)
        self.sd_identifier = sd_identifier
        self.wallet = wallet
        self.loading_metadata_deferred = None
        self.metadata = None
        self.factory = None
        self.factory_choice_strings = None  # (command, command_string, shortcut)
        self.factory_choices = None  # {command: factory}
        self.download_options = []
        self.options_left = []
        self.options_chosen = []
        self.current_option = None
        self.current_choice = None
        self.got_options_response = False
        self.loading_failed = False
        self.payment_rate_manager = PaymentRateManager(base_payment_rate_manager)

    def start(self):
        self.console.sendLine(self.cancel_prompt)
        self.loading_metadata_deferred.addCallback(self._handle_metadata)
        self.loading_metadata_deferred.addErrback(self._handle_load_canceled)
        self.loading_metadata_deferred.addErrback(self._handle_load_failed)

    def handle_line(self, line):
        # first, print that metadata is being looked up. give the option to cancel, and
        # listen for the word cancel
        # when that's done, present the metadata, how to change options, how to cancel,
        # and list the ways to download
        #
        #
        #if line is None:
        #    return False, defer.succeed(self.line_prompt)
        #if self.loading_failed is True:
        #    return True, None
        if self.loading_metadata_deferred is not None:
            if line.lower() == "cancel":
                self.loading_metadata_deferred.cancel()
                self.loading_metadata_deferred = None
            else:
                self.console.sendLine(self.cancel_prompt)
            return

        #if self.metadata is None:
        #    self.loading_metadata_deferred = self._load_metadata(line)
        #    cancel_prompt_d = defer.succeed(self.cancel_prompt)
        #    self.loading_metadata_deferred.addCallback(self._choose_factory)
        #    self.loading_metadata_deferred.addErrback(self._handle_load_canceled)
        #    self.loading_metadata_deferred.addErrback(self._handle_load_failed)
        #    return False, cancel_prompt_d, self.loading_metadata_deferred

        if self.current_option is not None:
            if self.current_choice is None:
                try:
                    self.current_choice = self._get_choice_from_input(line)
                except InvalidChoiceError:
                    self.console.sendLine(self._get_next_option_prompt(invalid_choice=True))
                    return
                choice = self.current_option.option_types[self.current_choice]
                if choice.value == float or choice.value == bool:
                    self.console.sendLine(self._get_choice_value_prompt())
                    return
                else:
                    value = choice.value
            else:
                try:
                    value = self._get_value_for_choice(line)
                except InvalidValueError:
                    self.console.sendLine(self._get_choice_value_prompt(invalid_value=True))
                    return
            self.options_chosen.append(value)
            self.current_choice = None
            self.current_option = None
            self.options_left = self.options_left[1:]
            if self.options_left:
                self.console.sendLine(self._get_next_option_prompt())
                return
            else:
                self.current_option = None
                self._show_factory_choices()
                return
        if self.factory_choice_strings is not None:
            command = self._get_factory_choice_command(line)
            if command == "cancel":
                self.console.sendLine(self.canceled_message)
                self.finished_deferred.callback(None)
            elif command == "options":
                self.options_left = self.download_options[:]
                self.options_chosen = []
                self.console.sendLine(self._get_next_option_prompt())
            else:
                if command in self.factory_choices:
                    self.factory = self.factory_choices[command]
                    self._start_download()
                    self.console.sendLine("Downloading in the background. Use the command 'status'\n"
                                          "to check the status of the download.")
                    self.finished_deferred.callback(None)
                else:
                    self._show_factory_choices()
            return

        #if self.factory is None:
        #    try:
        #        choice = int(line)
        #    except ValueError:
        #        return False, defer.succeed(self._show_factory_choices())
        #    if choice in xrange(len(self.metadata.factories)):
        #        self.factory = self.metadata.factories[choice]
        #        return False, defer.succeed(self._show_info_and_options())
        #    else:
        #        return False, defer.succeed(self._show_factory_choices())
        #if self.got_options_response is False:
        #    self.got_options_response = True
        #    if line == 'y' or line == 'Y' and self.options_left:
        #        return False, defer.succeed(self._get_next_option_prompt())
        #    else:
        #        self.options_chosen = [option.default_value for option in self.options_left]
        #        self.options_left = []
        #        return False, defer.succeed(self.line_prompt3)

        #if line == 'y' or line == 'Y':
        #    d = self._start_download()
        #else:
        #    d = defer.succeed("Download cancelled")
        #return True, d

    def _get_choice_from_input(self, line):
        try:
            choice_num = int(line)
        except ValueError:
            raise InvalidChoiceError()
        if 0 <= choice_num < len(self.current_option.option_types):
            return choice_num
        raise InvalidChoiceError()

    def _get_factory_choice_command(self, line):
        for command, printed_command, shortcut in self.factory_choice_strings:
            if line == command or line == shortcut:
                return command

    def _load_metadata(self, sd_file):
        return defer.fail(NotImplementedError())

    def _handle_load_canceled(self, err):
        err.trap(defer.CancelledError)
        self.console.sendLine(self.canceled_message)
        self.finished_deferred.callback(None)

    def _handle_load_failed(self, err):
        self.loading_failed = True
        log.error("An exception occurred attempting to load the stream descriptor: %s", err.getTraceback())
        log_file = get_log_file()
        self.console.sendLine("An unexpected error occurred attempting to load the stream's metadata.\n"
                              "See %s for further details.\n\n" % log_file)
        self.finished_deferred.callback(None)

    def _handle_metadata(self, metadata):
        self.loading_metadata_deferred = None
        self.metadata = metadata
        self.factory_choices = {}
        for factory in self.metadata.factories:
            self.factory_choices[factory.get_description()] = factory
        self.download_options = self.metadata.options.get_downloader_options(self.metadata.validator,
                                                                             self.payment_rate_manager)
        self.options_chosen = [option.default_value for option in self.download_options]
        self.factory_choice_strings = []
        factory_choice_names = ['cancel']
        if self.download_options:
            factory_choice_names.append('options')
        factory_choice_names += self.factory_choices.keys()
        shortcuts, names_with_shortcuts = get_shortcuts_for_options(factory_choice_names)
        self.factory_choice_strings = zip(factory_choice_names, names_with_shortcuts, shortcuts)
        #if len(self.metadata.factories) == 1:
        #    self.factory = self.metadata.factories[0]
        #    return self._show_info_and_options()
        self._show_info_and_options()
        return self._show_factory_choices()

    def _get_estimated_cost_string(self):
        estimated_cost_string = "unknown LBC"
        for option, option_value in zip(self.download_options, self.options_chosen):
            if option.short_description == "data payment rate":
                if option_value == None:
                    rate = self.payment_rate_manager.get_effective_min_blob_data_payment_rate()
                else:
                    rate = option_value
                stream_size = None
                for field, val in self.metadata.validator.info_to_show():
                    if field == "stream_size":
                        stream_size = int(val)
                if stream_size is not None and rate is not None:
                    estimated_cost_string = str(stream_size * 1.0 / 2**20 * rate) + " LBC"
        return estimated_cost_string

    def _show_factory_choices(self):
        prompt = "\n"
        prompt += "Estimated cost: " + self._get_estimated_cost_string()
        prompt += "\n\n"
        for factory_choice_string in self.factory_choice_strings:
            prompt += factory_choice_string[1] + '\n'
        self.console.sendLine(str(prompt))

    def _show_info_and_options(self):
        #self.download_options = self.metadata.options.get_downloader_options(self.metadata.validator,
        #                                                                     self.payment_rate_manager)
        prompt = "Stream info:\n"
        for field_name, value in self._get_info_to_show():
            if field_name == "stream_size":
                value = str(self._get_formatted_stream_size(int(value)))
            prompt += field_name + ": " + value + "\n"
        prompt += "\nOptions:\n"
        for option in self.download_options:
            prompt += option.long_description + ": " + str(option.default_value_description) + "\n"
        self.console.sendLine(str(prompt))

    @staticmethod
    def _get_formatted_stream_size(stream_size):
        if isinstance(stream_size, (int, long)):
            if stream_size >= 2**40:
                units = "TB"
                factor = 2**40
            elif stream_size >= 2**30:
                units = "GB"
                factor = 2**30
            elif stream_size >= 2**20:
                units = "MB"
                factor = 2**20
            elif stream_size >= 2**10:
                units = "KB"
                factor = 2**10
            else:
                return str(stream_size) + " B"
            return "%.1f %s" % (round((stream_size * 1.0 / factor), 1), units)
        return stream_size

    def _get_info_to_show(self):
        return self.metadata.validator.info_to_show()

    def _get_list_of_option_types(self):
        options_string = ""
        for i, option_type in enumerate(self.current_option.option_types):
            options_string += "[%s] %s\n" % (str(i), option_type.long_description)
        options_string += "Enter choice:"
        return options_string

    def _get_choice_value_prompt(self, invalid_value=False):
        choice = self.current_option.option_types[self.current_choice]
        choice_string = ""
        if invalid_value is True:
            "Invalid value entered. Try again.\n"
        if choice.short_description is not None:
            choice_string += choice.short_description + "\n"
        if choice.value == float:
            choice_string += "Enter floating point number (e.g. 1.0):"
        elif choice.value == bool:
            true_string = "Yes"
            false_string = "No"
            if choice.bool_options_description is not None:
                true_string, false_string = choice.bool_options_description
            choice_string += "[0] %s\n[1] %s\nEnter choice:" % (true_string, false_string)
        else:
            NotImplementedError()
        return choice_string

    def _get_value_for_choice(self, choice_input):
        choice = self.current_option.option_types[self.current_choice]
        if choice.value == float:
            try:
                return float(choice_input)
            except ValueError:
                raise InvalidValueError()
        elif choice.value == bool:
            if choice_input == "0":
                return True
            elif choice_input == "1":
                return False
            raise InvalidValueError()
        raise NotImplementedError()

    def _get_next_option_prompt(self, invalid_choice=False):
        assert len(self.options_left), "Something went wrong. There were no options left"
        self.current_option = self.options_left[0]
        choice_string = ""
        if invalid_choice is True:
            choice_string += "Invalid response entered. Try again.\n"

        choice_string += self.current_option.long_description + "\n"
        if len(self.current_option.option_types) > 1:
            choice_string += self._get_list_of_option_types()
        elif len(self.current_option.option_types) == 1:
            self.current_choice = 0
            choice_string += self._get_choice_value_prompt()
        return choice_string

    def _start_download(self):
        d = self._make_downloader()

        def do_download(stream_downloader):
            d = stream_downloader.start()
            d.addCallback(lambda result: self._download_succeeded(stream_downloader, result))
            return d

        d.addCallback(do_download)
        d.addErrback(self._handle_download_error)
        return d

    def _download_succeeded(self, stream_downloader, result):
        self.console.sendLine("%s: %s." % (str(stream_downloader), str(result)))

    def _handle_download_error(self, err):
        if err.check(InsufficientFundsError):
            self.console.sendLine("Download stopped due to insufficient funds.")
            d = self.wallet.get_most_recent_blocktime()
            d.addCallback(get_time_behind_blockchain)
            d.addCallback(self._show_time_behind_blockchain_download)
            d.addErrback(self._log_recent_blockchain_time_error_download)
            d.addCallback(lambda _: self.wallet.is_first_run())
            d.addCallback(self._show_first_run_insufficient_funds)
            d.addErrback(self._log_first_run_check_error)
        else:
            log.error("An unexpected error has caused the download to stop: %s" % err.getTraceback())
            log_file = get_log_file()
            self.console.sendLine("An unexpected error has caused the download to stop:\n%s\n\nSee %s for further details." % (err.getErrorMessage(), log_file))

    def _make_downloader(self):
        return self.factory.make_downloader(self.metadata, self.options_chosen,
                                            self.payment_rate_manager)

    def _show_time_behind_blockchain_download(self, rounded_time):
        if rounded_time.unit >= RoundedTime.HOUR:
            self.console.sendLine("\nThis application is %s behind the LBC blockchain, so some of your\n"
                                  "funds may not be available. Use 'get-blockchain-status' to check if\n"
                                  "your application is up to date with the blockchain.\n\n"
                                  "It may take a few minutes to catch up the first time you run this\n"
                                  "early version of LBRY. Please be patient =).\n\n" % str(rounded_time))

    def _log_recent_blockchain_time_error_download(self, err):
        log.error("An error occurred trying to look up the most recent blocktime: %s", err.getTraceback())

    def _show_first_run_insufficient_funds(self, is_first_run):
        if is_first_run:
            self.console.sendLine("\nThis appears to be the first time you have run LBRY. It can take\n"
                                  "a few minutes for your testing LBC to show up. If you haven't\n"
                                  "received them after a few minutes, please let us know.\n\n"
                                  "Thank you for your patience.\n\n")

    def _log_first_run_check_error(self, err):
        log.error("An error occurred checking if this was the first run: %s", err.getTraceback())


class AddStreamFromSD(AddStream):
    #prompt_description = "Add a stream from a stream descriptor file"
    #line_prompt = "Stream descriptor file name:"

    def start(self, sd_file):
        self.loading_metadata_deferred = self.sd_identifier.get_metadata_for_sd_file(sd_file)
        return AddStream.start(self)


class AddStreamFromSDFactory(CommandHandlerFactory):
    control_handler_class = AddStreamFromSD
    command = "get-sd"
    short_help = "Download a stream from a plaintext stream descriptor file"
    full_help = "Download a stream from a plaintext stream descriptor file.\n" \
                "Takes one argument, the filename of the stream descriptor.\n\n" \
                "get-sd <stream descriptor filename>"


class AddStreamFromHash(AddStream):
    #prompt_description = "Add a stream from a hash"
    #line_prompt = "Stream descriptor hash:"

    def __init__(self, console, sd_identifier, session, wallet):
        AddStream.__init__(self, console, sd_identifier, session.base_payment_rate_manager, wallet)
        self.session = session

    def start(self, sd_hash):
        self.loading_metadata_deferred = download_sd_blob(self.session, sd_hash,
                                                          self.payment_rate_manager)
        self.loading_metadata_deferred.addCallback(self.sd_identifier.get_metadata_for_sd_blob)
        AddStream.start(self)

    def _handle_load_failed(self, err):
        self.loading_failed = True
        if err.check(InvalidBlobHashError):
            self.console.sendLine("The hash you entered is invalid. It must be 96 characters long"
                                  " and contain only hex characters.\n\n")
            self.finished_deferred.callback(None)
            return
        if err.check(InsufficientFundsError):
            self.console.sendLine("Insufficient funds to download the metadata blob.")
            d = self.wallet.get_most_recent_blocktime()
            d.addCallback(get_time_behind_blockchain)
            d.addCallback(self._show_time_behind_blockchain_download)
            d.addErrback(self._log_recent_blockchain_time_error_download)
            d.addCallback(lambda _: self.wallet.is_first_run())
            d.addCallback(self._show_first_run_insufficient_funds)
            d.addErrback(self._log_first_run_check_error)
            d.addCallback(lambda _: self.console.sendLine("\n"))
            d.chainDeferred(self.finished_deferred)
            return
        return AddStream._handle_load_failed(self, err)


class AddStreamFromHashFactory(CommandHandlerFactory):
    control_handler_class = AddStreamFromHash
    command = "get-hash"
    short_help = "Download a stream from a hash"
    full_help = "Download a stream from the hash of the stream descriptor. The stream " \
                "descriptor file will be downloaded from LBRYnet and then read.\n" \
                "Takes one argument, the sha384 hashsum of the stream descriptor.\n\n" \
                "get-hash <stream descriptor sha384 hashsum>"


class AddStreamFromLBRYcrdName(AddStreamFromHash):
    #prompt_description = "Add a stream from a short name"
    #line_prompt = "Short name:"

    def __init__(self, console, sd_identifier, session, wallet):
        AddStreamFromHash.__init__(self, console, sd_identifier, session, wallet)
        self.wallet = wallet
        self.resolved_name = None
        self.description = None
        self.key_fee = None
        self.key_fee_address = None
        self.name = None

    def start(self, name):
        self.name = name
        self.loading_metadata_deferred = self._resolve_name(name)
        self.loading_metadata_deferred.addCallback(lambda stream_hash: download_sd_blob(self.session,
                                                                                        stream_hash,
                                                                                        self.payment_rate_manager))
        self.loading_metadata_deferred.addCallback(self.sd_identifier.get_metadata_for_sd_blob)
        AddStream.start(self)

    def _resolve_name(self, name):
        def get_name_from_info(stream_info):
            if 'stream_hash' not in stream_info:
                raise InvalidStreamInfoError(name)
            self.resolved_name = stream_info.get('name', None)
            self.description = stream_info.get('description', None)
            try:
                if 'key_fee' in stream_info:
                    self.key_fee = float(stream_info['key_fee'])
            except ValueError:
                self.key_fee = None
            self.key_fee_address = stream_info.get('key_fee_address', None)
            return stream_info['stream_hash']
        d = self.wallet.get_most_recent_blocktime()
        d.addCallback(get_time_behind_blockchain)
        d.addCallback(self._show_time_behind_blockchain_resolve)
        d.addErrback(self._log_recent_blockchain_time_error_resolve)
        d.addCallback(lambda _: self.wallet.get_stream_info_for_name(name))
        d.addCallback(get_name_from_info)
        return d

    def _show_time_behind_blockchain_resolve(self, rounded_time):
        if rounded_time.unit >= RoundedTime.HOUR:
            self.console.sendLine("\nThis application is %s behind the LBC blockchain, which may be\n"
                                  "preventing this name from being resolved correctly. Use 'get-blockchain-status'\n"
                                  "to check if your application is up to date with the blockchain.\n\n"
                                  "It may take a few minutes to catch up the first time you run\n"
                                  "this early version of LBRY. Please be patient =).\n\n" % str(rounded_time))
        else:
            self.console.sendLine("\n")

    def _log_recent_blockchain_time_error_resolve(self, err):
        log.error("An error occurred trying to look up the most recent blocktime: %s", err.getTraceback())

    def _handle_load_failed(self, err):
        self.loading_failed = True
        if err.check(UnknownNameError):
            if is_valid_blobhash(self.name):
                self.loading_failed = False
                self.loading_metadata_deferred = None
                AddStreamFromHash.start(self, self.name)
                return
            else:
                self.console.sendLine("The name %s could not be found." % err.getErrorMessage())
                self.finished_deferred.callback(True)
                return
        elif err.check(InvalidBlobHashError):
            self.console.sendLine("The metadata for this name is invalid. The stream cannot be downloaded.\n\n")
            self.finished_deferred.callback(None)
            return
        return AddStreamFromHash._handle_load_failed(self, err)

    def _start_download(self):
        d = self._pay_key_fee()
        d.addCallback(lambda _: AddStream._start_download(self))
        return d

    def _pay_key_fee(self):
        if self.key_fee is not None and self.key_fee_address is not None:
            reserved_points = self.wallet.reserve_points(self.key_fee_address, self.key_fee)
            if reserved_points is None:
                return defer.fail(InsufficientFundsError())
            return self.wallet.send_points_to_address(reserved_points, self.key_fee)
        return defer.succeed(True)

    def _get_info_to_show(self):
        i = AddStream._get_info_to_show(self)
        if self.description is not None:
            i.append(("description", str(self.description)))
        if self.key_fee is None or self.key_fee_address is None:
            i.append(("decryption key fee", "Free"))
        else:
            i.append(("decryption key fee", str(self.key_fee)))
            i.append(("address to pay key fee", str(self.key_fee_address)))
        return i


class AddStreamFromLBRYcrdNameFactory(CommandHandlerFactory):
    control_handler_class = AddStreamFromLBRYcrdName
    priority = 100
    command = "get"
    short_help = "Download a stream from a name"
    full_help = "Download a stream associated with a name on the LBRYcrd blockchain. The name will be" \
                " looked up on the blockchain, and if it is associated with a stream descriptor hash," \
                " that stream descriptor will be downloaded and read. If the given name is itself a valid " \
                "hash, and the name doesn't exist on the blockchain, then the name will be used as the " \
                "stream descriptor hash as in get-hash. Use get-hash if you want no ambiguity.\n" \
                "Takes one argument, the name.\n\n" \
                "Usage: get <name>"


class EncryptedFileChooser(RecursiveCommandHandler):

    def __init__(self, console, lbry_file_manager, factory_class, *args, **kwargs):
        """
        @param lbry_file_manager:

        @param factory_class:

        @param args: all arguments that will be passed to the factory

        @param kwargs: all arguments that will be passed to the superclass' __init__

        @return:
        """
        self.lbry_file_manager = lbry_file_manager
        self.factory_class = factory_class
        self.args = args
        RecursiveCommandHandler.__init__(self, console, **kwargs)

    def _get_control_handler_factories(self):
        control_handler_factories = []
        for lbry_file in self.lbry_file_manager.lbry_files:
            control_handler_factories.append(self.factory_class(self.console, lbry_file, *self.args))
        return control_handler_factories


class EncryptedFileChooserFactory(CommandHandlerFactory):
    def get_prompt_description(self):
        lbry_file = self.args[0]
        return lbry_file.file_name


class DeleteEncryptedFileChooser(EncryptedFileChooser):
    #prompt_description = "Delete LBRY File"

    def __init__(self, console, stream_info_manager, blob_manager, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, DeleteEncryptedFileFactory,
                                 stream_info_manager, blob_manager, lbry_file_manager,
                                 exit_after_one_done=True)


class DeleteEncryptedFileChooserFactory(CommandHandlerFactory):
    control_handler_class = DeleteEncryptedFileChooser
    command = "delete-lbryfile"
    short_help = "Delete an LBRY file"
    full_help = "Delete an LBRY file which has been downloaded or created by this application.\n" \
                "\nGives the option to also delete the encrypted chunks of data associated with " \
                "the file. If they are deleted, they will all have to be downloaded again if " \
                "lbrynet-console is asked to download that file again, and lbrynet-console will " \
                "not be able to upload those chunks of data to other peers on LBRYnet."


class DeleteEncryptedFile(CommandHandler):
    #prompt_description = "Delete LBRY File"
    delete_data_prompt = "Also delete data? (y/n): "
    confirm_prompt = "Are you sure? (y/n): "

    def __init__(self, console, lbry_file, stream_info_manager, blob_manager, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.stream_info_manager = stream_info_manager
        self.blob_manager = blob_manager
        self.lbry_file_manager = lbry_file_manager
        self.got_delete_data = False
        self.delete_data = False
        self.got_confirmation = False

    def start(self):
        self.console.send(self.delete_data_prompt)

    def handle_line(self, line):
        #if line is None:
        #    return False, defer.succeed(self.line_prompt)
        if self.got_delete_data is False:
            self.got_delete_data = True
            if line.lower() in ['y', 'yes']:
                self.delete_data = True
            self.console.send(self.confirm_prompt)
            return
        if self.got_confirmation is False:
            self.got_confirmation = True
            if line.lower() in ['y', 'yes']:
                d = self._delete_lbry_file()

                def show_done():
                    self.console.sendLine("Successfully deleted " + str(self.lbry_file.stream_name))

                def delete_failed(err):
                    self.console.sendLine("Deletion unsuccessful. Reason: %s" % err.getErrorMessage())

                d.addCallbacks(lambda _: show_done(), delete_failed)
                d.chainDeferred(self.finished_deferred)
            else:
                self.console.sendLine("Canceled deletion.")
                self.finished_deferred.callback(None)

    def _delete_lbry_file(self):
        d = self.lbry_file_manager.delete_lbry_file(self.lbry_file)

        def finish_deletion():
            if self.delete_data is True:
                d = self.lbry_file.delete_data()
            else:
                d = defer.succeed(True)
            d.addCallback(lambda _: self._delete_stream_data())
            return d

        d.addCallback(lambda _: finish_deletion())
        return d

    def _delete_stream_data(self):
        s_h = self.lbry_file.stream_hash
        d = self.lbry_file_manager.get_count_for_stream_hash(s_h)
        # TODO: could possibly be a timing issue here
        d.addCallback(lambda c: self.stream_info_manager.delete_stream(s_h) if c == 0 else True)
        return d


class DeleteEncryptedFileFactory(EncryptedFileChooserFactory):
    control_handler_class = DeleteEncryptedFile


class ToggleEncryptedFileRunningChooser(EncryptedFileChooser):
    #prompt_description = "Toggle whether an LBRY File is running"

    def __init__(self, console, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, ToggleEncryptedFileRunningFactory,
                                 lbry_file_manager, exit_after_one_done=True)


class ToggleEncryptedFileRunningChooserFactory(CommandHandlerFactory):
    control_handler_class = ToggleEncryptedFileRunningChooser
    command = "toggle-running"
    short_help = "Toggle whether an LBRY file is running"
    full_help = "Toggle whether an LBRY file, which is being saved by this application," \
                "is currently being downloaded."


class ToggleEncryptedFileRunning(CommandHandler):
    #prompt_description = "Toggle whether an LBRY File is running"

    def __init__(self, console, lbry_file, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.lbry_file_manager = lbry_file_manager

    def start(self):
        d = self.lbry_file_manager.toggle_lbry_file_running(self.lbry_file)
        d.addErrback(self._handle_download_error)
        self.finished_deferred.callback(None)

    @staticmethod
    def _handle_download_error(err):
        if err.check(InsufficientFundsError):
            return "Download stopped due to insufficient funds."
        else:
            log.error("An unexpected error occurred due to toggling an LBRY file running. %s", err.getTraceback())
            log_file = "console.log"
            if len(log.handlers):
                log_file = log.handlers[0].baseFilename
            return "An unexpected error occurred. See %s for details." % log_file


class ToggleEncryptedFileRunningFactory(EncryptedFileChooserFactory):
    control_handler_class = ToggleEncryptedFileRunning


class CreateEncryptedFile(CommandHandler):
    #prompt_description = "Create an LBRY File from file"
    line_prompt = "File name: "

    def __init__(self, console, session, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.session = session
        self.lbry_file_manager = lbry_file_manager

    def start(self, file_name):
        d = create_lbry_file(self.session, self.lbry_file_manager, file_name, open(file_name))
        d.addCallback(self.add_to_lbry_files)
        d.addCallback(lambda _: self.console.sendLine("Successfully created " + str(file_name)))
        self.console.sendLine("Creating an LBRY file from " + str(file_name) + " in the background.")
        self.finished_deferred.callback(None)

    def add_to_lbry_files(self, stream_hash):
        prm = PaymentRateManager(self.session.base_payment_rate_manager)
        d = self.lbry_file_manager.add_lbry_file(stream_hash, prm)
        d.addCallback(self.set_status)
        return d

    def set_status(self, lbry_file_downloader):
        d = self.lbry_file_manager.change_lbry_file_status(lbry_file_downloader,
                                                           ManagedEncryptedFileDownloader.STATUS_FINISHED)
        d.addCallback(lambda _: lbry_file_downloader.restore())
        return d


class CreateEncryptedFileFactory(CommandHandlerFactory):
    control_handler_class = CreateEncryptedFile
    command = "create-lbryfile"
    short_help = "LBRYize a file"
    full_help = "Encrypt a file, split it into chunks, and make those chunks available on LBRYnet. Also " \
                "create a 'stream descriptor file' which contains all of the metadata needed to download " \
                "the encrypted chunks from LBRYnet and put them back together. This plain stream descriptor " \
                "can be passed around via other file sharing methods like email. Additionally, this " \
                "application can publish the stream descriptor to LBRYnet so that the LBRY file can be " \
                "downloaded via the hash of the stream descriptor."


class PublishStreamDescriptorChooser(EncryptedFileChooser):
    #prompt_description = "Publish a stream descriptor file to the DHT for an LBRY File"

    def __init__(self, console, stream_info_manager, blob_manager, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, PublishStreamDescriptorFactory,
                                 stream_info_manager, blob_manager, lbry_file_manager,
                                 exit_after_one_done=True)


class PublishStreamDescriptorChooserFactory(CommandHandlerFactory):
    control_handler_class = PublishStreamDescriptorChooser
    command = "release-lbryfile"
    short_help = "Put a stream descriptor onto LBRYnet"
    full_help = "Make a stream descriptor available on LBRYnet at its sha384 hashsum. If the stream " \
                "descriptor is made available on LBRYnet, anyone will be able to download it via its " \
                "hash via LBRYnet, and the LBRY file can then be downloaded if it is available."


class PublishStreamDescriptor(CommandHandler):
    #prompt_description = "Publish a stream descriptor file to the DHT for an LBRY File"

    def __init__(self, console, lbry_file, stream_info_manager, blob_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.stream_info_manager = stream_info_manager
        self.blob_manager = blob_manager

    def start(self):
        d = publish_sd_blob(self.stream_info_manager, self.blob_manager, self.lbry_file.stream_hash)
        d.addCallback(lambda sd_hash: self.console.sendLine(sd_hash))
        d.chainDeferred(self.finished_deferred)

    #def _publish_sd_blob(self):
    #    descriptor_writer = BlobStreamDescriptorWriter(self.blob_manager)

    #    d = get_sd_info(self.stream_info_manager, self.lbry_file.stream_hash, True)
    #    d.addCallback(descriptor_writer.create_descriptor)

    #    def add_sd_blob_to_stream(sd_blob_hash):
    #        d = self.stream_info_manager.save_sd_blob_hash_to_stream(self.lbry_file.stream_hash, sd_blob_hash)
    #        d.addCallback(lambda _: sd_blob_hash)
    #        return d

    #    d.addCallback(add_sd_blob_to_stream)
    #    return d


class PublishStreamDescriptorFactory(EncryptedFileChooserFactory):
    control_handler_class = PublishStreamDescriptor


class ShowPublishedSDHashesChooser(EncryptedFileChooser):
    #prompt_description = "Show published stream descriptors for an LBRY File"

    def __init__(self, console, stream_info_manager, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, ShowPublishedSDHashesFactory,
                                 stream_info_manager, lbry_file_manager)


class ShowPublishedSDHashesChooserFactory(CommandHandlerFactory):
    control_handler_class = ShowPublishedSDHashesChooser
    command = "show-lbryfile-sd-hashes"
    short_help = "Show the published stream descriptor files associated with an LBRY file"
    full_help = "Show the published stream descriptor files associated with an LBRY file. " \
                "These files contain the metadata for LBRY files. These files can be accessed " \
                "on lbrynet via their hash. From that, lbrynet-console can download the LBRY file " \
                "if it is available on lbrynet."


class ShowPublishedSDHashes(CommandHandler):
    #prompt_description = "Show published stream descriptors for an LBRY File"

    def __init__(self, console, lbry_file, stream_info_manager, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.stream_info_manager = stream_info_manager
        self.lbry_file_manager = lbry_file_manager

    def start(self):
        d = self._show_sd_hashes()
        d.chainDeferred(self.finished_deferred)

    def _show_sd_hashes(self):
        d = self.stream_info_manager.get_sd_blob_hashes_for_stream(self.lbry_file.stream_hash)

        def format_blob_hashes(sd_blob_hashes):
            self.console.sendLine("\n".join([str(b) for b in sd_blob_hashes]))

        d.addCallback(format_blob_hashes)
        return d


class ShowPublishedSDHashesFactory(EncryptedFileChooserFactory):
    control_handler_class = ShowPublishedSDHashes


class CreatePlainStreamDescriptorChooser(EncryptedFileChooser):
    #prompt_description = "Create a plain stream descriptor file for an LBRY File"

    def __init__(self, console, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager,
                                 CreatePlainStreamDescriptorFactory, lbry_file_manager,
                                 exit_after_one_done=True)


class CreatePlainStreamDescriptorChooserFactory(CommandHandlerFactory):
    control_handler_class = CreatePlainStreamDescriptorChooser
    command = "create-stream-descriptor"
    short_help = "Create a plaintext stream descriptor file for an LBRY file"
    full_help = "Create a plaintext stream descriptor file for an LBRY file. This file, " \
                "which traditionally has the file extension .cryptsd, can be shared " \
                "through a variety of means, including email and file transfer. Anyone " \
                "possessing this file will be able to download the LBRY file if it is " \
                "available on lbrynet."


class CreatePlainStreamDescriptor(CommandHandler):
    prompt_description = "Create a plain stream descriptor file for an LBRY File"

    def __init__(self, console, lbry_file, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.lbry_file_manager = lbry_file_manager
        self.sd_file_name = None
        self.overwrite_old = False

    def start(self):
        self.console.sendLine(self._get_file_name_prompt())

    def handle_line(self, line):
        if self.sd_file_name is None:
            self.sd_file_name = line
            d = self._get_file_name()
            d.addCallback(lambda file_name: create_plain_sd(self.lbry_file_manager.stream_info_manager,
                                                            self.lbry_file.stream_hash, file_name,
                                                            self.overwrite_old))
            d.addCallback(lambda sd_file_name: self.console.sendLine("Wrote stream metadata to " + sd_file_name))
            d.chainDeferred(self.finished_deferred)

    def _get_file_name_prompt(self):
        file_name = self.lbry_file.file_name
        if not file_name:
            file_name = "_"
        file_name += ".cryptsd"
        return "Stream Descriptor file name (blank for default, %s):" % file_name

    def _get_file_name(self):
        if self.sd_file_name:
            file_name = self.sd_file_name
            self.overwrite_old = True
        else:
            file_name = self.lbry_file.file_name
        file_name = file_name + ".cryptsd"
        return defer.succeed(file_name)


class CreatePlainStreamDescriptorFactory(EncryptedFileChooserFactory):
    control_handler_class = CreatePlainStreamDescriptor


class ShowEncryptedFileStreamHashChooser(EncryptedFileChooser):
    #prompt_description = "Show an LBRY File's stream hash (not usually what you want)"

    def __init__(self, console, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, ShowEncryptedFileStreamHashFactory)


class ShowEncryptedFileStreamHashChooserFactory(CommandHandlerFactory):
    control_handler_class = ShowEncryptedFileStreamHashChooser
    command = "lbryfile-streamhash"
    short_help = "Show an LBRY file's stream hash"
    full_help = "Show the stream hash of an LBRY file, which is how the LBRY file is referenced internally" \
                " by this application and therefore not usually what you want to see."


class ShowEncryptedFileStreamHash(CommandHandler):
    #prompt_description = "Show an LBRY File's stream hash (not usually what you want)"

    def __init__(self, console, lbry_file):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file

    def start(self):
        self.console.sendLine(str(self.lbry_file.stream_hash))
        self.finished_deferred.callback(None)


class ShowEncryptedFileStreamHashFactory(EncryptedFileChooserFactory):
    control_handler_class = ShowEncryptedFileStreamHash


class ModifyEncryptedFileDataPaymentRate(ModifyPaymentRate):
    prompt_description = "Modify LBRY File data payment rate"

    def __init__(self, console, lbry_file, lbry_file_manager):
        ModifyPaymentRate.__init__(self, console)
        self._prompt_choices['unset'] = (self._unset, "Use the default LBRY file data rate")
        self.lbry_file = lbry_file
        self.lbry_file_manager = lbry_file_manager
        self.payment_rate_manager = lbry_file.payment_rate_manager

    def _unset(self):
        d = self._set_rate(None)
        d.addCallback(lambda _: "Using the default LBRY file data rate")
        return d

    def _set_rate(self, rate):
        self.payment_rate_manager.min_blob_data_payment_rate = rate
        return self.lbry_file_manager.set_lbry_file_data_payment_rate(self.lbry_file, rate)

    def _get_current_status(self):
        status = "The LBRY file's current data payment rate is "
        effective_rate = self.payment_rate_manager.get_effective_min_blob_data_payment_rate()
        if self.payment_rate_manager.min_blob_data_payment_rate is None:
            status += "set to use the default LBRY file data payment rate, "
        status += str(effective_rate)
        return status


class ModifyEncryptedFileDataPaymentRateFactory(CommandHandlerFactory):
    control_handler_class = ModifyEncryptedFileDataPaymentRate


class ModifyEncryptedFileOptionsChooser(EncryptedFileChooser):
    #prompt_description = "Modify an LBRY File's options"

    def __init__(self, console, lbry_file_manager):
        EncryptedFileChooser.__init__(self, console, lbry_file_manager, ModifyEncryptedFileOptionsFactory, lbry_file_manager)


class ModifyEncryptedFileOptionsChooserFactory(CommandHandlerFactory):
    control_handler_class = ModifyEncryptedFileOptionsChooser
    command = "modify-lbryfile-options"
    short_help = "Modify an LBRY file's options"
    full_help = "Modify an LBRY file's options. Options include, and are limited to, " \
                "changing the rate that the application will pay for data related to " \
                "this LBRY file."


class ModifyEncryptedFileOptions(RecursiveCommandHandler):
    #prompt_description = "Modify an LBRY File's options"

    def __init__(self, console, lbry_file, lbry_file_manager):
        self.lbry_file = lbry_file
        self.lbry_file_manager = lbry_file_manager
        RecursiveCommandHandler.__init__(self, console)

    def _get_control_handler_factories(self):
        factories = []
        factories.append(ModifyEncryptedFileDataPaymentRateFactory(self.lbry_file, self.lbry_file_manager))
        return factories


class ModifyEncryptedFileOptionsFactory(EncryptedFileChooserFactory):
    control_handler_class = ModifyEncryptedFileOptions


class ClaimName(CommandHandler):
    #prompt_description = "Publish to an lbry:// address"
    other_hash_prompt = "Enter the hash you would like to publish:"
    short_desc_prompt = "Enter a short description:"
    #sd_failure_message = "Unable to find a stream descriptor for that file."
    requested_price_prompt = "Enter the fee others should pay for the decryption key for this stream. Leave blank for no fee:"
    lbrycrd_address_prompt = "Enter the LBRYcrd address to which the key fee should be sent. If left blank a new address will be used from the wallet:"
    bid_amount_prompt = "Enter the number of credits you wish to use to support your bid for the name:"
    choose_name_prompt = "Enter the name to which you would like to publish:"

    def __init__(self, console, wallet, lbry_file_manager, blob_manager):
        CommandHandler.__init__(self, console)
        self.wallet = wallet
        self.lbry_file_manager = lbry_file_manager
        self.blob_manager = blob_manager
        self.file_type_options = []
        self.file_type_chosen = None
        self.lbry_file_list = []
        self.sd_hash = None
        self.key_fee = None
        self.key_fee_chosen = False
        self.need_address = True
        self.chosen_address = None
        self.bid_amount = None
        self.chosen_name = None
        self.short_description = None
        self.verified = False

    def start(self):
        self.console.sendLine(self._get_file_type_options())

    def handle_line(self, line):
        #if line is None:
        #    return False, defer.succeed(self._get_file_type_options())
        #if self.failed is True:
        #    return True, defer.succeed(None)
        if self.file_type_chosen is None:
            try:
                choice = int(line)
            except ValueError:
                choice = -1
            if choice < 0 or choice >= len(self.file_type_options):
                self.console.sendLine("You must enter a valid number.\n\n%s" % self._get_file_type_options())
                return
            if self.file_type_options[choice][0] is None:
                self.console.sendLine("Publishing canceled.")
                self.finished_deferred.callback(None)
                return
            self.file_type_chosen = self.file_type_options[choice][0]
            if self.file_type_chosen == "hash":
                self.console.sendLine(self.other_hash_prompt)
                return
            else:
                self._set_sd_hash_and_get_desc_prompt()
                return
        if self.sd_hash is None:
            self.sd_hash = line
            self.console.sendLine(self.short_desc_prompt)
            return
        if self.short_description is None:
            self.short_description = line
            self.console.sendLine(self.requested_price_prompt)
            return
        if self.key_fee_chosen is False:
            if line:
                try:
                    self.key_fee = float(line)
                except ValueError:
                    self.console.sendLine("Leave blank or enter a floating point number.\n\n%s" % self.requested_price_prompt)
                    return
            self.key_fee_chosen = True
            if self.key_fee is None or self.key_fee <= 0:
                self.need_address = False
                self.console.sendLine(self.bid_amount_prompt)
                return
            self.console.sendLine(self.lbrycrd_address_prompt)
            return
        if self.need_address is True:
            if line:
                self.chosen_address = line
                d = defer.succeed(None)
            else:
                d = self._get_new_address()
            self.need_address = False
            d.addCallback(lambda _: self.console.sendLine(self.bid_amount_prompt))
            return
        if self.bid_amount is None:
            try:
                self.bid_amount = float(line)
            except ValueError:
                self.console.sendLine("Must be a floating point number.\n\n%s" % self.bid_amount_prompt)
                return
            self.console.sendLine(self.choose_name_prompt)
            return
        if self.chosen_name is None:
            self.chosen_name = line
            self.console.sendLine(self._get_verification_prompt())
            return
        if self.verified is False:
            if line.lower() == "yes":
                d = self._claim_name()
            else:
                d = defer.succeed("Claim canceled")
            d.chainDeferred(self.finished_deferred)

    def _get_file_type_options(self):
        options = []
        pattern = "[%d] %s\n"
        prompt_string = "What would you like to publish?\n"
        prompt_string += "LBRY Files:\n"
        i = 0
        for lbry_file in self.lbry_file_manager.lbry_files:
            options.append((lbry_file, lbry_file.file_name))
            prompt_string += pattern % (i, lbry_file.file_name)
            i += 1
        prompt_string += "Other:\n"
        options.append(("hash", "Enter a hash"))
        prompt_string += pattern % (i, "Enter a hash")
        i += 1
        options.append((None, "Cancel"))
        prompt_string += pattern % (i, "Cancel")
        self.file_type_options = options
        return prompt_string

    def _choose_sd(self, sd_blob_hashes):
        if not sd_blob_hashes:
            return publish_sd_blob(self.lbry_file_manager.stream_info_manager, self.blob_manager,
                                   self.file_type_chosen.stream_hash)

        else:
            return defer.succeed(sd_blob_hashes[0])

    def _set_sd_hash_and_get_desc_prompt(self):
        d = self.lbry_file_manager.stream_info_manager.get_sd_blob_hashes_for_stream(self.file_type_chosen.stream_hash)
        d.addCallback(self._choose_sd)

        def set_sd_hash(sd_hash):
            self.sd_hash = sd_hash
            self.console.sendLine(self.short_desc_prompt)

        def sd_hash_failed(err):
            self.console.sendLine("An error occurred getting the stream descriptor hash: %s" % err.getErrorMessage())
            self.finished_deferred.callback(None)

        d.addCallback(set_sd_hash)
        d.addErrback(sd_hash_failed)
        return d

    def _get_new_address(self):
        d = self.wallet.get_new_address()

        def set_address(address):
            self.chosen_address = address

        d.addCallback(set_address)
        return d

    def _get_verification_prompt(self):
        v_string = "Ensure the following details are correct:\n"
        if self.file_type_chosen != "hash":
            v_string += "File name: %s\n" % str(self.file_type_chosen.file_name)
        v_string += "Hash: %s\n" % str(self.sd_hash)
        v_string += "Description: %s\n" % str(self.short_description)
        v_string += "Key fee: %s\n" % str(self.key_fee)
        if self.chosen_address is not None:
            v_string += "Key fee address: %s\n" % str(self.chosen_address)
        v_string += "Bid amount: %s\n" % str(self.bid_amount)
        v_string += "Name: %s\n" % str(self.chosen_name)
        v_string += "\nIf this is correct, type 'yes'. Otherwise, type 'no' and the bid will be aborted:"
        return v_string

    def _claim_name(self):
        d = self.wallet.claim_name(self.chosen_name, self.sd_hash, float(self.bid_amount),
                                   description=self.short_description, key_fee=self.key_fee,
                                   key_fee_address=self.chosen_address)
        d.addCallback(lambda response: self.console.sendLine(response))
        return d


class ClaimNameFactory(CommandHandlerFactory):
    control_handler_class = ClaimName
    command = "claim"
    short_help = "Dedicate some LBC toward an lbry:// address"
    full_help = "Dedicate some LBY toward associate an LBRY file, or any hash, with " \
                "an lbry:// address. On lbry, whoever dedicates the most credits to an " \
                "lbry:// address controls that address. This command will let you choose " \
                "to associate either on LBRY file or any given value with the address.\n" \
                "This command will ask for a few additional fields, explained here:\n\n" \
                "The title will be presented to users before they download the file.\n" \
                "The bid amount is the number of LBC that will be dedicated toward " \
                "the lbry://address being registered. On lbry, whoever dedicates the most " \
                "credits to the address controls that address.\n" \
                "The decryption key fee is the amount of LBC that users will be charged " \
                "when consuming this file. The fees will be sent to the provided key fee address.\n" \
                "The description will be presented to users before they download the file.\n"


class Publish(CommandHandler):
    couldnt_read_file_error = "Unable to read %s. The file must exist and you must have permission to read it."
    bid_amount_not_number = "Bid amount must be a number (e.g. 5 or 10.0)"
    key_fee_not_number = "Decryption key fee must be a number (e.g. 5 or 10.0)"

    def __init__(self, console, session, lbry_file_manager, wallet):
        CommandHandler.__init__(self, console)
        self.session = session
        self.lbry_file_manager = lbry_file_manager
        self.wallet = wallet
        self.received_file_name = False
        self.file_path = None
        self.file_name = None
        self.title = None
        self.publish_name = None
        self.bid_amount = None
        self.key_fee = None
        self.key_fee_address = None
        self.key_fee_address_chosen = False
        self.description = None
        self.verified = False
        self.lbry_file = None
        self.sd_hash = None
        self.tx_hash = None

    def start(self, file_name=None):#, title=None, publish_name=None, bid_amount=None,
        #      key_fee=None, key_fee_address=None):

        #def set_other_fields():
        #    self.title = title
        #    self.publish_name = publish_name
        #    if bid_amount is not None:
        #        try:
        #            self.bid_amount = float(bid_amount)
        #        except ValueError:
        #            self.console.sendLine(self.bid_amount_not_number)
        #            self.finished_deferred.callback(None)
        #            return
        #    if key_fee is not None:
        #        try:
        #            self.key_fee = float(key_fee)
        #        except ValueError:
        #            self.console.sendLine(self.key_fee_not_number)
        #            self.finished_deferred.callback(None)
        #            return
        #    if key_fee_address is not None:
        #        self.key_fee_address = key_fee_address
        #        self.key_fee_address_chosen = True
        #    self._send_next_prompt()

        def handle_error(err):
            if err.check(IOError):
                self.console.sendLine(self.couldnt_read_file_error % str(file_name))
            else:
                self.console.sendLine("An unexpected error occurred: %s" % str(err.getErrorMessage()))
            self.finished_deferred.callback(None)

        if file_name is not None:
            self.received_file_name = True
            d = self._check_file_path(file_name)
            #d.addCallback(lambda _: set_other_fields())
            d.addCallbacks(lambda _: self._send_next_prompt(), handle_error)
        else:
            self._send_next_prompt()

    def handle_line(self, line):
        d = defer.succeed(True)
        if self.file_name is None:
            if self.received_file_name is False:
                self.received_file_name = True
                d = self._check_file_path(line)

                def file_name_failed(err):
                    err.trap(IOError)
                    self.console.sendLine(self.couldnt_read_file_error % line)
                    self.finished_deferred.callback(None)
                    return False

                d.addErrback(file_name_failed)
        elif self.title is None:
            self.title = line
        elif self.publish_name is None:
            self.publish_name = line
        elif self.bid_amount is None:
            try:
                self.bid_amount = float(line)
            except ValueError:
                self.console.sendLine(self.bid_amount_not_number)
        elif self.key_fee is None:
            try:
                self.key_fee = float(line)
            except ValueError:
                self.console.sendLine(self.key_fee_not_number)
        elif self.key_fee_address_chosen is False and self.key_fee > 0:
            if line:
                self.key_fee_address = line
            else:
                d = self._get_new_address()
            self.key_fee_address_chosen = True
        elif self.description is None:
            self.description = line
        elif self.verified is False:
            if line.lower() in ['yes', 'y']:
                self._do_publish()
                self.console.sendLine("Publishing in the background.")
            else:
                self.console.sendLine("Canceled.")
            self.finished_deferred.callback(None)
            return
        else:
            return
        d.addCallbacks(lambda s: self._send_next_prompt() if s is True else None,
                       self.finished_deferred.errback)

    def _check_file_path(self, file_path):
        def check_file_threaded():
            f = open(file_path)
            f.close()
            self.file_path = file_path
            self.file_name = os.path.basename(self.file_path)
            return True
        return threads.deferToThread(check_file_threaded)

    def _get_new_address(self):
        d = self.wallet.get_new_address()

        def set_address(address):
            self.key_fee_address = address
            return True

        d.addCallback(set_address)
        return d

    def _send_next_prompt(self):
        prompt = None
        if self.file_name is None:
            prompt = "Path to file: "
        elif self.title is None:
            prompt = "Title: "
        elif self.publish_name is None:
            prompt = "Publish to: lbry://"
        elif self.bid_amount is None:
            prompt = "Bid amount for published name in LBC: "
        elif self.key_fee is None:
            prompt = "Decryption key fee in LBC: "
        elif self.key_fee_address_chosen is False and self.key_fee > 0:
            prompt = "Decryption key fee sent to (leave blank for a new address): "
        elif self.description is None:
            prompt = "Description: "
        elif self.verified is False:
            prompt = self._get_verification_prompt()
        if prompt is not None:
            self.console.send(prompt)

    def _get_verification_prompt(self):
        v_string = "\nPlease review the following details.\n\n"
        v_string += "Path to file: %s\n" % str(self.file_path)
        v_string += "File name: %s\n" % str(self.file_name)
        v_string += "Title: %s\n" % str(self.title)
        v_string += "Published to: lbry://%s\n" % str(self.publish_name)
        v_string += "Bid amount: %s LBC\n" % str(self.bid_amount)
        v_string += "Fee for decryption key: %s LBC\n" % str(self.key_fee)
        if self.key_fee > 0:
            v_string += "Decryption key address: %s\n" % str(self.key_fee_address)
        v_string += "Description: %s\n" % str(self.description)
        v_string += "Is this correct? (y/n): "
        return v_string

    def set_status(self, lbry_file_downloader):
        self.lbry_file = lbry_file_downloader
        d = self.lbry_file_manager.change_lbry_file_status(self.lbry_file,
                                                           ManagedEncryptedFileDownloader.STATUS_FINISHED)
        d.addCallback(lambda _: lbry_file_downloader.restore())
        return d

    def add_to_lbry_files(self, stream_hash):
        prm = PaymentRateManager(self.session.base_payment_rate_manager)
        d = self.lbry_file_manager.add_lbry_file(stream_hash, prm)
        d.addCallback(self.set_status)
        return d

    def _create_sd_blob(self):
        d = publish_sd_blob(self.lbry_file_manager.stream_info_manager, self.session.blob_manager,
                            self.lbry_file.stream_hash)

        def set_sd_hash(sd_hash):
            self.sd_hash = sd_hash

        d.addCallback(set_sd_hash)
        return d

    def _claim_name(self):
        d = self.wallet.claim_name(self.publish_name, self.sd_hash, self.bid_amount,
                                   description=self.description, key_fee=self.key_fee,
                                   key_fee_address=self.key_fee_address)

        def set_tx_hash(tx_hash):
            self.tx_hash = tx_hash

        d.addCallback(set_tx_hash)
        return d

    def _show_result(self):
        message = "Finished publishing %s to %s. The txid of the LBRYcrd claim is %s."
        self.console.sendLine(message % (str(self.file_name), str(self.publish_name), str(self.tx_hash)))

    def _show_time_behind_blockchain(self, rounded_time):
        if rounded_time.unit >= RoundedTime.HOUR:
            self.console.sendLine("This application is %s behind the LBC blockchain\n"
                                  "and therefore may not have all of the funds you expect\n"
                                  "available at this time. It may take a few minutes to\n"
                                  "catch up the first time you run this early version of LBRY.\n"
                                  "Please be patient =).\n" % str(rounded_time))

    def _log_best_blocktime_error(self, err):
        log.error("An error occurred checking the best time of the blockchain: %s", err.getTraceback())

    def _show_first_run_insufficient_funds(self, is_first_run):
        if is_first_run:
            self.console.sendLine("\nThis appears to be the first time you have run LBRY. It can take\n"
                                  "a few minutes for your testing LBC to show up. If you haven't\n"
                                  "received them after a few minutes, please let us know.\n\n"
                                  "Thank you for your patience.\n\n")

    def _log_first_run_check_error(self, err):
        log.error("An error occurred checking if this was the first run: %s", err.getTraceback())

    def _show_publish_error(self, err):
        message = "An error occurred publishing %s to %s. Error: %s."
        if err.check(InsufficientFundsError):
            d = self.wallet.get_most_recent_blocktime()
            d.addCallback(get_time_behind_blockchain)
            d.addCallback(self._show_time_behind_blockchain)
            d.addErrback(self._log_best_blocktime_error)
            d.addCallback(lambda _: self.wallet.is_first_run())
            d.addCallback(self._show_first_run_insufficient_funds)
            d.addErrback(self._log_first_run_check_error)
            error_message = "Insufficient funds"
        else:
            d = defer.succeed(True)
            error_message = err.getErrorMessage()
        self.console.sendLine(message % (str(self.file_name), str(self.publish_name), error_message))
        log.error(message, str(self.file_name), str(self.publish_name), err.getTraceback())
        return d

    def _do_publish(self):
        d = create_lbry_file(self.session, self.lbry_file_manager, self.file_name, open(self.file_path))
        d.addCallback(self.add_to_lbry_files)
        d.addCallback(lambda _: self._create_sd_blob())
        d.addCallback(lambda _: self._claim_name())
        d.addCallbacks(lambda _: self._show_result(), self._show_publish_error)
        return d


class PublishFactory(CommandHandlerFactory):
    control_handler_class = Publish
    priority = 90
    command = "publish"
    short_help = "Publish a file to lbrynet"
    full_help = "Publish a file to lbrynet.\n\n" \
                "Usage: publish [file_name]\n\n" \
                "This command takes (or prompts) for a file, prompts for additional " \
                "information about that file, and then makes that file available on " \
                "lbrynet via an lbry:// address.\n" \
                "The file given must exist or publish will fail.\n" \
                "The title will be presented to users before they download the file.\n" \
                "The bid amount is the number of LBC that will be dedicated toward " \
                "the lbry://address being registered. On lbry, whoever dedicates the most " \
                "credits to the address controls that address.\n" \
                "The decryption key fee is the amount of LBC that users will be charged " \
                "when consuming this file. The fees will be sent to the provided key fee address.\n" \
                "The description will be presented to users before they download the file.\n"


class ModifyDefaultDataPaymentRate(ModifyPaymentRate):
    prompt_description = "Modify default data payment rate"

    def __init__(self, console, payment_rate_manager, settings):
        ModifyPaymentRate.__init__(self, console)
        self.settings = settings
        self.payment_rate_manager = payment_rate_manager

    def _set_rate(self, rate):
        self.payment_rate_manager.min_blob_data_payment_rate = rate
        return self.settings.save_default_data_payment_rate(rate)

    def _get_current_status(self):
        status = "The current default data payment rate is "
        status += str(self.payment_rate_manager.min_blob_data_payment_rate)
        return status


class ModifyDefaultDataPaymentRateFactory(CommandHandlerFactory):
    control_handler_class = ModifyDefaultDataPaymentRate


class ForceCheckBlobFileConsistency(CommandHandler):
    prompt_description = "Verify consistency of stored chunks"

    def __init__(self, console, blob_manager):
        CommandHandler.__init__(self, console)
        self.blob_manager = blob_manager

    def start(self):
        self._check_consistency()
        self.console.sendLine("Checking consistency in the background.")
        self.finished_deferred.callback(None)

    def _check_consistency(self):
        d = self.blob_manager.check_consistency()
        d.addCallback(lambda _: self.console.sendLine("Finished checking stored blobs"))
        return d


class ForceCheckBlobFileConsistencyFactory(CommandHandlerFactory):
    control_handler_class = ForceCheckBlobFileConsistency


class ModifyApplicationDefaults(RecursiveCommandHandler):
    #prompt_description = "Modify application settings"

    def __init__(self, console, lbry_service):
        self.lbry_service = lbry_service
        RecursiveCommandHandler.__init__(self, console)

    def _get_control_handler_factories(self):
        return [ModifyDefaultDataPaymentRateFactory(self.lbry_service.session.base_payment_rate_manager,
                                                    self.lbry_service.settings),
                ForceCheckBlobFileConsistencyFactory(self.lbry_service.session.blob_manager)]


class ModifyApplicationDefaultsFactory(CommandHandlerFactory):
    control_handler_class = ModifyApplicationDefaults
    command = "modify-application-defaults"
    short_help = "Modify application settings"
    full_help = "Either change the default rate to pay for data downloads or check " \
                "that the chunks of data on disk match up with the chunks of data " \
                "the application thinks are on disk."


class ShowServerStatus(CommandHandler):
    #prompt_description = "Show the status of the server"

    def __init__(self, console, lbry_service):
        CommandHandler.__init__(self, console)
        self.lbry_service = lbry_service

    def start(self):
        #assert line is None, "Show server status should not be passed any arguments"
        d = self._get_status()
        d.chainDeferred(self.finished_deferred)

    def _get_status(self):
        status_string = "Server status:\n"
        status_string += "Port: " + str(self.lbry_service.peer_port) + "\n"
        status_string += "Running: " + str(self.lbry_service.lbry_server_port is not None) + "\n"
        if self.lbry_service.blob_request_payment_rate_manager is not None:
            rate = self.lbry_service.blob_request_payment_rate_manager.get_effective_min_blob_data_payment_rate()
            status_string += "Min blob data payment rate: "
            if self.lbry_service.blob_request_payment_rate_manager.min_blob_data_payment_rate is None:
                status_string += "Using application default (" + str(rate) + ")\n"
            else:
                status_string += str(rate)
            status_string += "\n"
        #status_string += "Min crypt info payment rate: "
        #status_string += str(self.lbry_service._server_payment_rate_manager.get_min_live_blob_info_payment_rate())
        #status_string += "\n"
        self.console.sendLine(status_string)
        return defer.succeed(None)


class ShowServerStatusFactory(CommandHandlerFactory):
    control_handler_class = ShowServerStatus
    command = "server-status"
    short_help = "Show the server's status"
    full_help = "Show the port on which the server is running, whether the server is running, and the" \
                " payment rate which the server accepts for data uploads"


class StartServer(CommandHandler):
    prompt_description = "Start the server"

    def __init__(self, console, lbry_service):
        CommandHandler.__init__(self, console)
        self.lbry_service = lbry_service

    def start(self):
        #assert line is None, "Start server should not be passed any arguments"
        d = self.lbry_service.start_server()
        d.addCallback(lambda _: self.lbry_service.settings.save_server_running_status(running=True))
        d.addCallback(lambda _: self.console.sendLine("Successfully started the server"))
        d.chainDeferred(self.finished_deferred)
        #return True, d


class StartServerFactory(CommandHandlerFactory):
    control_handler_class = StartServer


class StopServer(CommandHandler):
    prompt_description = "Stop the server"

    def __init__(self, console, lbry_service):
        CommandHandler.__init__(self, console)
        self.lbry_service = lbry_service

    def start(self):
        #assert line is None, "Stop server should not be passed any arguments"
        d = self.lbry_service.stop_server()
        d.addCallback(lambda _: self.lbry_service.settings.save_server_running_status(running=False))
        d.addCallback(lambda _: self.console.sendLine("Successfully stopped the server"))
        d.chainDeferred(self.finished_deferred)
        #return True, d


class StopServerFactory(CommandHandlerFactory):
    control_handler_class = StopServer


class ModifyServerDataPaymentRate(ModifyPaymentRate):
    prompt_description = "Modify server data payment rate"

    def __init__(self, console, payment_rate_manager, settings):
        ModifyPaymentRate.__init__(self, console)
        self._prompt_choices['unset'] = (self._unset, "Use the application default data rate")
        self.settings = settings
        self.payment_rate_manager = payment_rate_manager

    def _unset(self):
        d = self._set_rate(None)
        d.addCallback(lambda _: "Using the application default data rate")
        return d

    def _set_rate(self, rate):
        self.payment_rate_manager.min_blob_data_payment_rate = rate
        return self.settings.save_server_data_payment_rate(rate)

    def _get_current_status(self):
        effective_rate = self.payment_rate_manager.get_effective_min_blob_data_payment_rate()
        status = "The current server data payment rate is "
        if self.payment_rate_manager.min_blob_data_payment_rate is None:
            status += "set to use the application default, "
        status += str(effective_rate)
        return status


class ModifyServerDataPaymentRateFactory(CommandHandlerFactory):
    control_handler_class = ModifyServerDataPaymentRate


# class ModifyServerCryptInfoPaymentRate(ModifyPaymentRate):
#     prompt_description = "Modify server live stream metadata payment rate"
#
#     def __init__(self, payment_rate_manager, settings):
#         ModifyPaymentRate.__init__(self)
#         self._prompt_choices['unset'] = (self._unset,
#                                          "Use the application default live stream metadata rate")
#         self.settings = settings
#         self.payment_rate_manager = payment_rate_manager
#
#     def _unset(self):
#         d = self._set_rate(None)
#         d.addCallback(lambda _: "Using the application default live stream metadata rate")
#         return True, d
#
#     def _set_rate(self, rate):
#         self.payment_rate_manager.min_live_blob_info_payment_rate = rate
#         return self.settings.save_server_crypt_info_payment_rate(rate)
#
#     def _get_current_status(self):
#         effective_rate = self.payment_rate_manager.get_effective_min_live_blob_info_payment_rate()
#         status = "The current server live stream metadata payment rate is "
#         if self.payment_rate_manager.get_min_blob_data_payment_rate() is None:
#             status += "set to use the application default, "
#             status += str(effective_rate)
#         else:
#             status += str(effective_rate)
#         return status
#
#
# class ModifyServerCryptInfoPaymentRateFactory(ControlHandlerFactory):
#     control_handler_class = ModifyServerCryptInfoPaymentRate


class DisableQueryHandler(CommandHandler):
    def __init__(self, console, query_handlers, query_handler, settings):
        CommandHandler.__init__(self, console)
        self.query_handlers = query_handlers
        self.query_handler = query_handler
        self.settings = settings

    def start(self):
        #assert line is None, "DisableQueryHandler should not be passed any arguments"
        self.query_handlers[self.query_handler] = False
        d = self.settings.disable_query_handler(self.query_handler.get_primary_query_identifier())
        d.addCallback(lambda _: self.console.sendLine("Disabled the query handler"))
        d.chainDeferred(self.finished_deferred)


class DisableQueryHandlerFactory(CommandHandlerFactory):
    control_handler_class = DisableQueryHandler

    def get_prompt_description(self):
        query_handler = self.args[1]
        return "Disable " + str(query_handler.get_description())


class EnableQueryHandler(CommandHandler):
    def __init__(self, console, query_handlers, query_handler, settings):
        CommandHandler.__init__(self, console)
        self.query_handlers = query_handlers
        self.query_handler = query_handler
        self.settings = settings

    def start(self):
        #assert line is None, "EnableQueryHandler should not be passed any arguments"
        self.query_handlers[self.query_handler] = True
        d = self.settings.enable_query_handler(self.query_handler.get_primary_query_identifier())
        d.addCallback(lambda _: self.console.sendLine("Enabled the query handler"))
        d.chainDeferred(self.finished_deferred)


class EnableQueryHandlerFactory(CommandHandlerFactory):
    control_handler_class = EnableQueryHandler

    def get_prompt_description(self):
        query_handler = self.args[1]
        return "Enable " + str(query_handler.get_description())


class ModifyServerEnabledQueries(RecursiveCommandHandler):
    prompt_description = "Modify which queries the server will respond to"

    def __init__(self, console, query_handlers, settings):
        self.query_handlers = query_handlers
        self.settings = settings
        RecursiveCommandHandler.__init__(self, console, reset_after_each_done=True)

    def _get_control_handler_factories(self):
        factories = []
        for query_handler, enabled in self.query_handlers.iteritems():
            if enabled:
                factories.append(DisableQueryHandlerFactory(self.query_handlers, query_handler, self.settings))
            else:
                factories.append(EnableQueryHandlerFactory(self.query_handlers, query_handler, self.settings))
        return factories


class ModifyServerEnabledQueriesFactory(CommandHandlerFactory):
    control_handler_class = ModifyServerEnabledQueries


class ImmediateAnnounceAllBlobs(CommandHandler):
    prompt_description = "Immediately announce all hashes to the DHT"

    def __init__(self, console, blob_manager):
        CommandHandler.__init__(self, console)
        self.blob_manager = blob_manager

    def start(self):
        #assert line is None, "Immediate Announce should not be passed any arguments"
        d = self.blob_manager.immediate_announce_all_blobs()
        d.addCallback(lambda _: self.console.sendLine("Done announcing"))
        d.chainDeferred(self.finished_deferred)


class ImmediateAnnounceAllBlobsFactory(CommandHandlerFactory):
    control_handler_class = ImmediateAnnounceAllBlobs
    command = "announce-blobs"
    short_help = "Announce all blobs to the dht"
    full_help = "Immediately re-broadcast all hashes associated with the server to " \
                "the distributed hash table."


class ModifyServerSettings(RecursiveCommandHandler):
    #prompt_description = "Modify server settings"

    def __init__(self, console, lbry_service):
        self.lbry_service = lbry_service
        RecursiveCommandHandler.__init__(self, console, reset_after_each_done=True)

    def _get_control_handler_factories(self):
        factories = []
        if self.lbry_service.lbry_server_port is not None:
            factories.append(StopServerFactory(self.lbry_service))
        else:
            factories.append(StartServerFactory(self.lbry_service))
        factories.append(
            ModifyServerDataPaymentRateFactory(
                self.lbry_service.blob_request_payment_rate_manager,
                self.lbry_service.settings
            )
        )
        #factories.append(ModifyServerCryptInfoPaymentRateFactory(self.lbry_service._server_payment_rate_manager,
        #                                                         self.lbry_service.settings))
        factories.append(ModifyServerEnabledQueriesFactory(self.lbry_service.query_handlers,
                                                           self.lbry_service.settings))
        factories.append(ImmediateAnnounceAllBlobsFactory(self.lbry_service.session.blob_manager))
        return factories


class ModifyServerSettingsFactory(CommandHandlerFactory):
    control_handler_class = ModifyServerSettings
    command = "modify-server-settings"
    short_help = "Modify server settings"
    full_help = "Modify server settings. Settings that can be modified:\n\n" \
                "1) Queries that the server will answer from other peers. For example, " \
                "stop the server from responding to requests for metadata, or stop " \
                "the server from uploading data.\n" \
                "2) Change whether the server is running at all.\n" \
                "3) Change the minimum rate the server will accept for data uploads.\n" \
                "4) Immediately re-broadcast all hashes associated with the server to " \
                "the distributed hash table."


class PeerChooser(RecursiveCommandHandler):

    def __init__(self, console, peer_manager, factory_class, *args, **kwargs):
        """
        @param peer_manager:

        @param factory_class:

        @param args: all arguments that will be passed to the factory

        @param kwargs: all arguments that will be passed to the superclass' __init__

        @return:
        """
        self.peer_manager = peer_manager
        self.factory_class = factory_class
        self.args = args
        RecursiveCommandHandler.__init__(self, console, **kwargs)

    def _get_control_handler_factories(self):
        control_handler_factories = []
        for peer in self.peer_manager.peers:
            control_handler_factories.append(self.factory_class(peer, *self.args))
        return control_handler_factories


class PeerChooserFactory(CommandHandlerFactory):
    def get_prompt_description(self):
        peer = self.args[0]
        return str(peer)


class ShowPeerStats(CommandHandler):
    prompt_description = "Show the peer's stats"

    def __init__(self, console, peer):
        CommandHandler.__init__(self, console)
        self.peer = peer

    def start(self):
        self.console.sendLine(self._get_peer_stats_string())
        self.finished_deferred.callback(None)

    def _get_peer_stats_string(self):
        stats = "Statistics for " + str(self.peer) + '\n'
        stats += "  current_score: " + str(self.peer.score) + '\n'
        stats += "  is_available: " + str(self.peer.is_available()) + '\n'
        for stat_type, amount in self.peer.stats.iteritems():
            stats += "  " + stat_type + ": " + str(amount) + "\n"
        return stats


class ShowPeerStatsFactory(CommandHandlerFactory):
    control_handler_class = ShowPeerStats


class PeerStatsAndSettings(RecursiveCommandHandler):
    def __init__(self, console, peer):
        self.peer = peer
        RecursiveCommandHandler.__init__(self, console, reset_after_each_done=True)

    def _get_control_handler_factories(self):
        factories = []
        factories.append(ShowPeerStatsFactory(self.peer))
        return factories


class PeerStatsAndSettingsFactory(PeerChooserFactory):
    control_handler_class = PeerStatsAndSettings


class PeerStatsAndSettingsChooser(PeerChooser):
    #prompt_description = "View peer stats and modify peer settings"

    def __init__(self, console, peer_manager):
        PeerChooser.__init__(self, console, peer_manager, PeerStatsAndSettingsFactory)


class PeerStatsAndSettingsChooserFactory(CommandHandlerFactory):
    control_handler_class = PeerStatsAndSettingsChooser
    command = "peer-stats"
    short_help = "Show some peer statistics"
    full_help = "Show the list of peers that this application has been " \
                "in contact with. Give the option to show further details " \
                "for each peer including the number of bytes transferred " \
                "and the 'score' of the peer which is used in deciding " \
                "which peers to connect to."


class EncryptedFileStatusModifier(CommandHandler):
    def __init__(self, console, lbry_file, stream_info_manager, blob_manager, lbry_file_manager):
        CommandHandler.__init__(self, console)
        self.lbry_file = lbry_file
        self.stream_info_manager = stream_info_manager
        self.blob_manager = blob_manager
        self.lbry_file_manager = lbry_file_manager
        self.current_handler = None

    def start(self):
        d = self.lbry_file.status()
        d.addCallback(self._show_prompt)

    def handle_line(self, line):
        if self.current_handler is None:
            if line:
                if line.lower() == 'd':
                    self.current_handler = DeleteEncryptedFile(self.console, self.lbry_file,
                                                          self.stream_info_manager, self.blob_manager,
                                                          self.lbry_file_manager)
                elif line.lower() == 't':
                    self.current_handler = ToggleEncryptedFileRunning(self.console, self.lbry_file,
                                                                 self.lbry_file_manager)
                else:
                    self.console.sendLine("Invalid selection\n")
                    self.finished_deferred.callback(None)
                    return
            else:
                self.console.sendLine("")
                self.finished_deferred.callback(None)
                return
            try:
                self.current_handler.start()
            except Exception as e:
                self.console.sendLine("Operation failed. Error: %s\n" % str(e))
                import traceback
                log.error(traceback.format_exc())
                self.finished_deferred.callback(None)
                return
            self.current_handler.finished_deferred.chainDeferred(self.finished_deferred)
        else:
            try:
                self.current_handler.handle_line(line)
            except Exception as e:
                self.console.sendLine("Operation failed. Error: %s\n" % str(e))
                import traceback
                log.error(traceback.format_exc())
                self.finished_deferred.callback(None)

    def _show_prompt(self, status_report):
        self.console.sendLine("\n%s - %d chunks downloaded out of %d - %s" % (str(status_report.name),
                                                                              status_report.num_completed,
                                                                              status_report.num_known,
                                                                              str(status_report.running_status)))
        self.console.sendLine("\nTo delete this file, type 'd'. To toggle its running status, type 't'. "
                              "Then hit enter. To do nothing, just hit enter.")
        self.console.send("Choice: ")


class Status(CommandHandler):
    lbry_file_status_format = "[%d] %s - %s bytes - %s - %s - %s%% - %s"

    def __init__(self, console, lbry_service, rate_limiter, lbry_file_manager, blob_manager, wallet=None):
        CommandHandler.__init__(self, console)
        self.lbry_service = lbry_service
        self.rate_limiter = rate_limiter
        self.lbry_file_manager = lbry_file_manager
        self.blob_manager = blob_manager
        self.wallet = wallet
        self.chosen_lbry_file = None
        self.current_handler = None

    def start(self):
        d = self._show_general_status()
        d.addCallback(lambda _: self._show_lbry_file_status())
        d.addCallback(lambda _: self._show_prompt())

    def handle_line(self, line):
        if self.current_handler is None:
            if line:
                try:
                    index = int(line)
                except ValueError:
                    self.console.sendLine("Choice must be a number.\n")
                    self.finished_deferred.callback(None)
                    return
                if index < 0 or index >= len(self.lbry_files):
                    self.console.sendLine("Invalid choice.\n")
                    self.finished_deferred.callback(None)
                    return
                self.current_handler = EncryptedFileStatusModifier(self.console, self.lbry_files[index],
                                                              self.lbry_file_manager.stream_info_manager,
                                                              self.blob_manager, self.lbry_file_manager)
                try:
                    self.current_handler.start()
                except Exception as e:
                    self.console.sendLine("Selection failed. Error: %s\n" % str(e))
                    import traceback
                    log.error(traceback.format_exc())
                    self.finished_deferred.callback(None)
                    return
                self.current_handler.finished_deferred.chainDeferred(self.finished_deferred)
            else:
                self.console.sendLine("")
                self.finished_deferred.callback(None)
        else:
            try:
                self.current_handler.handle_line(line)
            except Exception as e:
                self.console.sendLine("Operation failed. Error: %s\n" % str(e))
                import traceback
                log.error(traceback.format_exc())
                self.finished_deferred.callback(None)

    def _show_general_status(self):
        #self.console.sendLine("Total bytes uploaded: %s" % str(self.rate_limiter.total_ul_bytes))
        #self.console.sendLine("Total bytes downloaded: %s" % str(self.rate_limiter.total_dl_bytes))
        #self.console.sendLine("Server running: %s" % str(self.lbry_service.lbry_server_port is not None))
        #self.console.sendLine("Server port: %s" % str(self.lbry_service.peer_port))
        return defer.succeed(True)

    def _get_name_and_validity_for_lbry_file(self, lbry_file):
        if self.wallet is None:
            return defer.succeed(None)
        d = self.lbry_file_manager.stream_info_manager.get_sd_blob_hashes_for_stream(lbry_file.stream_hash)
        d.addCallback(lambda sd_blob_hashes: self.wallet.get_name_and_validity_for_sd_hash(sd_blob_hashes[0]) if len(sd_blob_hashes) else None)
        return d

    def _show_lbry_file_status(self):
        self.lbry_files = self.lbry_file_manager.lbry_files
        status_ds = map(lambda lbry_file: lbry_file.status(), self.lbry_files)
        status_dl = defer.DeferredList(status_ds)

        size_ds = map(lambda lbry_file: lbry_file.get_total_bytes(), self.lbry_files)
        size_dl = defer.DeferredList(size_ds)

        name_validity_ds = map(self._get_name_and_validity_for_lbry_file, self.lbry_files)
        name_validity_dl = defer.DeferredList(name_validity_ds)

        dl = defer.DeferredList([status_dl, size_dl, name_validity_dl])

        def show_statuses(statuses):
            status_reports = statuses[0][1]
            sizes = statuses[1][1]
            name_validities = statuses[2][1]
            for i, (lbry_file, (succ1, status), (succ2, size), (succ3, name_validity)) in enumerate(zip(self.lbry_files, status_reports, sizes, name_validities)):
                percent_done = "unknown"
                name = lbry_file.file_name
                claimed_name = ""
                claimed_name_valid = ""
                if succ3 and name_validity:
                    validity = name_validity[1]
                    if validity == "valid":
                        claimed_name_valid = ""
                    else:
                        claimed_name_valid = "(" + validity + ")"
                    claimed_name = name_validity[0]
                total_bytes = "unknown"
                running_status = "unknown"
                if succ1:
                    percent_done = "0"
                    if status.num_known > 0:
                        percent = 100.0 * status.num_completed / status.num_known
                        percent_done = "%.2f" % percent
                    running_status = status.running_status
                if succ2:
                    total_bytes = "%d" % size
                self.console.sendLine(self.lbry_file_status_format % (i, str(name), total_bytes,
                                                                      str(claimed_name_valid),
                                                                      str(claimed_name),
                                                                      percent_done, str(running_status)))

        dl.addCallback(show_statuses)
        return dl

    def _show_prompt(self):
        self.console.sendLine("\n\nTo alter the status of any file shown above, type the number next to it "
                              "and then hit 'enter'. Otherwise, just hit 'enter'.")
        self.console.send("Choice: ")


class StatusFactory(CommandHandlerFactory):
    control_handler_class = Status
    priority = 20
    command = "status"
    short_help = "Show or alter status of files being downloaded"
    full_help = "Show or alter status of files being downloaded\n\n" \
                "Show the list of files that are currently downloading " \
                "or have been downloaded, and give the option to " \
                "toggle whether the file is actively downloading or " \
                "to remove the file."


# class AutoFetcherStart(CommandHandler):
#     def __init__(self, console, autofetcher):
#         CommandHandler.__init__(self, console)
#         self.autofetcher = autofetcher
#
#     def start(self):
#         self.autofetcher.start(self.console)
#         self.finished_deferred.callback(None)
#
#
# class AutoFetcherStop(CommandHandler):
#     def __init__(self, console, autofetcher):
#         CommandHandler.__init__(self, console)
#         self.autofetcher = autofetcher
#
#     def start(self):
#         self.autofetcher.stop(self.console)
#         self.finished_deferred.callback(None)
#
#
# class AutoFetcherStatus(CommandHandler):
#     def __init__(self, console, autofetcher):
#         CommandHandler.__init__(self, console)
#         self.autofetcher = autofetcher
#
#     def start(self):
#         self.autofetcher.check_if_running(self.console)
#         self.finished_deferred.callback(None)


# class AutoFetcherStartFactory(CommandHandlerFactory):
#     control_handler_class = AutoFetcherStart
#     command = "start-autofetcher"
#     short_help = "Start downloading all lbry files as they are published"
#
#
# class AutoFetcherStopFactory(CommandHandlerFactory):
#     control_handler_class = AutoFetcherStop
#     command = "stop-autofetcher"
#     short_help = "Stop downloading all lbry files as they are published"
#
#
# class AutoFetcherStatusFactory(CommandHandlerFactory):
#     control_handler_class = AutoFetcherStatus
#     command = "autofetcher-status"
#     short_help = "Check autofetcher status"


class BlockchainStatus(CommandHandler):
    def __init__(self, console, wallet=None):
        CommandHandler.__init__(self, console)
        self.wallet = wallet

    def start(self):
        d = self.wallet.get_most_recent_blocktime()
        d.addCallback(get_time_behind_blockchain)
        d.addCallbacks(self._show_time_behind_blockchain, self._show_error)
        d.chainDeferred(self.finished_deferred)
        return d

    def _show_time_behind_blockchain(self, rounded_time):
        if rounded_time.unit >= RoundedTime.HOUR:
            self.console.sendLine("This application is %s behind the LBC blockchain. It\n"
                                  "should only take a few minutes to catch up." % str(rounded_time))
        else:
            self.console.sendLine("This application is up to date with the LBC blockchain.")

    def _show_error(self, err):
        log.error(err.getTraceback())
        self.console.sendLine("Unable to determine the status of the blockchain.")


class BlockchainStatusFactory(CommandHandlerFactory):
    control_handler_class = BlockchainStatus
    command = "get-blockchain-status"
    short_help = "Show whether this application has caught up with the LBC blockchain"
    full_help = "Show whether this applications has caught up with the LBC blockchain"
