#!/usr/bin/env python3
"""Deploy Android through Bitrise instead of building locally.

Enabled by dropping a ``creds/bitrise.json`` config in the consumer repo:

    {
      "enabled": true,
      "app_slug": "8b2a7a24-....",     // Bitrise app slug
      "workflow": "android-deploy",     // Bitrise workflow to run
      "branch": "main",                 // optional; defaults to the pushed ref
      "api_token": "bitpat_...."        // optional; else env BITRISE_API_TOKEN
    }

Use this when the Google Play upload key / signing secrets live in Bitrise
(not in the repo) — the GitHub Actions runner cannot sign the AAB, so it
triggers the Bitrise ``android-deploy`` workflow (which holds the keystore
and service-account secrets) and waits for it to finish, mirroring the
build's success/failure back into the GitHub Actions job.

The Bitrise API token is a repo-scoped secret; committing it to a PRIVATE
repo's ``creds/`` follows the same trust model as the ASC ``.p8`` key that
already lives there. Prefer the ``BITRISE_API_TOKEN`` env (from an Actions
secret) when the repo is not private.
"""

from __future__ import annotations

import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

API = "https://api.bitrise.io/v0.1"
POLL_SECONDS = int(os.getenv("BITRISE_POLL_SECONDS", "20"))
# Bitrise build status codes: 0 = not finished, 1 = success, 2 = failed,
# 3 = aborted, 4 = aborted-with-success (skipped).
STATUS_TEXT = {0: "in-progress", 1: "success", 2: "failed", 3: "aborted",
               4: "aborted (success)"}


def fail(msg: str) -> "None":
    print(f"::error::{msg}")
    raise SystemExit(1)


def load_config() -> dict:
    workspace = Path(os.getenv("GITHUB_WORKSPACE", os.getcwd()))
    cfg_path = workspace / "creds" / "bitrise.json"
    if not cfg_path.is_file():
        fail("creds/bitrise.json not found but Bitrise mode was selected")
    try:
        cfg = json.loads(cfg_path.read_text())
    except ValueError as exc:
        fail(f"creds/bitrise.json is not valid JSON: {exc}")
    if not cfg.get("app_slug"):
        fail("creds/bitrise.json missing required 'app_slug'")
    return cfg


def resolve_token(cfg: dict) -> str:
    token = os.getenv("BITRISE_API_TOKEN") or cfg.get("api_token")
    if not token:
        fail("No Bitrise API token: set creds/bitrise.json 'api_token' or "
             "the BITRISE_API_TOKEN env/secret")
    return token


def resolve_branch(cfg: dict) -> str:
    if cfg.get("branch"):
        return cfg["branch"]
    ref = os.getenv("GITHUB_REF", "")
    if ref.startswith("refs/heads/"):
        return ref[len("refs/heads/"):]
    return "main"


def api(method: str, path: str, token: str, body: dict | None = None) -> dict:
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(
        API + path, data=data, method=method,
        headers={"Authorization": token, "Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.loads(resp.read().decode())
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode()[:400]
        fail(f"Bitrise API {method} {path} -> HTTP {exc.code}: {detail}")
    except urllib.error.URLError as exc:
        fail(f"Bitrise API {method} {path} unreachable: {exc}")
    return {}  # unreachable (fail raises)


def trigger(cfg: dict, token: str, branch: str) -> tuple[str, int, str]:
    workflow = cfg.get("workflow", "android-deploy")
    body = {
        "hook_info": {"type": "bitrise"},
        "build_params": {"branch": branch, "workflow_id": workflow},
    }
    commit = os.getenv("GITHUB_SHA")
    if commit:
        body["build_params"]["commit_hash"] = commit
    resp = api("POST", f"/apps/{cfg['app_slug']}/builds", token, body)
    if resp.get("status") != "ok":
        fail(f"Bitrise refused the build trigger: {resp}")
    return resp["build_slug"], resp["build_number"], workflow


def poll(app_slug: str, build_slug: str, token: str) -> int:
    last = None
    while True:
        data = api("GET", f"/apps/{app_slug}/builds/{build_slug}", token)["data"]
        status = data.get("status", 0)
        text = data.get("status_text", STATUS_TEXT.get(status, "?"))
        if text != last:
            print(f"  Bitrise build status: {text}")
            last = text
        if status != 0:
            return status
        time.sleep(POLL_SECONDS)


def print_tail_log(app_slug: str, build_slug: str, token: str) -> None:
    try:
        data = api("GET", f"/apps/{app_slug}/builds/{build_slug}/log", token)
    except SystemExit:
        return
    chunks = "".join(c.get("chunk", "") for c in data.get("log_chunks", []))
    if chunks:
        print("::group::Bitrise build log (tail)")
        print("\n".join(chunks.splitlines()[-60:]))
        print("::endgroup::")


def main() -> None:
    cfg = load_config()
    token = resolve_token(cfg)
    branch = resolve_branch(cfg)
    app_slug = cfg["app_slug"]

    build_slug, build_number, workflow = trigger(cfg, token, branch)
    url = f"https://app.bitrise.io/build/{build_slug}"
    print(f"Triggered Bitrise workflow '{workflow}' build #{build_number} "
          f"on branch '{branch}'")
    print(f"  {url}")

    status = poll(app_slug, build_slug, token)
    if status == 1:
        print(f"::notice::Bitrise build #{build_number} succeeded ({url})")
        return
    print_tail_log(app_slug, build_slug, token)
    fail(f"Bitrise build #{build_number} finished with status "
         f"'{STATUS_TEXT.get(status, status)}' ({url})")


if __name__ == "__main__":
    main()
