import datetime
import transaction
import ZODB.POSException

from pyramid_zodbconn import get_connection
from pyramid.httpexceptions import HTTPFound

from .. import mgmt_view
from ...objectmap import find_objectmap
from ...util import _

class UndoViews(object):
    transaction = transaction # for tests
    get_connection = staticmethod(get_connection) # for tests
    find_objectmap = staticmethod(find_objectmap) # for tests

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def _get_db(self):
        request = self.request
        conn = self.get_connection(request)
        db = conn.db()
        return db

    @mgmt_view(
        name='undo_recent',
        tab_condition=False, 
        check_csrf=True,
        permission='sdi.view',
        )
    def undo_recent(self):
        request = self.request
        undohash = request.params['undohash']
        undo = None
        db = self._get_db()
        userid = self.request.authenticated_userid
        # I am permitted to undo it if:
        #
        # - It happend within the most recent 50 transactions.
        #
        # - I personally did it.
        #
        # NB: do not break these assertions, as this view is protected only by
        # sdi.view permission.
        for record in db.undoLog(0, -50):
            try:
                r_userid = int(record.get('user_name', None))
            except:
                r_userid = None
            if (record.get('undohash') == undohash and r_userid == userid):
                undo = dict(record)
                break
        if undo is None:
            request.sdiapi.flash('Could not undo, sorry', 'danger')
        else:
            tid = undo['id']
            try:
                db.undo(tid)
                # provoke MultipleUndoErrors exception immediately
                msg = 'Undid: %s' % undo['description']
                self.transaction.get().note(msg)
                self.transaction.commit() 
                request.sdiapi.flash(msg, 'success')
            except ZODB.POSException.POSError:
                self.transaction.abort()
                msg = _('Could not undo, sorry')
                request.sdiapi.flash(msg, 'error')
        return HTTPFound(
            request.referrer or request.sdiapi.mgmt_path(request.context)
            )

    @mgmt_view(
        name='undo_multiple',
        tab_condition=False, 
        check_csrf=True,
        physical_path='/',
        permission='sdi.undo',
        )
    def undo_multiple(self):
        request = self.request

        transaction_info = self.request.POST.getall('transaction')

        tids = []
        descriptions = []

        uid = request.authenticated_userid

        for tid in transaction_info:
            if not isinstance(tid, bytes): #pragma NO COVER Py3k
                tid = tid.encode('ascii', 'surrogateescape')
            tid = tid.split(b' ', 1)
            if tid:
                tids.append(decode64(tid[0]))
                descriptions.append(tid[1].decode('ascii', 'surrogateescape'))

        if tids:
            undid = "Undid: %s" % ' '.join(descriptions)
            t = self.transaction.get()
            t.note(undid)
            t.setUser(uid)
            try:
                self._get_db().undoMultiple(tids)
                # provoke MultipleUndoErrors exception immediately
                self.transaction.commit() 
                request.sdiapi.flash(undid, 'success')
            except ZODB.POSException.POSError:
                self.transaction.abort()
                msg = _('Could not undo, sorry')
                request.sdiapi.flash(msg, 'error')

        return HTTPFound(request.sdiapi.mgmt_path(request.context, 'undo'))

    def _undoable_transactions(self, first, last):
        db = self._get_db()

        objectmap = self.find_objectmap(self.context)

        r = db.undoLog(first, last)

        tz = self.request.user.timezone
        
        for d in r:
            t = datetime.datetime.fromtimestamp(d['time'])
            t = tz.localize(t).strftime('%Y-%m-%d %H:%M:%S %Z')
            d['time'] = t
            desc = d['description'] or b''
            if not isinstance(desc, bytes): #pragma NO COVER Py3k
                desc = desc.encode('ascii', 'surrogateescape')
            tid = d['id']
            un = d['user_name']
            try:
                un = objectmap.object_for(int(un.split()[-1].strip()))
                d['user_name'] = un.__name__.encode('ascii', 'surrogateescape')
            except: # use original username if any trouble
                pass
            if len(desc) > 80:
                desc = desc[:76] + b' ...'
            tid = encode64(tid) + b' ' + desc
            d['id'] = tid

        return r

    @mgmt_view(
        name='undo',
        tab_title=_('Undo'),
        renderer='templates/undo.pt',
        physical_path='/',
        permission='sdi.undo',
        )
    def undo(self):
        first = int(self.request.GET.get('first', 0))
        size = 10
        last = first + size

        batch = self._undoable_transactions(first, last)

        if len(batch) < size:
            earlier = None
        else:
            earlier = last
            earlier = self.request.sdiapi.mgmt_path(
                self.context, 'undo', query={'first':earlier}
                )
        if first <= 0:
            later = None
        else:
            later = first - size
            later = self.request.sdiapi.mgmt_path(
                self.context, 'undo', query={'first':later}
                )

        batch_num = first / size
        
        return {'batch':batch, 'earlier':earlier, 'later':later,
                'batch_num':batch_num}
        
        
            
########################################################################
# Blech, need this cause binascii.b2a_base64 is too picky (from Zope)

import binascii

def encode64(s, b2a=binascii.b2a_base64):
    if len(s) < 58:
        return b2a(s)
    r = []
    a = r.append
    for i in range(0, len(s), 57):
        a(b2a(s[i:i+57])[:-1])
    return b''.join(r)


def decode64(s, a2b=binascii.a2b_base64):
    return a2b(s + b'\n')

del binascii
