# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2011-2024 Blender Authors
# <pep8 compliant>

import pathlib
import sys

from typing import Optional, Sequence

import worker.blender
import worker.utils


def sign_windows_files(
    service_env_id: str,
    file_paths: Sequence[pathlib.Path],
    description: Optional[str] = None,
    certificate_id: str = "",
) -> None:
    import conf.worker

    worker_config = conf.worker.get_config(service_env_id)

    # TODO: Rotate them if first 1 fails
    worker_config.sign_code_windows_time_servers[0]
    server_url = worker_config.sign_code_windows_server_url
    if not certificate_id:
        certificate_id = worker_config.sign_code_windows_certificate

    dry_run = False
    if service_env_id == "LOCAL" and not certificate_id:
        worker.utils.warning("Performing dry run on LOCAL service environment")
        dry_run = True

    cmd_args = [
        sys.executable,
        "C:\\tools\\codesign.py",
        "--server-url",
        worker.utils.HiddenArgument(server_url),
    ]
    if description:
        cmd_args += ["--description", description]

    cmd: worker.utils.CmdSequence = cmd_args

    # Signing one file at a time causes a stampede on servers, resulting in blocking.
    # Instead sign in chunks of multiple files.
    chunk_size = 25  # Sign how many files at a time
    retry_count = 3

    for i in range(0, len(file_paths), chunk_size):
        file_chunks = file_paths[i : i + chunk_size]
        worker.utils.call(
            list(cmd) + list(file_chunks), retry_count=retry_count, dry_run=dry_run
        )


def sign_windows(service_env_id: str, install_path: pathlib.Path) -> None:
    # TODO: Why use a junction? Is there some failure with long file paths?
    # worker.utils.info("Creating building link")
    # temp_build_root_path = pathlib.Path("C:/BlenderTemp")
    # os.makedirs(temp_build_root_path, exist_ok=True)
    # orig_install_path = install_path
    # install_path = temp_build_root_path / install_path.name

    try:
        # TODO
        # New-Item -type Junction -path install_path -value orig_install_path

        worker.utils.info("Collecting files to process")
        file_paths = list(install_path.glob("*.exe"))
        file_paths += list(install_path.glob("*.dll"))
        file_paths += list(install_path.glob("*.pyd"))
        file_paths = [f for f in file_paths if str(f).find("blender.crt") == -1]
        for f in file_paths:
            print(f)

        sign_windows_files(service_env_id, file_paths)
    finally:
        # worker.utils.info(f"Removing temporary folder {temp_build_root_path}")
        # worker.utils.remove_dir(temp_build_root_path, retry_count=5, retry_wait_time=5.0)

        # TODO: is this really necessary?
        # worker.utils.info("Flushing volume cache...")
        # Write-VolumeCache -DriveLetter C

        # core_shell_retry_command -retry_count 5 -delay_in_milliseconds 1000 -script_block `
        #    worker.utils.info("Junction information...")
        #    junction = Get-Item -Path install_path
        #    worker.utils.info(junction | Format-Table)
        #    worker.utils.info("Attempting to remove...")
        #    junction.Delete()
        #    worker.utils.info("Junction deleted!")
        pass

    worker.utils.info("End of codesign steps")


def sign_darwin_files(
    builder: worker.blender.CodeBuilder,
    file_paths: Sequence[pathlib.Path],
    entitlements_file_name: str,
) -> None:
    entitlements_path = (
        builder.code_path / "release" / "darwin" / entitlements_file_name
    )

    if not entitlements_path.exists():
        raise Exception(f"File {entitlements_path} not found, aborting")

    worker_config = builder.get_worker_config()
    certificate_id = worker_config.sign_code_darwin_certificate

    dry_run = False
    if builder.service_env_id == "LOCAL" and not certificate_id:
        worker.utils.warning("Performing dry run on LOCAL service environment")
        dry_run = True

    keychain_password = worker_config.darwin_keychain_password(builder.service_env_id)
    cmd: worker.utils.CmdSequence = [
        "security",
        "unlock-keychain",
        "-p",
        worker.utils.HiddenArgument(keychain_password),
    ]
    worker.utils.call(cmd, dry_run=dry_run)

    for file_path in file_paths:
        if file_path.is_dir() and file_path.suffix != ".app":
            continue

        # Remove signature
        if file_path.suffix != ".dmg":
            worker.utils.call(
                ["codesign", "--remove-signature", file_path],
                exit_on_error=False,
                dry_run=dry_run,
            )

        # Add signature
        worker.utils.call(
            [
                "codesign",
                "--force",
                "--timestamp",
                "--options",
                "runtime",
                f"--entitlements={entitlements_path}",
                "--sign",
                certificate_id,
                file_path,
            ],
            retry_count=3,
            dry_run=dry_run,
        )
        if file_path.suffix == ".app":
            worker.utils.info(f"Vaildating app bundle {file_path}")
            worker.utils.call(
                ["codesign", "-vvv", "--deep", "--strict", file_path], dry_run=dry_run
            )


def sign_darwin(builder: worker.blender.CodeBuilder) -> None:
    bundle_path = builder.install_dir / "Blender.app"

    # Executables
    sign_path = bundle_path / "Contents" / "MacOS"
    worker.utils.info(f"Collecting files to process in {sign_path}")
    sign_darwin_files(builder, list(sign_path.rglob("*")), "entitlements.plist")

    # Thumbnailer app extension.
    thumbnailer_appex_path = (
        bundle_path / "Contents" / "PlugIns" / "blender-thumbnailer.appex"
    )
    if thumbnailer_appex_path.exists():
        sign_path = thumbnailer_appex_path / "Contents" / "MacOS"
        worker.utils.info(f"Collecting files to process in {sign_path}")
        sign_darwin_files(
            builder, list(sign_path.rglob("*")), "thumbnailer_entitlements.plist"
        )

    # Shared librarys and Python
    sign_path = bundle_path / "Contents" / "Resources"
    worker.utils.info(f"Collecting files to process in {sign_path}")
    file_paths = list(
        set(sign_path.rglob("*.dylib"))
        | set(sign_path.rglob("*.so"))
        | set(sign_path.rglob("python3.*"))
    )
    sign_darwin_files(builder, file_paths, "entitlements.plist")

    # Bundle
    worker.utils.info(f"Signing app bundle {bundle_path}")
    sign_darwin_files(builder, [bundle_path], "entitlements.plist")


def sign(builder: worker.blender.CodeBuilder) -> None:
    builder.setup_build_environment()

    if builder.platform == "windows":
        sign_windows(builder.service_env_id, builder.install_dir)
    elif builder.platform == "darwin":
        sign_darwin(builder)
    else:
        worker.utils.info("No code signing to be done on this platform")