# 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 sys
from pathlib import Path
from urllib.parse import urlparse

import jsonschema
import yaml
from jsonschema.validators import validator_for

HEADER_LINE = (
    "// This file was generated by generate_feature_manifest.py from FeatureManifest.yaml."
    " DO NOT EDIT.\n"
)

FEATURE_SCHEMA = Path("schemas", "ExperimentFeature.schema.json")

NIMBUS_FALLBACK_PREFS = (
    "constexpr std::pair<nsLiteralCString, nsLiteralCString>"
    "NIMBUS_FALLBACK_PREFS[]{{{}}};"
)

# Do not add new feature IDs to this list! isEarlyStartup is being deprecated.
# See https://bugzilla.mozilla.org/show_bug.cgi?id=1875331 for details.
ALLOWED_ISEARLYSTARTUP_FEATURE_IDS = {
    "aboutwelcome",
    "bounceTrackingProtection",
    "newtab",
    "opaqueResponseBlocking",  # Requires C++ API for exposure events
    "pocketNewtab",
    "preonboarding",
    "testFeature",
    "upgradeDialog",
}

DISALLOWED_PREFS = {
    # Disabling either of these prefs will cause unenrollment from all active
    # enrollments, which will then cause the pref to reset.
    "app.shield.optoutstudies.enabled": (
        "disabling Nimbus causes immediate unenrollment"
    ),
    "datareporting.healthreport.uploadEnabled": (
        "disabling telemetry causes immediate unenrollment"
    ),
    # Changing the Remote Settings endpoint will cause unenrollment from the recipe.
    "services.settings.server": (
        "changing the Remote Settings endpoint will break clients"
    ),
    "messaging-system.rsexperimentloader.collection": (
        "changing the Nimbus collection will break clients"
    ),
    "nimbus.debug": "internal Nimbus preference for QA",
    # This pref controls the return value of xpc::IsInAutomation(), which is
    # used by code to check if we are in a test.
    "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer": (
        "this pref is automation-only and is unsafe to enable outside tests"
    ),
    "security.sandbox.content.level": (
        "changing this value can lower the security of clients"
    ),
}


def write_fm_headers(fd):
    fd.write(HEADER_LINE)


def validate_feature_manifest(schema_path, manifest_path, manifest):
    TOPSRCDIR = Path(__file__).parent.parent.parent.parent.parent

    with open(schema_path) as f:
        schema = json.load(f)

    set_prefs = {}
    fallback_prefs = {}

    for feature_id, feature in manifest.items():
        try:
            jsonschema.validate(feature, schema)

            is_early_startup = feature.get("isEarlyStartup", False)
            allowed_is_early_startup = feature_id in ALLOWED_ISEARLYSTARTUP_FEATURE_IDS
            if is_early_startup != allowed_is_early_startup:
                if is_early_startup:
                    print(f"Feature {feature_id} is marked isEarlyStartup: true")
                    print(
                        "isEarlyStartup is deprecated and no new isEarlyStartup features can be added"
                    )
                    print(
                        "See https://bugzilla.mozilla.org/show_bug.cgi?id=1875331 for details"
                    )
                    raise Exception("isEarlyStartup is deprecated")
                else:
                    print(
                        f"Feature {feature_id} is not early startup but is in the allow list."
                    )
                    print("Please remove it from generate_feature_manifest.py")
                raise Exception("isEarlyStartup is deprecated")

            if is_early_startup and feature.get("allowCoenrollment", False):
                raise Exception(
                    "A feature cannot use isEarlyStartup and allowCoenrollment"
                )

            for variable, variable_def in feature.get("variables", {}).items():
                set_pref = variable_def.get("setPref", {}).get("pref")
                if set_pref is None:
                    continue

                if reason := DISALLOWED_PREFS.get(set_pref):
                    raise Exception(
                        f"Pref {set_pref} cannot be controlled by Nimbus: {reason}"
                    )

                if feature.get("allowCoenrollment", False):
                    raise Exception("A feature cannot use co-enrollment and setPref")

                if set_pref in set_prefs:
                    other_feature = set_prefs[set_pref][0]
                    other_variable = set_prefs[set_pref][1]
                    print("Multiple variables cannot declare the same setPref")
                    print(
                        f"{feature_id} variable {variable} wants to set pref {set_pref}"
                    )
                    print(
                        f"{other_feature} variable {other_variable} wants to set pref "
                        f"{set_pref}"
                    )
                    raise Exception("Set prefs are exclusive")

                set_prefs[set_pref] = (feature_id, variable)

                fallback_pref = variable_def.get("fallbackPref")
                if fallback_pref is not None:
                    fallback_prefs[fallback_pref] = (feature_id, variable)

                conflicts = [
                    (
                        "setPref",
                        fallback_pref,
                        "fallbackPref",
                        set_prefs.get(fallback_pref),
                    ),
                    ("fallbackPref", set_pref, "setPref", fallback_prefs.get(set_pref)),
                ]

                for kind, pref, other_kind, conflict in conflicts:
                    if conflict is not None:
                        print(
                            "The same pref cannot be specified in setPref and fallbackPref"
                        )
                        print(
                            f"{feature_id} variable {variable} has specified {kind} {pref}"
                        )
                        print(
                            f"{conflict[0]} variable {conflict[1]} has specified {other_kind} "
                            f"{pref}"
                        )
                        raise Exception("Set prefs and fallback prefs cannot overlap")

            if "schema" in feature:
                schema_path = TOPSRCDIR / feature["schema"]["path"]
                if not schema_path.exists():
                    raise Exception(f"Schema does not exist at {schema_path}")

                with schema_path.open() as f:
                    try:
                        schema_contents = json.load(f)
                    except Exception as e:
                        raise Exception(f"Cannot parse schema for {feature_id}") from e

                try:
                    validator_for(schema_contents).check_schema(schema_contents)
                except Exception as e:
                    raise Exception(f"Invalid schema for {feature_id}") from e

                uri = urlparse(feature["schema"]["uri"])
                if uri.scheme not in ("resource", "chrome"):
                    raise Exception(
                        "Only resource:// and chrome:// URIs are supported for schemas"
                    )

        except Exception as e:
            print("Error while validating FeatureManifest.yaml")
            print(f"On key: {feature_id}")
            print(f"Input file: {manifest_path}")
            raise e


def generate_feature_manifest(fd, input_file):
    write_fm_headers(fd)

    try:
        with open(input_file, encoding="utf-8") as f:
            manifest = yaml.safe_load(f)

        validate_feature_manifest(
            Path(input_file).parent / FEATURE_SCHEMA, input_file, manifest
        )

        fd.write(f"export const FeatureManifest = {json.dumps(manifest)};")
    except OSError as e:
        print(f"{input_file}: error:\n  {e}\n")
        sys.exit(1)


def platform_feature_manifest_array(features):
    entries = []
    for feature, featureData in features.items():
        # Features have to be tagged isEarlyStartup to be accessible
        # to Nimbus platform API
        if not featureData.get("isEarlyStartup", False):
            continue
        entries.extend(
            '{{ "{}_{}"_ns, "{}"_ns }}'.format(
                feature, variable, variableData["fallbackPref"]
            )
            for (variable, variableData) in featureData.get("variables", {}).items()
            if variableData.get("fallbackPref", False)
        )
    return NIMBUS_FALLBACK_PREFS.format(", ".join(entries))


def generate_platform_feature_manifest(fd, input_file):
    write_fm_headers(fd)

    def file_structure(data):
        return "\n".join([
            "#ifndef mozilla_NimbusFeaturesManifest_h",
            "#define mozilla_NimbusFeaturesManifest_h",
            "#include <utility>",
            '#include "mozilla/Maybe.h"',
            '#include "nsStringFwd.h"',
            "namespace mozilla {",
            platform_feature_manifest_array(data),
            '#include "./lib/NimbusFeatureManifest.inc.h"',
            "}  // namespace mozilla",
            "#endif  // mozilla_NimbusFeaturesManifest_h",
        ])

    try:
        with open(input_file, encoding="utf-8") as yaml_input:
            data = yaml.safe_load(yaml_input)
            fd.write(file_structure(data))
    except OSError as e:
        print(f"{input_file}: error:\n  {e}\n")
        sys.exit(1)
