import hashlib
import json
import logging
import urllib
from collections import OrderedDict

import requests
from bitpay import key_utils
from bitpay.client import Client
from bitpay.exceptions import BitPayConnectionError, BitPayBitPayError, BitPayArgumentError
from django import forms
from django.core import signing
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from typing import Union

from pretix.base.models import OrderPayment, OrderRefund
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.multidomain.urlreverse import build_absolute_uri
from .models import ReferencedBitPayObject

logger = logging.getLogger(__name__)


class BitPay(BasePaymentProvider):
    identifier = 'bitpay'
    verbose_name = _('BitPay')
    public_name = _('Bitcoin')

    def settings_content_render(self, request):
        if not self.settings.token:
            return (
                "<p>{}</p>"
                "<p>"
                "<a href='{}' target='_blank' class='btn btn-primary btn-lg'>{}</a> "
                "<a href='{}' target='_blank' class='btn btn-default btn-lg'>{}</a> "
                "</p>"
                "<p>{}</p>"
                "</form>"  # This is a hell of a hack, sorry…
                "<form class='form-inline' action='{}' method='GET' target='_blank'>"
                "<input type='text' name='url' class='form-control' value='https://btcpay.lightbo.lt'> "
                "<button class='btn btn-default'>{}</button>"
                "</form>"
                "<form>"
            ).format(
                _('To accept payments via BitPay, you will need an account at BitPay. By clicking on the '
                  'following button, you can connect pretix to your BitPay account. A BitPay site will open in a new '
                  'tab. Return to this page and refresh it after you authorized the token at BitPay.'),
                reverse('plugins:pretix_bitpay:auth.start', kwargs={
                    'organizer': self.event.organizer.slug,
                    'event': self.event.slug,
                }),
                _('Connect with BitPay'),
                reverse('plugins:pretix_bitpay:auth.start', kwargs={
                    'organizer': self.event.organizer.slug,
                    'event': self.event.slug,
                }) + '?test=1',
                _('Connect with test.bitpay.com'),
                _('Alternatively, you can connect with a third-party provider using a BitPay-compatible API. Enter'
                  'the URL here you want to connect to.'),
                reverse('plugins:pretix_bitpay:auth.start', kwargs={
                    'organizer': self.event.organizer.slug,
                    'event': self.event.slug,
                }),
                _('Start pairing')
            )
        else:
            return (
                "<button formaction='{}' class='btn btn-danger'>{}</button>"
            ).format(
                reverse('plugins:pretix_bitpay:auth.disconnect', kwargs={
                    'organizer': self.event.organizer.slug,
                    'event': self.event.slug,
                }),
                _('Disconnect from BitPay')
            )

    @property
    def settings_form_fields(self):
        if not self.settings.token:
            return {}
        d = OrderedDict(
            [
                ('url',
                 forms.CharField(
                     label=_('API URL'),
                     disabled=True
                 )),
            ] + list(super().settings_form_fields.items())
        )
        d.move_to_end('_enabled', last=False)
        return d

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (
                build_absolute_uri(request.event, 'plugins:pretix_bitpay:redirect') + '?url=' +
                urllib.parse.quote(signer.sign(url))
            )
        else:
            return str(url)

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_bitpay/checkout_payment_form.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretix_bitpay/checkout_payment_confirm.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings}
        return template.render(ctx)

    def checkout_prepare(self, request, total):
        return True

    def payment_is_valid_session(self, request):
        return True

    @property
    def test_mode_message(self):
        if '//test.bitpay.com' in self.settings.url:
            return _('The BitPay plugin is operating in test mode. No money will actually be transferred.')
        return None

    @cached_property
    def client(self):
        return Client(api_uri=self.settings.url, pem=self.settings.pem)

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        request.session['payment_bitpay_order_secret'] = payment.order.secret

        try:
            inv = self.client.create_invoice({
                "price": float(payment.amount),
                "currency": self.event.currency,
                "orderId": payment.order.full_code,
                "transactionSpeed": "medium",
                "extendedNotifications": "true",
                "notificationURL": build_absolute_uri(self.event, "plugins:pretix_bitpay:webhook"),
                "redirectURL": build_absolute_uri(self.event, "plugins:pretix_bitpay:return", kwargs={
                    'order': payment.order.code,
                    'payment': payment.pk,
                    'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
                }),
                # "buyer": {"email": "test@customer.com"},
                "token": self.settings.token
            })
        except (BitPayConnectionError, BitPayBitPayError, BitPayArgumentError) as e:
            logger.exception('Failure during bitpay payment.')
            raise PaymentException(_('We had trouble communicating with BitPay. Please try again and get in touch '
                                     'with us if this problem persists.'))
        ReferencedBitPayObject.objects.get_or_create(order=payment.order, payment=payment, reference=inv['id'])
        payment.info = json.dumps(inv)
        payment.save(update_fields=['info'])
        return self.redirect(request, inv['url'])

    def payment_pending_render(self, request: HttpRequest, payment: OrderPayment):
        template = get_template('pretix_bitpay/pending.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings,
               'order': payment.order}
        return template.render(ctx)

    def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
        template = get_template('pretix_bitpay/control.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings,
               'payment_info': payment.info_data, 'order': payment.order, 'provname': self.verbose_name}
        return template.render(ctx)

    abort_pending_allowed = True

    def payment_refund_supported(self, payment: OrderPayment):
        return True

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return True

    def _refund(self, refund):
        payload = json.dumps({
            'token': refund.payment.info_data.get('token'),
            'amount': refund.payment.info_data.get('price'),
            'currency': refund.payment.info_data.get('currency'),
            'refundEmail': refund.order.email
        })
        uri = self.client.uri + "/invoices/" + refund.payment.info_data.get('id') + "/refunds"
        xidentity = key_utils.get_compressed_public_key_from_pem(self.client.pem)
        xsignature = key_utils.sign(uri + payload, self.client.pem)
        headers = {"content-type": "application/json", 'accept': 'application/json', 'X-Identity': xidentity,
                   'X-Signature': xsignature, 'X-accept-version': '2.0.0'}
        try:
            response = requests.post(uri, data=payload, headers=headers, verify=self.client.verify)
        except Exception:
            logger.exception('Failure during bitpay refund.')
            raise PaymentException(_('We had trouble communicating with BitPay. Please try again and get in touch '
                                     'with us if this problem persists.'))
        if not response.ok:
            e = response.json()['error']
            logger.exception('Failure during bitpay refund: {}'.format(e))
            raise PaymentException(_('BitPay reported an error: {}').format(e))

    def execute_refund(self, refund: OrderRefund):
        try:
            self._refund(refund)
        except requests.exceptions.RequestException as e:
            logger.exception('BitPay error: %s' % str(e))
            raise PaymentException(_('We had trouble communicating with BitPay. Please try again and contact '
                                     'support if the problem persists.'))
        else:
            refund.done()

    def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
        d = obj.info_data
        for k, v in list(d.items()):
            if v not in ('id', 'status', 'price', 'currency', 'invoiceTime', 'paymentSubtotals',
                         'paymentTotals', 'transactionCurrency', 'amountPaid'):
                d[k] = '█'
        obj.info_data = d
        obj.save()

        for le in obj.order.all_logentries().filter(action_type="pretix_bitpay.event").exclude(data=""):
            d = le.parsed_data
            if 'data' in d:
                for k, v in list(d['data'].items()):
                    if v not in ('id', 'status', 'price', 'currency', 'invoiceTime', 'paymentSubtotals',
                                 'paymentTotals', 'transactionCurrency', 'amountPaid'):
                        d['data'][k] = '█'
                le.data = json.dumps(d)
                le.shredded = True
                le.save(update_fields=['data', 'shredded'])
