"""
Report functionality centered around individual runs.
"""

from collections import namedtuple
import time
import lnt.server.reporting.analysis
import lnt.server.ui.app
import lnt.util.stats


def generate_run_data(session, run, baseurl, num_comparison_runs=0,
                      result=None, compare_to=None, baseline=None,
                      aggregation_fn=lnt.util.stats.safe_min,
                      confidence_lv=.05, styles=dict(), classes=dict()):
    """
    Generate raw data for a report on the results of the given individual
    run. They are meant as inputs to jinja templates which could create
    email reports or presentations on a web page.
    """
    assert num_comparison_runs >= 0

    start_time = time.time()

    ts = run.testsuite
    machine = run.machine
    machine_parameters = machine.parameters

    if baseline is None:
        # If a baseline has not been given, look up the run closest to
        # the default baseline revision for which this machine also
        # reported.
        baseline = machine.get_baseline_run(session)

    # If the baseline is the same as the comparison run, ignore it.
    visible_note = None
    if baseline is compare_to:
        visible_note = "Baseline and compare_to are the same: " \
                       "disabling baseline."
        baseline = None

    # Gather the runs to use for statistical data.
    comparison_start_run = compare_to or run
    comparison_window = list(ts.get_previous_runs_on_machine(
            session, comparison_start_run, num_comparison_runs))
    if baseline:
        baseline_window = list(ts.get_previous_runs_on_machine(
                session, baseline, num_comparison_runs))
    else:
        baseline_window = []

    # If we don't have an explicit baseline run or a comparison run, use the
    # previous run.
    if compare_to is None and comparison_window:
        compare_to = comparison_window[0]

    # Create the run info analysis object.
    runs_to_load = set(r.id for r in comparison_window)
    for r in baseline_window:
        runs_to_load.add(r.id)
    runs_to_load.add(run.id)
    if compare_to:
        runs_to_load.add(compare_to.id)
    if baseline:
        runs_to_load.add(baseline.id)
    sri = lnt.server.reporting.analysis.RunInfo(
        session, ts, runs_to_load, aggregation_fn, confidence_lv)

    # Get the test names, metric fields and total test counts.
    test_names = session.query(ts.Test.name, ts.Test.id).\
        order_by(ts.Test.name).\
        filter(ts.Test.id.in_(sri.test_ids)).all()
    metric_fields = list(ts.Sample.get_metric_fields())
    num_total_tests = len(metric_fields) * len(test_names)

    # Gather the run-over-run changes to report, organized by field and then
    # collated by change type.
    run_to_run_info, test_results = _get_changes_by_type(
        ts, run, compare_to, metric_fields, test_names, num_comparison_runs,
        sri)

    # If we have a baseline, gather the run-over-baseline results and
    # changes.
    if baseline:
        run_to_baseline_info, baselined_results = _get_changes_by_type(
            ts, run, baseline, metric_fields, test_names, num_comparison_runs,
            sri)
    else:
        run_to_baseline_info = baselined_results = None

    # Gather the run-over-run changes to report.

    # Collect the simplified results, if desired, for sending back to clients.
    if result is not None:
        pset_results = []
        result['test_results'] = [{'pset': (), 'results': pset_results}]
        for field, field_results in test_results:
            for _, bucket, _ in field_results:
                for name, cr, _ in bucket:
                    # FIXME: Include additional information about performance
                    # changes.
                    pset_results.append(("%s.%s" % (name, field.name),
                                         cr.get_test_status(),
                                         cr.get_value_status()))

    # Aggregate counts across all bucket types for our num item
    # display
    def aggregate_counts_across_all_bucket_types(i, name):
        num_items = sum(len(field_results[i][1])
                        for _, field_results in test_results)
        if baseline:
            num_items_vs_baseline = sum(
                len(field_results[i][1])
                for _, field_results in baselined_results)
        else:
            num_items_vs_baseline = None

        return i, name, num_items, num_items_vs_baseline

    num_item_buckets = [aggregate_counts_across_all_bucket_types(x[0], x[1][0])
                        for x in enumerate(test_results[0][1])]

    def maybe_sort_bucket(bucket, bucket_name, show_perf):
        if not bucket or bucket_name == 'Unchanged Test' or not show_perf:
            return bucket
        else:
            return sorted(
                bucket,
                key=lambda bucket_entry: -abs(bucket_entry.cr.pct_delta))

    def prioritize_buckets(test_results):
        prioritized = [(priority, field, bucket_name,
                        maybe_sort_bucket(bucket, bucket_name, show_perf),
                        [name for name, _, __ in bucket], show_perf)
                       for field, field_results in test_results
                       for priority, (bucket_name, bucket,
                                      show_perf) in enumerate(field_results)]
        prioritized.sort(key=lambda item: (item[0], item[1].name))
        return prioritized

    # Generate prioritized buckets for run over run and run over baseline data.
    prioritized_buckets_run_over_run = prioritize_buckets(test_results)
    if baseline:
        prioritized_buckets_run_over_baseline = \
            prioritize_buckets(baselined_results)
    else:
        prioritized_buckets_run_over_baseline = None

    # Prepare auxillary variables for rendering.
    # Create Subject
    subject = """%s test results""" % (machine.name,)

    # Define URLS.
    if baseurl[-1] == '/':
        baseurl = baseurl[:-1]
    ts_url = """%s/v4/%s""" % (baseurl, ts.name)
    run_url = """%s/%d""" % (ts_url, run.id)
    report_url = run_url
    url_fields = []
    if compare_to:
        url_fields.append(('compare_to', str(compare_to.id)))
    if baseline:
        url_fields.append(('baseline', str(baseline.id)))
    report_url = "%s?%s" % (run_url, "&amp;".join("%s=%s" % (k, v)
                                                  for k, v in url_fields))

    # Compute static CSS styles for elemenets. We use the style directly on
    # elements instead of via a stylesheet to support major email clients (like
    # Gmail) which can't deal with embedded style sheets.
    #
    # These are derived from the static style.css file we use elsewhere.
    #
    # These are just defaults however, and the caller can override them with
    # the 'styles' and 'classes' kwargs.
    styles_ = {
        "body": ("color:#000000; background-color:#ffffff; "
                 "font-family: Helvetica, sans-serif; font-size:9pt"),
        "h1": ("font-size: 14pt"),
        "table": "font-size:9pt; border-spacing: 0px; border: 1px solid black",
        "th": ("background-color:#eee; color:#666666; font-weight: bold; "
               "cursor: default; text-align:center; font-weight: bold; "
               "font-family: Verdana; padding:5px; padding-left:8px"),
        "td": "padding:5px; padding-left:8px",
    }
    classes_ = {
    }

    styles_.update(styles)
    classes_.update(classes)

    data = {
        'ts': ts,
        'subject': subject,
        'report_url': report_url,
        'ts_url': ts_url,
        'compare_to': compare_to,
        'run': run,
        'run_url': run_url,
        'baseline': baseline,
        'machine': machine,
        'machine_parameters': machine_parameters,
        'num_item_buckets': num_item_buckets,
        'num_total_tests': num_total_tests,
        'run_to_run_info': run_to_run_info,
        'prioritized_buckets_run_over_run': prioritized_buckets_run_over_run,
        'run_to_baseline_info': run_to_baseline_info,
        'prioritized_buckets_run_over_baseline':
            prioritized_buckets_run_over_baseline,
        'styles': styles_,
        'classes': classes_,
        'start_time': start_time,
        'sri': sri,
        'visible_note': visible_note,
    }
    return data


BucketEntry = namedtuple('BucketEntry', ['name', 'cr', 'test_id'])


def _get_changes_by_type(ts, run_a, run_b, metric_fields, test_names,
                         num_comparison_runs, sri):
    comparison_results = {}
    results_by_type = []
    for field in metric_fields:
        new_failures = []
        new_passes = []
        perf_regressions = []
        perf_improvements = []
        removed_tests = []
        added_tests = []
        existing_failures = []
        unchanged_tests = []
        for name, test_id in test_names:
            cr = sri.get_run_comparison_result(
                run_a, run_b, test_id, field,
                ts.Sample.get_hash_of_binary_field())
            comparison_results[(name, field)] = cr
            test_status = cr.get_test_status()
            perf_status = cr.get_value_status()
            if test_status == lnt.server.reporting.analysis.REGRESSED:
                bucket = new_failures
            elif test_status == lnt.server.reporting.analysis.IMPROVED:
                bucket = new_passes
            elif cr.current is None and cr.previous is not None:
                bucket = removed_tests
            elif cr.current is not None and cr.previous is None:
                bucket = added_tests
            elif test_status == lnt.server.reporting.analysis.UNCHANGED_FAIL:
                bucket = existing_failures
            elif perf_status == lnt.server.reporting.analysis.REGRESSED:
                bucket = perf_regressions
            elif perf_status == lnt.server.reporting.analysis.IMPROVED:
                bucket = perf_improvements
            else:
                bucket = unchanged_tests

            bucket.append(BucketEntry(name, cr, test_id))

        results_by_type.append(
            (field, (('New Failures', new_failures, False),
                     ('New Passes', new_passes, False),
                     ('Performance Regressions', perf_regressions, True),
                     ('Performance Improvements', perf_improvements, True),
                     ('Removed Tests', removed_tests, False),
                     ('Added Tests', added_tests, False),
                     ('Existing Failures', existing_failures, False),
                     ('Unchanged Tests', unchanged_tests, False))))
    return comparison_results, results_by_type
