import logging
import json
import uuid
import os
import sys
import traceback
import inspect
from functools import wraps
from pprint import pformat
from flask import request, Response
from klue.swagger.apipool import ApiPool
from klue_microservice.utils import timenow, is_ec2_instance
from klue_microservice.exceptions import UnhandledServerError


log = logging.getLogger(__name__)


try:
    from flask import _app_ctx_stack as stack
except ImportError:
    from flask import _request_ctx_stack as stack


#
# Default error reporting
#

def default_error_reporter(title, message):
    """By default, error messages are just logged"""
    log.error("error: %s" % title)
    log.error("details:\n%s" % message)

error_reporter = default_error_reporter

def set_error_reporter(callback=None):
    """Here you can override the default crash reporter (and send yourself
    emails, sms, slacks...)"""

    global error_reporter
    if error_reporter:
        error_reporter = callback


def report_error(title=None, data={}, caught=None, is_fatal=False):
    """Format a crash report and send it somewhere relevant. There are two
    types of crashes: fatal crashes (backend errors) or non-fatal ones (just
    reporting a glitch, but the api call did not fail)"""

    # Fill error report with tons of usefull data
    if 'user' not in data:
        populate_error_report(data)

    # Add the message
    data['title'] = title
    data['is_fatal_error'] = is_fatal

    # Add the error caught, if any:
    if caught:
        data['error_caught'] = "%s" % caught

    # Add a trace - Formatting traceback may raise a UnicodeDecodeError...
    data['stack'] = []
    try:
        data['stack'] = [l for l in traceback.format_stack()]
    except Exception as ee:
        data['stack'] = 'Skipped trace - contained non-ascii chars'

    # inspect may raise a UnicodeDecodeError...
    fname = ''
    try:
        fname = inspect.stack()[1][3]
    except Exception as e:
        fname = 'unknown-method'

    # Format the error's title
    status, code = 'unknown_status', 'unknown_error_code'
    if 'response' in data:
        status = data['response'].get('status', status)
        code = data['response'].get('error_code', code)
        title_details = "%s %s %s" % (ApiPool().current_server_name, status, code)
    else:
        title_details = "%s %s()" % (ApiPool().current_server_name, fname)

    if is_fatal:
        title_details = 'FATAL ERROR %s' % title_details
    else:
        title_details = 'NON-FATAL ERROR %s' % title_details

    if title:
        title = "%s: %s" % (title_details, title)
    else:
        title = title_details

    # Don't report errors if NO_ERROR_REPORTING set to 1 (set by run_acceptance_tests)
    if not os.environ.get('DO_REPORT_ERROR', None) and os.environ.get('NO_ERROR_REPORTING', '') == '1':
        log.info("NO_ERROR_REPORTING is set: not sending error to slack or email")
        return

    global error_reporter
    log.info("Reporting crash...")

    try:
        error_reporter(title, json.dumps(data, sort_keys=True, indent=4))
    except Exception as e:
        # Don't block on replying to api caller
        log.error("Failed to send email report: %s" % str(e))


def populate_error_report(data):
    """Add generic stats to the error report"""

    # Did klue-client-server set a call_id and call_path?
    call_id, call_path = '', ''
    if hasattr(stack.top, 'call_id'):
        call_id = stack.top.call_id
    if hasattr(stack.top, 'call_path'):
        call_path = stack.top.call_path

    # Unique ID associated to all responses associated to a given
    # call to klue-api, across all micro-services
    data['call_id'] = call_id
    data['call_path'] = call_path

    # Are we in aws?
    data['is_ec2_instance'] = is_ec2_instance()

    # If user is authenticated, get her id
    user_data = {
        'id': '',
        'is_auth': 0,
        'ip': '',
    }

    if stack.top:
        # We are in a request context
        user_data['ip'] = request.remote_addr

        if 'X-Forwarded-For' in request.headers:
            user_data['forwarded_ip'] = request.headers.get('X-Forwarded-For', '')

        if 'User-Agent' in request.headers:
            user_data['user_agent'] = request.headers.get('User-Agent', '')

    if hasattr(stack.top, 'current_user'):
        user_data['is_auth'] = 1
        user_data['id'] = stack.top.current_user.get('sub', '')
        for k in ('name', 'email', 'is_expert', 'is_admin', 'is_support', 'is_tester', 'language'):
            v = stack.top.current_user.get(k, None)
            if v:
                user_data[k] = v

    data['user'] = user_data

    # Is the current code running as a server?
    if ApiPool().current_server_api:
        # Server info
        server = request.base_url
        server = server.replace('http://', '')
        server = server.replace('https://', '')
        server = server.split('/')[0]
        parts = server.split(':')
        fqdn = parts[0]
        port = parts[1] if len(parts) == 2 else ''

        data['server'] = {
            'fqdn': fqdn,
            'port': port,
            'api_name': ApiPool().current_server_name,
            'api_version': ApiPool().current_server_api.get_version(),
        }

        # Endpoint data
        data['endpoint'] = {
            'id': "%s %s %s" % (ApiPool().current_server_name, request.method, request.path),
            'url': request.url,
            'base_url': request.base_url,
            'path': request.path,
            'method': request.method
        }


def crash_handler(f):
    """Return a decorator that reports failed api calls via the error_reporter,
    for use on every server endpoint"""

    @wraps(f)
    def wrapper(*args, **kwargs):
        """Generate a report of this api call, and if the call failed or was too slow,
        forward this report via the error_reporter"""

        data = {}
        t0 = timenow()
        exception_string = ''

        # Call endpoint and log execution time
        try:
            res = f(*args, **kwargs)
        except Exception as e:
            # An unhandled exception occured!
            exception_string = str(e)
            exc_type, exc_value, exc_traceback = sys.exc_info()
            trace = traceback.format_exception(exc_type, exc_value, exc_traceback, 30)
            data['trace'] = trace

            # If it is a KlueMicroServiceException, just call its http_reply()
            if hasattr(e, 'http_reply'):
                res = e.http_reply()
            else:
                # Otherwise, forge a Response
                e = UnhandledServerError(exception_string)
                log.error("UNHANDLED EXCEPTION: %s" % '\n'.join(trace))
                res = e.http_reply()

        t1 = timenow()

        # Is the response an Error instance?
        response_type = type(res).__name__
        status_code = 200
        is_an_error = 0
        error = ''
        error_description = ''
        error_user_message = ''

        error_id = ''

        if isinstance(res, Response):
            # Got a flask.Response object
            res_data = None

            status_code = str(res.status_code)

            if str(status_code) == '200':

                # It could be any valid json response, but it could also be an Error model
                # that klue-client-server handled as a status 200 because it does not know of
                # klue-microservice Errors
                if res.content_type == 'application/json':
                    s = str(res.data)
                    if '"error":' in s and '"error_description":' in s and '"status":' in s:
                        # This looks like an error, let's decode it
                        res_data = res.get_data()
            else:
                # Assuming it is a KlueMicroServiceException.http_reply()
                res_data = res.get_data()

            if res_data:
                if type(res_data) is bytes:
                    res_data = res_data.decode("utf-8")

                is_json = True
                try:
                    j = json.loads(res_data)
                except ValueError as e:
                    # This was a plain html response. Fake an error
                    is_json = False
                    j = {'error': res_data, 'status': status_code}

                # Make sure that the response gets the same status as the Klue Error it contained
                status_code = j['status']
                res.status_code = int(status_code)

                error = j.get('error', 'NO_ERROR_IN_JSON')
                error_description = j.get('error_description', res_data)
                if error_description == '':
                    error_description = res_data

                if not exception_string:
                    exception_string = error_description

                error_user_message = j.get('user_message', '')
                is_an_error = 1

                # Patch Response to contain a unique id
                if is_json and 'error_id' not in j:
                    # If the error is forwarded by multiple micro-services, we
                    # want the error_id to be set only on the original error
                    error_id = str(uuid.uuid4())
                    j['error_id'] = error_id
                    res.set_data(json.dumps(j))

        request_args = []
        if len(args):
            request_args.append(args)
        if kwargs:
            request_args.append(kwargs)

        data.update({
            # Set only on the original error, not on forwarded ones, not on
            # success responses
            'error_id': error_id,

            # Call results
            'time': {
                'start': t0.isoformat(),
                'end': t1.isoformat(),
                'microsecs': (t1.timestamp() - t0.timestamp()) * 1000000,
            },

            # Response details
            'response': {
                'type': response_type,
                'status': str(status_code),
                'is_error': is_an_error,
                'error_code': error,
                'error_description': error_description,
                'user_message': error_user_message,
            },

            # Request details
            'request': {
                'params': pformat(request_args),
            },
        })

        populate_error_report(data)
        log.info("Analytics: " + pformat(data))

        # inspect may raise a UnicodeDecodeError...
        fname = f.__name__

        #
        # Should we report this call?
        #

        if 'server' not in data:
            log.info("This is a unitest running: not reporting error")
            pass
        else:
            fqdn = data['server']['fqdn']
            if not data['is_ec2_instance'] and not os.environ.get('DO_REPORT_ERROR'):
                log.info("This is a test setup: not reporting error")
            else:
                # If it is an internal errors, report it
                # If it is too slow, report it
                if data['response']['status'] and int(data['response']['status']) >= 500:
                    report_error(
                        title="%s(): %s" % (fname, exception_string),
                        data=data,
                        is_fatal=True
                    )
                elif int(data['time']['microsecs']) > 5000000:
                    report_error(
                        title='%s() calltime exceeded 5 sec!' % fname,
                        data=data
                    )

        return res

    return wrapper
