import os
import json
import logging
from twisted.internet import defer

from torba.basemanager import BaseWalletManager

from lbryschema.uri import parse_lbry_uri
from lbryschema.error import URIParseError
from lbryschema.claim import ClaimDict

from .ledger import MainNetLedger
from .account import generate_certificate
from .transaction import Transaction
from .database import WalletDatabase

log = logging.getLogger(__name__)


class BackwardsCompatibleNetwork:
    def __init__(self, manager):
        self.manager = manager

    def get_local_height(self):
        for ledger in self.manager.ledgers.values():
            assert isinstance(ledger, MainNetLedger)
            return ledger.headers.height

    def get_server_height(self):
        return self.get_local_height()


class LbryWalletManager(BaseWalletManager):

    @property
    def ledger(self) -> MainNetLedger:
        return self.default_account.ledger

    @property
    def db(self) -> WalletDatabase:
        return self.ledger.db

    @property
    def wallet(self):
        return self

    @property
    def network(self):
        return BackwardsCompatibleNetwork(self)

    @property
    def use_encryption(self):
        # TODO: implement this
        return False

    @property
    def is_first_run(self):
        return True

    def check_locked(self):
        return defer.succeed(False)

    @classmethod
    def from_lbrynet_config(cls, settings, db):

        ledger_id = {
            'lbrycrd_main':    'lbc_mainnet',
            'lbrycrd_testnet': 'lbc_testnet',
            'lbrycrd_regtest': 'lbc_regtest'
        }[settings['blockchain_name']]

        ledger_config = {
            'auto_connect': True,
            'default_servers': settings['lbryum_servers'],
            'data_path': settings['lbryum_wallet_dir'],
            'use_keyring': settings['use_keyring'],
            #'db': db
        }

        wallet_file_path = os.path.join(settings['lbryum_wallet_dir'], 'default_wallet')
        if os.path.exists(wallet_file_path):
            with open(wallet_file_path, 'r') as f:
                json_data = f.read()
                json_dict = json.loads(json_data)
            # TODO: After several public releases of new torba based wallet, we can delete
            #       this lbryum->torba conversion code and require that users who still
            #       have old structured wallets install one of the earlier releases that
            #       still has the below conversion code.
            if 'master_public_keys' in json_dict:
                json_data = json.dumps({
                    'version': 1,
                    'name': 'My Wallet',
                    'accounts': [{
                        'version': 1,
                        'name': 'Main Account',
                        'ledger': 'lbc_mainnet',
                        'encrypted': json_dict['use_encryption'],
                        'seed': json_dict['seed'],
                        'seed_version': json_dict['seed_version'],
                        'private_key': json_dict['master_private_keys']['x/'],
                        'public_key': json_dict['master_public_keys']['x/'],
                        'certificates': json_dict.get('claim_certificates', {}),
                        'address_generator': {
                            'name': 'deterministic-chain',
                            'receiving': {'gap': 20, 'maximum_uses_per_address': 2},
                            'change': {'gap': 6, 'maximum_uses_per_address': 2}
                        }
                    }]
                }, indent=4, sort_keys=True)
                with open(wallet_file_path, 'w') as f:
                    f.write(json_data)

        manager = cls.from_config({
            'ledgers': {ledger_id: ledger_config},
            'wallets': [wallet_file_path]
        })
        if manager.default_account is None:
            ledger = manager.get_or_create_ledger('lbc_mainnet')
            log.info('Wallet at %s is empty, generating a default account.', wallet_file_path)
            manager.default_wallet.generate_account(ledger)
            manager.default_wallet.save()
        return manager

    def get_best_blockhash(self):
        return defer.succeed('')

    def get_unused_address(self):
        return self.default_account.receiving.get_or_create_usable_address()

    def get_new_address(self):
        return self.get_unused_address()

    def list_addresses(self):
        return self.default_account.get_addresses()

    def reserve_points(self, address, amount):
        # TODO: check if we have enough to cover amount
        return ReservedPoints(address, amount)

    def send_points_to_address(self, reserved, amount):
        destination_address = reserved.identifier.encode('latin1')
        return self.send_amount_to_address(amount, destination_address)

    def get_wallet_info_query_handler_factory(self):
        return LBRYcrdAddressQueryHandlerFactory(self)

    def get_info_exchanger(self):
        return LBRYcrdAddressRequester(self)

    def resolve(self, *uris, **kwargs):
        page = kwargs.get('page', 0)
        page_size = kwargs.get('page_size', 10)
        ledger = self.default_account.ledger  # type: MainNetLedger
        return ledger.resolve(page, page_size, *uris)

    def get_name_claims(self):
        return defer.succeed([])

    def address_is_mine(self, address):
        return defer.succeed(True)

    def get_history(self):
        return defer.succeed([])

    @defer.inlineCallbacks
    def claim_name(self, name, amount, claim_dict, certificate=None, claim_address=None):
        account = self.default_account
        claim = ClaimDict.load_dict(claim_dict)
        if not claim_address:
            claim_address = yield account.receiving.get_or_create_usable_address()
        if certificate:
            claim = claim.sign(
                certificate.private_key, claim_address, certificate.claim_id
            )
        existing_claims = yield account.get_unspent_outputs(include_claims=True, claim_name=name)
        if len(existing_claims) == 0:
            tx = yield Transaction.claim(
                name, claim, amount, claim_address, [account], account
            )
        elif len(existing_claims) == 1:
            tx = yield Transaction.update(
                existing_claims[0], claim, amount, claim_address, [account], account
            )
        else:
            raise NameError("More than one other claim exists with the name '{}'.".format(name))
        yield account.ledger.broadcast(tx)
        yield self.old_db.save_claims([self._old_get_temp_claim_info(
            tx, tx.outputs[0], claim_address, claim_dict, name, amount
        )])
        # TODO: release reserved tx outputs in case anything fails by this point
        defer.returnValue(tx)

    def _old_get_temp_claim_info(self, tx, txo, address, claim_dict, name, bid):
        return {
            "claim_id": txo.claim_id,
            "name": name,
            "amount": bid,
            "address": address,
            "txid": tx.id,
            "nout": txo.position,
            "value": claim_dict,
            "height": -1,
            "claim_sequence": -1,
        }

    @defer.inlineCallbacks
    def abandon_claim(self, claim_id, txid, nout):
        account = self.default_account
        claim = yield account.get_claim(claim_id)
        tx = yield Transaction.abandon(claim, [account], account)
        yield account.ledger.broadcast(tx)
        # TODO: release reserved tx outputs in case anything fails by this point
        defer.returnValue(tx)

    @defer.inlineCallbacks
    def claim_new_channel(self, channel_name, amount):
        try:
            parsed = parse_lbry_uri(channel_name)
            if not parsed.is_channel:
                raise Exception("Cannot make a new channel for a non channel name")
            if parsed.path:
                raise Exception("Invalid channel uri")
        except (TypeError, URIParseError):
            raise Exception("Invalid channel name")
        if amount <= 0:
            raise Exception("Invalid amount")
        account = self.default_account
        address = yield account.receiving.get_or_create_usable_address()
        cert, key = generate_certificate()
        tx = yield Transaction.claim(channel_name, cert, amount, address, [account], account)
        yield account.ledger.broadcast(tx)
        account.add_certificate_private_key(tx.outputs[0].ref, key.decode())
        # TODO: release reserved tx outputs in case anything fails by this point
        defer.returnValue(tx)

    def channel_list(self):
        return self.default_account.get_channels()

    def get_certificates(self, name):
        return self.db.get_certificates(name, [self.default_account], exclude_without_key=True)

    def update_peer_address(self, peer, address):
        pass  # TODO: Data payments is disabled

    def get_unused_address_for_peer(self, peer):
        # TODO: Data payments is disabled
        return self.get_unused_address()

    def add_expected_payment(self, peer, amount):
        pass  # TODO: Data payments is disabled

    def send_points(self, reserved_points, amount):
        defer.succeed(True)  # TODO: Data payments is disabled

    def cancel_point_reservation(self, reserved_points):
        pass # fixme: disabled for now.

    def save(self):
        for wallet in self.wallets:
            wallet.save()


class ReservedPoints:
    def __init__(self, identifier, amount):
        self.identifier = identifier
        self.amount = amount


class ClientRequest:
    def __init__(self, request_dict, response_identifier=None):
        self.request_dict = request_dict
        self.response_identifier = response_identifier


class LBRYcrdAddressRequester:

    def __init__(self, wallet):
        self.wallet = wallet
        self._protocols = []

    def send_next_request(self, peer, protocol):
        if not protocol in self._protocols:
            r = ClientRequest({'lbrycrd_address': True}, 'lbrycrd_address')
            d = protocol.add_request(r)
            d.addCallback(self._handle_address_response, peer, r, protocol)
            d.addErrback(self._request_failed, peer)
            self._protocols.append(protocol)
            return defer.succeed(True)
        else:
            return defer.succeed(False)

    def _handle_address_response(self, response_dict, peer, request, protocol):
        if request.response_identifier not in response_dict:
            raise ValueError(
                "Expected {} in response but did not get it".format(request.response_identifier))
        assert protocol in self._protocols, "Responding protocol is not in our list of protocols"
        address = response_dict[request.response_identifier]
        self.wallet.update_peer_address(peer, address)

    def _request_failed(self, error, peer):
        raise Exception(
            "A peer failed to send a valid public key response. Error: {}, peer: {}".format(
                error.getErrorMessage(), str(peer)
            )
        )


class LBRYcrdAddressQueryHandlerFactory:

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

    def build_query_handler(self):
        q_h = LBRYcrdAddressQueryHandler(self.wallet)
        return q_h

    def get_primary_query_identifier(self):
        return 'lbrycrd_address'

    def get_description(self):
        return "LBRYcrd Address - an address for receiving payments via LBRYcrd"


class LBRYcrdAddressQueryHandler:

    def __init__(self, wallet):
        self.wallet = wallet
        self.query_identifiers = ['lbrycrd_address']
        self.address = None
        self.peer = None

    def register_with_request_handler(self, request_handler, peer):
        self.peer = peer
        request_handler.register_query_handler(self, self.query_identifiers)

    def handle_queries(self, queries):

        def create_response(address):
            self.address = address
            fields = {'lbrycrd_address': address}
            return fields

        if self.query_identifiers[0] in queries:
            d = self.wallet.get_unused_address_for_peer(self.peer)
            d.addCallback(create_response)
            return d
        if self.address is None:
            raise Exception("Expected a request for an address, but did not receive one")
        else:
            return defer.succeed({})