"""Retain only release binaries, symbols and bounded non-secret build metadata."""
from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import sys

SCHEMA = "gowalk-cicd/mobile-binary.v1"
IOS_ENV = ("CFG_BUNDLE_ID", "CFG_TEAM_ID", "CFG_APP_STORE_APPLE_ID", "CFG_CONFIGURATION",
           "CFG_PROJECT", "CFG_WORKSPACE", "CFG_SCHEME", "CFG_LOCALE", "CFG_USES_NON_EXEMPT",
           "CFG_WHATS_NEW", "MARKETING_VERSION", "BUILD_NUMBER", "SOURCE_MARKETING_VERSION")
ANDROID_ENV = ("ANDROID_PACKAGE_NAME", "ANDROID_BUILD_NUMBER", "ANDROID_PROJECT_KIND")


def digest(path: Path) -> str:
    with path.open("rb") as handle:
        return hashlib.file_digest(handle, "sha256").hexdigest()


def android_config():
    root = Path(__file__).resolve().parents[2]
    folder = root / "android-app/scripts"
    sys.path.insert(0, str(folder if folder.is_dir() else root / "android-action/scripts"))
    import android_config as config

    return config


def credentials(platform: str) -> dict:
    root = Path(os.environ["GITHUB_WORKSPACE"]) / "creds"
    patterns = ("*.p8", "cert.p12", "cert.meta.json", "cert.registry.json") if platform == "ios" else (
        "*.jks", "*.keystore")
    paths = {path for pattern in patterns for path in root.glob(pattern)}
    if platform == "android":
        for path in root.glob("*.json"):
            try:
                if json.loads(path.read_text()).get("type") == "service_account":
                    paths.add(path)
            except (OSError, ValueError, AttributeError):
                continue
    # Only digests of the app's existing signing/account material travel; never key bytes or passwords.
    if any(path.is_symlink() or not path.is_file() for path in paths):
        raise ValueError("mobile_artifact_credential_scope_invalid")
    result = {path.name: digest(path) for path in sorted(paths)}
    if platform == "android":
        signing = android_config().find_signing_config(root.parent)
        result["selected-keystore"] = digest(signing.keystore_path)
        result["selected-alias"] = hashlib.sha256(signing.key_alias.encode()).hexdigest()
    return result


def files(root: Path) -> dict:
    result = {}
    for path in sorted(root.rglob("*")):
        if path.is_symlink():
            raise ValueError("mobile_artifact_symlink")
        if path.is_file() and path.name != "mobile-binary.json":
            name = path.relative_to(root).as_posix()
            if "\n" in name or "\r" in name or len(result) >= 10000:
                raise ValueError("mobile_artifact_path_invalid")
            result[name] = digest(path)
    return result


def metadata(platform: str) -> dict:
    event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text())
    keys = IOS_ENV if platform == "ios" else ANDROID_ENV
    return {"schema": SCHEMA, "platform": platform, "repository": os.environ["GITHUB_REPOSITORY"],
            "fingerprint": os.environ["MOBILE_ARTIFACT_FINGERPRINT"],
            "run_id": int(os.environ["GITHUB_RUN_ID"]), "run_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
            "source_sha": os.environ["GITHUB_SHA"],
            "head_sha": event.get("pull_request", {}).get("head", {}).get("sha", os.environ["GITHUB_SHA"]),
            "credential_fingerprints": credentials(platform),
            "environment": {key: os.environ.get(key, "") for key in keys}}


def pack(platform: str, directory: Path) -> None:
    temp = Path(os.environ["RUNNER_TEMP"])
    if any(directory.iterdir()):
        raise ValueError("mobile_artifact_output_not_empty")
    if platform == "ios":
        binaries = list((temp / "export").glob("*.ipa"))
        if len(binaries) != 1:
            raise ValueError("mobile_artifact_binary_ambiguous")
        (directory / "export").mkdir()
        shutil.copyfile(binaries[0], directory / "export" / "app.ipa")
        symbols = ["firebase-symbols", "dart-symbols/ios"]
    else:
        binary = Path(os.environ["MOBILE_ARTIFACT_BINARY"])
        if not binary.is_file() or binary.suffix != ".aab" or binary.is_symlink():
            raise ValueError("mobile_artifact_binary_absent")
        shutil.copyfile(binary, directory / "app.aab")
        symbols = ["dart-symbols/android"]
    for relative in symbols:
        if (temp / relative).is_dir():
            shutil.copytree(temp / relative, directory / relative, symlinks=True)
    value = {**metadata(platform), "files": files(directory)}
    (directory / "mobile-binary.json").write_text(json.dumps(value, indent=2) + "\n")


def verify(platform: str, directory: Path) -> dict:
    receipt = directory / "mobile-binary.json"
    if receipt.is_symlink() or receipt.stat().st_size > 2 * 1024 * 1024:
        raise ValueError("mobile_artifact_receipt_invalid")
    value = json.loads(receipt.read_text())
    if (value.get("schema") != SCHEMA or value.get("platform") != platform
            or value.get("repository") != os.environ["GITHUB_REPOSITORY"]
            or value.get("fingerprint") != os.environ["MOBILE_ARTIFACT_FINGERPRINT"]
            or value.get("credential_fingerprints") != credentials(platform)
            or value.get("files") != files(directory)):
        raise ValueError("mobile_artifact_inputs_mismatch")
    expected = set(IOS_ENV if platform == "ios" else ANDROID_ENV)
    environment = value.get("environment", {})
    if set(environment) != expected or any(not isinstance(v, str) or len(v) > 16000 for v in environment.values()):
        raise ValueError("mobile_artifact_environment_invalid")
    number = environment["BUILD_NUMBER" if platform == "ios" else "ANDROID_BUILD_NUMBER"]
    if not re.fullmatch(r"[1-9][0-9]{0,17}", number):
        raise ValueError("mobile_artifact_version_invalid")
    identifier = environment["CFG_BUNDLE_ID" if platform == "ios" else "ANDROID_PACKAGE_NAME"]
    if not re.fullmatch(r"[A-Za-z0-9_.-]{3,200}", identifier):
        raise ValueError("mobile_artifact_application_invalid")
    if platform == "ios" and (not environment["CFG_APP_STORE_APPLE_ID"].isdigit()
                              or not re.fullmatch(r"[A-Z0-9]{10}", environment["CFG_TEAM_ID"])):
        raise ValueError("mobile_artifact_signing_scope_invalid")
    binary = "export/app.ipa" if platform == "ios" else "app.aab"
    if binary not in value["files"]:
        raise ValueError("mobile_artifact_binary_absent")
    return value


def emit(path: Path, name: str, value: str) -> None:
    # A per-value delimiter keeps multiline What's New text inside a single environment value.
    delimiter = "MOBILE_VALUE_" + hashlib.sha256(value.encode()).hexdigest()
    with path.open("a") as handle:
        handle.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n")


def restore_ios(directory: Path, value: dict) -> None:
    from read_config import emit_credentials

    temp, env = Path(os.environ["RUNNER_TEMP"]), Path(os.environ["GITHUB_ENV"])
    emit_credentials(env, Path(os.environ["GITHUB_WORKSPACE"]))
    for key, text in value["environment"].items():
        emit(env, key, text)
    emit(env, "REUSED_BUILD_NUMBER", value["environment"]["BUILD_NUMBER"])
    notes = temp / "candidate-whats-new.txt"
    notes.write_text(value["environment"]["CFG_WHATS_NEW"])
    emit(env, "CFG_WHATS_NEW_FILE", str(notes))
    for relative in ("export", "firebase-symbols", "dart-symbols/ios"):
        if (directory / relative).is_dir():
            shutil.copytree(directory / relative, temp / relative, dirs_exist_ok=True)
    receipt = temp / "firebase-symbols/firebase-symbols.json"
    if receipt.is_file():
        firebase = json.loads(receipt.read_text())
        name = f"ios-crashlytics-symbols-{os.environ['GITHUB_RUN_ID']}-{os.environ['GITHUB_RUN_ATTEMPT']}"
        firebase.update(source_sha=os.environ["GITHUB_SHA"], artifact=name)
        receipt.write_text(json.dumps(firebase, indent=2))
        print("::notice title=firebase_symbols_pending::" + json.dumps(firebase, separators=(",", ":")))


def restore(platform: str, directory: Path) -> None:
    value = verify(platform, directory)
    if platform == "ios":
        restore_ios(directory, value)
        return
    # Use the current checkout's account, never a service-account file retained in an artifact.
    env = value["environment"]
    values = {"package-name": env["ANDROID_PACKAGE_NAME"], "build-number": env["ANDROID_BUILD_NUMBER"],
              "project-kind": env["ANDROID_PROJECT_KIND"], "bundle-path": str(directory / "app.aab"),
              "play-service-account": str(android_config().find_play_service_account(
                  Path(os.environ["GITHUB_WORKSPACE"])))}
    for key, text in values.items():
        emit(Path(os.environ["GITHUB_OUTPUT"]), key, text)


if __name__ == "__main__":
    operation, selected, target = sys.argv[1:]
    if selected not in {"ios", "android"} or operation not in {"pack", "restore", "verify"}:
        raise SystemExit("mobile_artifact_command_invalid")
    try:
        {"pack": pack, "restore": restore, "verify": verify}[operation](selected, Path(target))
    except Exception:
        raise SystemExit("mobile_artifact_files_refused") from None
