"""Async PyPI JSON API client with SHA-256 verification."""
from __future__ import annotations

import hashlib
import zipfile
import tarfile
from pathlib import Path
from typing import Any

import httpx

PYPI_BASE = "https://pypi.org/pypi"
TIMEOUT = httpx.Timeout(30.0, connect=10.0)
_RETRIES = 3


async def _get(url: str) -> dict[str, Any]:
    async with httpx.AsyncClient(timeout=TIMEOUT, follow_redirects=True) as client:
        for attempt in range(_RETRIES):
            try:
                resp = await client.get(url)
                resp.raise_for_status()
                result: dict[str, Any] = resp.json()
                return result
            except (httpx.HTTPStatusError, httpx.RequestError):
                if attempt == _RETRIES - 1:
                    raise
    return {}


async def fetch_metadata(name: str, version: str | None = None) -> dict[str, Any]:
    """Return the PyPI JSON API payload for *name* (optionally pinned to *version*)."""
    url = f"{PYPI_BASE}/{name}/json" if version is None else f"{PYPI_BASE}/{name}/{version}/json"
    return await _get(url)


async def fetch_release_history(name: str) -> list[dict[str, Any]]:
    """Return releases sorted oldest-first as ``[{"version": ..., "upload_time": ..., "yanked": ...}, ...]``."""
    data = await fetch_metadata(name)
    releases: list[dict[str, Any]] = []
    for ver, files in data.get("releases", {}).items():
        if not files:
            continue
        earliest = min(files, key=lambda f: f.get("upload_time", ""))
        releases.append(
            {
                "version": ver,
                "upload_time": earliest.get("upload_time", ""),
                "yanked": any(f.get("yanked", False) for f in files),
            }
        )
    releases.sort(key=lambda r: r["upload_time"])
    return releases


async def download_package(url: str, expected_sha256: str, dest: Path) -> Path:
    """Stream *url* to *dest*, verify SHA-256, return the downloaded path."""
    async with httpx.AsyncClient(timeout=httpx.Timeout(120.0), follow_redirects=True) as client:
        async with client.stream("GET", url) as resp:
            resp.raise_for_status()
            hasher = hashlib.sha256()
            dest.parent.mkdir(parents=True, exist_ok=True)
            with dest.open("wb") as fh:
                async for chunk in resp.aiter_bytes(chunk_size=65536):
                    hasher.update(chunk)
                    fh.write(chunk)
    digest = hasher.hexdigest()
    if digest != expected_sha256:
        dest.unlink(missing_ok=True)
        raise ValueError(f"SHA-256 mismatch for {dest.name}: expected {expected_sha256}, got {digest}")
    return dest


def extract_archive(archive_path: Path, dest_dir: Path) -> None:
    """Extract a wheel (.whl / .zip) or sdist (.tar.gz) to *dest_dir*."""
    dest_dir.mkdir(parents=True, exist_ok=True)
    name = archive_path.name.lower()
    if name.endswith(".whl") or name.endswith(".zip"):
        with zipfile.ZipFile(archive_path) as zf:
            zf.extractall(dest_dir)
    elif name.endswith(".tar.gz") or name.endswith(".tgz"):
        with tarfile.open(archive_path, "r:gz") as tf:
            tf.extractall(dest_dir, filter="data")
    elif name.endswith(".tar.bz2"):
        with tarfile.open(archive_path, "r:bz2") as tf:
            tf.extractall(dest_dir, filter="data")
    else:
        raise ValueError(f"Unsupported archive format: {archive_path.name}")
