import random
import string
from datetime import datetime as dt
import hashlib
import time
import urllib

from decimal import Decimal

from quixote import (redirect, get_publisher, get_request, get_session,
        get_response)
from quixote.directory import Directory

if not set:
    from sets import Set as set

eopayment = None
try:
    import eopayment
except ImportError:
    pass

from qommon import errors, get_logger, get_cfg, emails
from qommon.storage import StorableObject
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
    WidgetDict

from wcs.formdef import FormDef
from wcs.formdata import Evolution
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
from wcs.users import User

def is_payment_supported():
    if not eopayment:
        return False
    return get_cfg('aq-permissions', {}).get('payments', None) is not None


class Regie(StorableObject):
    _names = 'regies'

    label = None
    description = None
    service = None
    service_options = None

    def get_payment_object(self):
        return eopayment.Payment(kind=self.service,
                                 options=self.service_options,
                                 logger=get_logger())


class Invoice(StorableObject):
    _names = 'invoices'
    _hashed_indexes = ['user_id', 'user_hash', 'regie_id']
    _indexes = ['external_id']

    user_id = None
    user_hash = None
    regie_id = None
    formdef_id = None
    formdata_id = None
    subject = None
    details = None
    amount = None
    date = None
    paid = False
    paid_date = None
    canceled = False
    canceled_date = None
    canceled_reason = None
    next_status = None
    external_id = None
    request_kwargs = {}

    def __init__(self, id=None, regie_id=None, formdef_id=None):
        self.id = id
        self.regie_id = regie_id
        self.formdef_id = formdef_id
        if get_publisher() and not self.id:
            self.id = self.get_new_id()

    def get_user(self):
        if self.user_id:
            return User.get(self.user_id, ignore_errors=True)
        return None

    @property
    def username(self):
        user = self.get_user()
        return user.name if user else ''

    def get_new_id(self, create=False):
        # format : date-regie-formdef-alea-check
        r = random.SystemRandom()
        self.fresh = True
        while True:
            id = '-'.join([
                dt.now().strftime('%Y%m%d'),
                'r%s' % (self.regie_id or 'x'),
                'f%s' % (self.formdef_id or 'x'),
                ''.join([r.choice(string.digits) for x in range(5)])
                ])
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
            id = id + '-' + crc
            if not self.has_key(id):
                return id

    def store(self, *args, **kwargs):
        if getattr(self, 'fresh', None) is True:
            del self.fresh
            notify_new_invoice(self)
        return super(Invoice, self).store(*args, **kwargs)

    def check_crc(cls, id):
        try:
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
        except:
            return False
    check_crc = classmethod(check_crc)

    def pay(self):
        self.paid = True
        self.paid_date = dt.now()
        self.store()
        get_logger().info(_('invoice %s paid'), self.id)
        notify_paid_invoice(self)

    def unpay(self):
        self.paid = False
        self.paid_date = None
        self.store()
        get_logger().info(_('invoice %s unpaid'), self.id)

    def cancel(self, reason=None):
        self.canceled = True
        self.canceled_date = dt.now()
        if reason:
            self.canceled_reason = reason
        self.store()
        notify_canceled_invoice(self)
        get_logger().info(_('invoice %s canceled'), self.id)

    def payment_url(self):
        base_url = get_publisher().get_frontoffice_url()
        return '%s/invoices/%s' % (base_url, self.id)


INVOICE_EVO_VIEW = {
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
}

class InvoiceEvolutionPart:
    action = None
    id = None
    subject = None
    amount = None
    transaction = None

    def __init__(self, action, invoice, transaction=None):
        self.action = action
        self.id = invoice.id
        self.subject = invoice.subject
        self.amount = invoice.amount
        self.transaction = transaction

    def view(self):
        vars = {
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
            'id': self.id,
            'subject': self.subject,
            'amount': self.amount,
        }
        if self.transaction:
            vars['transaction_order_id'] = self.transaction.order_id
        return htmltext('<p class="invoice-%s">' % self.action + \
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')


class Transaction(StorableObject):
    _names = 'transactions'
    _hashed_indexes = ['invoice_ids']
    _indexes = ['order_id']

    invoice_ids = None

    order_id = None
    start = None
    end = None
    bank_data = None

    def __init__(self, *args, **kwargs):
        self.invoice_ids = list()
        StorableObject.__init__(self, *args, **kwargs)

    def get_new_id(cls, create=False):
        r = random.SystemRandom()
        while True:
            id = ''.join([r.choice(string.digits) for x in range(16)])
            if not cls.has_key(id):
                return id
    get_new_id = classmethod(get_new_id)

class PaymentWorkflowStatusItem(WorkflowStatusItem):
    description = N_('Payment Creation')
    key = 'payment'
    endpoint = False
    category = ('aq-payment', N_('Payment'))
    support_substitution_variables = True

    subject = None
    details = None
    amount = None
    regie_id = None
    next_status = None
    request_kwargs = {}

    def is_available(self):
        return is_payment_supported()
    is_available = classmethod(is_available)

    def render_as_line(self):
        if self.regie_id:
            return _('Payable to %s' % Regie.get(self.regie_id).label)
        else:
            return _('Payable (not completed)')

    def get_parameters(self):
        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
                'request_kwargs')

    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
        if 'subject' in parameters:
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
                     value=self.subject, size=40)
        if 'details' in parameters:
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
                     value=self.details, cols=80, rows=10)
        if 'amount' in parameters:
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
        if 'regie_id' in parameters:
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
                title=_('Regie'), value=self.regie_id,
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
        if 'next_status' in parameters:
            form.add(SingleSelectWidget, '%snext_status' % prefix,
                title=_('Status after validation'), value = self.next_status,
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
        if 'request_kwargs' in parameters:
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
            hint = ''
            hint +=_('If the value starts by = it will be '
                'interpreted as a Python expression.')
            hint += ' '
            hint += _('Standard keys are: %s.') % (', '.join(keys))
            form.add(WidgetDict, 'request_kwargs',
                title=_('Parameters for the payment system'),
                hint=hint,
                value = self.request_kwargs)

    def perform(self, formdata):
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
        invoice.user_id = formdata.user_id
        invoice.user_hash = formdata.user_hash
        invoice.formdata_id = formdata.id
        invoice.next_status = self.next_status
        if self.subject:
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
        else:
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
                    'form_name': formdata.formdef.name,
                    'formdata_id': formdata.id }
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
        invoice.amount = Decimal(self.compute(self.amount))
        invoice.date = dt.now()
        invoice.request_kwargs = {}
        if self.request_kwargs:
            for key, value in self.request_kwargs.iteritems():
                invoice.request_kwargs[key] = self.compute(value)
        invoice.store()
        # add a message in formdata.evolution
        evo = Evolution()
        evo.time = time.localtime()
        evo.status = formdata.status
        evo.add_part(InvoiceEvolutionPart('create', invoice))
        if not formdata.evolution:
            formdata.evolution = []
        formdata.evolution.append(evo)
        formdata.store()
        # redirect the user to "my invoices"
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'

register_item_class(PaymentWorkflowStatusItem)

class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
    description = N_('Payment Cancel')
    key = 'payment-cancel'
    endpoint = False
    category = ('aq-payment', N_('Payment'))

    reason = None
    regie_id = None

    def is_available(self):
        return is_payment_supported()
    is_available = classmethod(is_available)

    def render_as_line(self):
        if self.regie_id:
            if self.regie_id == '_all':
                return _('Cancel all Payments')
            else:
                return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
        else:
            return _('Cancel Payments (non completed)')

    def get_parameters(self):
        return ('reason', 'regie_id')

    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
        if 'reason' in parameters:
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
                     value=self.reason, size=40)
        if 'regie_id' in parameters:
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
                title=_('Regie'), value=self.regie_id,
                options = [(None, '---'), ('_all', _('All Regies'))] + \
                            [(x.id, x.label) for x in Regie.select()])

    def perform(self, formdata):
        invoices_id = []
        # get all invoices for the formdata and the selected regie
        for evo in [evo for evo in formdata.evolution if evo.parts]:
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
                if part.action == 'create':
                    invoices_id.append(part.id)
                elif part.id in invoices_id:
                    invoices_id.remove(part.id)
        invoices = [Invoice.get(id) for id in invoices_id]
        # select invoices for the selected regie (if not "all regies")
        if self.regie_id != '_all':
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
        # security filter: check user 
        invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
                or (i.user_hash == formdata.user_hash)]
        # security filter: check formdata & formdef
        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
                and (i.formdef_id == formdata.formdef.id)]
        evo = Evolution()
        evo.time = time.localtime()
        for invoice in invoices:
            if not (invoice.paid or invoice.canceled):
                invoice.cancel(self.reason)
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
        if not formdata.evolution:
            formdata.evolution = []
        formdata.evolution.append(evo)
        formdata.store()
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'

register_item_class(PaymentCancelWorkflowStatusItem)


def request_payment(invoice_ids, url, add_regie=True):
    for invoice_id in invoice_ids:
        if not Invoice.check_crc(invoice_id):
            raise KeyError()
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
    regie_ids = set([invoice.regie_id for invoice in invoices])
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
    if len(invoices) == 0 or len(regie_ids) != 1:
        url = get_publisher().get_frontoffice_url()
        if get_session().user:
            # FIXME: add error messages
            url += '/myspace/invoices/'
        return redirect(url)
    if add_regie:
        url = '%s%s' % (url, list(regie_ids)[0])

    transaction = Transaction()
    transaction.store()
    transaction.invoice_ids = invoice_ids
    transaction.start = dt.now()

    amount = Decimal(0)
    for invoice in invoices:
        amount += Decimal(invoice.amount)

    regie = Regie.get(invoice.regie_id)
    payment = regie.get_payment_object()
    # initialize request_kwargs using informations from the first invoice
    # and update using current user informations
    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
    request = get_request()
    if request.user and request.user.email:
        request_kwargs['email'] = request.user.email
    if request.user and request.user.display_name:
        request_kwargs['name'] = request.user.display_name
    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
    transaction.order_id = order_id
    transaction.store()

    for invoice in invoices:
        if invoice.formdef_id and invoice.formdata_id:
            formdef = FormDef.get(invoice.formdef_id)
            formdata = formdef.data_class().get(invoice.formdata_id)
            evo = Evolution()
            evo.time = time.localtime()
            evo.status = formdata.status
            evo.add_part(InvoiceEvolutionPart('try', invoice,
                transaction=transaction))
            if not formdata.evolution:
                formdata.evolution = []
            formdata.evolution.append(evo)
            formdata.store()

    if kind == eopayment.URL:
        return redirect(data)
    elif kind == eopayment.FORM:
        raise NotImplementedError()
    else:
        raise NotImplementedError()


class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
    description = N_('Payment Validation')
    key = 'payment-validation'
    endpoint = False
    category = ('aq-payment', N_('Payment'))

    next_status = None

    def is_available(self):
        return is_payment_supported()
    is_available = classmethod(is_available)

    def render_as_line(self):
        return _('Wait for payment validation')

    def get_parameters(self):
        return ('next_status',)

    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
        if 'next_status' in parameters:
            form.add(SingleSelectWidget, '%snext_status' % prefix,
                title=_('Status once validated'), value = self.next_status,
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])

register_item_class(PaymentValidationWorkflowStatusItem)


class PublicPaymentRegieBackDirectory(Directory):
    def __init__(self, asynchronous):
        self.asynchronous = asynchronous

    def _q_lookup(self, component):
        logger = get_logger()
        request = get_request()
        query_string = get_request().get_query()
        if request.get_method() == 'POST' and query_string == '':
            query_string = urllib.urlencode(request.form)
        try:
            regie = Regie.get(component)
        except KeyError:
            raise errors.TraversalError()
        if self.asynchronous:
            logger.debug('received asynchronous notification %r' % query_string)
        payment = regie.get_payment_object()
        payment_response = payment.response(query_string)
        logger.debug('payment response %r', payment_response)
        order_id = payment_response.order_id
        bank_data = payment_response.bank_data

        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
        if transaction is None:
            raise errors.TraversalError()
        commit = False
        if not transaction.end:
            commit = True
            transaction.end = dt.now()
            transaction.bank_data = bank_data
            transaction.store()
        if payment_response.signed and payment_response.is_paid() and commit:
            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
                                    order_id, payment_response.transaction_id, bank_data))

            for invoice_id in transaction.invoice_ids:
                # all invoices are now paid
                invoice = Invoice.get(invoice_id)
                invoice.pay()

                # workflow for each related formdata
                if invoice.formdef_id and invoice.formdata_id:
                    next_status = invoice.next_status
                    formdef = FormDef.get(invoice.formdef_id)
                    formdata = formdef.data_class().get(invoice.formdata_id)
                    wf_status = formdata.get_status()
                    for item in wf_status.items:
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
                            next_status = item.next_status
                            break
                    if next_status is not None:
                        formdata.status = 'wf-%s' % next_status
                        evo = Evolution()
                        evo.time = time.localtime()
                        evo.status = formdata.status
                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
                            transaction=transaction))
                        if not formdata.evolution:
                            formdata.evolution = []
                        formdata.evolution.append(evo)
                        formdata.store()
                        # performs the items of the new status
                        formdata.perform_workflow()

        elif payment_response.is_error() and commit:
            logger.error('transaction %s finished with failure, bank_data:%s' % (
                                    order_id, bank_data))
        elif commit:
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
                                    order_id, bank_data))
        if payment_response.return_content != None and self.asynchronous:
            get_response().set_content_type('text/plain')
            return payment_response.return_content
        else:
            if payment_response.is_error():
                # TODO: here return failure message
                get_session().message = ('info', _('Payment failed'))
            else:
                # TODO: Here return success message
                get_session().message = ('error', _('Payment succeeded'))
            url = get_publisher().get_frontoffice_url()
            if get_session().user:
                url += '/myspace/invoices/'
            return redirect(url)

class PublicPaymentDirectory(Directory):
    _q_exports = ['', 'init', 'back', 'back_asynchronous']

    back = PublicPaymentRegieBackDirectory(False)
    back_asynchronous = PublicPaymentRegieBackDirectory(True)


    def init(self):
        invoice_ids = get_request().form.get('invoice_ids').split(' ')

        for invoice_id in invoice_ids:
            if not Invoice.check_crc(invoice_id):
                raise KeyError()

        url = get_publisher().get_frontoffice_url() + '/payment/back/'

        return request_payment(invoice_ids, url)

def notify_new_invoice(invoice):
    notify_invoice(invoice, 'payment-new-invoice-email')

def notify_paid_invoice(invoice):
    notify_invoice(invoice, 'payment-invoice-paid-email')

def notify_canceled_invoice(invoice):
    notify_invoice(invoice, 'payment-invoice-deleted-email')

def notify_invoice(invoice, template):
    user = invoice.get_user()
    assert user is not None
    regie = Regie.get(id=invoice.regie_id)
    emails.custom_ezt_email(template, { 
                'user': user,
                'invoice': invoice,
                'regie': regie, 
                'invoice_url': invoice.payment_url() 
            }, user.email, fire_and_forget = True)

