#!/usr/bin/env python

# See docs/CI/Release_Automation.md for more details
# Run this from the .signing directory with all the keys and properties files in it.

# python -m venv venv; source venv/bin/activate; pip install requests pynacl

import os
import json
import base64
import argparse
import requests
import nacl.encoding
import nacl.public
import textwrap

PUBLISH_APPROVERS = ["kewisch", "coreycb", "wmontwe"]

CHANNEL_ENVIRONMENTS = {
    "thunderbird_release": {
        "branch": "release",
        "variables": {
            "RELEASE_TYPE": "release",
            "MATRIX_INCLUDE": [
                {
                    "appName": "thunderbird",
                    "releaseTarget": "ftp|github",
                    "packageFormat": "apk",
                    "packageFlavor": "foss",
                },
                {
                    "appName": "thunderbird",
                    "releaseTarget": "play",
                    "playTargetTrack": "internal",
                    "packageFormat": "aab",
                    "packageFlavor": "full",
                },
                {
                    "appName": "k9mail",
                    "releaseTarget": "ftp|github",
                    "packageFormat": "apk",
                    "packageFlavor": "foss",
                },
                {
                    "appName": "k9mail",
                    "releaseTarget": "play",
                    "playTargetTrack": "internal",
                    "packageFormat": "apk",
                    "packageFlavor": "full",
                },
            ],
        },
    },
    "thunderbird_beta": {
        "branch": "beta",
        "variables": {
            "RELEASE_TYPE": "beta",
            "MATRIX_INCLUDE": [
                {
                    "appName": "thunderbird",
                    "releaseTarget": "ftp|github",
                    "packageFormat": "apk",
                    "packageFlavor": "foss",
                },
                {
                    "appName": "thunderbird",
                    "releaseTarget": "play",
                    "playTargetTrack": "internal",
                    "packageFormat": "aab",
                    "packageFlavor": "full",
                },
            ],
        },
    },
    "thunderbird_daily": {
        "branch": "main",
        "variables": {
            "RELEASE_TYPE": "daily",
            "MATRIX_INCLUDE": [
                {
                    "appName": "thunderbird",
                    "releaseTarget": "ftp",
                    "packageFormat": "apk",
                    "packageFlavor": "foss",
                },
                {
                    "appName": "thunderbird",
                    "releaseTarget": "play",
                    "packageFormat": "aab",
                    "playTargetTrack": "internal",
                    "packageFlavor": "full",
                },
            ],
        },
    },
}


SIGNING_ENVIRONMENTS = {
    "k9mail_release_foss": {
        "props": "k9.release.signing.properties",
        "branch": "release",
    },
    "k9mail_release_full": {
        "props": "k9.release.signing.properties",
        "branch": "release",
    },
    "k9mail_beta_foss": {
        "props": "k9.release.signing.properties",
        "branch": "beta",
    },
    "k9mail_beta_full": {
        "props": "k9.release.signing.properties",
        "branch": "beta",
    },
    "thunderbird_daily_foss": {
        "props": "tb.daily.signing.properties",
        "branch": "main",
    },
    "thunderbird_daily_full": {
        "props": "tb.daily.upload.properties",
        "branch": "main",
    },
    "thunderbird_beta_foss": {
        "props": "tb.beta.signing.properties",
        "branch": "beta",
    },
    "thunderbird_beta_full": {
        "props": "tb.beta.upload.properties",
        "branch": "beta",
    },
    "thunderbird_release_foss": {
        "props": "tb.release.signing.properties",
        "branch": "release",
    },
    "thunderbird_release_full": {
        "props": "tb.release.upload.properties",
        "branch": "release",
    },
}


# Function to read the key properties file
def read_key_properties(file_path):
    key_properties = {}
    with open(file_path, "r") as file:
        for line in file:
            if "=" in line:
                key, value = line.strip().split("=", 1)
                final_key = key.split(".")[-1]
                key_properties[final_key] = value
    return key_properties


# Function to base64 encode the .jks file
def encode_jks_file(jks_file_path):
    with open(jks_file_path, "rb") as file:
        encoded_key = base64.b64encode(file.read()).decode("utf-8")
    return encoded_key


# Function to get the public key from GitHub for encryption
def get_github_public_key(repo, environment_name):
    url = f"https://api.github.com/repos/{repo}/environments/{environment_name}/secrets/public-key"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        data = response.json()
        return [data["key_id"], data["key"]]
    else:
        raise Exception(
            f"Failed to fetch public key from GitHub. Response: {response.status_code}, {response.text}"
        )


# Function to encrypt a secret using the GitHub public key
def encrypt_secret(public_key: str, secret_value: str):
    public_key_bytes = base64.b64decode(public_key)
    sealed_box = nacl.public.SealedBox(nacl.public.PublicKey(public_key_bytes))
    encrypted_secret = sealed_box.encrypt(secret_value.encode("utf-8"))
    return base64.b64encode(encrypted_secret).decode("utf-8")


# Function to set encrypted secret in GitHub environment
def set_github_environment_secret(
    repo, secret_name, encrypted_value, key_id, environment_name
):
    url = f"https://api.github.com/repos/{repo}/environments/{environment_name}/secrets/{secret_name}"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {"encrypted_value": encrypted_value, "key_id": key_id}
    response = requests.put(url, headers=headers, json=data)
    if response.status_code == 201:
        print(f"\tSecret {secret_name} created successfully in {environment_name}.")
    elif response.status_code == 204:
        print(f"\tSecret {secret_name} updated successfully in {environment_name}.")
    else:
        raise Exception(
            f"Failed to create secret {secret_name} in {environment_name}. Response: {response.status_code}, {response.text}"
        )


def print_github_environment_variable(repo, environment_name):
    url = (
        f"https://api.github.com/repos/{repo}/environments/{environment_name}/variables"
    )
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    response = requests.get(url, headers=headers)
    data = response.json()

    if response.status_code == 200:
        for variable in data["variables"]:
            value = variable["value"]
            if value[0] in "{[":
                try:
                    value = textwrap.indent(
                        json.dumps(json.loads(value), indent=2), "\t\t"
                    ).lstrip()
                except:
                    pass

            print(f"\t{variable['name']}={value}")
    else:
        raise Exception(
            f"Unexpected response getting variables from {environment_name}: {response.status_code} {response.text}"
        )


def set_github_environment_variable(repo, name, value, environment_name):
    url = (
        f"https://api.github.com/repos/{repo}/environments/{environment_name}/variables"
    )
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {"name": name, "value": value}
    response = requests.post(url, headers=headers, json=data)
    if response.status_code == 201:
        print(f"\tVariable {name} created successfully in {environment_name}.")
    elif response.status_code == 409:
        url = f"https://api.github.com/repos/{repo}/environments/{environment_name}/variables/{name}"
        response = requests.patch(url, headers=headers, json=data)
        if response.status_code == 204:
            print(f"\tVariable {name} updated successfully in {environment_name}.")
        else:
            raise Exception(
                f"Failed to update variable {name} in {environment_name}. Response: {response.status_code}, {response.text}"
            )
    else:
        raise Exception(
            f"Failed to create variable {name} in {environment_name}. Response: {response.status_code}, {response.text}"
        )


def print_github_environment(repo, environment_name):
    url = f"https://api.github.com/repos/{repo}/environments/{environment_name}"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        data = response.json()
        print(f"Environment {environment_name}")
        print("\tProtection rules")
        needs_branch_policies = False
        for rule in data["protection_rules"]:
            if rule["type"] == "branch_policy":
                continue

            print(f"\t\tType: {rule['type']}")
            if rule["type"] == "required_reviewers":
                reviewers = ", ".join(
                    map(
                        lambda reviewer: reviewer["reviewer"]["login"],
                        rule["reviewers"],
                    )
                )
                print(f"\t\t\tReviewers: {reviewers}")

        print(f"\t\tBranch policy: {data['deployment_branch_policy']}")
        if (
            data["deployment_branch_policy"]
            and data["deployment_branch_policy"]["custom_branch_policies"]
        ):
            url += "/deployment-branch-policies"
            response = requests.get(url, headers=headers)
            if response.status_code == 200:
                policies = map(
                    lambda policy: policy["name"], response.json()["branch_policies"]
                )
                print("\t\tBranches: " + ", ".join(policies))

    else:
        raise Exception(
            f"Unexpected response getting variables from {environment_name}: {response.status_code} {response.text}"
        )


# Function to create GitHub environment if it doesn't exist
def create_github_environment(repo, environment_name, branches=None, approvers=None):
    url = f"https://api.github.com/repos/{repo}/environments/{environment_name}"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {}
    if branches:
        data["deployment_branch_policy"] = {
            "custom_branch_policies": True,
            "protected_branches": False,
        }

    if approvers:
        reviewers = map(
            lambda approver: {
                "type": "User",
                "id": get_user_id_from_username(approver),
            },
            approvers,
        )
        data["reviewers"] = list(reviewers)

    response = requests.put(url, headers=headers, json=data)
    if response.status_code == 200:
        print(f"Environment {environment_name} created successfully.")
    elif response.status_code == 409:
        print(f"Environment {environment_name} already exists.")
    else:
        raise Exception(
            f"Failed to create environment {environment_name}. Response: {response.status_code}, {response.text}"
        )

    for branch in branches or []:
        url = f"https://api.github.com/repos/{repo}/environments/{environment_name}/deployment-branch-policies"
        data = {"name": branch, "type": "branch"}
        response = requests.post(url, headers=headers, json=data)

        if response.status_code == 200:
            print(
                f"\tBranch protection on {branch} for {environment_name} created successfully."
            )
        elif response.status_code == 409:
            print(
                f"\tBranch protection on {branch} for {environment_name} already exists."
            )
        else:
            raise Exception(
                f"Failed to create branch protection for {branch} on {environment_name}. Response: {response.status_code}, {response.text}"
            )


# Function to get the GitHub user ID from a username
def get_user_id_from_username(username):
    url = f"https://api.github.com/users/{username}"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }

    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        user_data = response.json()
        return user_data["id"]
    else:
        print(
            f"Failed to fetch user ID for username '{username}'. Response: {response.status_code}, {response.text}"
        )
        return None


def create_approver_environment(repo, environment_name, approvers):

    reviewers = map(
        lambda approver: {"type": "User", "id": get_user_id_from_username(approver)},
        approvers,
    )

    url = f"https://api.github.com/repos/{repo}/environments/{environment_name}"
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {"reviewers": list(reviewers)}
    response = requests.put(url, headers=headers, json=data)
    if response.status_code == 200:
        print(f"Environment {environment_name} created successfully.")
    elif response.status_code == 409:
        print(f"Environment {environment_name} already exists.")
    else:
        raise Exception(
            f"Failed to create environment {environment_name}. Response: {response.status_code}, {response.text}"
        )


def create_signing_environment(repo, environment, branch, props_file):
    # Read the key.properties file
    key_props = read_key_properties(props_file)

    KEY_ALIAS = key_props.get("keyAlias")
    KEY_PASSWORD = key_props.get("keyPassword")
    KEY_STORE_PASSWORD = key_props.get("storePassword")

    if not all([KEY_ALIAS, KEY_PASSWORD, KEY_STORE_PASSWORD]):
        print(
            "Missing values in key.properties file. Please ensure all fields are present."
        )
        return

    # Base64 encode the JKS file to create SIGNING_KEY
    SIGNING_KEY = encode_jks_file(key_props.get("storeFile"))

    # Create the environment if it doesn't exist
    create_github_environment(repo, environment, branches=[branch])

    # Fetch the public key from GitHub for the specific environment
    key_id, public_key = get_github_public_key(repo, environment)

    # Encrypt the secrets using the public key
    encrypted_signing_key = encrypt_secret(public_key, SIGNING_KEY)
    encrypted_key_alias = encrypt_secret(public_key, KEY_ALIAS)
    encrypted_key_password = encrypt_secret(public_key, KEY_PASSWORD)
    encrypted_key_store_password = encrypt_secret(public_key, KEY_STORE_PASSWORD)

    # Set the encrypted secrets in the GitHub environment
    secrets_to_set = {
        "SIGNING_KEY": encrypted_signing_key,
        "KEY_ALIAS": encrypted_key_alias,
        "KEY_PASSWORD": encrypted_key_password,
        "KEY_STORE_PASSWORD": encrypted_key_store_password,
    }

    for secret_name, encrypted_value in secrets_to_set.items():
        set_github_environment_secret(
            repo, secret_name, encrypted_value, key_id, environment
        )


def make_bot_environment(repo, environment):
    key_id, public_key = get_github_public_key(repo, environment)

    with open("botmobile.key.pem") as fp:
        encrypted_bot_key = encrypt_secret(public_key, fp.read())
    with open("botmobile.clientid.txt") as fp:
        bot_client_id = fp.read().strip()
    with open("botmobile.userid.txt") as fp:
        bot_user_id = fp.read().strip()


    set_github_environment_secret(
        repo, "BOT_PRIVATE_KEY", encrypted_bot_key, key_id, environment
    )

    set_github_environment_variable(repo, "BOT_CLIENT_ID", bot_client_id, environment)
    set_github_environment_variable(repo, "BOT_USER_ID", bot_user_id, environment)


def create_channel_environment(repo, environment, branch, variables):
    create_github_environment(repo, environment, branches=[branch])

    for name, value in variables.items():
        if isinstance(value, dict) or isinstance(value, list):
            value = json.dumps(value)

        set_github_environment_variable(repo, name, value, environment)


def create_release_environment(repo, branches):
    environment = "publish_release"

    create_github_environment(repo, environment, branches=branches)

    key_id, public_key = get_github_public_key(repo, environment)

    with open("play-store-account.json") as fp:
        encrypted_play_account = encrypt_secret(public_key, fp.read())

    set_github_environment_secret(
        repo, "PLAY_STORE_ACCOUNT", encrypted_play_account, key_id, environment
    )


def create_matrix_environment(repo, branches):
    environment = "notify_matrix"

    create_github_environment(repo, environment, branches=branches)

    key_id, public_key = get_github_public_key(repo, environment)

    with open("matrix-account.json") as fp:
        mxdata = json.load(fp)
        encrypted_token = encrypt_secret(public_key, mxdata["token"])

    set_github_environment_secret(
        repo, "MATRIX_NOTIFY_TOKEN", encrypted_token, key_id, environment
    )

    set_github_environment_variable(
        repo, "MATRIX_NOTIFY_HOMESERVER", mxdata["homeserver"], environment
    )
    set_github_environment_variable(
        repo, "MATRIX_NOTIFY_ROOM", mxdata["room"], environment
    )
    set_github_environment_variable(
        repo, "MATRIX_NOTIFY_USER_MAP", json.dumps(mxdata["userMap"]), environment
    )


def main():
    # Argument parsing for positional inputs and repo flag
    parser = argparse.ArgumentParser(
        description="Set GitHub environment secrets for specific or all environments."
    )
    parser.add_argument(
        "--repo",
        "-r",
        required=True,
        help="GitHub repository in the format 'owner/repo'.",
    )
    parser.add_argument(
        "--print", "-p", action="store_true", help="Print instead of set"
    )
    parser.add_argument(
        "--skip", "-s", action="append", help="Skip this named environment"
    )
    parser.add_argument(
        "--only", "-o", action="append", help="Only include this named environment"
    )

    args = parser.parse_args()

    global GITHUB_TOKEN
    GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
    if not GITHUB_TOKEN:
        raise Exception(
            "GITHUB_TOKEN environment variable is not set. Please set it before running the script."
        )

    if args.skip and args.only:
        print("Error: Cannot supply both skip and only")
        return

    includeset = set(
        list(CHANNEL_ENVIRONMENTS.keys())
        + list(SIGNING_ENVIRONMENTS.keys())
        + ["publish_hold", "publish_release", "notify_matrix", "botmobile"]
    )
    if args.skip:
        for skip in args.skip:
            includeset.remove(skip)

    if args.only:
        includeset = set(args.only)

    # Publish hold environment
    if "publish_hold" in includeset:
        if args.print:
            print_github_environment(args.repo, "publish_hold")
        else:
            create_github_environment(
                args.repo, "publish_hold", approvers=PUBLISH_APPROVERS
            )

    # Channel environments
    for environment_name, data in CHANNEL_ENVIRONMENTS.items():
        if environment_name not in includeset:
            continue

        if args.print:
            print(f"Environment {environment_name}")
            print_github_environment_variable(args.repo, environment_name)
        else:
            create_channel_environment(args.repo, environment_name, **data)
            make_bot_environment(args.repo, environment_name)

    # Signing environments
    for environment_name, data in SIGNING_ENVIRONMENTS.items():
        if environment_name not in includeset:
            continue

        if args.print:
            print_github_environment(args.repo, environment_name)
        else:
            if not os.path.exists(data["props"]):
                print(f"Skipping {environment_name}: Missing key .properties file")
                continue

            create_signing_environment(
                args.repo, environment_name, data["branch"], data["props"]
            )

    # Publish environment
    if "publish_release" in includeset:
        if args.print:
            print_github_environment(args.repo, "publish_release")
        else:
            create_release_environment(args.repo, ["main", "beta", "release"])
            make_bot_environment(args.repo, "publish_release")

    # Botmobile environment
    if "botmobile" in includeset:
        if args.print:
            print_github_environment(args.repo, "botmobile")
        else:
            create_github_environment(args.repo, "botmobile", branches=["main"])
            make_bot_environment(args.repo, "botmobile")

    # Notify
    if "notify_matrix" in includeset:
        if args.print:
            print_github_environment(args.repo, "notify_matrix")
        else:
            create_matrix_environment(args.repo, ["main", "beta", "release"])


if __name__ == "__main__":
    main()
