"""Private Flutter transfers publish only complete JSON or an archive matching the manifest digest."""
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time

MAX_ARCHIVE_BYTES = 8 * 1024 * 1024 * 1024
PROGRESS_SECONDS = 60


def archive_digest() -> str:
    value = os.environ.get("GOWALK_FLUTTER_ARCHIVE_SHA256", "")
    if not re.fullmatch(r"[a-fA-F0-9]{64}", value):
        raise ValueError("flutter_archive_digest_required")
    return value.lower()


def transfer(url: str, destination: Path, kind: str, remaining: float, env: dict) -> tuple[int, int, int]:
    resume = kind == "archive" and destination.stat().st_size > 0
    budget = min(20 if kind == "manifest" else 600, remaining)
    command = ["curl", "--disable", "--silent", "--show-error", "--fail", "--retry", "0",
               "--connect-timeout", "15", "--max-time", str(budget),
               "--max-filesize", str(16777216 if kind == "manifest" else MAX_ARCHIVE_BYTES),
               "--output", str(destination), "--write-out", "%{http_code} %{http_connect}"]
    if resume:
        command += ["--continue-at", "-"]
    with subprocess.Popen([*command, url], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env) as process:
        try:
            return wait_transfer(process, destination, kind, budget)
        finally:
            if process.poll() is None:
                process.kill()
                process.communicate()


def wait_transfer(process, destination: Path, kind: str, budget: float) -> tuple[int, int, int]:
    started = time.monotonic()
    while True:
        left = budget - (time.monotonic() - started)
        if left <= 0:
            return 28, 0, 0
        try:
            output, _private_error = process.communicate(timeout=min(PROGRESS_SECONDS, left))
            status = output.split()
            http, proxy = (map(int, status) if len(status) == 2
                           and all(part.isdigit() for part in status) else (0, 0))
            return process.returncode, http, proxy
        except subprocess.TimeoutExpired:
            if kind == "archive":
                progress(destination, started, "transferring")


def progress(destination: Path, started: float, event: str, **fields) -> None:
    note = {"schema": "gowalk-cicd/flutter-download-progress.v1", "phase": "archive", "event": event,
            "elapsed_ms": round((time.monotonic() - started) * 1000),
            "retained_bytes": destination.stat().st_size, **fields}
    print("::notice title=flutter_download_progress::" + json.dumps(note, separators=(",", ":")),
          file=sys.stderr, flush=True)


def archive_matches(destination: Path) -> bool:
    if destination.stat().st_size > MAX_ARCHIVE_BYTES:
        raise ValueError("flutter_archive_size_exceeded")
    digest = hashlib.sha256()
    with destination.open("rb") as source:
        for chunk in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest() == archive_digest()


def publish(destination: Path, kind: str) -> None:
    if kind == "manifest":
        json.loads(destination.read_bytes())
    elif not archive_matches(destination):
        raise ValueError("flutter_archive_checksum_mismatch")
    with destination.open("rb") as source:
        shutil.copyfileobj(source, sys.stdout.buffer, length=1024 * 1024)


def failure(kind: str, code: int, http: int, proxy: int, attempts: int, retained: int) -> None:
    print(f"flutter_{kind}_transport_failed curl_exit={code} http_status={http} "
          f"proxy_status={proxy} attempts={attempts} retained_bytes={retained}", file=sys.stderr)
    note = {"schema": "gowalk-cicd/flutter-download-failed.v1", "phase": kind,
            "curl_exit": code, "http_status": http, "proxy_status": proxy,
            "attempts": attempts, "retained_bytes": retained}
    print("::error title=flutter_download_failed::" + json.dumps(note, separators=(",", ":")), file=sys.stderr)
