from django.conf import settings
from django.db import models
from django.utils.encoding import force_str

from analog.exceptions import NoExtraField, UnknownLogKind
from analog.settings import KIND_IDS, KIND_LABELS, KINDS


def _map_kind(kind):
    if isinstance(kind, str):
        if kind not in KINDS:
            raise UnknownLogKind(kind)
        kind = KINDS[kind]
    assert isinstance(kind, int)
    if kind not in KIND_IDS:
        raise UnknownLogKind(kind)
    return kind


class BaseLogEntry(models.Model):
    """
    Abstract base model class for the various log models.

    The concrete models are created by :func:`define_log_model`.

    In addition, directly deriving a model from `BaseLogEntry` is supported
    (for instance, to allow for log entries that are not attached to other
    models), though the `add_log_entry` function will naturally not be
    automatically augmented to pass a `target` parameter.
    """

    target = None  # This will be overridden dynamically
    created_on = models.DateTimeField(auto_now_add=True, editable=False)
    user = models.ForeignKey(
        getattr(settings, "AUTH_USER_MODEL", "auth.User"),
        null=True, on_delete=models.PROTECT
    )
    message = models.CharField(max_length=256)
    identifier = models.CharField(max_length=64, blank=True)
    kind = models.IntegerField(default=0, db_index=True)

    class Meta:
        abstract = True

    def get_kind_display(self):
        """
        Get the kind label for this log entry.

        This emulates the behavior Django would have for fields that
        have the ``choices`` kwarg.
        """
        return KIND_LABELS.get(self.kind, self.kind)

    def save(self, *args, **kwargs):
        if self.pk:
            raise ValueError("%r objects may not be modified" % self.__class__)
        super().save(*args, **kwargs)

    @classmethod
    def add_log_entry(
        cls,
        target, message, identifier=None, kind="other",
        user=None, extra=None, save=True,
        **kwargs
    ):
        """
        Add a log entry.

        .. note::

           This method should not be used directly; instead, the same method,
           aside from the `target` argument, is available on models that
           have been anointed by :func:`define_log_model`.

        :param target: Target model instance
        :param message: Log message
        :type message: str
        :param identifier: Log message identifier.
                           Useful for tagging log entries in a way that is
                           not necessarily human-readable.
        :type identifier: str|None
        :param kind: Log entry kind. Either a mnemonic string (preferred and
                     readable), or the actual entry ID integer (if you really
                     have to). Kinds are configured in your project's settings.
        :type kind: int|str
        :param user: An optional user object (an instance of
                     ``settings.AUTH_USER_MODEL``) to attach to this log entry.
        :param extra: Extra data, if applicable. If set, this must be
                      serializable to JSON; ``dict``s are a good idea.
                      This is only applicable if the model contains an `extra`
                      field; passing non-None here if the model
                      does not contain `extra` is an error.
        :type extra: object|None
        :param kwargs: Any other fields to pass to the constructor of the
                       class. Mainly useful with log classes derived from
                       `BaseLogEntry`.
        :type kwargs: dict
        :param save: Whether to immediately save the log entry. Default True.
        :type save: bool
        :return: The created log entry
        """

        if target is not None and not getattr(target, "pk", None):
            raise ValueError("Can not create log entry for unsaved object")

        kind = _map_kind(kind)

        if not getattr(user, "pk", None):
            user = None

        kwargs = dict(
            target=target,
            message=message,
            identifier=force_str(identifier or "", errors="ignore")[:64],
            user=user,
            kind=kind,
            **kwargs
        )

        has_extra_field = ('extra' in cls._meta._forward_fields_map)
        if extra is not None:
            if not has_extra_field:
                raise NoExtraField(
                    'The %r class has no `extra` field,'
                    'but non-None extra was passed!' % cls
                )
            kwargs['extra'] = extra

        if target is None:
            kwargs.pop('target')
        log_entry = cls(**kwargs)
        log_entry.clean()
        if save:
            log_entry.save()
        return log_entry
