"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
"""

import calendar
import io
import json
import logging
import os
import platform
import sys
import threading
from datetime import datetime
from uuid import uuid4

import requests
from django.conf import settings

from .common import get_app_version, get_client_ip
from .io import find_file, get_config_dir

logger = logging.getLogger(__name__)


def _load_log_payloads():
    try:
        all_urls_file = find_file('all_urls.json')
        with open(all_urls_file) as f:
            log_payloads = json.load(f)
    except Exception as exc:
        logger.error(exc)
        return None
    out = {}
    for item in log_payloads:
        out[item['name']] = {
            'exclude_from_logs': item.get('exclude_from_logs', False),
            'log_payloads': item.get('log_payloads'),
        }
    return out


class ContextLog(object):

    _log_payloads = _load_log_payloads()

    def __init__(self):
        self.version = get_app_version()
        self.server_id = self._get_server_id()

    def _get_server_id(self):
        user_id_file = os.path.join(get_config_dir(), 'user_id')
        if not os.path.exists(user_id_file):
            user_id = str(uuid4())
            try:
                with io.open(user_id_file, mode='w', encoding='utf-8') as fout:
                    fout.write(user_id)
            except OSError:
                return 'np-' + user_id  # not persistent user id
        else:
            with io.open(user_id_file, encoding='utf-8') as f:
                user_id = f.read()
        return user_id

    def _is_docker(self):
        path = '/proc/self/cgroup'
        return (
            os.path.exists('/.dockerenv')
            or os.path.isfile(path)
            and any('docker' in line for line in open(path, encoding='utf-8'))
        )

    def _get_timestamp_now(self):
        return calendar.timegm(datetime.now().utctimetuple())

    def _get_response_content(self, response):
        try:
            return json.loads(response.content)
        except:  # noqa: E722
            return

    def _assert_field_in_test(self, field, payload, view_name):
        if settings.TEST_ENVIRONMENT:
            assert field in payload, f'The field "{field}" should be presented for "{view_name}"'

    def _assert_type_in_test(self, type, payload, view_name):
        if settings.TEST_ENVIRONMENT:
            assert isinstance(payload, type), f'The type of payload is not "{type}" for "{view_name}"'

    def _get_fields(self, view_name, payload, fields):
        out = {}
        for field in fields:
            self._assert_field_in_test(field, payload, view_name)
            out[field] = payload.get(field)
        if not out:
            return None
        return out

    def _secure_data(self, payload, request):
        view_name = payload['view_name']

        if view_name in ('user-signup', 'user-login') and payload['method'] == 'POST':
            payload['json'] = None

        if payload['status_code'] < 200 or payload['status_code'] > 299:
            if payload['status_code'] >= 400:
                payload['json'] = None
            return

        # ======== CUSTOM ======
        if view_name == 'data_manager:dm-actions' and payload['values'].get('id') == 'next_task':
            self._assert_type_in_test(dict, payload['response'], view_name)
            new_response = {}
            self._assert_field_in_test('drafts', payload['response'], view_name)
            new_response['drafts'] = (
                len(payload['response']['drafts'])
                if isinstance(payload['response']['drafts'], list)
                else payload['response']['drafts']
            )
            for key in [
                'id',
                'inner_id',
                'cancelled_annotations',
                'total_annotations',
                'total_predictions',
                'updated_by',
                'created_at',
                'updated_at',
                'overlap',
                'comment_count',
                'unresolved_comment_count',
                'last_comment_updated_at',
                'project',
                'comment_authors',
                'queue',
            ]:
                self._assert_field_in_test(key, payload['response'], view_name)
                new_response[key] = payload['response'][key]
            payload['response'] = new_response
            return

        if view_name == 'user-list' and payload['method'] == 'GET':
            self._assert_type_in_test(list, payload['response'], view_name)
            payload['response'] = {'count': len(payload['response'])}
            return

        if view_name == 'projects:api-templates:template-list' and payload['method'] == 'GET':
            self._assert_type_in_test(list, payload['response'].get('templates'), view_name)
            payload['response']['templates'] = [t['title'] for t in payload['response']['templates']]
            return

        if view_name == 'data_manager:dm-actions' and payload['method'] == 'GET':
            self._assert_type_in_test(list, payload['response'], view_name)
            payload['response'] = [item.get('id') for item in payload['response']]
            return

        if view_name == 'data_manager:dm-columns' and payload['method'] == 'GET':
            self._assert_field_in_test('columns', payload['response'], view_name)
            payload['response']['columns'] = [item.get('id') for item in payload['response']['columns']]
            return

        if view_name == 'data_export:api-projects:project-export-formats' and payload['method'] == 'GET':
            self._assert_type_in_test(list, payload['response'], view_name)
            payload['response'] = [item.get('title') for item in payload['response']]
            return

        if (
            (view_name == 'tasks:api:task-annotations' and payload['method'] in 'POST')
            or (view_name == 'tasks:api-annotations:annotation-detail' and payload['method'] == 'PATCH')
            or (view_name == 'tasks:api:task-annotations-drafts' and payload['method'] == 'POST')
            or (view_name == 'tasks:api-drafts:draft-detail' and payload['method'] == 'PATCH')
        ):
            self._assert_field_in_test('lead_time', payload['json'], view_name)
            self._assert_field_in_test('result', payload['json'], view_name)
            self._assert_type_in_test(list, payload['json']['result'], view_name)
            payload['json']['result'] = [
                self._get_fields(view_name, item, ('from_name', 'to_name', 'type', 'origin'))
                for item in payload['json']['result']
            ]

        # ======== DEFAULT ======
        log_payloads = self._log_payloads.get(view_name)

        if not log_payloads or not log_payloads.get('log_payloads'):
            return

        log_payloads = log_payloads['log_payloads']
        for payload_key in log_payloads:
            if not payload.get(payload_key):
                payload[payload_key] = None
                continue
            log_fields = log_payloads[payload_key].get(payload['method'])
            if log_fields is not None:
                payload[payload_key] = self._get_fields(view_name, payload[payload_key], log_fields)

    def _exclude_endpoint(self, request):
        if request.resolver_match and request.resolver_match.view_name:
            view_name = request.resolver_match.view_name
            if view_name not in self._log_payloads:
                return True
            if self._log_payloads[view_name].get('exclude_from_logs'):
                return True
        if request.GET.get('interaction', None) == 'timer':
            return True

    def dont_send(self, request):
        return not settings.COLLECT_ANALYTICS or self._exclude_endpoint(request)

    def send(self, request=None, response=None, body=None):
        if self.dont_send(request):
            return
        try:
            payload = self.create_payload(request, response, body)
        except Exception as exc:
            logger.debug(exc, exc_info=True)
            if settings.TEST_ENVIRONMENT:
                raise
        else:
            if settings.TEST_ENVIRONMENT:
                pass
            elif settings.DEBUG_CONTEXTLOG:
                logger.debug('In DEBUG mode, contextlog is not sent.')
                logger.debug(json.dumps(payload, indent=2))
            elif settings.CONTEXTLOG_SYNC:
                self.send_job(request, response, body)
            else:
                thread = threading.Thread(target=self.send_job, args=(request, response, body))
                thread.start()

    @staticmethod
    def browser_exists(request):
        return (
            hasattr(request, 'user_agent')
            and request.user_agent
            and hasattr(request.user_agent, 'browser')
            and request.user_agent.browser
        )

    def create_payload(self, request, response, body):
        advanced_json = None
        user_id, user_email = None, None
        if hasattr(request, 'user') and hasattr(request.user, 'id'):
            user_id = request.user.id
            if hasattr(request.user, 'email'):
                user_email = request.user.email
        if hasattr(request, 'advanced_json'):
            advanced_json = request.advanced_json
        elif hasattr(request, 'user') and hasattr(request.user, 'advanced_json'):
            advanced_json = request.user.advanced_json

        url = request.build_absolute_uri()
        view_name = request.resolver_match.view_name if request.resolver_match else None
        metrics_payload = request.GET.get('__')
        is_metrics_payload = view_name == 'collect_metrics' and metrics_payload is not None

        if is_metrics_payload:
            values = json.loads(metrics_payload)
        else:
            values = request.GET.dict()

        # If the values contains url use it as the url, otherwise use the absolute uri
        if is_metrics_payload and 'url' in values:
            url = values.pop('url')

        # If this is a metrics payload, we will add the namespace and view name
        # to describe the payload as an event payload
        if is_metrics_payload:
            namespace = 'collect_metrics'
            view_name = f'event:{values.pop("event")}'
            status_code = 200
            content_type = None
            response_content = None
        else:
            namespace = request.resolver_match.namespace if request.resolver_match else None
            status_code = response.status_code
            content_type = getattr(response, 'content_type', None)
            response_content = self._get_response_content(response)

        payload = {
            'url': url,
            'server_id': self.server_id,
            'user_id': user_id,
            'user_email': user_email,
            'server_time': self._get_timestamp_now(),
            'session_id': request.session.get('uid', None),
            'client_ip': get_client_ip(request),
            'is_docker': self._is_docker(),
            'python': str(sys.version_info[0]) + '.' + str(sys.version_info[1]),
            'version': self.version,
            'view_name': view_name,
            'namespace': namespace,
            'scheme': request.scheme,
            'method': request.method,
            'values': values,
            'json': body,
            'advanced_json': advanced_json,
            'language': request.LANGUAGE_CODE,
            'content_type': content_type,
            'content_length': (
                int(request.environ.get('CONTENT_LENGTH')) if request.environ.get('CONTENT_LENGTH') else None
            ),
            'status_code': status_code,
            'response': response_content,
        }
        if self.browser_exists(request):
            payload.update(
                {
                    'is_mobile': request.user_agent.is_mobile,
                    'is_tablet': request.user_agent.is_tablet,
                    'is_touch_capable': request.user_agent.is_touch_capable,
                    'is_pc': request.user_agent.is_pc,
                    'is_bot': request.user_agent.is_bot,
                    'browser': request.user_agent.browser.family,
                    'browser_version': request.user_agent.browser.version_string,
                    'os': request.user_agent.os.family,
                    'platform_system': platform.system(),
                    'platform_release': platform.release(),
                    'os_version': request.user_agent.os.version_string,
                    'device': request.user_agent.device.family,
                }
            )
        self._secure_data(payload, request)
        for key in ('json', 'response', 'values'):
            payload[key] = payload[key] or None
        return payload

    def send_job(self, request, response, body):
        try:
            payload = self.create_payload(request, response, body)
        except:  # noqa: E722
            pass
        else:
            try:
                url = 'https://tele.labelstud.io'
                requests.post(url=url, json=payload, timeout=3.0)
            except:  # noqa: E722
                pass
