from sessionlog.models import SessionLog

from django.db import models
from django.conf import settings

import copy

from rest.models import RestModel
from rest import helpers
from rest.log import getLogger
from auditlog import cloudwatch

import traceback

AUDIT_LOGGER = getLogger("auditlog", filename="auditlog.log")
AUDITLOG_LOGGER_BY_USER = getattr(settings, "AUDITLOG_LOGGER_BY_USER", False)
AUDITLOG_LOGGERS = getattr(settings, "AUDITLOG_LOGGERS", {})

PERSISTENT_LOG_PRINT = getattr(settings, "PERSISTENT_LOG_PRINT", False)


MAX_AUDIT_LINES = 400
MAX_AUDIT_LENGTH = 9000
MAX_AUDIT_LINE_LENGTH = 200


class ConsoleColors:
    BLACK = '\033[30m'
    RED = '\033[31m'
    GREEN = '\033[32m'
    YELLOW = '\033[33m'
    PINK = "\033[35m"
    BLUE = '\033[34m'
    WHITE = '\033[37m'

    HBLACK = '\033[90m'
    HRED = '\033[91m'
    HGREEN = '\033[92m'
    HYELLOW = '\033[93m'
    HBLUE = '\033[94m'
    HPINK = "\033[95m"
    HWHITE = '\033[97m'

    HEADER = '\033[95m'
    FAIL = '\033[91m'
    OFF = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


LOG_FILTERS = getattr(settings, "AUDIT_LOG_FILTERS", {})


class PersistentLog(models.Model, RestModel):
    class RestMeta:
        DEFAULT_SORT = "-when"
        CAN_SAVE = False
        GROUP_FIELD = None  # do this so when we sort by group it is always exact
        QUERY_FIELDS = [
            "level", "component", "pkey", "action", 
            "request_method", "request_path", 
            "session__ip", "user",
            "tid", "group", "user__username"]
        SEARCH_FIELDS = ["user__username", "user__last_name", "message", "session__ip", "request_path"]
        SEARCH_TERMS = [
            "message", "action", "component", "tid",
            "pkey", ("component_id", "pkey"),
            ("ip", "session__ip"),
            ("path", "request_path"),
            ("method"), "request_method",
            ("username", "user__username"),
            ("group", "group__pk"),
            ("country", "session__location__country"),
            ("state", "session__location__state"),
            ("country", "session__location__country")]
        DATE_RANGE_FIELD = "when"
        # number of days to go back (increase performance)
        DATE_RANGE_DEFAULT = 7

        FORMATS = {
            "csv": [
                "when",
                "level",
                "request_method",
                "request_path",
                "action",
                "message",
                "session.ip",
                "user.username",
                "group.name",
                "tid"
            ]
        }

        GRAPHS = {
            "default": {
                "graphs": {
                    "user": "default",
                    "group": "default",
                    "session": "default"
                }
            },
            "list": {
                "graphs": {
                    "user": "default",
                    "group": "default",
                    "session": "default"
                }
            }
        }

    when = models.DateTimeField(db_index=True, auto_now_add=True, editable=False, help_text="When change was logged")
    level = models.IntegerField(default=0)
    message = models.TextField(blank=True, null=True, default=None)

    action = models.TextField(max_length=127, blank=True, null=True, default=None, db_index=True)
    component = models.CharField(max_length=127, blank=True, null=True, default=None, db_index=True)
    pkey = models.IntegerField(blank=True, null=True, default=None, db_index=True)
    session = models.ForeignKey(SessionLog, null=True, blank=True, on_delete=models.SET_NULL)
    request_method = models.CharField(max_length=254, blank=True, null=True, default=None)
    tid = models.CharField(max_length=254, blank=True, null=True, default=None, db_index=True)
    request_path = models.TextField(blank=True, null=True, default=None)
    user = models.ForeignKey("account.User", null=True, default=None, on_delete=models.CASCADE)
    group = models.ForeignKey("account.Group", null=True, default=None, on_delete=models.CASCADE)

    def setMessage(self, message, truncate=True, sanatize=True):
        if message is None:
            self.message = "None"
            return
        if isinstance(message, dict) or type(message) is list:
            if sanatize and isinstance(message, dict):
                message = PersistentLog.sanatize(message)
            self.message = helpers.dictToString(message, no_truncate=not truncate)
        else:
            self.message = helpers.toString(message)

        if truncate:
            if self.message.count('\n') > MAX_AUDIT_LINES:
                p = helpers.find_nth(self.message, '\n', MAX_AUDIT_LINES)
                self.message = self.message[:p]
                self.message += "\n ... <truncated lines>"

            if len(self.message) > MAX_AUDIT_LENGTH:
                lines = self.message.split('\n')
                tlines = []
                for ln in lines:
                    if len(ln) >= MAX_AUDIT_LINE_LENGTH:
                        tlines.append(ln[:MAX_AUDIT_LINE_LENGTH] + '...<truncated line len>')
                    else:
                        tlines.append(ln)
                self.message = '\n'.join(tlines)

        # check for invalid characters
        if self.message.find("\0") >= 0:
            self.message = self.message.replace("\0", "<null>")
            # helpers.log_error("found null characters", self.message)

    def printToLog(self):
        print_log = ["{}{}:{}{}".format(
            ConsoleColors.YELLOW, self.request_path, self.request_method, ConsoleColors.OFF)]
        if self.action == "request":
            print_log.append("{0}------- BEGIN --------------".format(ConsoleColors.YELLOW))
        print_log.append('{0}***** BEGIN {1}.{2} *****{3}'.format(
            ConsoleColors.YELLOW, self.component, self.action, ConsoleColors.HGREEN))
        if self.user and self.user.is_authenticated:
            print_log.append("USER: {}".format(self.user.username))
        if self.group:
            print_log.append("GROUP: {}".format(self.group.name))
        if self.tid:
            print_log.append("TID: {}".format(self.tid))
        if self.message:
            print_log.append(helpers.toString(self.message))
        else:
            print_log.append("no data")
        print_log.append('{0}***** END {1}.{2} *****{3}'.format(
            ConsoleColors.YELLOW, self.component, self.action, ConsoleColors.OFF))
        if self.action == "response":
            print_log.append("{}------- END {}:{}  --------------{}\n".format(
                ConsoleColors.YELLOW, self.request_path, self.request_method, ConsoleColors.OFF))
        if print_log:
            helpers.log_print(*print_log)

    @staticmethod
    def sanatize_password(value, n=1):
        if value is None or not isinstance(value, str):
            return value
        st = value
        return ''.join("*" if i % n == 0 else char for i, char in enumerate(st, 1))

    @staticmethod
    def sanatize_track(value):
        if value is None or not isinstance(value, str):
            return value
        st = value
        if len(st) > 12:
            pan = None
            pan2 = None
            parts = st.split('?')
            for p1 in parts:
                p1 = p1.upper().replace(';', '').replace('%B', '').replace('B', '')
                ex_data = None
                if '=' in p1:
                    pos = p1.find('=')
                    pan = p1[:pos]
                    ex_data = p1[pos + 8:]
                    st = st.replace(ex_data, "*" * len(ex_data))
                elif '^' in p1:
                    pan = p1[:p1.find('^')]
                    pos = p1.rfind('^')
                    ex_data = p1[pos + 8:]
                    st = st.replace(ex_data, "*" * len(ex_data))
                elif "D2" in p1:
                    p1 = p1.replace("D2", "=")
                    pos = p1.find('=')
                    pan = p1[:pos]
                    ex_data = p1[pos + 8:]
                    st = st.replace(ex_data, "*" * len(ex_data))
                else:
                    continue
                if not pan2:
                    pan2 = pan[:6] + "***" + pan[-4:]
            if pan is not None:
                return st.replace(pan, pan2)
        return value

    @staticmethod
    def sanatize_pan(value):
        if value and len(value) > 10:
            return "{}***{}".format(value[:6], value[-4:])
        return value

    @staticmethod
    def sanatize_epb(value):
        if isinstance(value, str):
            return "{}:*****".format(len(value))
        return value

    @staticmethod
    def sanatize_ssn(value):
        if value:
            return "***-**-{}".format(value[-4:])
        return value

    @staticmethod
    def sanatize_all(value):
        if isinstance(value, str):
            return "*" * len(value)
        return value

    @staticmethod
    def sanatize(obj):
        # deepcopy vs copy?
        sobj = copy.copy(obj)
        for key in sobj:
            if key is None:
                continue
            lkey = key.lower()
            if lkey in LOG_FILTERS:
                funcn = LOG_FILTERS.get(lkey)
                if callable(funcn):
                    sobj[key] = funcn(sobj[key])
                elif hasattr(PersistentLog, funcn):
                    sobj[key] = getattr(PersistentLog, funcn)(sobj[key])
            elif lkey in ["pan", "cardnumber"]:
                sobj[key] = PersistentLog.sanatize_pan(sobj[key])
            elif "key" in lkey or lkey in ["5a", "57", "99"]:
                sobj[key] = PersistentLog.sanatize_all(sobj[key])
            elif lkey.startswith("track"):
                sobj[key] = PersistentLog.sanatize_track(sobj[key])
            elif "password" in lkey:
                sobj[key] = PersistentLog.sanatize_password(sobj[key])
            elif lkey == "epb":
                sobj[key] = PersistentLog.sanatize_epb(sobj[key])
            elif lkey == "ssn":
                sobj[key] = PersistentLog.sanatize_ssn(sobj[key])
            # lets truncate
            if type(sobj[key]) in [str, str] and len(sobj[key]) > MAX_AUDIT_LINE_LENGTH:
                sobj[key] = sobj[key][:MAX_AUDIT_LINE_LENGTH] + '...<truncated linelen>'
            if isinstance(sobj[key], dict):
                sobj[key] = PersistentLog.sanatize(sobj[key])
        return sobj

    @staticmethod
    def log(message, level=0, request=None, component=None,
            pkey=None, action=None, group=None, path=None,
            method=None, tid=None, no_truncate=False,
            aws_log_group=None, aws_log_stream=None):
        plog = PersistentLog.createLogFromRequest(
            request, component=component, tid=tid, pkey=pkey, action=action, group=group,
            path=path, method=method, level=level)

        plog.setMessage(message, truncate=not no_truncate)

        try:
            plog.save()
            if aws_log_group and aws_log_stream:
                cloudwatch.log(plog.toDict(), aws_log_group, aws_log_stream)
        except Exception:
            helpers.log_exception(plog.message)

        if PERSISTENT_LOG_PRINT:
            plog.printToLog()
        return plog

    @staticmethod
    def logException(message=None, level=50, request=None, component=None,
                     pkey=None, action="error", group=None, path=None,
                     method=None, tid=None, no_truncate=True):
        # LETS LOG THE REQUEST DATA TO BE SURE
        if request is None:
            from rest.middleware import get_request
            request = get_request()

        helpers.log_exception(message)
        if request:
            helpers.log_error(request.DATA.toDict())
            # request.DATA.log()
            helpers.log_error("tid={}\npath={}\nip={}\nuser={}".format(
                tid, path, request.ip, request.user))

        # LOG THE EXCEPTION TO local LOGGERS
        plog = PersistentLog.createLogFromRequest(
            request=request, component=component, tid=tid, pkey=pkey, action=action, group=group,
            path=path, method=method, level=level)
        stack = str(traceback.format_exc())
        plog.setMessage(stack, truncate=not no_truncate)
        try:
            plog.save()
        except Exception:
            helpers.log_exception(plog.message)
        return plog

    @staticmethod
    def logError(message=None, level=0, request=None, component=None,
                 pkey=None, action="error", group=None, path=None,
                 method=None, tid=None, no_truncate=True):
        # LETS LOG THE REQUEST DATA TO BE SURE
        if request is None:
            from rest.middleware import get_request
            request = get_request()
        if request:
            request.DATA.log()
        plog = PersistentLog.createLogFromRequest(
            request, component=component, tid=tid, pkey=pkey, action=action, group=group,
            path=path, method=method, level=level)
        stack = str(traceback.format_exc())
        plog.setMessage(stack, truncate=not no_truncate)
        try:
            plog.save()
        except Exception:
            helpers.log_exception(plog.message)
        # print all errors to local logs
        plog.printToLog()
        return plog

    @staticmethod
    def createLogFromRequest(request=None, component=None, tid=None,
                             pkey=None, action=None, group=None,
                             path=None, method=None, level=10):
        if request is None:
            from rest.middleware import get_request
            request = get_request()
        if pkey is None and request is not None and hasattr(request, "_log_component"):
            if request._log_pk is not None:
                component = request._log_component
                pkey = request._log_pk
        plog = PersistentLog(
            group=group, level=level, component=component, tid=tid,
            pkey=pkey, action=action, request_path=path, request_method=method)
        if request:
            if request.user.is_authenticated:
                plog.user = request.user
            if not group and hasattr(request, "group"):
                plog.group = request.group
            if not path:
                plog.request_path = request.path
            else:
                plog.request_path = path
            if not method:
                plog.request_method = request.method
            else:
                plog.request_method = method
            plog.session = SessionLog.GetSession(request)
            if plog.tid is None:
                if hasattr(request, "device_id") and request.device_id:
                    plog.tid = request.device_id
                elif hasattr(request, "buid") and request.buid:
                    plog.tid = request.buid
        return plog


class AuditLog(models.Model):
    """
    Audit Log
    """

    class Meta:
        permissions = (
            ("can_read", "Can read audit log"),
        )

    when = models.DateTimeField(auto_now_add=True, editable=False, help_text="When change was logged")
    model = models.CharField(max_length=127, db_index=True)
    pkey = models.IntegerField(db_index=True)
    attribute = models.CharField(max_length=127)
    user = models.ForeignKey("account.User", null=True, on_delete=models.CASCADE)
    session = models.ForeignKey(SessionLog, null=True, blank=True, on_delete=models.CASCADE)
    how = models.TextField(max_length=127, null=True, blank=True)
    referer = models.TextField(null=True, blank=True)
    stack = models.TextField(null=True, blank=True)
    oldval = models.TextField(null=True, blank=True)
    newval = models.TextField(null=True, blank=True)

    def __unicode__(self):
        return "[%s] %s.%s" % (self.when, self.model, self.attribute)

    def reference(self):
        if self.referer:
            try:
                ret = self.referer.partition(settings.BASE_URL)
            except AttributeError:
                pass
            else:
                if ret[0] == '':
                    return ret[2]
            try:
                ret = self.referer.partition(settings.BASE_URL_SECURE)
            except AttributeError:
                pass
            else:
                if ret[0] == '':
                    return ret[2]

        try:
            return self.session.user_agent.partition(' ')[0]
        except (SessionLog.DoesNotExist, AttributeError):
            pass

        return None
