# -*- coding: utf-8 -*-
# Copyright (c) 2016, 2017, 2018, 2019 Sqreen. All rights reserved.
# Please refer to our terms for more information:
#
#     https://www.sqreen.io/terms.html
#
""" Various utils
"""
import json
import sys
from datetime import datetime, timedelta
from functools import WRAPPER_ASSIGNMENTS
from inspect import isclass
from logging import getLogger
from operator import methodcaller

from ._vendors.ipaddress import (
    ip_address as _ip_address,
    ip_network as _ip_network,
)

if sys.version_info >= (3, 2):
    # Introduced in Python 3.2
    from collections.abc import Iterable, Mapping  # noqa: F401
else:
    # Removed in Python 3.9
    from collections import Iterable, Mapping  # noqa: F401


LOGGER = getLogger(__name__)
PYTHON_VERSION = sys.version_info[0]
HAS_ASYNCIO = sys.version_info >= (3, 4)

NONE_TYPE = type(None)

if PYTHON_VERSION == 2:
    ALL_STRING_CLASS = basestring  # noqa
    STRING_CLASS = str
    UNICODE_CLASS = unicode  # noqa

    def iterkeys(d, **kwargs):
        return d.iterkeys(**kwargs)

    def itervalues(d, **kwargs):
        return d.itervalues(**kwargs)

elif PYTHON_VERSION == 3:
    ALL_STRING_CLASS = str
    STRING_CLASS = str
    UNICODE_CLASS = str

    def iterkeys(d, **kwargs):
        return d.keys(**kwargs)

    def itervalues(d, **kwargs):
        return d.values(**kwargs)


def is_string(value):
    """ Check if a value is a valid string, compatible with python 2 and python 3

    >>> is_string('foo')
    True
    >>> is_string(u'✌')
    True
    >>> is_string(42)
    False
    >>> is_string(('abc',))
    False
    """
    return isinstance(value, ALL_STRING_CLASS)


def is_unicode(value):
    """ Check if a value is a valid unicode string, compatible with python 2 and python 3

    >>> is_unicode(u'foo')
    True
    >>> is_unicode(u'✌')
    True
    >>> is_unicode(b'foo')
    False
    >>> is_unicode(42)
    False
    >>> is_unicode(('abc',))
    False
    """
    return isinstance(value, UNICODE_CLASS)


def to_latin_1(value):
    """ Return the input string encoded in latin1 with replace mode for errors
    """
    return value.encode("latin-1", "replace")


def is_json_serializable(value):
    """ Check that a single value is json serializable
    """
    return isinstance(value, (ALL_STRING_CLASS, NONE_TYPE, bool, int, float))


def update_wrapper(wrapper, wrapped):
    """ Update wrapper attribute to make it look like wrapped function.
    Don't use original update_wrapper because it can breaks if wrapped don't
    have all attributes.
    """
    for attr in WRAPPER_ASSIGNMENTS:
        if hasattr(wrapped, attr):
            setattr(wrapper, attr, getattr(wrapped, attr))
    return wrapper


def to_unicode_safe(value):
    """ Safely convert a value to a unicode string.
    """
    if value is None:
        return value
    # If value is a byte string (string without encoding)
    # Try to decode it as unicode, this operation will
    # always succeed because non UTF-8 characters will
    # get replaced by the UTF-8 replacement character.
    if isinstance(value, bytes):
        return value.decode("utf-8", errors="replace")
    # If the value is not already a unicode string
    # Try to convert it to a unicode string
    # by calling the standard Python __str__ method on
    # the value.
    elif not is_unicode(value):
        return UNICODE_CLASS(value)
    # Value is already a unicode string
    return value


###
# Raven configuration
###


def _raven_ignoring_handler(logger, *args, **kwargs):
    """ Ignore all logging messages from sqreen.* loggers, effectively
    disabling raven to log sqreen log messages as breadcrumbs
    """
    try:
        if logger.name.startswith("sqreen"):
            return True
    except Exception:
        LOGGER.warning("Error in raven ignore handler", exc_info=True)


def configure_raven_breadcrumbs():
    """ Configure raven breadcrumbs logging integration if raven is present
    """
    try:
        from raven import breadcrumbs
    except ImportError:
        return

    # Register our logging handler to stop logging sqreen log messages
    # as breadcrumbs
    try:
        breadcrumbs.register_logging_handler(_raven_ignoring_handler)
    except Exception:
        LOGGER.warning("Error while configuring breadcrumbs", exc_info=True)


###
# JSON Encoder
###


def qualified_class_name(obj):
    """ Return the full qualified name of the class name of obj in form of
    `full_qualified_module.class_name`
    """
    if isclass(obj):
        instance_class = obj
    else:
        instance_class = obj.__class__

    return ".".join([instance_class.__module__, instance_class.__name__])


def django_user_conversion(obj):
    """ Convert a Django user either by returning USERNAME_FIELD or convert
    it to str.
    """
    if hasattr(obj, "USERNAME_FIELD"):
        return to_unicode_safe(getattr(obj, getattr(obj, "USERNAME_FIELD"), None))
    else:
        return UNICODE_CLASS(obj)


OBJECT_STRING_MAPPING = {
    "bson.objectid.ObjectId": UNICODE_CLASS,
    # Convert datetime to isoformat, compatible with Node Date()
    "datetime.datetime": methodcaller("isoformat"),
    "django.contrib.auth.models.AbstractUser": django_user_conversion,
    "psycopg2.sql.Composable": methodcaller("as_string", None),
    "sqlalchemy.sql.elements.ClauseElement": UNICODE_CLASS,
    "sqreen._vendors.ipaddress.IPv4Address": UNICODE_CLASS,
    "sqreen._vendors.ipaddress.IPv6Address": UNICODE_CLASS,
}


def convert_to_string(obj, object_string_mapping=OBJECT_STRING_MAPPING):
    """ Return a string representation for objects that are known to safely convert to string.
    """
    if type(obj) == type:
        instance_class = obj
    else:
        instance_class = obj.__class__

    # Manually do isinstance without needed to have a reference to the class
    for klass in instance_class.__mro__:
        qualified_name = qualified_class_name(klass)
        func = object_string_mapping.get(qualified_name)
        if func is not None:
            try:
                return func(obj)
            except Exception:
                LOGGER.warning("Error converting an instance of type %r", obj.__class__, exc_info=True)
    raise ValueError("cannot convert safely {} to string".format(instance_class))


class CustomJSONEncoder(json.JSONEncoder):

    MAPPING = OBJECT_STRING_MAPPING

    def default(self, obj):
        """ Return the repr of unkown objects
        """
        try:
            return convert_to_string(obj, object_string_mapping=self.MAPPING)
        except ValueError:
            # If we don't, or if we except, fallback on repr
            return repr(obj)


def ip_address(address):
    return _ip_address(UNICODE_CLASS(address))


def ip_network(address, strict=True):
    return _ip_network(UNICODE_CLASS(address), strict=strict)


def truncate_time(dt=None, round_to=60):
    """Return a datetime rounded to the previous given second interval."""
    if dt is None:
        dt = datetime.utcnow()
    dt = dt.replace(microsecond=0)
    seconds = (dt - dt.min).total_seconds()
    return dt - timedelta(seconds=seconds % round_to)
