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

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"

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 set_all_lib_versions(version, major, minor, patch, build):
    grep_major = check_output(["grep", "define.*NSS_VMAJOR", nss_h])
    grep_minor = check_output(["grep", "define.*NSS_VMINOR", nss_h])

    old_major = int(grep_major.split()[2])
    old_minor = int(grep_minor.split()[2])

    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


def generate_release_notes_index(args):
    ensure_arguments_count(args, 2, "latest_release_version  latest_esr_version")
    latest_version = args[0].strip()  # e.g. 3.116
    esr_version = args[1].strip()  # e.g. 3.112.1

    latest_underscore = version_string_to_underscore(latest_version)
    esr_underscore = version_string_to_underscore(esr_version)

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

    # Get all nss_*.rst files (excluding index.rst)
    release_files = []
    for filename in os.listdir(release_dir):
        if (
            filename.startswith("nss_")
            and filename.endswith(".rst")
            and filename != "index.rst"
        ):
            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.rst
        parts = filename.replace("nss_", "").replace(".rst", "").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.rst content
    index_content = f""".. _mozilla_projects_nss_releases:

Release Notes
=============

.. 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_underscore}_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_underscore}_release_notes`

"""

    index_file = os.path.join(release_dir, "index.rst")
    with open(index_file, "w") as f:
        f.write(index_content)

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


def release_nss(args):
    ensure_arguments_count(args, 3, "version_string  esr_version  remote")
    version_string = args[0].strip()
    esr_version = args[1].strip()
    remote = args[2].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/rst/releases/nss_{version_underscore}.rst"

    print_separator()
    print("RELEASE NSS")
    print_separator()
    print(f"Release version: {version}")
    print(f"ESR version: {esr_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()

    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, esr_version])
    print_separator()

    input(
        "Are you making an ESR release? If so, please manually edit doc/rst/releases/index.rst to adjust the ESR / main version note. Press enter when done."
    )

    # Step 9: Commit the release notes
    print("Step 9: 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 10: Tag the release version
    print(f"Step 10: Tagging release version {rtm_tag}...")
    check_call_noisy(["hg", "tag", rtm_tag])
    print_separator()

    # Step 11: Switch to default branch and graft the release notes
    print("Step 11: Switching to default branch and grafting release notes...")
    check_call_noisy(["hg", "checkout", "default"])
    check_call_noisy(["hg", "graft", "-r", docs_commit])
    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 12: Check cf_status_nss on Bugzilla
    print("Step 12: Checking 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])
    input("Review the cf_status_nss report above. Press Enter to continue.")
    print_separator()

    # Step 13: 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",
            "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)
