# 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/.

import logging
import os
import re
from operator import itemgetter

import requests
from mozilla_version.gecko import GeckoVersion
from taskcluster import Notify, optionsFromEnvironment

BUGLIST_TEMPLATE = "* [Bugs since previous changeset]({url})\n"
BACKOUT_REGEX = re.compile(r"back(\s?)out|backed out|backing out", re.IGNORECASE)
BACKOUT_TEMPLATE = "* [Backouts since previous changeset]({url})\n"
BUGZILLA_BUGLIST_TEMPLATE = "https://bugzilla.mozilla.org/buglist.cgi?bug_id={bugs}"
BUG_NUMBER_REGEX = re.compile(r"bug \d+", re.IGNORECASE)
CHANGELOG_TO_FROM_STRING = "{product}_{version}_RELEASE"
CHANGESET_URL_TEMPLATE = (
    "{repo}/{logtype}" "?rev={to_version}+%25+{from_version}&revcount=1000"
)
FULL_CHANGESET_TEMPLATE = "* [Full Mercurial changelog]({url})\n"
LIST_DESCRIPTION_TEMPLATE = "Comparing Mercurial tag {from_version} to {to_version}:\n"
MAX_BUGS_IN_BUGLIST = 250
MERCURIAL_TAGS_URL_TEMPLATE = "{repo}/json-tags"
NO_BUGS = ""  # Return this when bug list can't be created
URL_SHORTENER_TEMPLATE = "https://bugzilla.mozilla.org/rest/bitly/shorten?url={url}"

log = logging.getLogger(__name__)


def create_bugs_url(product, current_version, current_revision, repo=None):
    """
    Creates list of bugs and backout bugs for release-drivers email

    :param release: dict -> containing information about release, from Ship-It
    :return: str -> description of compared releases, with Bugzilla links
        containing all bugs in changeset
    """
    try:
        # Extract the important data, ignore if beta1 release
        if current_version.beta_number == 1:
            # If the version is beta 1, don't make any links
            return NO_BUGS

        if repo is None:
            repo = get_repo_by_version(current_version)
        # Get the tag version, for display purposes
        current_version_tag = tag_version(product, current_version)

        # Get all Hg tags for this branch, determine the previous version
        tag_url = MERCURIAL_TAGS_URL_TEMPLATE.format(repo=repo)
        mercurial_tags_json = requests.get(tag_url).json()
        previous_version_tag = get_previous_tag_version(
            product, current_version, current_version_tag, mercurial_tags_json
        )

        # Get the changeset between these versions, parse for all unique bugs and backout bugs
        resp = requests.get(
            CHANGESET_URL_TEMPLATE.format(
                repo=repo,
                from_version=previous_version_tag,
                to_version=current_revision,
                logtype="json-log",
            )
        )
        changeset_data = resp.json()
        unique_bugs, unique_backout_bugs = get_bugs_in_changeset(changeset_data)

        # Return a descriptive string with links if any relevant bugs are found
        if unique_bugs or unique_backout_bugs:
            description = LIST_DESCRIPTION_TEMPLATE.format(
                from_version=previous_version_tag, to_version=current_version_tag
            )

            if unique_bugs:
                description += BUGLIST_TEMPLATE.format(
                    url=create_buglist_url(unique_bugs)
                )
            if unique_backout_bugs:
                description += BACKOUT_TEMPLATE.format(
                    url=create_buglist_url(unique_backout_bugs)
                )

            changeset_html = CHANGESET_URL_TEMPLATE.format(
                repo=repo,
                from_version=previous_version_tag,
                to_version=current_revision,
                logtype="log",
            )
            description += FULL_CHANGESET_TEMPLATE.format(url=changeset_html)

            return description
        else:
            return NO_BUGS

    except Exception as err:
        log.info(err)
        return NO_BUGS


def get_bugs_in_changeset(changeset_data):
    unique_bugs, unique_backout_bugs = set(), set()
    for changeset in changeset_data["entries"]:
        if is_excluded_change(changeset):
            continue

        changeset_desc = changeset["desc"]
        bug_re = BUG_NUMBER_REGEX.search(changeset_desc)

        if bug_re:
            bug_number = bug_re.group().split(" ")[1]

            if is_backout_bug(changeset_desc):
                unique_backout_bugs.add(bug_number)
            else:
                unique_bugs.add(bug_number)

    return unique_bugs, unique_backout_bugs


def is_excluded_change(changeset):
    excluded_change_keywords = [
        "a=test-only",
        "a=release",
    ]
    return any(keyword in changeset["desc"] for keyword in excluded_change_keywords)


def is_backout_bug(changeset_description):
    return bool(BACKOUT_REGEX.search(changeset_description))


def create_buglist_url(buglist):
    return BUGZILLA_BUGLIST_TEMPLATE.format(bugs="%2C".join(buglist))


def tag_version(product, version):
    underscore_version = str(version).replace(".", "_")
    return CHANGELOG_TO_FROM_STRING.format(
        product=product.upper(), version=underscore_version
    )


def parse_tag_version(tag):
    dot_version = ".".join(tag.split("_")[1:-1])
    return GeckoVersion.parse(dot_version)


def get_previous_tag_version(
    product,
    current_version,
    current_version_tag,
    mercurial_tags_json,
):
    """
    Gets the previous hg version tag for the product and branch, given the current version tag
    """

    def _invalid_tag_filter(tag):
        """Filters by product and removes incorrect major version + base, end releases"""
        prod_major_version_re = rf"^{product.upper()}_{current_version.major_number}"

        return (
            "BASE" not in tag
            and "END" not in tag
            and "RELEASE" in tag
            and re.match(prod_major_version_re, tag)
        )

    # Get rid of irrelevant tags, sort by date and extract the tag string
    tags = {
        (parse_tag_version(item["tag"]), item["tag"])
        for item in mercurial_tags_json["tags"]
        if _invalid_tag_filter(item["tag"])
    }
    # Add the current version to the list
    tags.add((current_version, current_version_tag))
    tags = sorted(tags, key=lambda tag: tag[0])

    # Find where the current version is and go back one to get the previous version
    next_version_index = list(map(itemgetter(0), tags)).index(current_version) - 1

    return tags[next_version_index][1]


def get_repo_by_version(version):
    """
    Get the repo a given version is found on.
    """
    if version.is_beta:
        return "https://hg.mozilla.org/releases/mozilla-beta"
    elif version.is_release:
        return "https://hg.mozilla.org/releases/mozilla-release"
    elif version.is_esr:
        return f"https://hg.mozilla.org/releases/mozilla-esr{version.major_number}"
    else:
        raise Exception(
            f"Unsupported version type {version.version_type.name}: {version}"
        )


def email_release_drivers(
    addresses,
    product,
    version,
    build_number,
    repo,
    revision,
    task_group_id,
):
    # Send an email to the mailing after the build
    email_buglist_string = create_bugs_url(product, version, revision, repo=repo)

    content = """\
A new build has been started:

Commit: [{revision}]({repo}/rev/{revision})
Task group: [{task_group_id}]({root_url}/tasks/groups/{task_group_id})

{email_buglist_string}
""".format(
        repo=repo,
        revision=revision,
        root_url=os.environ["TASKCLUSTER_ROOT_URL"],
        task_group_id=task_group_id,
        email_buglist_string=email_buglist_string,
    )

    # On r-d, we prefix the subject of the email in order to simplify filtering
    subject_prefix = ""
    if product in {"fennec"}:
        subject_prefix = "[mobile] "
    if product in {"firefox", "devedition"}:
        subject_prefix = "[desktop] "

    subject = f"{subject_prefix} Build of {product} {version} build {build_number}"

    # use proxy if configured, otherwise local credentials from env vars
    if "TASKCLUSTER_PROXY_URL" in os.environ:
        notify_options = {"rootUrl": os.environ["TASKCLUSTER_PROXY_URL"]}
    else:
        notify_options = optionsFromEnvironment()

    notify = Notify(notify_options)
    for address in addresses:
        notify.email(
            {
                "address": address,
                "subject": subject,
                "content": content,
            }
        )
