"""Retry the official Flutter manifest and SDK archive GETs without exposing partial JSON or proxy secrets."""
from __future__ import annotations

import os
from pathlib import Path
import re
import sys
import time
from urllib.parse import urlsplit

import flutter_download

#: Seconds an archive transfer may take before its receipt is published. The
#: enclosing job's own room is the only thing that bounds it usefully, and the
#: two differ by a factor of three: a validation job must report inside 30
#: minutes, while a release job has 45-60. A default-branch deploy cannot
#: restore the SDK cache a task branch built (GitHub scopes branch caches away
#: from the default branch), so it downloads the whole archive through the
#: account's residential exit at a few hundred KB/s — and a fixed 20-minute cap
#: made that a guaranteed failure: obd-scanner's release retained 428 MB of its
#: archive, failed on curl 28, and shipped nothing (2026-09-09).
ARCHIVE_BUDGET_S = 1200
MAX_ARCHIVE_BUDGET_S = 3000


def archive_budget() -> int:
    """The archive deadline, from ``GOWALK_FLUTTER_ARCHIVE_BUDGET_S`` when the
    workflow declares one. Clamped: never below the current default, never
    beyond a release job's own timeout."""
    try:
        declared = int(os.environ.get("GOWALK_FLUTTER_ARCHIVE_BUDGET_S", "") or ARCHIVE_BUDGET_S)
    except ValueError:
        return ARCHIVE_BUDGET_S
    return max(ARCHIVE_BUDGET_S, min(declared, MAX_ARCHIVE_BUDGET_S))
import flutter_archive_cache
import flutter_release_cache

RETRY_CODES = {5, 6, 7, 18, 28, 35, 52, 55, 56, 92, 95}
RETRY_HTTP = {408, 429, 500, 502, 503, 504}
#: Attempts each kind of transfer may make. The archive's deadline already
#: bounds its pass, so four stands there. The MANIFEST is the one that gave up
#: early: the account exit refused four TLS handshakes in ten seconds (curl 35,
#: proxy_status 200) and the iOS release failed with fifty seconds of its own
#: sixty-second budget unspent — an hour after the same fetch had succeeded
#: (obd-scanner, 2026-09-09). It is a few KB, so riding out an exit that is
#: briefly refusing costs one small request each time and nothing else.
MAX_ATTEMPTS = {"manifest": 12, "archive": 4}


def download(arguments: list[str], directory: Path) -> int:
    kind = request_kind(arguments)
    if kind == "archive":
        flutter_download.archive_digest()
    child_env = dict(os.environ)
    deadline, code, http, proxy_http = time.monotonic() + (60 if kind == "manifest" else archive_budget()), 1, 0, 0
    destination = directory / "download"
    destination.touch(mode=0o600, exist_ok=True)
    destination.chmod(0o600)
    if flutter_release_cache.reuse(arguments[-1], destination, kind):
        return 0
    if flutter_archive_cache.reuse_complete(destination, kind):
        return 0
    attempted = 0
    limit = MAX_ATTEMPTS[kind]
    for attempt in range(1, limit + 1):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            break
        attempted = attempt
        started = time.monotonic()
        code, http, proxy_http = flutter_download.transfer(arguments[-1], destination, kind, remaining, child_env)
        if kind == "archive":
            flutter_download.progress(destination, started, "attempt_finished", attempt=attempt,
                                      curl_exit=code, http_status=http, proxy_status=proxy_http)
        if code == 0 and http in ({200, 206} if kind == "archive" else {200}):
            flutter_download.publish(destination, kind)
            flutter_release_cache.remember(arguments[-1], destination, kind)
            if attempt > 1:
                print(f"flutter_{kind}_recovered attempts={attempt}", file=sys.stderr)
            return 0
        if attempt == limit or proxy_http in {401, 403, 407} or http in {401, 403}:
            break
        restart = kind == "archive" and code == 33 and http == 200
        if (not restart and code not in RETRY_CODES and http not in RETRY_HTTP
                and proxy_http not in {502, 503, 504}):
            break
        if kind == "manifest" or restart:
            destination.write_bytes(b"")
        if destination.stat().st_size > flutter_download.MAX_ARCHIVE_BYTES:
            raise ValueError("flutter_archive_size_exceeded")
        left = max(0.0, deadline - time.monotonic())
        # The manifest waits a flat second between tries: it is riding out an
        # exit that is refusing right now, and an exponential backoff would
        # spend its sixty-second window sleeping instead of retrying. The
        # archive keeps the growing wait — its attempts are minutes long.
        pause = 1 if kind == "manifest" else 2 ** (attempt - 1)
        time.sleep(min(pause, left))
    flutter_download.failure(kind, code, http, proxy_http, attempted, destination.stat().st_size)
    return code or 1


def request_kind(arguments):
    if not arguments:
        raise ValueError("flutter_download_arguments_missing")
    url = urlsplit(arguments[-1])
    if (url.scheme != "https" or not url.hostname or url.username or url.password
            or url.query or url.fragment or ".." in url.path):
        raise ValueError("flutter_download_url_unrecognized")
    if arguments[:-1] == ["--silent", "--connect-timeout", "15", "--retry", "5"] and re.fullmatch(
            r"/flutter_infra_release/releases/releases_(linux|macos|windows)\.json", url.path):
        return "manifest"
    if arguments[:-1] == ["--connect-timeout", "15", "--retry", "5"] and re.fullmatch(
            r"/flutter_infra_release/releases/[A-Za-z0-9_./+-]+\.(tar\.xz|tar\.gz|zip)", url.path):
        return "archive"
    raise ValueError("flutter_download_arguments_unrecognized")


def main() -> int:
    try:
        with flutter_archive_cache.workspace(request_kind(sys.argv[1:])) as scratch:
            result = download(sys.argv[1:], scratch)
            if result == 0:
                (scratch / "download").unlink(missing_ok=True)
            return result
    except (ValueError, OSError, KeyError) as error:
        safe = {"flutter_archive_digest_required", "flutter_archive_size_exceeded", "flutter_archive_checksum_mismatch"}
        reason = str(error) if str(error) in safe else "flutter_manifest_invalid_request_or_response"
        print("::error::" + reason, file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
