import math
from decimal import Decimal

from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from constance import config as site_config

from django.conf import settings as project_settings
from .base import BackendBase


import hmac
import hashlib
import requests
import logging
import json
from urllib.parse import urlencode
logger = logging.getLogger(__name__)


class CoinPaymentsError(Exception):
    pass


class CoinPayments:
    def __init__(self, pkey, skey, api_url=None):
        self.public_key = pkey
        self.secret_key = skey.encode('utf-8')
        self.api_url = api_url or 'https://www.coinpayments.net/api.php'

    def _sign(self, params):
        body = urlencode(params).encode('utf-8')
        mac = hmac.new(self.secret_key, body, hashlib.sha512)
        return body, mac.hexdigest()

    def _request(self, cmd, params):
        params.update({
            'cmd': cmd,
            'key': self.public_key,
            'format': 'json',
            'version': 1,
        })
        print(params)
        post_body, mac = self._sign(params)

        headers = {
            'HMAC': mac,
            'Content-Type': 'application/x-www-form-urlencoded',
        }

        r = requests.post(self.api_url, data=post_body,
                          headers=headers)
        try:
            r.raise_for_status()
            j = r.json()
        except Exception as e:
            raise CoinPaymentsError(str(e)) from e

        if j.get('error') == 'ok':
            return j.get('result')
        else:
            raise CoinPaymentsError(j.get('error'))

    def create_transaction(self, **params):
        assert 'amount' in params
        assert 'currency1' in params
        assert 'currency2' in params
        return self._request('create_transaction', params)

    def get_account_info(self, **params):
        return self._request('get_basic_info', params)

    def get_rates(self, **params):
        return self._request('rates', params)

    def get_balances(self, **params):
        return self._request('balances', params)

    def get_deposit_address(self, **params):
        assert 'currency' in params
        return self._request('get_deposit_address', params)

    def get_callback_address(self, **params):
        assert 'currency' in params
        return self._request('get_callback_address', params)

    def get_tx_info(self, **params):
        assert 'txid' in params
        return self._request('get_tx_info', params)

    def get_tx_info_multi(self, ids=None, **params):
        if ids is not None:
            params['txid'] = '|'.join(str(i) for i in ids)
        assert 'txid' in params
        return self._request('get_tx_info_multi', params)

    def get_tx_ids(self, **params):
        return self._request('get_tx_ids', params)

    def create_transfer(self, **params):
        assert 'amount' in params
        assert 'currency' in params
        assert 'merchant' in params or 'pbntag' in params
        return self._request('create_transfer', params)

    def create_withdrawal(self, **params):
        assert 'amount' in params
        assert 'currency' in params
        assert 'address' in params or 'pbntag' in params
        return self._request('create_withdrawal', params)

    def create_mass_withdrawal(self, **params):
        assert 'wd' in params
        return self._request('create_mass_withdrawal', params)

    def convert(self, **params):
        assert 'amount' in params
        assert 'from' in params
        assert 'to' in params
        return self._request('convert', params)

    def get_withdrawal_history(self, **params):
        return self._request('get_withdrawal_history', params)

    def get_withdrawal_info(self, **params):
        assert 'id' in params
        return self._request('get_withdrawal_info', params)

    def get_conversion_info(self, **params):
        assert 'id' in params
        return self._request('get_conversion_info', params)

    def get_pbn_info(self, **params):
        assert 'pbntag' in params
        return self._request('get_pbn_info', params)

    def get_pbn_list(self, **params):
        return self._request('get_pbn_list', params)

    def update_pbn_tag(self, **params):
        assert 'tagid' in params
        return self._request('update_pbn_tag', params)

    def claim_pbn_tag(self, **params):
        assert 'tagid' in params
        assert 'name' in params
        return self._request('claim_pbn_tag', params)


class IpnError(Exception):
    pass


def ipn_assert(request, remote, local, key=None, delta=None):
    if (delta is None and remote != local) or (delta is not None and not math.isclose(remote, local, abs_tol=delta)):
        logger.debug("Invalid IPN %r: local=%r remote=%r",
                     key, local, remote)
        raise IpnError("Unexpected value: %s" % key)


def ipn_assert_post(request, key, local):
    remote = request.POST.get(key)
    ipn_assert(request, remote, local, key=key)


class CoinPaymentsBackend(BackendBase):
    backend_id = 'coinpayments'
    backend_verbose_name = _("CoinPayments")
    backend_display_name = _("Cryptocurrencies")
    backend_has_recurring = False

    def __init__(self, settings):
        self.merchant_id = settings.get('merchant_id')
        self.currency = settings.get('currency', 'EUR')
        self.api_base = settings.get('api_base', None)
        self.title = settings.get('title', 'VPN Payment')
        self.secret = settings.get('secret', '').encode('utf-8')

        if self.merchant_id and self.secret:
            self.backend_enabled = True

    def new_payment(self, payment):
        ROOT_URL = project_settings.ROOT_URL
        params = {
            'cmd': '_pay',
            'reset': '1',
            'want_shipping': '0',
            'merchant': self.merchant_id,
            'currency': self.currency,
            'amountf': '%.2f' % (payment.amount / 100),
            'item_name': self.title,
            'ipn_url': ROOT_URL + reverse('payments:cb_coinpayments', args=(payment.id,)),
            'success_url': ROOT_URL + reverse('payments:view', args=(payment.id,)),
            'cancel_url': ROOT_URL + reverse('payments:cancel', args=(payment.id,)),
        }

        payment.status_message = _("Waiting for CoinPayments to confirm the transaction... " +
                                   "It can take up to a few minutes...")
        payment.save()

        form = '<form action="https://www.coinpayments.net/index.php" method="POST" id="cp-form">'
        for k, v in params.items():
            form += '<input type="hidden" name="%s" value="%s" />' % (k, v)
        form += '''
            <img src="/static/img/spinner.gif" style="margin: auto;" alt="redirecting..." id="cp-spinner" />
            <input type="submit" class="button" name="submitbutton" value="Continue" />
        </form>
        <script>
        document.addEventListener("DOMContentLoaded", function(event) {{
            var f = document.getElementById("cp-form");
            f.elements["submitbutton"].style.display = "none";
            document.getElementById("cp-spinner").style.display = "block";
            f.submit();
        }});
        </script>
        '''
        return form

    def handle_ipn(self, payment, request):
        sig = request.META.get('HTTP_HMAC')
        if not sig:
            raise IpnError("Missing HMAC")

        mac = hmac.new(self.secret, request.body, hashlib.sha512).hexdigest()

        # Sanity checks, if it fails the IPN is to be ignored
        ipn_assert(request, sig, mac, 'HMAC')
        ipn_assert_post(request, 'ipn_mode', 'hmac')
        ipn_assert_post(request, 'merchant', self.merchant_id)

        try:
            status = int(request.POST.get('status'))
        except ValueError:
            raise IpnError("Invalid status (%r)" % status)

        # Some states are final (can't cancel a timeout or refund)
        if payment.status not in ('new', 'confirmed', 'error'):
            m = "Unexpected state change for %s: is %s, received status=%r" % (
                payment.id, payment.status, status
            )
            raise IpnError(m)

        # whatever the status, we can safely update the text and save the tx id
        payment.status_text = request.POST.get('status_text') or payment.status_text
        payment.backend_extid = request.POST.get('txn_id')

        received_amount = request.POST.get('amount1')
        if received_amount:
            payment.paid_amount = float(received_amount) * 100

        # And now the actual processing
        if status == 1:  # A payment is confirmed paid
            if payment.status != 'confirmed':
                if payment.paid_amount != payment.amount:
                    ipn_assert(request, payment.paid_amount, payment.amount, 'paid',
                               delta=10)
                vpnuser = payment.user.vpnuser
                vpnuser.add_paid_time(payment.time)
                vpnuser.on_payment_confirmed(payment)
                vpnuser.save()

                # We save the new state *at the end*
                # (it will be retried if there's an error)
                payment.status = 'confirmed'
                payment.status_message = None
                payment.save()

        elif status > 1:  # Waiting (that's further confirmation about funds getting moved)
            # we have nothing to do, except updating status_text
            payment.save()
            return

        elif status == -1:  # Cancel / Time out
            payment.status = 'cancelled'
            payment.save()

        elif status == -2:  # A refund
            if payment.status == 'confirmed':  # (paid -> refunded)
                payment.status = 'refunded'
                # TODO

        elif status <= -3:  # Unknown error
            payment.status = 'error'
            payment.save()

    def callback(self, payment, request):
        try:
            self.handle_ipn(payment, request)
            return True
        except IpnError as e:
            payment.status = 'error'
            payment.status_message = ("Error processing the payment. "
                                      "Please contact support.")
            payment.backend_data['ipn_exception'] = repr(e)
            payment.backend_data['ipn_last_data'] = repr(request.POST)
            payment.save()
            logger.warn("IPN error: %s", e)
            raise


