# -*- coding: utf-8 -*-

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""
Outputter to generate Markdown documentation for metrics.
"""

from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import urlsplit, parse_qs


from . import __version__
from . import metrics
from . import pings
from . import util
from collections import defaultdict


def extra_info(obj: Union[metrics.Metric, pings.Ping]) -> List[Tuple[str, str]]:
    """
    Returns a list of string to string tuples with extra information for the type
    (e.g. extra keys for events) or an empty list if nothing is available.
    """
    extra_info = []

    if isinstance(obj, metrics.Event):
        for key in obj.allowed_extra_keys:
            extra_info.append((key, obj.extra_keys[key]["description"]))

    if isinstance(obj, metrics.Labeled) and obj.ordered_labels is not None:
        for label in obj.ordered_labels:
            extra_info.append((label, None))

    if isinstance(obj, metrics.Quantity):
        extra_info.append(("unit", obj.unit))

    return extra_info


def ping_desc(
    ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
) -> str:
    """
    Return a text description of the ping. If a custom_pings_cache
    is available, look in there for non-reserved ping names description.
    """
    desc = ""

    if ping_name in pings.RESERVED_PING_NAMES:
        desc = (
            "This is a built-in ping that is assembled out of the "
            "box by the Glean SDK."
        )
    elif ping_name == "all-pings":
        desc = "These metrics are sent in every ping."
    elif custom_pings_cache is not None and ping_name in custom_pings_cache:
        desc = custom_pings_cache[ping_name].description

    return desc


def metrics_docs(obj_name: str) -> str:
    """
    Return a link to the documentation entry for the Glean SDK metric of the
    requested type.
    """
    # We need to fixup labeled stuff, as types are singular and docs refer
    # to them as plural.
    fixedup_name = obj_name
    if obj_name.startswith("labeled_"):
        fixedup_name += "s"

    return f"https://mozilla.github.io/glean/book/user/metrics/{fixedup_name}.html"


def ping_docs(ping_name: str) -> str:
    """
    Return a link to the documentation entry for the requested Glean SDK
    built-in ping.
    """
    if ping_name not in pings.RESERVED_PING_NAMES:
        return ""

    return f"https://mozilla.github.io/glean/book/user/pings/{ping_name}.html"


def if_empty(
    ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
) -> bool:
    if custom_pings_cache is not None and ping_name in custom_pings_cache:
        return custom_pings_cache[ping_name].send_if_empty
    else:
        return False


def ping_reasons(
    ping_name: str, custom_pings_cache: Dict[str, pings.Ping]
) -> Dict[str, str]:
    """
    Returns the reasons dictionary for the ping.
    """
    if ping_name == "all-pings":
        return {}
    elif ping_name in custom_pings_cache:
        return custom_pings_cache[ping_name].reasons

    return {}


def ping_data_reviews(
    ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
) -> Optional[List[str]]:
    if custom_pings_cache is not None and ping_name in custom_pings_cache:
        return custom_pings_cache[ping_name].data_reviews
    else:
        return None


def ping_review_title(data_url: str, index: int) -> str:
    """
    Return a title for a data review in human readable form.

    :param data_url: A url for data review.
    :param index: Position of the data review on list (e.g: 1, 2, 3...).
    """
    url_object = urlsplit(data_url)

    # Bugzilla urls like `https://bugzilla.mozilla.org/show_bug.cgi?id=1581647`
    query = url_object.query
    params = parse_qs(query)

    # GitHub urls like `https://github.com/mozilla-mobile/fenix/pull/1707`
    path = url_object.path
    short_url = path[1:].replace("/pull/", "#")

    if params and params["id"]:
        return f"Bug {params['id'][0]}"
    elif url_object.netloc == "github.com":
        return short_url

    return f"Review {index}"


def ping_bugs(
    ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
) -> Optional[List[str]]:
    if custom_pings_cache is not None and ping_name in custom_pings_cache:
        return custom_pings_cache[ping_name].bugs
    else:
        return None


def ping_include_client_id(
    ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
) -> bool:
    if custom_pings_cache is not None and ping_name in custom_pings_cache:
        return custom_pings_cache[ping_name].include_client_id
    else:
        return False


def data_sensitivity_numbers(
    data_sensitivity: Optional[List[metrics.DataSensitivity]],
) -> str:
    if data_sensitivity is None:
        return "unknown"
    else:
        return ", ".join(str(x.value) for x in data_sensitivity)


def output_markdown(
    objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
) -> None:
    """
    Given a tree of objects, output Markdown docs to `output_dir`.

    This produces a single `metrics.md`. The file contains a table of
    contents and a section for each ping metrics are collected for.

    :param objects: A tree of objects (metrics and pings) as returned from
        `parser.parse_objects`.
    :param output_dir: Path to an output directory to write to.
    :param options: options dictionary, with the following optional key:
        - `project_title`: The projects title.
    """
    if options is None:
        options = {}

    # Build a dictionary that associates pings with their metrics.
    #
    # {
    #  "baseline": [
    #    { ... metric data ... },
    #    ...
    #  ],
    #  "metrics": [
    #    { ... metric data ... },
    #    ...
    #  ],
    #  ...
    # }
    #
    # This also builds a dictionary of custom pings, if available.
    custom_pings_cache: Dict[str, pings.Ping] = defaultdict()
    metrics_by_pings: Dict[str, List[metrics.Metric]] = defaultdict(list)
    for _category_key, category_val in objs.items():
        for obj in category_val.values():
            # Filter out custom pings. We will need them for extracting
            # the description
            if isinstance(obj, pings.Ping):
                custom_pings_cache[obj.name] = obj
                # Pings that have `send_if_empty` set to true,
                # might not have any metrics. They need to at least have an
                # empty array of metrics to show up on the template.
                if obj.send_if_empty and not metrics_by_pings[obj.name]:
                    metrics_by_pings[obj.name] = []

            # If this is an internal Glean metric, and we don't
            # want docs for it.
            if isinstance(obj, metrics.Metric) and not obj.is_internal_metric():
                # If we get here, obj is definitely a metric we want
                # docs for.
                for ping_name in obj.send_in_pings:
                    metrics_by_pings[ping_name].append(obj)

    # Sort the metrics by their identifier, to make them show up nicely
    # in the docs and to make generated docs reproducible.
    for ping_name in metrics_by_pings:
        metrics_by_pings[ping_name] = sorted(
            metrics_by_pings[ping_name], key=lambda x: x.identifier()
        )

    project_title = options.get("project_title", "this project")
    introduction_extra = options.get("introduction_extra")

    template = util.get_jinja2_template(
        "markdown.jinja2",
        filters=(
            ("extra_info", extra_info),
            ("metrics_docs", metrics_docs),
            ("ping_desc", lambda x: ping_desc(x, custom_pings_cache)),
            ("ping_send_if_empty", lambda x: if_empty(x, custom_pings_cache)),
            ("ping_docs", ping_docs),
            ("ping_reasons", lambda x: ping_reasons(x, custom_pings_cache)),
            ("ping_data_reviews", lambda x: ping_data_reviews(x, custom_pings_cache)),
            ("ping_review_title", ping_review_title),
            ("ping_bugs", lambda x: ping_bugs(x, custom_pings_cache)),
            (
                "ping_include_client_id",
                lambda x: ping_include_client_id(x, custom_pings_cache),
            ),
            ("data_sensitivity_numbers", data_sensitivity_numbers),
        ),
    )

    filename = "metrics.md"
    filepath = output_dir / filename

    with filepath.open("w", encoding="utf-8") as fd:
        fd.write(
            template.render(
                parser_version=__version__,
                metrics_by_pings=metrics_by_pings,
                project_title=project_title,
                introduction_extra=introduction_extra,
            )
        )
        # Jinja2 squashes the final newline, so we explicitly add it
        fd.write("\n")
