#!/usr/bin/env python3
"""Analyze PISCES data."""
import argparse
import json
import os
import tempfile

from bokeh.embed import file_html
from bokeh.models import Tabs
from bokeh.resources import INLINE
import boto3
import pandas as pd
import requests

from base.notifications import slack_notifier
from mined_metric.builder.metrics_impl.pisces.utils.utils import Experiment
from mined_metric.builder.metrics_impl.pisces.utils.report_tab import (
    PredictionReportTab,
    PlannerReportTab,
    PerceptionReportTab,
    TrackerReportTab,
    SteeringComparisonReportTab,
    SummaryTab,
    PISCES_URL,
)

SLACK_TOKEN = "xoxb-4306046359-567010040450-7kh9B8WuQlNOdDLHUZ9W52m5"

# Remove implementation once we have utilities
def get_slack_user_id(email):
    data = {
        "token": "xoxp-4306046359-516093536435-880370286278-20f6f201477c8172cd0ce6f8ad91754a",
        "email": email,
    }
    response = requests.post(
        "https://slack.com/api/users.lookupByEmail", data=data
    )
    if not (200 <= response.status_code < 300 and response.json()["ok"]):
        raise RuntimeError(
            "Could not lookup user by email (%s): %s" % (email, response.text)
        )
    return response.json()["user"]["id"]


def parse_args():
    parser = argparse.ArgumentParser("Analyze Results")
    parser.add_argument(
        "--directory",
        "-d",
        required=True,
        type=str,
        help="Directory containing experiment results",
    )
    parser.add_argument(
        "--experiment-id",
        "--id",
        required=True,
        type=str,
        help=(
            "ID of the experiment containing comparison. Corresponds to "
            "MetricHub validation ID."
        ),
    )
    parser.add_argument(
        "--control", required=True, type=str, help="Name of control to use"
    )
    parser.add_argument(
        "--control-sha",
        required=False,
        default=None,
        type=str,
        help="Git SHA of control",
    )
    parser.add_argument(
        "--candidate", required=True, type=str, help="Name of candidate to use"
    )
    parser.add_argument(
        "--candidate-sha",
        required=False,
        default=None,
        type=str,
        help="Git SHA of candidate",
    )
    parser.add_argument(
        "--tracking",
        required=False,
        type=str,
        default=None,
        help="Branch name to use when recording stats in this job.",
    )
    parser.add_argument(
        "--notify",
        required=False,
        type=str,
        default=None,
        help="The slack user or channel to notify",
    )
    parser.add_argument(
        "--email",
        required=False,
        type=str,
        default=None,
        help=(
            "The email used to look up a slack user to use for notificiation. "
            "For dev/scripting use."
        ),
    )
    parser.add_argument(
        "--argus-host",
        required=False,
        type=str,
        default="argus.zooxlabs.com",
        help="The hostname to use for argus links.",
    )
    parser.add_argument(
        "--pipedream-id",
        required=False,
        type=str,
        default=None,
        help="The id for the pipedream job. For regenerating PISCES table.",
    )
    parser.add_argument(
        "--raise-on-changed",
        action="store_true",
        help=(
            "Raise error when changes detected. This is to be used in CI runs"
            "to notify when changes detected."
        ),
    )

    args = parser.parse_args()
    if args.email:
        args.notify = get_slack_user_id(args.email)
    return args


def render_label(label, sha):
    if sha is not None:
        label = (
            f"{label} (<code>"
            f'<a href="https://git.zooxlabs.com/zooxco/driving/tree/{sha}">'
            f"{sha:.8s}</a></code>)"
        )
    return label


def make_template():
    template = "mined_metric/builder/metrics_impl/pisces/utils/pisces.tpl"
    with open(template) as f:
        return f.read()


def build_report_tabs(
    comparators,
    experiment,
    control,
    control_sha,
    control_stats,
    candidate,
    candidate_sha,
    candidate_stats,
    argus_host,
    tracking,
    pipe_id,
):
    """Returns (bokeh Tabs containing each ReportTab, summary metric dict)
       The summary metric dict contains the # of metrics per metric type,
       where changes are detected between base and candidate.
    """
    th_props = [
        ("color", "#6d6d6d"),
        ("background-color", "#f7f7f9"),
        ("text-align", "left"),
        ("padding", "0.5em"),
    ]
    styles = [
        dict(selector="th", props=th_props),
        dict(selector="td", props=[("padding", "0.5em")]),
    ]

    report_tabs = []
    built_tabs = []

    # Create Planner Page.
    plan_df = pd.DataFrame(
        (
            comp.diff()["diffs"]
            for comp in comparators["Planner"]
            if comp is not None
        )
    )

    if not plan_df.empty:
        planner_tab = PlannerReportTab(
            plan_df,
            styles,
            argus_host,
            control_stats["Planner"],
            candidate_stats["Planner"],
        )
        planner_tab.make_action_search_plots(
            experiment, candidate, candidate_sha, control, control_sha
        )

        # Build Planner Page for rendering.
        report_tabs.append(planner_tab)
        built_tabs.append(planner_tab.build("Planner"))

        # Create Prediction Page.
        pred_df = pd.DataFrame(
            (
                comp.diff()
                for comp in comparators["Prediction"]
                if comp is not None
            )
        )
        prediction_tab = PredictionReportTab(
            pred_df,
            styles,
            argus_host,
            control,
            candidate,
            control_stats["Prediction"],
            candidate_stats["Prediction"],
        )
        report_tabs.append(prediction_tab)
        built_tabs.append(prediction_tab.build("Prediction"))

        # Create Perception Page.
        pcp_df = pd.DataFrame(
            (
                comp.diff()
                for comp in comparators["Perception"]
                if comp is not None
            )
        )
        pcp_tab = PerceptionReportTab(
            pcp_df, styles, argus_host, control, candidate
        )
        report_tabs.append(pcp_tab)
        built_tabs.append(pcp_tab.build("Perception"))

        # Create Tracker Page.
        tracker_tab = TrackerReportTab(planner_tab.df, styles, argus_host)
        report_tabs.append(tracker_tab)
        built_tabs.append(tracker_tab.build("Tracker"))

    # Create Steering Comparison Page.
    steering_data = list(
        comp.diff() for comp in comparators["Steering"] if comp is not None
    )

    if len(steering_data) > 0:
        candidate_steering_tab = SteeringComparisonReportTab(
            pd.DataFrame(x["candidate"] for x in steering_data),
            candidate,
            candidate_sha,
            styles,
            argus_host,
            pipe_id,
            tracking,
        )
        report_tabs.append(candidate_steering_tab)
        built_tabs.append(
            candidate_steering_tab.build(f"VH6 2ws vs 4ws ({candidate})")
        )

        # Show comparison on control
        control_steering_tab = SteeringComparisonReportTab(
            pd.DataFrame(x["control"] for x in steering_data),
            control,
            control_sha,
            styles,
            argus_host,
            pipe_id,
            None,
        )
        report_tabs.append(control_steering_tab)
        built_tabs.append(
            control_steering_tab.build(f"VH6 2ws vs 4ws ({control})")
        )

    # Create Summary Page.
    summary_tab = SummaryTab(report_tabs, styles)
    built_tabs.insert(0, summary_tab.build("Summary"))

    # Return all separate pages together.
    return Tabs(tabs=built_tabs), summary_tab.get_changed_metrics()


def render_and_upload_results(
    control, control_sha, candidate, candidate_sha, pipe_id, experiment_id, tabs
):
    """Renders the bokeh Tabs and uploads to s3."""
    control_label = render_label(control, control_sha)
    candidate_label = render_label(candidate, candidate_sha)
    title = f"PISCES Results {control_label} vs {candidate_label}"

    if pipe_id is not None:
        pipedream_url = f"https://piper.zooxlabs.com/pipeline/{pipe_id}"
    else:
        pipedream_url = None

    metrichub_url = (
        f"https://mined-metrics.zooxlabs.com/validation_result/{experiment_id}"
    )

    template_variables = dict(
        pipedream_link=pipedream_url, metrichub_link=metrichub_url
    )
    html = file_html(
        [tabs],
        INLINE,
        title=title,
        template=make_template(),
        template_variables=template_variables,
    )

    # Write HTML to a temporary file in a separate context to make sure contents
    # are written to disk before uploading to s3. Because "delete=False", we
    # need to manually remove this file (below).
    with tempfile.NamedTemporaryFile("wb", delete=False) as temp:
        temp.write(html.encode("utf-8"))

    try:
        s3 = boto3.client("s3")
        index_key = f"pisces/{experiment_id}/index.html"
        with open(temp.name, "rb") as fileobj:
            s3.upload_fileobj(
                fileobj,
                "zoox-web",
                index_key,
                ExtraArgs={"ContentType": "text/html"},
            )
    finally:
        os.remove(temp.name)


def load_experiment(directory, experiment_id):
    e = Experiment(directory, experiment_id)
    e.load_data()

    return e


def send_notification(target, extra):
    notifier = slack_notifier.SlackNotifier(SLACK_TOKEN)
    resp = notifier.send_message(target, extra=extra)


def build_message(experiment, control, candidate, pipe_id, changed_metrics):
    """
    Build the report message to send via slack.
    """
    actions = []
    actions.append(
        {
            "type": "button",
            "text": "View Results",
            "url": f"http://{PISCES_URL}/{experiment}/",
            "style": "primary",
        }
    )

    fields = []
    fields.append(
        {"title": "Summary", "value": "Click the button", "short": False}
    )

    if pipe_id is not None:
        footer = f"Pipedream: https://piper.zooxlabs.com/pipeline/{pipe_id}"
    else:
        footer = "Local run"

    changed_detected = sum(changed_metrics.values())
    changed_str = (
        "No Changes Detected"
        if not changed_detected
        else f"Changes Detected: {changed_metrics}"
    )
    msg = f"`{control}` (control) vs `{candidate}` (candidate)\n{changed_str}"

    extra = {
        "attachments": json.dumps(
            [
                {
                    "title": "PISCES results",
                    "fallback": f"Results comparing {candidate} to {control}",
                    "text": msg,
                    "color": "good" if not changed_detected else "bad",
                    "actions": actions,
                    "footer": footer,
                }
            ]
        )
    }
    return extra


if __name__ == "__main__":
    args = parse_args()
    pipe_id = os.environ.get("PIPEDREAM_PIPE_ID", args.pipedream_id)

    exp = load_experiment(args.directory, args.experiment_id)
    comparators = exp.compare(args.candidate, args.control)
    candidate_stats = exp.describe(args.candidate)
    control_stats = exp.describe(args.control)

    tabs, changed_metrics = build_report_tabs(
        comparators,
        exp,
        args.control,
        args.control_sha,
        control_stats,
        args.candidate,
        args.candidate_sha,
        candidate_stats,
        args.argus_host,
        args.tracking,
        pipe_id,
    )
    render_and_upload_results(
        args.control,
        args.control_sha,
        args.candidate,
        args.candidate_sha,
        pipe_id,
        exp.id,
        tabs,
    )

    if args.notify:
        msg = build_message(
            args.experiment_id,
            args.control,
            args.candidate,
            pipe_id,
            changed_metrics,
        )
        send_notification(args.notify, msg)

    if args.raise_on_changed and sum(changed_metrics.values()) > 0:
        print(
            "\n===================================\n"
            + f"Results posted to http://pisces.web.zooxlabs.com/{args.experiment_id}"
        )
        # Throw error is a quick way to fail loudly. This is useful in CI
        # runs so it will block the PR from merging.
        raise RuntimeError(
            f"The following changes are detected:\n {changed_metrics}\n"
        )
