import inspect
import sys
import threading
import time
import traceback
from collections import defaultdict

from flask_monitoringdashboard import config
from flask_monitoringdashboard.core.cache import update_duration_cache
from flask_monitoringdashboard.core.logger import log
from flask_monitoringdashboard.core.profiler.util import order_histogram
from flask_monitoringdashboard.core.profiler.util.path_hash import PathHash
from flask_monitoringdashboard.database import session_scope
from flask_monitoringdashboard.database.request import add_request
from flask_monitoringdashboard.database.stack_line import add_stack_line

FILENAME = 'flask_monitoringdashboard/core/measurement.py'
FILENAME_LEN = len(FILENAME)


class StacktraceProfiler(threading.Thread):
    """
    Used for profiling the performance per line code.
    This is used when monitoring-level == 2 and monitoring-level == 3
    """

    def __init__(self, thread_to_monitor, endpoint, ip, group_by, outlier_profiler=None):
        threading.Thread.__init__(self)
        self._keeprunning = True
        self._thread_to_monitor = thread_to_monitor
        self._endpoint = endpoint
        self._ip = ip
        self._group_by = group_by
        self._duration = 0
        self._histogram = defaultdict(float)
        self._path_hash = PathHash()
        self._lines_body = []
        self._total = 0
        self._outlier_profiler = outlier_profiler
        self._status_code = 404

    def run(self):
        """
        Continuously takes a snapshot from the stacktrace (only the main-thread). Filters
        everything before the endpoint has been called (i.e. the Flask library).
        Directly computes the histogram, since this is more efficient for performance
        :return:
        """
        current_time = time.time()
        while self._keeprunning:
            newcurrent_time = time.time()
            duration = newcurrent_time - current_time
            current_time = newcurrent_time

            try:
                frame = sys._current_frames()[self._thread_to_monitor]
            except KeyError:
                log('Can\'t get the stacktrace of the main thread. Stopping StacktraceProfiler')
                log('Thread to monitor: %s' % self._thread_to_monitor)
                log('Running threads: %s' % sys._current_frames().keys())
                break
            in_endpoint_code = False
            self._path_hash.set_path('')
            # filename, line number, function name, source code line
            for fn, ln, fun, line in traceback.extract_stack(frame):
                if self._endpoint.name == fun:
                    in_endpoint_code = True
                if in_endpoint_code:
                    key = (self._path_hash.get_path(fn, ln), fun, line)
                    self._histogram[key] += duration
                if len(fn) > FILENAME_LEN and fn[-FILENAME_LEN:] == FILENAME and fun == "wrapper":
                    in_endpoint_code = True
            if in_endpoint_code:
                self._total += duration

            elapsed = time.time() - current_time
            if config.sampling_period > elapsed:
                time.sleep(config.sampling_period - elapsed)

        self._on_thread_stopped()

    def stop(self, duration, status_code):
        self._duration = duration * 1000
        self._status_code = status_code
        if self._outlier_profiler:
            self._outlier_profiler.stop_by_profiler()
        self._keeprunning = False

    def _on_thread_stopped(self):
        update_duration_cache(endpoint_name=self._endpoint.name, duration=self._duration)
        with session_scope() as session:
            request_id = add_request(
                session,
                duration=self._duration,
                endpoint_id=self._endpoint.id,
                ip=self._ip,
                status_code=self._status_code,
                group_by=self._group_by,
            )
            self._lines_body = order_histogram(self._histogram.items())
            self.insert_lines_db(session, request_id)
            if self._outlier_profiler:
                self._outlier_profiler.add_outlier(session, request_id)

    def insert_lines_db(self, session, request_id):
        position = 0
        for code_line in self.get_funcheader():
            add_stack_line(
                session,
                request_id,
                position=position,
                indent=0,
                duration=self._duration,
                code_line=code_line,
            )
            position += 1

        for key, val in self._lines_body:
            path, fun, line = key
            fn, ln = self._path_hash.get_last_fn_ln(path)
            indent = self._path_hash.get_indent(path)
            duration = val * self._duration / self._total if self._total != 0 else 0
            add_stack_line(
                session,
                request_id,
                position=position,
                indent=indent,
                duration=duration,
                code_line=(fn, ln, fun, line),
            )
            position += 1

    def get_funcheader(self):
        lines_returned = []
        try:
            fun = config.app.view_functions[self._endpoint.name]
        except AttributeError:
            fun = None
        if hasattr(fun, 'original'):
            original = fun.original
            fn = inspect.getfile(original)
            lines, ln = inspect.getsourcelines(original)
            count = 0
            for line in lines:
                lines_returned.append((fn, ln + count, 'None', line.strip()))
                count += 1
                if line.strip()[:4] == 'def ':
                    return lines_returned
        raise ValueError('Cannot retrieve the function header')
