# -*- coding: utf-8 -*-
import datetime
import django
from django.db import models as dj_models
from django.apps import apps

from django.utils.translation import ugettext_lazy as _

from . import models

_table_cache = {}

#models.AuditTable.objects.all().delete()

def extract_object_data(obj):
    """
        Extract data fields from a Model object.
    """
    af_fields = []  # array of Field objects
    af = getattr(obj, '_audit_fields', None)
    afe = getattr(obj, '_audit_fields_exclude', None)

    if af:
        for f in af:
            if afe and f in afe:    # respect exclude fields
                continue
            af_fields.append(obj._meta.get_field(f))
    else:
        if afe:
            for f in obj._meta.get_fields():
                if afe and f.name in afe:    # respect exclude fields
                    continue
                af_fields.append(f)
        else:
            af_fields = obj._meta.get_fields()

    # walk through data fields, extract values
    data = {}
    for df in obj._meta.get_fields():
        # skip if audit_fields is explicitly defined
        if af and df.name not in af:
            continue

        if df.concrete:
            if type(df) == dj_models.ForeignKey:
                data[df.name] = getattr(obj, df.name+"_id")
            elif type(df) == dj_models.ManyToManyField:
                data[df.name] = set(getattr(obj, df.name).all().values_list('pk', flat=True))
            else:
                data[df.name] = getattr(obj, df.name)
    return data

class DataAudited:
    """
        Ensure auditing of changes by storing updated fields in a changelog
        with timestamp, change-transaction-id and the user who made the change.
    """

    class DataNotSavedException(Exception):
        """
            Throw this exception when data was partially saved,
            but the transaction will be rolled back. Then, we should
            not save the diff, because it will actually not happen.
        """
        pass

    def __init__(self):
        self.__audit_table = None
        """ Link to the object structure cache. """

        self.__orig_data = None
        """ Is None when not in DataAudited "with" block of code.  """

        self.__audit_user = None
        """ The user actually doing the changes in DB. """

        self.__save = self.save
        """ Original save method. """

        self.__audit_start_ts = None
        """ Timestamp of AuditContext creation. """

        # setup our save method
        self.save = self.__audited_save__

    def __cache_setup__(self):
        """ Check and update the object cache.
        
            This calculation CAN NOT be done in __init__ because of 
            possible circular dependencies in the models.
        """

        tbl_name = self._meta.db_table
        if tbl_name in _table_cache:
            self.__audit_table = _table_cache[tbl_name]
        else:
            # create the record
            _table_cache[tbl_name] = {
                'db_table' : tbl_name,
                'id' : None,
                'fields' : {},
            }
            ltc = _table_cache[tbl_name]    # local table cache variable
            self.__audit_table = ltc

            # load or create AuditTable
            rslt = models.AuditTable.objects.filter(name=tbl_name)
            if len(rslt) == 1:
                tbl = rslt[0]
            elif len(rslt) == 0:
                #print("Creating AuditTable", tbl_name )
                tbl = models.AuditTable()
                tbl.name = tbl_name
                tbl.save()
            else:
                1/0
            ltc['id'] = tbl.id

            # update AuditFields
            known_af = tbl.auditfield_set.all()
            wanted_af = set(extract_object_data(self).keys())
            for fld_obj in known_af:
                if fld_obj.name in wanted_af:
                    # create cache record
                    ltc['fields'][fld_obj.name] = {
                        'id' : fld_obj.id,
                        'name' : fld_obj.name,
                    }
                    # remove known fields
                    wanted_af.remove(fld_obj.name)
            
            # create remaining wanted_af (there can be some new)
            for fld_name in wanted_af:
                #print("Creating AuditField", tbl_name, fld_name )
                fld = models.AuditField(table=tbl, name=fld_name)
                fld.save()
                # create cache record
                ltc['fields'][fld_name] = {
                    'id' : fld.id,
                    'name' : fld_name,
                }


    def __audited_save__(self, *args, **kwargs):
        """ We audit all save()s, not just the explicit.

            We save the original save() method and provide our own
            variant to ensure auditing of save()-s even outside
            of the 'with' blocks of code.

            This leaves only direct database updates unaudited.
        """
        if self.__orig_data:
            # probably already in DataAudited with block, just save
            self.__save(*args, **kwargs)
        else:
            if self.id is None:
                # just save
                self.__save(*args, **kwargs)
            else:
                # do the audit
                obj_orig = type(self).objects.get(pk=self.id)
                self.__audit_start_ts = datetime.datetime.now()
                self.__orig_data = extract_object_data(obj_orig)
                self.__save(*args, **kwargs)
                self.__exit__(None, None, None)
                self.__orig_data = None
                self.__audit_start_ts = None

    def audit_context(self, user=None):
        """ Setup self as the context.
        
            We also record the user doing the changes.
            Transaction start timestamp is recorded to differentiate
            nested DataAudited blocks.

            The transaction number 'tnum' is growing, but later TNUM
            can be saved earlier then the parent object.

            Thus, we use timestamp to mitigate for this - correct 
            Audit log ordering is (ts, tnum). 
        """
        self.__audit_user = user
        self.__audit_start_ts = datetime.datetime.now()
        return self

    def __enter__(self):
        #print("__enter__")
        self.__orig_data = extract_object_data(self)

        #for k in self.__orig_data:
        #    print('    %s -> %s' % (k, self.__orig_data[k]) )
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        #print("__exit__", exc_type, exc_value, traceback)
        if exc_type:
            if exc_type == self.DataNotSavedException:
                return True
            else:
                return False

        data_diff = {}
        data_new = extract_object_data(self)
        af_private = getattr(self, '_audit_fields_private', None)

        # We assume no new fields appear after __enter__ and before __exit__,
        #   so we do not look for keys in data_new that are not present in data_orig.
        for f in self.__orig_data:
            if self.__orig_data[f] != data_new[f]:
                if af_private and f in af_private:
                    self.__orig_data[f] = "< ... hidden ... >"
                    data_new[f] = "< ... hidden ... >"
                data_diff[f] = ( self.__orig_data[f], data_new[f] )

        self.__save_diff(data_diff)

        self.__orig_data = None
        self.__audit_start_ts = None

        return True

    def __save_diff(self, data_diff):
        """ Save changes recorded in data_diff to DB.  """

        tnum = 1

        if self.__audit_table is None:
            self.__cache_setup__()

        print("AuditLog", self.__audit_table['db_table'], 'id', self.id)
        for k in data_diff:
            print('    %s: \t %s -> %s' % (k, data_diff[k][0], data_diff[k][1]) )

            al = models.AuditLog(
                ts = self.__audit_start_ts,
                tnum = tnum,
                user_id = self.__audit_user.id if self.__audit_user else None,
                table_id = self.__audit_table['id'],
                fld_id = self.__audit_table['fields'][k]['id'],
                rid = self.id,
            )
            al.val = data_diff[k][0]
            al.save()


