#!/usr/bin/env python3
"""Resolve Android credentials and app metadata without exposing secrets."""

from __future__ import annotations

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

import gradle_wrapper
from play_inventory import read_inventory
from android_config import (
    ConfigError,
    detect_package_name,
    find_gradle_project,
    find_play_service_account,
    find_signing_config,
    project_kind,
    set_gradle_version,
)


def append_value(path: Path, name: str, value: str) -> None:
    with path.open("a") as stream:
        stream.write(f"{name}={value}\n")


def resolve_build_number(raw: str) -> int:
    try:
        number = int(raw)
    except ValueError as exc:
        raise ConfigError(f"Android build number must be an integer: {raw!r}") from exc
    if not 1 <= number <= 2_100_000_000:
        raise ConfigError("Android build number must be between 1 and 2100000000")
    return number


def auto_build_number(highest: int | None) -> int:
    """Choose the next version only from a successfully read Play inventory."""
    run_number = resolve_build_number(os.environ.get("GITHUB_RUN_NUMBER", "1"))
    candidate = resolve_build_number(str(max(run_number, (highest or 0) + 1)))
    if candidate != run_number:
        print(f"Play already holds versionCode {highest}; using {candidate} "
              f"instead of GITHUB_RUN_NUMBER {run_number}")
    return candidate


def apply_gradle_version(project, build_number: int, build_name: str) -> None:
    """Write the resolved versionCode/versionName into the Gradle module.

    A miss is not a problem: the build step applies the same values through the
    Variant API (scripts/version_override.init.gradle), which needs no literal
    to rewrite and is what makes the resolved versionCode authoritative. This
    rewrite stays because it also fixes up anything that reads the build file
    directly, and because it is what the fleet has been running.
    """
    applied = set_gradle_version(project.build_file, build_number, build_name or None)
    relative = project.build_file.name
    if applied["version_code"]:
        print(f"Pinned versionCode {build_number} in {project.module}/{relative}")
    else:
        print(
            f"No literal versionCode in {project.module}/{relative}; the Gradle build "
            f"will be given versionCode {build_number} through the Variant API instead"
        )
    if build_name and not applied["version_name"]:
        print(
            f"No literal versionName in {project.module}/{relative}; the Gradle build "
            f"will be given versionName {build_name} through the Variant API instead"
        )


def flutter_root() -> Path | None:
    """Where the runner's Flutter SDK lives.

    flutter-action exports FLUTTER_ROOT; a self-hosted runner with Flutter
    merely on PATH does not, so fall back to walking up from the binary
    (<root>/bin/flutter).
    """
    declared = os.environ.get("FLUTTER_ROOT")
    if declared:
        return Path(declared)
    binary = shutil.which("flutter")
    if not binary:
        return None
    return Path(binary).resolve().parent.parent


def prepare_flutter_toolchain(workspace: Path) -> None:
    # Flutter stable can raise its Gradle/AGP/Kotlin floor. Repair the ephemeral
    # checkout only after the store inventory has been read successfully.
    root = flutter_root()
    if root is None:
        print("::warning::Flutter SDK not found; skipping the build toolchain check")
    else:
        for message in gradle_wrapper.ensure_toolchain(workspace, root):
            print(message)


def main() -> None:
    workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve()
    gradle_root = ""
    gradle_module = ""
    try:
        kind = project_kind(workspace)
        project = None if kind == "flutter" else find_gradle_project(workspace)
        signing = find_signing_config(workspace)
        service_account = find_play_service_account(workspace)
        package_name = os.environ.get("INPUT_PACKAGE_NAME") or detect_package_name(
            workspace, project.build_file if project else None
        )
        inventory = read_inventory(package_name, service_account)
        pinned = os.environ.get("INPUT_BUILD_NUMBER") or ""
        if pinned:
            build_number = resolve_build_number(pinned)
        else:
            build_number = auto_build_number(inventory.highest_version)
        if project is not None:
            gradle_root = str(project.root)
            gradle_module = project.module
            apply_gradle_version(
                project, build_number, os.environ.get("INPUT_BUILD_NAME") or ""
            )
        else:
            prepare_flutter_toolchain(workspace)
    except (ConfigError, KeyError, OSError) as exc:
        print(f"::error::{exc}")
        raise SystemExit(1) from exc

    values = {
        "ANDROID_PACKAGE_NAME": package_name,
        "ANDROID_BUILD_NUMBER": str(build_number),
        "ANDROID_SIGNING_PROPERTIES": str(signing.properties_path),
        "ANDROID_KEYSTORE_PATH": str(signing.keystore_path),
        "ANDROID_PLAY_SERVICE_ACCOUNT": str(service_account),
        "ANDROID_PLAY_API_READY": "true" if inventory.ready else "false",
        "ANDROID_PROJECT_KIND": kind,
        "ANDROID_GRADLE_ROOT": gradle_root,
        "ANDROID_GRADLE_MODULE": gradle_module,
    }
    write_config(values, signing)
    print(f"Resolved {kind} Android package {package_name}, build {build_number}")


def owner_package() -> None:
    """Resolve only local identity before the Node owner starts any provider operation."""
    workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve()
    kind = project_kind(workspace)
    project = None if kind == "flutter" else find_gradle_project(workspace)
    package = os.environ.get("INPUT_PACKAGE_NAME") or detect_package_name(
        workspace, project.build_file if project else None)
    account = find_play_service_account(workspace)
    value = json.loads(account.read_text())
    email = value.get("client_email")
    if not isinstance(email, str) or not re.fullmatch(r"[^\s@]+@[^\s@]+", email):
        raise ConfigError("Play principal identity is unavailable")
    print(json.dumps({"package": package, "principal_sha256": hashlib.sha256(email.lower().encode()).hexdigest()}))


def write_config(values: dict[str, str], signing) -> None:
    env_path = Path(os.environ["GITHUB_ENV"])
    output_path = Path(os.environ["GITHUB_OUTPUT"])
    for secret in (signing.store_password, signing.key_password):
        print(f"::add-mask::{secret}")
    for name, value in values.items():
        append_value(env_path, name, value)
    for name, source in (
        ("package_name", "ANDROID_PACKAGE_NAME"),
        ("service_account", "ANDROID_PLAY_SERVICE_ACCOUNT"),
        ("build_number", "ANDROID_BUILD_NUMBER"),
        ("play_api_ready", "ANDROID_PLAY_API_READY"),
        ("project_kind", "ANDROID_PROJECT_KIND"),
        ("gradle_root", "ANDROID_GRADLE_ROOT"),
        ("gradle_module", "ANDROID_GRADLE_MODULE"),
    ):
        append_value(output_path, name, values[source])


if __name__ == "__main__":
    owner_package() if sys.argv[1:] == ["--owner-package"] else main()
