# 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 json
import os

from mozilla_taskgraph.util.attributes import release_level
from taskcluster.exceptions import TaskclusterRestFailure
from taskgraph.parameters import Parameters
from taskgraph.taskgraph import TaskGraph
from taskgraph.util.taskcluster import get_artifact, list_task_group_incomplete_tasks

from gecko_taskgraph.actions.registry import register_callback_action
from gecko_taskgraph.decision import taskgraph_decision
from gecko_taskgraph.util.attributes import RELEASE_PROMOTION_PROJECTS
from gecko_taskgraph.util.partials import populate_release_history
from gecko_taskgraph.util.partners import (
    fix_partner_config,
    get_partner_config_by_url,
    get_partner_url_config,
    get_token,
)
from gecko_taskgraph.util.taskgraph import (
    find_decision_task,
    find_existing_tasks_from_previous_kinds,
)


def is_release_promotion_available(parameters):
    return parameters["project"] in RELEASE_PROMOTION_PROJECTS


def get_partner_config(partner_url_config, github_token):
    partner_config = {}
    for kind, url in partner_url_config.items():
        if url:
            partner_config[kind] = get_partner_config_by_url(url, kind, github_token)
    return partner_config


def get_flavors(graph_config, param):
    """
    Get all flavors with the given parameter enabled.
    """
    promotion_flavors = graph_config["release-promotion"]["flavors"]
    return sorted(
        flavor
        for (flavor, config) in promotion_flavors.items()
        if config.get(param, False)
    )


@register_callback_action(
    name="release-promotion",
    title="Release Promotion",
    symbol="${input.release_promotion_flavor}",
    description="Promote a release.",
    permission="release-promotion",
    order=500,
    context=[],
    available=is_release_promotion_available,
    schema=lambda graph_config: {
        "type": "object",
        "properties": {
            "build_number": {
                "type": "integer",
                "default": 1,
                "minimum": 1,
                "title": "The release build number",
                "description": (
                    "The release build number. Starts at 1 per "
                    "release version, and increments on rebuild."
                ),
            },
            "do_not_optimize": {
                "type": "array",
                "description": (
                    "Optional: a list of labels to avoid optimizing out "
                    "of the graph (to force a rerun of, say, "
                    "funsize docker-image tasks)."
                ),
                "items": {
                    "type": "string",
                },
            },
            "revision": {
                "type": "string",
                "title": "Optional: revision to promote",
                "description": (
                    "Optional: the revision to promote. If specified, "
                    "and `previous_graph_kinds is not specified, find the "
                    "push graph to promote based on the revision."
                ),
            },
            "release_promotion_flavor": {
                "type": "string",
                "description": "The flavor of release promotion to perform.",
                "default": "FILL ME OUT",
                "enum": sorted(graph_config["release-promotion"]["flavors"].keys()),
            },
            "rebuild_kinds": {
                "type": "array",
                "description": (
                    "Optional: an array of kinds to ignore from the previous graph(s)."
                ),
                "default": graph_config["release-promotion"].get("rebuild-kinds", []),
                "items": {
                    "type": "string",
                },
            },
            "previous_graph_ids": {
                "type": "array",
                "description": (
                    "Optional: an array of taskIds of decision or action "
                    "tasks from the previous graph(s) to use to populate "
                    "our `previous_graph_kinds`."
                ),
                "items": {
                    "type": "string",
                },
            },
            "version": {
                "type": "string",
                "description": (
                    "Optional: override the version for release promotion. "
                    "Occasionally we'll land a taskgraph fix in a later "
                    "commit, but want to act on a build from a previous "
                    "commit. If a version bump has landed in the meantime, "
                    "relying on the in-tree version will break things."
                ),
                "default": "",
            },
            "next_version": {
                "type": "string",
                "description": (
                    "Next version. Required in the following flavors: {}".format(
                        get_flavors(graph_config, "version-bump")
                    )
                ),
                "default": "",
            },
            # Example:
            #   'partial_updates': {
            #       '38.0': {
            #           'buildNumber': 1,
            #           'locales': ['de', 'en-GB', 'ru', 'uk', 'zh-TW']
            #       },
            #       '37.0': {
            #           'buildNumber': 2,
            #           'locales': ['de', 'en-GB', 'ru', 'uk']
            #       }
            #   }
            "partial_updates": {
                "type": "object",
                "description": (
                    "Partial updates. Required in the following flavors: {}".format(
                        get_flavors(graph_config, "partial-updates")
                    )
                ),
                "default": {},
                "additionalProperties": {
                    "type": "object",
                    "properties": {
                        "buildNumber": {
                            "type": "number",
                        },
                        "locales": {
                            "type": "array",
                            "items": {
                                "type": "string",
                            },
                        },
                    },
                    "required": [
                        "buildNumber",
                        "locales",
                    ],
                    "additionalProperties": False,
                },
            },
            "release_eta": {
                "type": "string",
                "default": "",
            },
            "release_enable_partner_repack": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating partner repacks",
            },
            "release_enable_partner_attribution": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating partner attribution",
            },
            "release_partner_build_number": {
                "type": "integer",
                "default": 1,
                "minimum": 1,
                "description": (
                    "The partner build number. This translates to, e.g. "
                    "`v1` in the path. We generally only have to "
                    "bump this on off-cycle partner rebuilds."
                ),
            },
            "release_partners": {
                "type": "array",
                "description": (
                    "A list of partners to repack, or if null or empty then use "
                    "the current full set"
                ),
                "items": {
                    "type": "string",
                },
            },
            "release_partner_config": {
                "type": "object",
                "description": "Partner configuration to use for partner repacks.",
                "properties": {},
                "additionalProperties": True,
            },
            "release_enable_emefree": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating EME-free repacks",
            },
        },
        "required": ["release_promotion_flavor", "build_number"],
    },
)
def release_promotion_action(
    push_parameters, graph_config, input, task_group_id, task_id
):
    release_promotion_flavor = input["release_promotion_flavor"]
    promotion_config = graph_config["release-promotion"]["flavors"][
        release_promotion_flavor
    ]
    release_history = {}
    product = promotion_config["product"]

    next_version = str(input.get("next_version") or "")
    if promotion_config.get("version-bump", False):
        # We force str() the input, hence the 'None'
        if next_version in ["", "None"]:
            raise Exception(
                f"`next_version` property needs to be provided for `{release_promotion_flavor}` "
                "target."
            )

    if promotion_config.get("partial-updates", False):
        partial_updates = input.get("partial_updates", {})
        if (
            not partial_updates
            and release_level(graph_config["release-branches"], push_parameters)
            == "production"
        ):
            raise Exception(
                f"`partial_updates` property needs to be provided for `{release_promotion_flavor}`"
                "target."
            )
        balrog_prefix = product.title()
        os.environ["PARTIAL_UPDATES"] = json.dumps(partial_updates, sort_keys=True)
        release_history = populate_release_history(
            balrog_prefix, push_parameters["project"], partial_updates=partial_updates
        )

    target_tasks_method = promotion_config["target-tasks-method"].format(
        project=push_parameters["project"]
    )
    rebuild_kinds = input.get(
        "rebuild_kinds", promotion_config.get("rebuild-kinds", [])
    )
    do_not_optimize = input.get(
        "do_not_optimize", promotion_config.get("do-not-optimize", [])
    )

    # Make sure no pending tasks remain from a previous run
    own_task_id = os.environ.get("TASK_ID", "")
    try:
        for t in list_task_group_incomplete_tasks(own_task_id):
            if t == own_task_id:
                continue
            raise Exception(
                f"task group has unexpected pre-existing incomplete tasks (e.g. {t})"
            )
    except TaskclusterRestFailure as e:
        # 404 means the task group doesn't exist yet, and we're fine
        if e.status_code != 404:
            raise

    # Build previous_graph_ids from ``previous_graph_ids``, ``revision``,
    # or the action parameters.
    previous_graph_ids = input.get("previous_graph_ids")
    head_rev_param = "{}head_rev".format(graph_config["project-repo-param-prefix"])
    if not previous_graph_ids:
        revision = input.get("revision")
        if revision:
            push_parameters[head_rev_param] = revision
        previous_graph_ids = [find_decision_task(push_parameters, graph_config)]

    # Download parameters from the first decision task
    parameters = get_artifact(previous_graph_ids[0], "public/parameters.yml")
    # Override `head_rev` - this should always be the revision that this action
    # task was fired from. If the first `previous_graph_id` given was from an
    # earlier revision, it will end up being wrong. This will cause any created
    # tasks to have the wrong revision set, which causes problems such as showing
    # up in the wrong place on Treeherder, and associated cached task digests
    # with unmatched sources.
    parameters["head_rev"] = push_parameters["head_rev"]
    # If no `project-repo-param-prefix' is set this will end up doing the same
    # as the above...but that's no harm.
    parameters[head_rev_param] = push_parameters[head_rev_param]

    # Download and combine full task graphs from each of the previous_graph_ids.
    # Sometimes previous relpro action tasks will add tasks, like partials,
    # that didn't exist in the first full_task_graph, so combining them is
    # important. The rightmost graph should take precedence in the case of
    # conflicts.
    combined_full_task_graph = {}
    for graph_id in previous_graph_ids:
        full_task_graph = get_artifact(graph_id, "public/full-task-graph.json")
        combined_full_task_graph.update(full_task_graph)
    _, combined_full_task_graph = TaskGraph.from_json(combined_full_task_graph)
    parameters["existing_tasks"] = find_existing_tasks_from_previous_kinds(
        combined_full_task_graph, previous_graph_ids, rebuild_kinds
    )
    parameters["do_not_optimize"] = do_not_optimize
    parameters["target_tasks_method"] = target_tasks_method
    parameters["build_number"] = int(input["build_number"])
    parameters["next_version"] = next_version
    parameters["release_history"] = release_history
    parameters["release_eta"] = input.get("release_eta", "")
    parameters["release_product"] = product
    # When doing staging releases on try, we still want to re-use tasks from
    # previous graphs.
    parameters["optimize_target_tasks"] = True

    if release_promotion_flavor == "promote_firefox_partner_repack":
        release_enable_partner_repack = True
        release_enable_partner_attribution = False
        release_enable_emefree = False
    elif release_promotion_flavor == "promote_firefox_partner_attribution":
        release_enable_partner_repack = False
        release_enable_partner_attribution = True
        release_enable_emefree = False
    else:
        # for promotion or ship phases, we use the action input to turn the repacks/attribution off
        release_enable_partner_repack = input["release_enable_partner_repack"]
        release_enable_partner_attribution = input["release_enable_partner_attribution"]
        release_enable_emefree = input["release_enable_emefree"]

    partner_url_config = get_partner_url_config(parameters, graph_config)
    if (
        release_enable_partner_repack
        and not partner_url_config["release-partner-repack"]
    ):
        raise Exception("Can't enable partner repacks when no config url found")
    if (
        release_enable_partner_attribution
        and not partner_url_config["release-partner-attribution"]
    ):
        raise Exception("Can't enable partner attribution when no config url found")
    if release_enable_emefree and not partner_url_config["release-eme-free-repack"]:
        raise Exception("Can't enable EMEfree repacks when no config url found")
    parameters["release_enable_partner_repack"] = release_enable_partner_repack
    parameters["release_enable_partner_attribution"] = (
        release_enable_partner_attribution
    )
    parameters["release_enable_emefree"] = release_enable_emefree

    partner_config = input.get("release_partner_config")
    if not partner_config and any([
        release_enable_partner_repack,
        release_enable_partner_attribution,
        release_enable_emefree,
    ]):
        github_token = get_token(parameters)
        partner_config = get_partner_config(partner_url_config, github_token)
    if partner_config:
        parameters["release_partner_config"] = fix_partner_config(partner_config)
    parameters["release_partners"] = input.get("release_partners")
    if input.get("release_partner_build_number"):
        parameters["release_partner_build_number"] = input[
            "release_partner_build_number"
        ]

    if input["version"]:
        parameters["version"] = input["version"]

    parameters["dontbuild"] = False
    # make parameters read-only
    parameters = Parameters(**parameters)

    taskgraph_decision({"root": graph_config.root_dir}, parameters=parameters)
