import asyncio
import logging

from lbry.error import (
    InsufficientFundsError,
    ServerPaymentFeeAboveMaxAllowedError,
    ServerPaymentInvalidAddressError,
    ServerPaymentWalletLockedError
)
from lbry.wallet.dewies import lbc_to_dewies
from lbry.wallet.stream import StreamController
from lbry.wallet.transaction import Output, Transaction

log = logging.getLogger(__name__)


class WalletServerPayer:
    def __init__(self, payment_period=24 * 60 * 60, max_fee='1.0', analytics_manager=None):
        self.ledger = None
        self.wallet = None
        self.running = False
        self.task = None
        self.payment_period = payment_period
        self.analytics_manager = analytics_manager
        self.max_fee = max_fee
        self._on_payment_controller = StreamController()
        self.on_payment = self._on_payment_controller.stream
        self.on_payment.listen(None, on_error=lambda e: log.warning(e.args[0]))

    async def pay(self):
        while self.running:
            try:
                await self._pay()
            except (asyncio.TimeoutError, ConnectionError):
                if not self.running:
                    break
                delay = max(self.payment_period / 24, 10)
                log.warning("Payement failed. Will retry after %g seconds.", delay)
                asyncio.sleep(delay)
            except BaseException as e:
                if not isinstance(e, asyncio.CancelledError):
                    log.exception("Unexpected exception. Payment task exiting early.")
                self.running = False
                raise

    async def _pay(self):
        while self.running:
            await asyncio.sleep(self.payment_period)
            features = await self.ledger.network.get_server_features()
            log.debug("pay loop: received server features: %s", str(features))
            address = features['payment_address']
            amount = str(features['daily_fee'])
            if not address or not amount:
                log.debug("pay loop: no address or no amount")
                continue

            if not self.ledger.is_pubkey_address(address):
                log.info("pay loop: address not pubkey")
                self._on_payment_controller.add_error(ServerPaymentInvalidAddressError(address))
                continue

            if self.wallet.is_locked:
                log.info("pay loop: wallet is locked")
                self._on_payment_controller.add_error(ServerPaymentWalletLockedError())
                continue

            amount = lbc_to_dewies(features['daily_fee'])  # check that this is in lbc and not dewies
            limit = lbc_to_dewies(self.max_fee)
            if amount > limit:
                log.info("pay loop: amount (%d) > limit (%d)", amount, limit)
                self._on_payment_controller.add_error(
                    ServerPaymentFeeAboveMaxAllowedError(features['daily_fee'], self.max_fee)
                )
                continue

            try:
                tx = await Transaction.create(
                    [],
                    [Output.pay_pubkey_hash(amount, self.ledger.address_to_hash160(address))],
                    self.wallet.get_accounts_or_all(None),
                    self.wallet.get_account_or_default(None)
                )
            except InsufficientFundsError:
                self._on_payment_controller.add_error(InsufficientFundsError())
                continue

            await self.ledger.broadcast_or_release(tx, blocking=True)
            if self.analytics_manager:
                await self.analytics_manager.send_credits_sent()
            self._on_payment_controller.add(tx)

    async def start(self, ledger=None, wallet=None):
        if lbc_to_dewies(self.max_fee) < 1:
            return
        self.ledger = ledger
        self.wallet = wallet
        self.running = True
        self.task = asyncio.ensure_future(self.pay())
        self.task.add_done_callback(self._done_callback)

    def _done_callback(self, f):
        if f.cancelled():
            reason = "Cancelled"
        elif f.exception():
            reason = f'Exception: {f.exception()}'
        elif not self.running:
            reason = "Stopped"
        else:
            reason = ""
        log.info("Stopping wallet server payments. %s", reason)

    async def stop(self):
        if self.running:
            self.running = False
            self.task.cancel()