# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import datetime
import inspect
import sys
import time
import uuid

from applicationinsights.channel import TelemetryContext, contracts
from django.http import Http404

from . import common


try:
    basestring  # Python 2
except NameError:  # Python 3
    basestring = (str,)  # pylint: disable=invalid-name

# Pick a function to measure time; starting with 3.3, time.monotonic is available.
try:
    TIME_FUNC = time.monotonic
except AttributeError:
    TIME_FUNC = time.time


class ApplicationInsightsMiddleware:
    """This class is a Django middleware that automatically enables request and exception telemetry.  Django versions
    1.7 and newer are supported.

    To enable, add this class to your settings.py file in MIDDLEWARE_CLASSES (pre-1.10) or MIDDLEWARE (1.10 and newer):

    .. code:: python

        # If on Django < 1.10
        MIDDLEWARE_CLASSES = [
            # ... or whatever is below for you ...
            'django.middleware.security.SecurityMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            'django.middleware.common.CommonMiddleware',
            'django.middleware.csrf.CsrfViewMiddleware',
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
            'django.contrib.messages.middleware.MessageMiddleware',
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
            # ... or whatever is above for you ...
            'botbuilder.applicationinsights.django.ApplicationInsightsMiddleware',   # Add this middleware to the end
        ]

        # If on Django >= 1.10
        MIDDLEWARE = [
            # ... or whatever is below for you ...
            'django.middleware.security.SecurityMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            'django.middleware.common.CommonMiddleware',
            'django.middleware.csrf.CsrfViewMiddleware',
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'django.contrib.messages.middleware.MessageMiddleware',
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
            # ... or whatever is above for you ...
            'botbuilder.applicationinsights.django.ApplicationInsightsMiddleware',   # Add this middleware to the end
        ]

    And then, add the following to your settings.py file:

    .. code:: python

        APPLICATION_INSIGHTS = {
            # (required) Your Application Insights instrumentation key
            'ikey': "00000000-0000-0000-0000-000000000000",

            # (optional) By default, request names are logged as the request method
            # and relative path of the URL.  To log the fully-qualified view names
            # instead, set this to True.  Defaults to False.
            'use_view_name': True,

            # (optional) To log arguments passed into the views as custom properties,
            # set this to True.  Defaults to False.
            'record_view_arguments': True,

            # (optional) Exceptions are logged by default, to disable, set this to False.
            'log_exceptions': False,

            # (optional) Events are submitted to Application Insights asynchronously.
            # send_interval specifies how often the queue is checked for items to submit.
            # send_time specifies how long the sender waits for new input before recycling
            # the background thread.
            'send_interval': 1.0, # Check every second
            'send_time': 3.0, # Wait up to 3 seconds for an event

            # (optional, uncommon) If you must send to an endpoint other than the
            # default endpoint, specify it here:
            'endpoint': "https://dc.services.visualstudio.com/v2/track",
        }

    Once these are in place, each request will have an `appinsights` object placed on it.
    This object will have the following properties:

    * `client`: This is an instance of the :class:`applicationinsights.TelemetryClient` type, which will
      submit telemetry to the same instrumentation key, and will parent each telemetry item to the current
      request.
    * `request`: This is the :class:`applicationinsights.channel.contracts.RequestData` instance for the
      current request.  You can modify properties on this object during the handling of the current request.
      It will be submitted when the request has finished.
    * `context`: This is the :class:`applicationinsights.channel.TelemetryContext` object for the current
      ApplicationInsights sender.

    These properties will be present even when `DEBUG` is `True`, but it may not submit telemetry unless
    `debug_ikey` is set in `APPLICATION_INSIGHTS`, above.
    """

    def __init__(self, get_response=None):
        self.get_response = get_response

        # Get configuration
        self._settings = common.load_settings()
        self._client = common.create_client(self._settings)

    # Pre-1.10 handler
    def process_request(self, request):  # pylint: disable=useless-return
        # Populate context object onto request
        addon = RequestAddon(self._client)
        data = addon.request
        context = addon.context
        request.appinsights = addon

        # Basic request properties
        data.start_time = datetime.datetime.utcnow().isoformat() + "Z"
        data.http_method = request.method
        data.url = request.build_absolute_uri()
        data.name = "%s %s" % (request.method, request.path)
        context.operation.name = data.name
        context.operation.id = data.id
        context.location.ip = request.META.get("REMOTE_ADDR", "")
        context.user.user_agent = request.META.get("HTTP_USER_AGENT", "")

        # User
        if hasattr(request, "user"):
            if (
                request.user is not None
                and not request.user.is_anonymous
                and request.user.is_authenticated
            ):
                context.user.account_id = request.user.get_short_name()

        # Run and time the request
        addon.start_stopwatch()
        return None

    # Pre-1.10 handler
    def process_response(self, request, response):
        if hasattr(request, "appinsights"):
            addon = request.appinsights

            data = addon.request
            context = addon.context

            # Fill in data from the response
            data.duration = addon.measure_duration()
            data.response_code = response.status_code
            data.success = response.status_code < 400 or response.status_code == 401

            # Submit and return
            self._client.track(data, context)

        return response

    # 1.10 and up...
    def __call__(self, request):
        self.process_request(request)
        response = self.get_response(request)
        self.process_response(request, response)
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        if not hasattr(request, "appinsights"):
            return None

        data = request.appinsights.request
        context = request.appinsights.context

        # Operation name is the method + url by default (set in __call__),
        # If use_view_name is set, then we'll look up the name of the view.
        if self._settings.use_view_name:
            mod = inspect.getmodule(view_func)
            if hasattr(view_func, "__name__"):
                name = view_func.__name__
            elif hasattr(view_func, "__class__") and hasattr(
                view_func.__class__, "__name__"
            ):
                name = view_func.__class__.__name__
            else:
                name = "<unknown>"

            if mod:
                opname = "%s %s.%s" % (data.http_method, mod.__name__, name)
            else:
                opname = "%s %s" % (data.http_method, name)
            data.name = opname
            context.operation.name = opname

        # Populate the properties with view arguments
        if self._settings.record_view_arguments:
            for i, arg in enumerate(view_args):
                data.properties["view_arg_" + str(i)] = arg_to_str(arg)

            for k, v in view_kwargs.items():  # pylint: disable=invalid-name
                data.properties["view_arg_" + k] = arg_to_str(v)

        return None

    def process_exception(self, request, exception):
        if not self._settings.log_exceptions:
            return None

        if isinstance(exception, Http404):
            return None

        _, _, tb = sys.exc_info()  # pylint: disable=invalid-name
        if tb is None or exception is None:
            # No actual traceback or exception info, don't bother logging.
            return None

        client = common.get_telemetry_client_with_processor(
            self._client.context.instrumentation_key, self._client.channel
        )
        if hasattr(request, "appinsights"):
            client.context.operation.parent_id = request.appinsights.request.id

        client.track_exception(type(exception), exception, tb)

        return None

    def process_template_response(self, request, response):
        if hasattr(request, "appinsights") and hasattr(response, "template_name"):
            data = request.appinsights.request
            data.properties["template_name"] = response.template_name

        return response


class RequestAddon:
    def __init__(self, client):
        self._baseclient = client
        self._client = None
        self.request = contracts.RequestData()
        self.request.id = str(uuid.uuid4())
        self.context = TelemetryContext()
        self.context.instrumentation_key = client.context.instrumentation_key
        self.context.operation.id = self.request.id
        self._process_start_time = None

    @property
    def client(self):
        if self._client is None:
            # Create a client that submits telemetry parented to the request.
            self._client = common.get_telemetry_client_with_processor(
                self.context.instrumentation_key, self._baseclient.channel
            )
            self._client.context.operation.parent_id = self.context.operation.id

        return self._client

    def start_stopwatch(self):
        self._process_start_time = TIME_FUNC()

    def measure_duration(self):
        end_time = TIME_FUNC()
        return ms_to_duration(int((end_time - self._process_start_time) * 1000))


def ms_to_duration(n):  # pylint: disable=invalid-name
    duration_parts = []
    for multiplier in [1000, 60, 60, 24]:
        duration_parts.append(n % multiplier)
        n //= multiplier

    duration_parts.reverse()
    duration = "%02d:%02d:%02d.%03d" % tuple(duration_parts)
    if n:
        duration = "%d.%s" % (n, duration)

    return duration


def arg_to_str(arg):
    if isinstance(arg, basestring):
        return arg
    if isinstance(arg, int):
        return str(arg)
    return repr(arg)
