import cProfile
import json
import os
import pstats
import pytest
import logging
import re
from faker import Faker
from docker import Client

try:
    import StringIO
except ImportError:
    from io import StringIO


PROFILE_RESULTS_FILE = 'reports/global.prof'
TOASTER_TIMINGS_JSON = '/tmp/toaster-timings.json'
NODE_EXPORTER_METRIC_FILE = '/var/lib/node_exporter/textfile_collector/salt_toaster.prom'


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class Handler(logging.Handler):

    def emit(self, report):
        pytest.logentries.append(self.format(report))  # pylint: disable=no-member


class ExtraSaltPlugin(object):

    @pytest.hookimpl()
    def pytest_namespace(self):
        return dict(logentries=[])

    @pytest.hookimpl(hookwrapper=True)
    def pytest_sessionstart(self, session):
        handler = Handler()
        logging.root.addHandler(handler)
        yield

    @pytest.hookimpl(hookwrapper=True)
    def pytest_terminal_summary(self, terminalreporter):
        for item in pytest.logentries:  # pylint: disable=no-member
            terminalreporter.write_line(item)
        yield

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_setup(self, item):
        if not item.module.__name__ in pytest.logentries:  # pylint: disable=no-member
            logger.info(item.module.__name__)
        yield



@pytest.fixture(scope="session")
def docker_client():
    client = Client(base_url='unix://var/run/docker.sock', timeout=180)
    return client


@pytest.fixture(autouse=True)
def tagschecker(request):
    tags = set(request.config.getini('TAGS'))

    tags_marker = request.node.get_closest_marker('tags')
    xfailtags_marker = request.node.get_closest_marker('xfailtags')
    skiptags_marker = request.node.get_closest_marker('skiptags')

    if xfailtags_marker and not tags.isdisjoint(set(xfailtags_marker.args)):
        request.node.add_marker(pytest.mark.xfail())
    elif (
        tags_marker and tags.isdisjoint(set(tags_marker.args)) or
        skiptags_marker and not tags.isdisjoint(set(skiptags_marker.args))
    ):
        pytest.skip('skipped for this tags: {}'.format(tags))


@pytest.fixture(scope='module')
def module_config(request):
    fake = Faker()
    return {
        "masters": [
            {
                "minions": [
                    {
                        "config": {
                            "container__config__name": 'minion_{0}_{1}_{2}'.format(fake.word(), fake.word(), os.environ.get('ST_JOB_ID', '')),  # pylint: disable=no-member
                            "container__config__image": (
                                request.config.getini('MINION_IMAGE') or
                                request.config.getini('IMAGE')
                            )
                        }
                    }
                ]
            }
        ]
    }


@pytest.fixture(scope="module")
def master(setup):
    config, initconfig = setup
    return config['masters'][0]['fixture']


@pytest.fixture(scope="module")
def minion(setup):
    config, initconfig = setup
    minions = config['masters'][0]['minions']
    return minions[0]['fixture'] if minions else None


class SaltToasterException(Exception):
    pass

class ToasterTestsProfiling(object):
    """Toaster Tests Profiling plugin for pytest."""

    AVAILABLE_MODES = ['boolean', 'cumulative', "deltas"]

    global_profile = None
    mode = None
    metrics = {}

    def __init__(self, mode="default"):
        self.global_profile = cProfile.Profile()
        self.global_profile.enable()
        if mode in self.AVAILABLE_MODES:
            self.mode = mode
        else:
            raise SaltToasterException("Mode '{}' is not supported".format(mode))
        from_json = True if self.mode == "cumulative" else False
        self.metrics = self.read_initial_values(from_json=from_json)

    def read_initial_values(self, from_json=False):
        timings = {
            'pytest_runtest_setup': 0,
            'pytest_runtest_call': 0,
            'pytest_runtest_teardown': 0
        }
        if from_json:
            # Read possible values on the JSON file
            try:
                with open(TOASTER_TIMINGS_JSON) as infile:
                    timings.update(json.load(infile))
            except IOError as exc:
                logger.error("Failed to read JSON file: {}".format(exc))
        return timings

    def export_metrics_to_prometheus(self, metrics):
        # Export metrics to prometheus node exporter
        if self.mode == "boolean":
            metrics_header = \
'''
# HELP node_salt_toaster Pytest step being executed at the moment (1 = yes, 0 = no).
# TYPE node_salt_toaster gauge
'''
        else:
            metrics_header = \
'''
# HELP node_salt_toaster Seconds pytest spent in each Salt toaster step.
# TYPE node_salt_toaster counter
'''
        metrics_str = metrics_header + \
'''
node_salt_toaster{{step="pytest_runtest_setup"}} {pytest_runtest_setup}
node_salt_toaster{{step="pytest_runtest_call"}} {pytest_runtest_call}
node_salt_toaster{{step="pytest_runtest_teardown"}} {pytest_runtest_teardown}
'''
        try:
            with open(NODE_EXPORTER_METRIC_FILE, 'w') as metrics_file:
                metrics_file.write(
                    metrics_str.format(
                        pytest_runtest_setup=metrics['pytest_runtest_setup'],
                        pytest_runtest_call=metrics['pytest_runtest_call'],
                        pytest_runtest_teardown=metrics['pytest_runtest_teardown'],
                    ).lstrip()
                )
        except IOError as exc:
            logger.error("Failed to export metrics to Prometheus node " \
                "exporter file {}: {}".format(NODE_EXPORTER_METRIC_FILE, exc))

    def accumulate_values_to_json(self, values, json_filename):
        # Accumulate current values with the initial ones
        for item in self.metrics.keys():
            values[item] += self.metrics[item]
        with open(json_filename, 'w') as outfile:
            json.dump(values, outfile)
        self.export_metrics_to_prometheus(values)

    def export_metrics_delta(self, old_metrics, new_metrics, json_filename):
        deltas = {}
        for item in self.metrics.keys():
            deltas[item] = new_metrics[item] - old_metrics[item]
        with open(json_filename, 'w') as outfile:
            json.dump(new_metrics, outfile)
        self.metrics = new_metrics
        self.export_metrics_to_prometheus(deltas)

    def process_stats(self):  # @UnusedVariable
        timings = {
            'pytest_runtest_setup': 0,
            'pytest_runtest_call': 0,
            'pytest_runtest_teardown': 0
        }
        self.global_profile.disable()
        self.global_profile.dump_stats(PROFILE_RESULTS_FILE)
        self.global_profile.enable()
        stream = StringIO.StringIO()
        stats = pstats.Stats(PROFILE_RESULTS_FILE, stream=stream)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_setup', 1)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_call', 1)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_teardown', 1)
        for line in stream.getvalue().split('\n'):
            if re.match('.+\d+.+\d+\.\d+.+\d+\.\d+.+\d+\.\d+.+\d+\.\d+.*', line):
                line_list = [item for item in line.split(' ') if item]
                if 'pytest_runtest_setup' in line:
                   timings['pytest_runtest_setup'] = float(line_list[3])
                elif 'pytest_runtest_call' in line:
                   timings['pytest_runtest_call'] = float(line_list[3])
                elif 'pytest_runtest_teardown' in line:
                   timings['pytest_runtest_teardown'] = float(line_list[3])
        if self.mode == "deltas":
            self.export_metrics_delta(self.metrics, timings, TOASTER_TIMINGS_JSON)
        elif self.mode == "cumulative":
            self.accumulate_values_to_json(timings, TOASTER_TIMINGS_JSON)

    def process_stats_switch_on(self, stepname):
        self.metrics[stepname] = 1
        self.export_metrics_to_prometheus(self.metrics)

    def process_stats_switch_off(self, stepname):
        self.metrics[stepname] = 0
        self.export_metrics_to_prometheus(self.metrics)

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_setup(self, item):  # @UnusedVariable
        if self.mode == "boolean":
            self.process_stats_switch_on("pytest_runtest_setup")
            yield
            self.process_stats_switch_off("pytest_runtest_setup")
        else:
            yield

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_call(self, item):  # @UnusedVariable
        if self.mode == "boolean":
            self.process_stats_switch_on("pytest_runtest_call")
            yield
            self.process_stats_switch_off("pytest_runtest_call")
        else:
            yield

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_teardown(self, item, nextitem):  # @UnusedVariable
        if self.mode == "boolean":
            self.process_stats_switch_on("pytest_runtest_teardown")
            yield
            self.process_stats_switch_off("pytest_runtest_teardown")
        elif self.mode in ["cumulative", "deltas"]:
            yield
            self.process_stats()
        else:
            yield

    def pytest_terminal_summary(self, terminalreporter):
        self.global_profile.disable()
        self.global_profile.dump_stats(PROFILE_RESULTS_FILE)
        terminalreporter.write_sep("-",
            "generated cProfile stats file on: {}".format(PROFILE_RESULTS_FILE))
        terminalreporter.write_sep("-", "Salt Toaster Profiling Stats")
        stats = pstats.Stats(self.global_profile, stream=terminalreporter)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_setup', 1)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_call', 1)
        stats.sort_stats('cumulative').print_stats('pytest_runtest_teardown', 1)


def pytest_configure(config):
    plugin = ExtraSaltPlugin()
    config.pluginmanager.register(plugin, 'ExtraSaltPlugin')
    config.pluginmanager.register(ToasterTestsProfiling(mode="boolean"))