#!/usr/bin/env python3
# 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 os
import sys
import shutil
import re
import tempfile
from optparse import OptionParser
from subprocess import check_call
from subprocess import check_output

nss_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

nssutil_h = "lib/util/nssutil.h"
softkver_h = "lib/softoken/softkver.h"
nss_h = "lib/nss/nss.h"
nssckbi_h = "lib/ckfw/builtins/nssckbi.h"
abi_base_version_file = "automation/abi-check/previous-nss-release"
release_notes_dir = "doc/src/releases"
release_notes_index = "doc/src/releases/index.md"

abi_report_files = [
    "automation/abi-check/expected-report-libfreebl3.so.txt",
    "automation/abi-check/expected-report-libfreeblpriv3.so.txt",
    "automation/abi-check/expected-report-libnspr4.so.txt",
    "automation/abi-check/expected-report-libnss3.so.txt",
    "automation/abi-check/expected-report-libnssckbi.so.txt",
    "automation/abi-check/expected-report-libnssdbm3.so.txt",
    "automation/abi-check/expected-report-libnsssysinit.so.txt",
    "automation/abi-check/expected-report-libnssutil3.so.txt",
    "automation/abi-check/expected-report-libplc4.so.txt",
    "automation/abi-check/expected-report-libplds4.so.txt",
    "automation/abi-check/expected-report-libsmime3.so.txt",
    "automation/abi-check/expected-report-libsoftokn3.so.txt",
    "automation/abi-check/expected-report-libssl3.so.txt",
]


def check_call_noisy(cmd, *args, **kwargs):
    print("Executing command: {}".format(cmd))
    check_call(cmd, *args, **kwargs)


def print_separator():
    print("=" * 70)


def exit_with_failure(what):
    print("failure: {}".format(what))
    sys.exit(2)


def check_files_exist():
    if (
        not os.path.exists(nssutil_h)
        or not os.path.exists(softkver_h)
        or not os.path.exists(nss_h)
        or not os.path.exists(nssckbi_h)
    ):
        exit_with_failure(
            "cannot find expected header files, must run from inside NSS hg directory"
        )


class Replacement:
    def __init__(self, regex="", repl=""):
        self.regex = regex
        self.repl = repl
        self.matcher = re.compile(self.regex)

    def replace(self, line):
        return self.matcher.sub(self.repl, line)


def inplace_replace(replacements=[], filename=""):
    for r in replacements:
        if not isinstance(r, Replacement):
            raise TypeError("Expecting a list of Replacement objects")

    with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp_file:
        with open(filename) as in_file:
            for line in in_file:
                for r in replacements:
                    line = r.replace(line)
                tmp_file.write(line)
        tmp_file.flush()

        shutil.copystat(filename, tmp_file.name)
        shutil.move(tmp_file.name, filename)
        os.utime(filename, None)


def toggle_beta_status(is_beta):
    check_files_exist()
    if is_beta:
        print("adding Beta status to version numbers")
        inplace_replace(
            filename=nssutil_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *NSSUTIL_VERSION *\"[0-9.]+)\" *$",
                    repl=r'\g<1> Beta"',
                ),
                Replacement(
                    regex=r"^(#define *NSSUTIL_BETA *)PR_FALSE *$", repl=r"\g<1>PR_TRUE"
                ),
            ],
        )
        inplace_replace(
            filename=softkver_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *SOFTOKEN_VERSION *\"[0-9.]+\" *SOFTOKEN_ECC_STRING) *$",
                    repl=r'\g<1> " Beta"',
                ),
                Replacement(
                    regex=r"^(#define *SOFTOKEN_BETA *)PR_FALSE *$",
                    repl=r"\g<1>PR_TRUE",
                ),
            ],
        )
        inplace_replace(
            filename=nss_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *NSS_VERSION *\"[0-9.]+\" *_NSS_CUSTOMIZED) *$",
                    repl=r'\g<1> " Beta"',
                ),
                Replacement(
                    regex=r"^(#define *NSS_BETA *)PR_FALSE *$", repl=r"\g<1>PR_TRUE"
                ),
            ],
        )
    else:
        print("removing Beta status from version numbers")
        inplace_replace(
            filename=nssutil_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *NSSUTIL_VERSION *\"[0-9.]+) *Beta\" *$",
                    repl=r'\g<1>"',
                ),
                Replacement(
                    regex=r"^(#define *NSSUTIL_BETA *)PR_TRUE *$", repl=r"\g<1>PR_FALSE"
                ),
            ],
        )
        inplace_replace(
            filename=softkver_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *SOFTOKEN_VERSION *\"[0-9.]+\" *SOFTOKEN_ECC_STRING) *\" *Beta\" *$",
                    repl=r"\g<1>",
                ),
                Replacement(
                    regex=r"^(#define *SOFTOKEN_BETA *)PR_TRUE *$",
                    repl=r"\g<1>PR_FALSE",
                ),
            ],
        )
        inplace_replace(
            filename=nss_h,
            replacements=[
                Replacement(
                    regex=r"^(#define *NSS_VERSION *\"[0-9.]+\" *_NSS_CUSTOMIZED) *\" *Beta\" *$",
                    repl=r"\g<1>",
                ),
                Replacement(
                    regex=r"^(#define *NSS_BETA *)PR_TRUE *$", repl=r"\g<1>PR_FALSE"
                ),
            ],
        )

    print(
        "please run 'hg stat' and 'hg diff' to verify the files have been verified correctly"
    )


def print_beta_versions():
    check_call_noisy(
        ["egrep", "#define *NSSUTIL_VERSION|#define *NSSUTIL_BETA", nssutil_h]
    )
    check_call_noisy(
        ["egrep", "#define *SOFTOKEN_VERSION|#define *SOFTOKEN_BETA", softkver_h]
    )
    check_call_noisy(["egrep", "#define *NSS_VERSION|#define *NSS_BETA", nss_h])


def remove_beta_status():
    print("--- removing beta flags. Existing versions were:")
    print_beta_versions()
    toggle_beta_status(False)
    print("=" * 70)
    print("--- finished modifications, new versions are:")
    print("=" * 70)
    print_beta_versions()


def set_beta_status():
    print("--- adding beta flags. Existing versions were:")
    print_beta_versions()
    toggle_beta_status(True)
    print("--- finished modifications, new versions are:")
    print_beta_versions()


def print_library_versions():
    check_files_exist()
    check_call_noisy(
        [
            "egrep",
            "#define *NSSUTIL_VERSION|#define NSSUTIL_VMAJOR|#define *NSSUTIL_VMINOR|#define *NSSUTIL_VPATCH|#define *NSSUTIL_VBUILD|#define *NSSUTIL_BETA",
            nssutil_h,
        ]
    )
    check_call_noisy(
        [
            "egrep",
            "#define *SOFTOKEN_VERSION|#define SOFTOKEN_VMAJOR|#define *SOFTOKEN_VMINOR|#define *SOFTOKEN_VPATCH|#define *SOFTOKEN_VBUILD|#define *SOFTOKEN_BETA",
            softkver_h,
        ]
    )
    check_call_noisy(
        [
            "egrep",
            "#define *NSS_VERSION|#define NSS_VMAJOR|#define *NSS_VMINOR|#define *NSS_VPATCH|#define *NSS_VBUILD|#define *NSS_BETA",
            nss_h,
        ]
    )


def print_root_ca_version():
    check_files_exist()
    check_call_noisy(["grep", "define *NSS_BUILTINS_LIBRARY_VERSION", nssckbi_h])


def ensure_arguments_count(args, how_many, usage):
    if len(args) != how_many:
        exit_with_failure(
            "incorrect number of arguments, expected parameters are:\n" + usage
        )


def set_major_versions(major):
    for name, file in [
        ["NSSUTIL_VMAJOR", nssutil_h],
        ["SOFTOKEN_VMAJOR", softkver_h],
        ["NSS_VMAJOR", nss_h],
    ]:
        inplace_replace(
            filename=file,
            replacements=[
                Replacement(
                    regex=r"^(#define *{} ?).*$".format(name),
                    repl=r"\g<1>{}".format(major),
                )
            ],
        )


def set_minor_versions(minor):
    for name, file in [
        ["NSSUTIL_VMINOR", nssutil_h],
        ["SOFTOKEN_VMINOR", softkver_h],
        ["NSS_VMINOR", nss_h],
    ]:
        inplace_replace(
            filename=file,
            replacements=[
                Replacement(
                    regex=r"^(#define *{} ?).*$".format(name),
                    repl=r"\g<1>{}".format(minor),
                )
            ],
        )


def set_patch_versions(patch):
    for name, file in [
        ["NSSUTIL_VPATCH", nssutil_h],
        ["SOFTOKEN_VPATCH", softkver_h],
        ["NSS_VPATCH", nss_h],
    ]:
        inplace_replace(
            filename=file,
            replacements=[
                Replacement(
                    regex=r"^(#define *{} ?).*$".format(name),
                    repl=r"\g<1>{}".format(patch),
                )
            ],
        )


def set_build_versions(build):
    for name, file in [
        ["NSSUTIL_VBUILD", nssutil_h],
        ["SOFTOKEN_VBUILD", softkver_h],
        ["NSS_VBUILD", nss_h],
    ]:
        inplace_replace(
            filename=file,
            replacements=[
                Replacement(
                    regex=r"^(#define *{} ?).*$".format(name),
                    repl=r"\g<1>{}".format(build),
                )
            ],
        )


def set_full_lib_versions(version):
    for name, file in [
        ["NSSUTIL_VERSION", nssutil_h],
        ["SOFTOKEN_VERSION", softkver_h],
        ["NSS_VERSION", nss_h],
    ]:
        inplace_replace(
            filename=file,
            replacements=[
                Replacement(
                    regex=r"^(#define *{} *\")([0-9.]+)(.*)$".format(name),
                    repl=r"\g<1>{}\g<3>".format(version),
                )
            ],
        )


def set_root_ca_version(args):
    ensure_arguments_count(args, 2, "major_version  minor_version")
    major = args[0].strip()
    minor = args[1].strip()
    version = major + "." + minor

    inplace_replace(
        filename=nssckbi_h,
        replacements=[
            Replacement(
                regex=r"^(#define *NSS_BUILTINS_LIBRARY_VERSION *\").*$",
                repl=r'\g<1>{}"'.format(version),
            ),
            Replacement(
                regex=r"^(#define *NSS_BUILTINS_LIBRARY_VERSION_MAJOR ?).*$",
                repl=r"\g<1>{}".format(major),
            ),
            Replacement(
                regex=r"^(#define *NSS_BUILTINS_LIBRARY_VERSION_MINOR ?).*$",
                repl=r"\g<1>{}".format(minor),
            ),
        ],
    )


def get_current_versions():
    """Return the (major, minor) version numbers of the checked out tree."""
    grep_major = check_output(["grep", "define.*NSS_VMAJOR", nss_h])
    grep_minor = check_output(["grep", "define.*NSS_VMINOR", nss_h])
    return int(grep_major.split()[2]), int(grep_minor.split()[2])


def set_all_lib_versions(version, major, minor, patch, build):
    old_major, old_minor = get_current_versions()

    new_major = int(major)
    new_minor = int(minor)

    if old_major < new_major or (old_major == new_major and old_minor < new_minor):
        print("You're increasing the minor (or major) version:")
        print("- erasing ABI comparison expectations")
        new_branch = "NSS_" + str(old_major) + "_" + str(old_minor) + "_BRANCH"
        print(
            "- setting reference branch to the branch of the previous version: "
            + new_branch
        )
        with open(abi_base_version_file, "w") as abi_base:
            abi_base.write("%s\n" % new_branch)
        for report_file in abi_report_files:
            with open(report_file, "w") as report_file_handle:
                report_file_handle.truncate()

    set_full_lib_versions(version)
    set_major_versions(major)
    set_minor_versions(minor)
    set_patch_versions(patch)
    set_build_versions(build)


def set_version_to_minor_release(args):
    ensure_arguments_count(args, 2, "major_version  minor_version")
    major = args[0].strip()
    minor = args[1].strip()
    version = major + "." + minor
    patch = "0"
    build = "0"
    set_all_lib_versions(version, major, minor, patch, build)


def set_version_to_patch_release(args):
    ensure_arguments_count(args, 3, "major_version  minor_version  patch_release")
    major = args[0].strip()
    minor = args[1].strip()
    patch = args[2].strip()
    version = major + "." + minor + "." + patch
    build = "0"
    set_all_lib_versions(version, major, minor, patch, build)


def set_release_candidate_number(args):
    ensure_arguments_count(args, 1, "release_candidate_number")
    build = args[0].strip()
    set_build_versions(build)


def set_4_digit_release_number(args):
    ensure_arguments_count(
        args, 4, "major_version  minor_version  patch_release  4th_digit_release_number"
    )
    major = args[0].strip()
    minor = args[1].strip()
    patch = args[2].strip()
    build = args[3].strip()
    version = major + "." + minor + "." + patch + "." + build
    set_all_lib_versions(version, major, minor, patch, build)


def make_release_branch(args):
    ensure_arguments_count(args, 2, "version_string remote")
    version_string = args[0].strip()
    remote = args[1].strip()

    major, minor, patch = parse_version_string(version_string)
    if patch is not None:
        exit_with_failure(
            "make_release_branch expects a minor version (e.g., '3.117'), not a patch version."
        )

    version = f"{major}.{minor}"
    branch_name = f"NSS_{major}_{minor}_BRANCH"
    tag_name = f"NSS_{major}_{minor}_BETA1"

    print_separator()
    print("MAKE RELEASE BRANCH")
    print_separator()
    print(f"Version: {version}")
    print(f"Remote: {remote}")
    print_separator()

    response = input("Are these parameters correct? [yN]: ")
    if "y" not in response.lower():
        print("Aborted.")
        sys.exit(0)
    print_separator()

    # Step 1: Update local repo
    print("Step 1: Updating local repository...")
    check_call_noisy(["hg", "pull"])
    check_call_noisy(["hg", "checkout", "default"])
    print_separator()

    print("Step 2: Checking working directory is clean")
    hg_status = check_output(["hg", "status"]).decode("utf-8").strip()
    if hg_status:
        print()
        print("ERROR: Working directory is not clean")
        print(hg_status)
        print()
        exit_with_failure(
            "Please commit or revert changes then run this command again. You can reset your working directory with 'hg update -C' and 'hg purge if you want to discard all local changes."
        )

    branches = check_output(["hg", "branches"]).decode("utf-8").strip()
    if branch_name in branches:
        exit_with_failure(f"Branch {branch_name} already exists.")
    print_separator()

    # Step 2: Verify version numbers are correct
    print("Step 2: Verifying version numbers are correct...")
    set_version_to_minor_release([major, minor])
    print("=" * 70)
    set_beta_status()
    print("=" * 70)
    # Check if there are any uncommitted changes
    hg_status = check_output(["hg", "status"]).decode("utf-8").strip()
    if hg_status:
        print()
        print("ERROR: Version numbers are not correctly set")
        print()
        print()
        exit_with_failure(
            "Please check the correct version to freeze, or update the version numbers then run this command again."
        )

    print("Version numbers verified - no changes needed.")
    print_separator()

    # Step 3: Create branch
    print(f"Step 3: Creating branch {branch_name}...")
    check_call_noisy(["hg", "branch", branch_name])
    print_separator()

    # Step 4: Create tag
    print(f"Step 4: Creating tag {tag_name}...")
    check_call_noisy(["hg", "tag", tag_name])
    print_separator()

    # Step 5: Show outgoing changes
    response = input("Display outgoing changes? [yN]: ")
    if "y" in response.lower():
        print()
        check_call_noisy(["hg", "outgoing", "-p", remote])
    print_separator()

    # Step 6: Prompt user and push if confirmed
    response = input("Push this branch and tag to the NSS repository? [yN]: ")
    if "y" in response.lower():
        print("Pushing branch and tag...")
        check_call_noisy(["hg", "push", "--new-branch", remote])
        print_separator()
        print("SUCCESS: Branch and tag have been pushed!")
        print_separator()
        print()
        print("NEXT STEPS:")
        print(
            f"1. Wait for the changes to sync to Github: https://github.com/mozilla/nss/tree/{branch_name}"
        )
        print("2. In your mozilla-unified repository, run:")
        print(f"   ./mach nss-uplift {tag_name}")
        print()
    else:
        print("Branch and tag have NOT been pushed to the repository.")
        print("The local branch and tag remain in your working directory.")
        print_separator()


def parse_version_string(version_string):
    """Parse a version string like '3.117' or '3.117.1' and return (major, minor, patch)

    For versions like '3.117', patch will be None.
    Returns: tuple of (major, minor, patch) where patch can be None
    """
    parts = version_string.split(".")
    if len(parts) < 2:
        exit_with_failure(
            f"Invalid version string '{version_string}'. Expected format: 'major.minor' or 'major.minor.patch'"
        )

    major = parts[0].strip()
    minor = parts[1].strip()
    patch = parts[2].strip() if len(parts) >= 3 else None

    # Validate that they're numbers
    try:
        int(major)
        int(minor)
        if patch is not None:
            int(patch)
    except ValueError:
        exit_with_failure(
            f"Invalid version string '{version_string}'. Version components must be numbers."
        )

    return major, minor, patch


def version_string_to_RTM_tag(version_string):
    parts = version_string.split(".")
    return "NSS_" + "_".join(parts) + "_RTM"


def version_string_to_underscore(version_string):
    return version_string.replace(".", "_")


def generate_release_note(args):
    ensure_arguments_count(
        args,
        3,
        "this_release_version_string revision_or_tag previous_release_version_string ",
    )

    version = args[0].strip()
    this_tag = args[1].strip()  # Typically going to be .
    version_underscore = version_string_to_underscore(version)
    prev_tag = version_string_to_RTM_tag(args[2].strip())

    # Get the NSPR version
    nspr_version = (
        check_output(
            ["hg", "cat", "-r", this_tag, "automation/release/nspr-version.txt"]
        )
        .decode("utf-8")
        .split("\n")[0]
        .strip()
    )

    # Get the current date
    from datetime import datetime

    current_date = datetime.now().strftime("%-d %B %Y")

    # Get the list of bugs from hg log
    # Get log entries between previous tag and current HEAD
    command = [
        "hg",
        "log",
        "-r",
        f"{prev_tag}:{this_tag}",
        "--template",
        "{desc|firstline}\\n",
    ]
    log_output = check_output(command).decode("utf-8")

    # Extract bug numbers and descriptions
    bug_lines = []
    for line in reversed(log_output.split("\n")):
        if "Bug" in line or "bug" in line:
            line = line.strip()
            line = line.split("r=")[0].strip()

            # Match patterns like "Bug 1234567 Something" and convert to "Bug 1234567 - Something"
            line = re.sub(
                r"(Bug\s+\d+)\s+([^-])", r"\1 - \2", line, flags=re.IGNORECASE
            )

            # Add a full stop at the end if there isn't one
            if line:
                line = line.rstrip(",")

            if line and not line.endswith("."):
                line = line + "."

            if line and line not in bug_lines:
                bug_lines.append(line)

    changes_text = "\n".join([f"   - {line}" for line in bug_lines])

    # Create the release notes content
    rst_content = f""".. _mozilla_projects_nss_nss_{version_underscore}_release_notes:

NSS {version} release notes
===============================

`Introduction <#introduction>`__
--------------------------------

.. container::

   Network Security Services (NSS) {version} was released on *{current_date}**.

`Distribution Information <#distribution_information>`__
--------------------------------------------------------

.. container::

   The HG tag is NSS_{version_underscore}_RTM. NSS {version} requires NSPR {nspr_version} or newer.

   NSS {version} source distributions are available on ftp.mozilla.org for secure HTTPS download:

   -  Source tarballs:
      https://ftp.mozilla.org/pub/mozilla.org/security/nss/releases/NSS_{version_underscore}_RTM/src/

   Other releases are available :ref:`mozilla_projects_nss_releases`.

.. _changes_in_nss_{version}:

`Changes in NSS {version} <#changes_in_nss_{version}>`__
------------------------------------------------------------------

.. container::

{changes_text}

"""
    return rst_content


latest_note_regex = re.compile(r"^\*\*NSS ([0-9.]+)\*\* is the latest version of NSS\.")
esr_note_regex = re.compile(
    r"^\*\*NSS ([0-9.]+) \(ESR\)\*\* is the latest ESR version of NSS\."
)


def read_release_notes_index_versions():
    """Return the (latest, esr) versions recorded in the release notes index.

    Either element is None if the corresponding note could not be found.
    """
    latest_version = None
    esr_version = None
    if not os.path.exists(release_notes_index):
        return latest_version, esr_version

    with open(release_notes_index) as index_file:
        for line in index_file:
            latest_match = latest_note_regex.match(line)
            if latest_match:
                latest_version = latest_match.group(1)
            esr_match = esr_note_regex.match(line)
            if esr_match:
                esr_version = esr_match.group(1)

    return latest_version, esr_version


def generate_release_notes_index(args, is_esr=False):
    """Rewrite doc/src/releases/index.md.

    With two arguments both the latest release and the ESR version in the note
    at the bottom of the index are set explicitly. With one argument only one
    of them is updated -- the ESR version if is_esr is set, the latest release
    version otherwise -- and the other is carried over from the existing index.
    """
    if len(args) == 2:
        latest_version = args[0].strip()  # e.g. 3.116
        esr_version = args[1].strip()  # e.g. 3.112.1
    elif len(args) == 1:
        version = args[0].strip()
        latest_version, esr_version = read_release_notes_index_versions()
        if is_esr:
            esr_version = version
        else:
            latest_version = version
        if latest_version is None or esr_version is None:
            exit_with_failure(
                "cannot read the current versions from {}, please pass both of "
                "them explicitly:\n"
                "latest_release_version  latest_esr_version".format(release_notes_index)
            )
    else:
        exit_with_failure(
            "incorrect number of arguments, expected parameters are:\n"
            "release_version [latest_esr_version]"
        )

    print(f"Latest release version: {latest_version}")
    print(f"Latest ESR version: {esr_version}")

    # The release notes define their anchor with dashes, so the references to
    # them have to use dashes too.
    latest_dash = latest_version.replace(".", "-")
    esr_dash = esr_version.replace(".", "-")

    # Read all release note files from doc/src/releases/
    release_dir = release_notes_dir
    if not os.path.exists(release_dir):
        exit_with_failure(f"Release notes directory not found: {release_dir}")

    # Get all nss_*.md files (excluding index.md)
    release_files = []
    for filename in os.listdir(release_dir):
        if (
            filename.startswith("nss_")
            and filename.endswith(".md")
            and filename != "index.md"
        ):
            release_files.append(filename)

    # Sort release files in reverse order (newest first)
    # Extract version numbers for proper sorting
    def version_key(filename):
        # Extract version parts from filename like nss_3_116.md
        parts = filename.replace("nss_", "").replace(".md", "").split("_")
        # Convert to integers for proper numerical sorting
        return [int(p) for p in parts]

    release_files.sort(key=version_key, reverse=True)

    # Build the toctree content
    toctree_lines = "\n".join([f"   {f}" for f in release_files])

    # Create the index.md content (MyST Markdown)
    index_content = f"""(mozilla-projects-nss-releases)=

# Release Notes

```{{eval-rst}}
.. toctree::
   :maxdepth: 0
   :glob:
   :hidden:

{toctree_lines}
```

:::{{note}}
**NSS {latest_version}** is the latest version of NSS.
Complete release notes are available here: {{ref}}`mozilla-projects-nss-nss-{latest_dash}-release-notes`

**NSS {esr_version} (ESR)** is the latest ESR version of NSS.
Complete release notes are available here: {{ref}}`mozilla-projects-nss-nss-{esr_dash}-release-notes`
:::
"""

    with open(release_notes_index, "w") as f:
        f.write(index_content)

    print(f"Generated {release_notes_index}")
    print()
    print("=" * 70)
    print("Content:")
    print("=" * 70)
    print(index_content)


def bump_version_on_default(released_major, released_minor):
    """Move the default branch past the version that was just released."""
    major, minor = get_current_versions()
    if (major, minor) > (released_major, released_minor):
        print(f"Default branch is at {major}.{minor}, no bump needed.")
        return

    next_major = released_major
    next_minor = released_minor + 1
    print(
        f"Default branch is at {major}.{minor}, bumping it to "
        f"{next_major}.{next_minor} Beta..."
    )
    set_version_to_minor_release([str(next_major), str(next_minor)])
    set_beta_status()

    hg_status = check_output(["hg", "status"]).decode("utf-8").strip()
    if not hg_status:
        print("Version numbers are unchanged, nothing to commit.")
        return

    check_call_noisy(
        ["hg", "commit", "-m", f"Set version numbers to {next_major}.{next_minor} Beta"]
    )


def release_nss(args):
    ensure_arguments_count(args, 2, "version_string  remote")
    version_string = args[0].strip()
    remote = args[1].strip()

    major, minor, patch = parse_version_string(version_string)

    # Build version string and related names
    version = version_string
    version_underscore = version_string_to_underscore(version_string)
    branch_name = f"NSS_{major}_{minor}_BRANCH"
    rtm_tag = f"NSS_{version_underscore}_RTM"
    release_note_file = f"doc/src/releases/nss_{version_underscore}.md"

    print_separator()
    print("RELEASE NSS")
    print_separator()
    is_esr = "y" in input("Is this an ESR release? [yN]: ").lower()
    print(f"Release version: {version}")
    print(f"ESR release: {'yes' if is_esr else 'no'}")
    print(f"Remote: {remote}")
    print_separator()

    response = input("Are these parameters correct? [yN]: ")
    if "y" not in response.lower():
        print("Aborted.")
        sys.exit(0)
    print_separator()

    print("=" * 70)
    print(f"Starting NSS {version} release process")
    print("=" * 70)
    print()

    # Step 1: Update local repo
    print("Step 1: Updating local repository...")
    check_call_noisy(["hg", "pull"])
    print_separator()

    # Step 2: Checking working directory is clean
    print("Step 2: Checking working directory is clean...")
    hg_status = check_output(["hg", "status"]).decode("utf-8").strip()
    if hg_status:
        print()
        print("ERROR: Working directory is not clean")
        print(hg_status)
        print()
        exit_with_failure(
            "Please commit or revert changes then run this command again. You can reset your working directory with 'hg update -C' and 'hg purge if you want to discard all local changes."
        )
    print_separator()

    # Step 3: Make sure we're on the appropriate branch
    print(f"Step 3: Checking out branch {branch_name}...")
    try:
        check_call_noisy(["hg", "checkout", branch_name])
    except Exception as e:
        exit_with_failure(f"Failed to checkout branch {branch_name}. Does it exist?")
    print_separator()

    # Step 4: Check for any existing commits or tags
    print("Step 4: Checking for existing release commits or tags...")

    # Check if RTM tag already exists
    tags_output = check_output(["hg", "tags"]).decode("utf-8")
    if rtm_tag in tags_output:
        exit_with_failure(
            f"Tag {rtm_tag} already exists. Has this release already been made?"
        )

    # Check for recent commits with the same commit messages we're about to make
    version_commit_message = f"Set version numbers to {version} final"
    release_notes_commit_message = f"Release notes for NSS {version}"

    recent_log = check_output(
        ["hg", "log", "-l", "5", "--template", "{desc|firstline}\\n"]
    ).decode("utf-8")

    if version_commit_message in recent_log:
        exit_with_failure(
            f"Found recent commit with message '{version_commit_message}'. Has this release already been started?"
        )

    if release_notes_commit_message in recent_log:
        exit_with_failure(
            f"Found recent commit with message '{release_notes_commit_message}'. Has this release already been started?"
        )

    print("No existing release commits or tags found.")
    print_separator()

    # Step 5: Update the NSS version numbers (remove beta)
    print("Step 5: Removing beta status from version numbers...")
    if patch:
        set_version_to_patch_release([major, minor, patch])
    else:
        set_version_to_minor_release([major, minor])
    remove_beta_status()

    print_separator()

    # Step 6: Commit the change
    print("Step 6: Committing version number changes...")
    check_call_noisy(["hg", "commit", "-m", version_commit_message])
    print_separator()

    # Step 7: Generate release note
    print("Step 7: Generating release notes...")
    generate_doc_script = os.path.join(
        os.path.dirname(__file__), "generate_release_doc.py"
    )
    check_call_noisy([sys.executable, generate_doc_script, version])
    print_separator()

    # Step 8: Generate new release note index
    print("Step 8: Generating release notes index...")
    generate_release_notes_index([version], is_esr=is_esr)
    print_separator()

    # Step 9: Check the documentation still builds cleanly
    print("Step 9: Running doc-lint...")
    check_call_noisy([os.path.join(nss_root, "mach"), "doc-lint"])
    print_separator()

    # Step 10: Commit the release notes
    print("Step 10: Committing release notes...")
    check_call_noisy(["hg", "add", release_note_file])
    check_call_noisy(["hg", "commit", "-m", release_notes_commit_message])

    # Get the commit hash
    docs_commit = (
        check_output(["hg", "log", "-r", ".", "--template", "{node|short}"])
        .decode("utf-8")
        .strip()
    )
    print(f"Release notes committed. Commit hash: {docs_commit}")
    print_separator()

    # Step 11: Tag the release version
    print(f"Step 11: Tagging release version {rtm_tag}...")
    check_call_noisy(["hg", "tag", rtm_tag])
    print_separator()

    # Step 12: Switch to default branch and graft the release notes
    print("Step 12: Switching to default branch and grafting release notes...")
    check_call_noisy(["hg", "checkout", "default"])
    check_call_noisy(["hg", "graft", "-r", docs_commit])
    print_separator()

    # Step 13: Make sure the default branch is ahead of the release
    print("Step 13: Checking the version numbers on the default branch...")
    bump_version_on_default(int(major), int(minor))
    print_separator()

    # Step 14: Update cf_status_nss on Bugzilla
    print("Step 14: Updating cf_status_nss on Bugzilla...")
    cf_status_script = os.path.join(
        os.path.dirname(__file__), "bugzilla_cf_status_nss.py"
    )
    check_call_noisy([sys.executable, cf_status_script, version, "--fix"])
    input(
        "Review the cf_status_nss report above; any skipped bugs need to be "
        "handled manually. Press Enter to continue."
    )
    print_separator()

    response = input("Display the outgoing changes? [yN]: ")
    if "y" in response.lower():
        check_call_noisy(
            ["hg", "outgoing", "--graph", "-b", "default", "-b", branch_name, remote]
        )
    print_separator()

    # Step 15: Push changes
    response = input("Push these changes to the NSS repository? [yN]: ")
    if "y" in response.lower():
        print("Pushing changes to default branch...")
        check_call_noisy(["hg", "push", "-b", "default", remote])
        print(f"Pushing changes to {branch_name} branch...")
        check_call_noisy(["hg", "push", "-b", branch_name, remote])
        print_separator()
        print("SUCCESS: NSS release process completed!")
        print_separator()
        print()
        print("NEXT STEPS:")
        print(f"1. Wait for the changes to sync to Github")
        print("2. In your mozilla-unified repository, run:")
        print(f"   ./mach nss-uplift {rtm_tag}")
        print()
    else:
        print("Changes have NOT been pushed to the repository.")
        print("The local commits remain in your working directory.")
        print_separator()


def create_nss_release_archive(args):
    ensure_arguments_count(args, 2, "nss_release_version  path_to_stage_directory")
    nssrel = args[0].strip()  # e.g. 3.19.3
    stagedir = args[1].strip()  # e.g. ../stage

    # Determine which tar command to use (prefer gtar if available)
    tar_cmd = "gtar"
    try:
        check_call(
            ["which", "gtar"],
            stdout=open(os.devnull, "w"),
            stderr=open(os.devnull, "w"),
        )
    except:
        tar_cmd = "tar"

    # Generate the release tag from the version
    nssreltag = version_string_to_RTM_tag(nssrel)

    print_separator()
    print("CREATE NSS RELEASE ARCHIVE")
    print_separator()
    print(f"NSS release version: {nssrel}")
    print(f"Stage directory: {stagedir}")
    print_separator()

    response = input("Are these parameters correct? [yN]: ")
    if "y" not in response.lower():
        print("Aborted.")
        sys.exit(0)
    print_separator()

    with open("automation/release/nspr-version.txt") as nspr_version_file:
        nsprrel = next(nspr_version_file).strip()

    nspr_tar = "nspr-" + nsprrel + ".tar.gz"
    nspr_dir = stagedir + "/v" + nsprrel + "/src/"
    nsprtar_with_path = nspr_dir + nspr_tar

    nspr_releases_url = "https://ftp.mozilla.org/pub/nspr/releases"
    if not os.path.exists(nsprtar_with_path):
        os.makedirs(nspr_dir, exist_ok=True)
        check_call_noisy(
            [
                "wget",
                f"{nspr_releases_url}/v{nsprrel}/src/nspr-{nsprrel}.tar.gz",
                f"--output-document={nsprtar_with_path}",
            ]
        )

    if not os.path.exists(nsprtar_with_path):
        exit_with_failure(
            "cannot find nspr archive at expected location " + nsprtar_with_path
        )

    nss_stagedir = stagedir + "/" + nssreltag + "/src"
    if os.path.exists(nss_stagedir):
        exit_with_failure("nss stage directory already exists: " + nss_stagedir)

    nss_tar = "nss-" + nssrel + ".tar.gz"

    check_call_noisy(["mkdir", "-p", nss_stagedir])
    check_call_noisy(
        [
            "hg",
            "archive",
            "-r",
            nssreltag,
            "--prefix=nss-" + nssrel + "/nss",
            stagedir + "/" + nssreltag + "/src/" + nss_tar,
            "-X",
            ".hgtags",
        ]
    )
    check_call_noisy([tar_cmd, "-xz", "-C", nss_stagedir, "-f", nsprtar_with_path])
    print("changing to directory " + nss_stagedir)
    os.chdir(nss_stagedir)
    check_call_noisy([tar_cmd, "-xz", "-f", nss_tar])
    check_call_noisy(["mv", "-i", "nspr-" + nsprrel + "/nspr", "nss-" + nssrel + "/"])
    check_call_noisy(["rmdir", "nspr-" + nsprrel])

    nss_nspr_tar = "nss-" + nssrel + "-with-nspr-" + nsprrel + ".tar.gz"

    check_call_noisy([tar_cmd, "-cz", "-f", nss_nspr_tar, "nss-" + nssrel])
    check_call_noisy(["rm", "-rf", "nss-" + nssrel])
    check_call("sha1sum " + nss_tar + " " + nss_nspr_tar + " > SHA1SUMS", shell=True)
    check_call(
        "sha256sum " + nss_tar + " " + nss_nspr_tar + " > SHA256SUMS", shell=True
    )
    print("created directory " + nss_stagedir + " with files:")
    check_call_noisy(["ls", "-l"])

    if "y" not in input("Upload release tarball?[yN]"):
        print("Release tarballs have NOT been uploaded")
        exit(0)
    os.chdir("../..")
    gcp_proj = "moz-fx-productdelivery-pr-38b5"
    check_call_noisy(["gcloud", "auth", "login"])
    check_call_noisy(
        [
            "gcloud",
            "--project",
            gcp_proj,
            f"--impersonate-service-account=nss-team-prod@{gcp_proj}.iam.gserviceaccount.com",
            "storage",
            "cp",
            "--recursive",
            "--no-clobber",
            nssreltag,
            f"gs://{gcp_proj}-productdelivery/pub/security/nss/releases/",
        ]
    )
    print_separator()
    print(
        f"Release tarballs have been uploaded to Google Cloud Storage. You can find them at https://ftp.mozilla.org/pub/security/nss/releases/{nssreltag}/"
    )
    print_separator()


o = OptionParser(
    usage="client.py [options] "
    + " | ".join(
        [
            "remove_beta",
            "set_beta",
            "print_library_versions",
            "print_root_ca_version",
            "set_root_ca_version",
            "set_version_to_minor_release",
            "set_version_to_patch_release",
            "set_release_candidate_number",
            "set_4_digit_release_number",
            "make_release_branch",
            "release_nss",
            "create_nss_release_archive",
            "generate_release_note",
            "generate_release_notes_index",
        ]
    )
)

try:
    options, args = o.parse_args()
    action = args[0]
    action_args = args[1:]  # Get all arguments after the action
except IndexError:
    o.print_help()
    sys.exit(2)

if action in ("remove_beta"):
    remove_beta_status()

elif action in ("set_beta"):
    set_beta_status()

elif action in ("print_library_versions"):
    print_library_versions()

elif action in ("print_root_ca_version"):
    print_root_ca_version()

elif action in ("set_root_ca_version"):
    set_root_ca_version(action_args)

# x.y version number - 2 parameters
elif action in ("set_version_to_minor_release"):
    set_version_to_minor_release(action_args)

# x.y.z version number - 3 parameters
elif action in ("set_version_to_patch_release"):
    set_version_to_patch_release(action_args)

# change the release candidate number, usually increased by one,
# usually if previous release candiate had a bug
# 1 parameter
elif action in ("set_release_candidate_number"):
    set_release_candidate_number(action_args)

# use the build/release candiate number in the identifying version number
# 4 parameters
elif action in ("set_4_digit_release_number"):
    set_4_digit_release_number(action_args)

# create a freeze branch and beta tag for a new release
# 2 parameters
elif action in ("make_release_branch"):
    make_release_branch(action_args)

elif action in ("create_nss_release_archive"):
    create_nss_release_archive(action_args)

elif action in ("generate_release_note"):
    print(generate_release_note(action_args))

elif action in ("generate_release_notes_index"):
    generate_release_notes_index(action_args)


elif action in ("release_nss"):
    release_nss(action_args)

else:
    o.print_help()
    sys.exit(2)

sys.exit(0)
