"""Prepare the pinned Crashlytics JAR before compiling, with bounded proxied GETs.

Firebase CLI 15.28.2 accepts CRASHLYTICS_LOCAL_JAR. Its own streaming download
has no total deadline; an idle relay can consume fifteen minutes after a build.
Only size/checksum-verified bytes enter this dependency cache or reach Java.
"""
from __future__ import annotations

import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time


VERSION = "3.0.3"
URL = ("https://dl.google.com/android/maven2/com/google/firebase/"
       f"firebase-crashlytics-buildtools/{VERSION}/firebase-crashlytics-buildtools-{VERSION}.jar")
SIZE = 8437846
SHA256 = "953b6f3942283f8332cc17a59bca2649ea49d14e0072f2c1f01762ebaebdf357"
TOTAL_SECONDS = 180
ATTEMPT_SECONDS = 60
ATTEMPTS = 3
RETRY_EXITS = {5, 6, 7, 18, 28, 52, 55, 56}
RETRY_STATUS = {408, 429, 500, 502, 503, 504}


class BootstrapError(RuntimeError):
    def __init__(self, reason: str, attempt: int = 0, exit_code: int = 0, http_status: int = 0):
        super().__init__(reason)
        self.receipt = {"schema": "gowalk-cicd/firebase-symbols-bootstrap.v1",
                        "phase": "buildtools_download", "reason": reason,
                        "attempts": attempt, "exit_code": exit_code, "http_status": http_status}

    def annotate(self) -> None:
        print("::error title=firebase_symbols_bootstrap_failed::" + json.dumps(self.receipt, separators=(",", ":")))


def verified(path: Path) -> bool:
    return (path.is_file() and path.stat().st_size == SIZE
            and hashlib.sha256(path.read_bytes()).hexdigest() == SHA256)


def transfer(path: Path, env: dict, seconds: float, attempt: int) -> None:
    command = ["curl", "--disable", "--silent", "--show-error", "--fail", "--proto", "=https",
               "--max-redirs", "0", "--connect-timeout", "15", "--max-time", str(seconds),
               "--max-filesize", str(SIZE), "--output", str(path), "--write-out", "%{http_code} %{http_connect}", URL]
    try:
        result = subprocess.run(command, env=env, capture_output=True, timeout=seconds + 5, check=False)
    except FileNotFoundError:
        raise BootstrapError("curl_missing", attempt) from None
    except subprocess.TimeoutExpired:
        raise BootstrapError("download_timeout", attempt, 28) from None
    codes = result.stdout.split()
    http, connect = (map(int, codes) if len(codes) == 2 and all(code.isdigit() for code in codes) else (0, 0))
    status = http or (connect if connect >= 400 else 0)
    if result.returncode == 0 and status == 200:
        if not verified(path):
            raise BootstrapError("checksum_or_size_mismatch", attempt, 0, status)
        return
    reason = ("download_timeout" if result.returncode == 28 else
              "tls_refused" if result.returncode in {35, 51, 60, 77, 83, 90, 91} else
              "size_limit" if result.returncode == 63 else
              "http_refused" if status >= 300 else "transport_failed")
    raise BootstrapError(reason, attempt, result.returncode, status)


def prepare() -> Path:
    env = dict(os.environ)
    root = Path(env.get("RUNNER_TOOL_CACHE") or Path.home() / ".cache") / "gowalk-cicd" / "crashlytics"
    root.mkdir(parents=True, exist_ok=True)
    target = root / f"{VERSION}-{SHA256}.jar"
    if verified(target):
        return target
    deadline = time.monotonic() + TOTAL_SECONDS
    for attempt in range(1, ATTEMPTS + 1):
        with tempfile.TemporaryDirectory(prefix="download-", dir=root) as scratch:
            partial = Path(scratch) / "buildtools.jar"
            seconds = min(ATTEMPT_SECONDS, deadline - time.monotonic())
            if seconds <= 0:
                raise BootstrapError("download_timeout", attempt - 1, 28)
            try:
                transfer(partial, env, seconds, attempt)
            except BootstrapError as error:
                status = error.receipt["http_status"]
                retryable = (status in RETRY_STATUS if status >= 300
                             else error.receipt["exit_code"] in RETRY_EXITS)
                if not retryable or attempt == ATTEMPTS:
                    raise
                print(f"Crashlytics buildtools GET attempt {attempt} failed ({error.receipt['reason']}); retrying.")
                time.sleep(min(attempt, max(0, deadline - time.monotonic())))
                continue
            partial.replace(target)
            return target
    raise AssertionError("download attempts exhausted without a result")
