from __future__ import annotations

import hashlib
import io
import os
import tarfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from urllib.parse import urljoin

import requests

DEFAULT_POLICY_REGISTRY_URL = "http://localhost:8081"


class PolicyRegistryError(RuntimeError):
    pass


@dataclass(frozen=True)
class PolicyBundle:
    bundle_id: str
    title: str
    description: str
    version: str | None
    url: str
    sha256: str | None

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.bundle_id,
            "title": self.title,
            "description": self.description,
            "version": self.version,
            "url": self.url,
            "sha256": self.sha256,
        }


def get_policy_registry_url(override: str | None = None) -> str:
    return override or os.getenv("GUARDRAIL_POLICY_REGISTRY_URL", DEFAULT_POLICY_REGISTRY_URL)


def fetch_registry_index(registry_url: str | None = None) -> dict[str, Any]:
    base_url = get_policy_registry_url(registry_url).rstrip("/") + "/"
    response = requests.get(urljoin(base_url, "registry.json"), timeout=10)
    if response.status_code != 200:
        raise PolicyRegistryError(f"Registry lookup failed: {response.status_code}")
    return response.json()


def _resolve_bundle_url(base_url: str, bundle_url: str) -> str:
    if bundle_url.startswith("http://") or bundle_url.startswith("https://"):
        return bundle_url
    return urljoin(base_url, bundle_url.lstrip("/"))


def list_policy_bundles(registry_url: str | None = None) -> list[PolicyBundle]:
    base_url = get_policy_registry_url(registry_url).rstrip("/") + "/"
    data = fetch_registry_index(base_url)
    bundles = data.get("bundles", [])
    if not isinstance(bundles, list):
        raise PolicyRegistryError("Registry bundles must be a list.")

    parsed: list[PolicyBundle] = []
    for bundle in bundles:
        if not isinstance(bundle, dict):
            continue
        bundle_id = bundle.get("id")
        title = bundle.get("title")
        description = bundle.get("description")
        url = bundle.get("url")
        if not all([bundle_id, title, description, url]):
            continue
        parsed.append(
            PolicyBundle(
                bundle_id=bundle_id,
                title=title,
                description=description,
                version=bundle.get("version"),
                url=_resolve_bundle_url(base_url, url),
                sha256=bundle.get("sha256"),
            )
        )
    return parsed


def get_policy_bundle(bundle_id: str, registry_url: str | None = None) -> PolicyBundle:
    bundles = list_policy_bundles(registry_url)
    for bundle in bundles:
        if bundle.bundle_id == bundle_id:
            return bundle
    raise PolicyRegistryError(f"Bundle '{bundle_id}' not found.")


def _safe_extract(tar: tarfile.TarFile, destination: Path) -> None:
    destination = destination.resolve()
    for member in tar.getmembers():
        member_path = (destination / member.name).resolve()
        if not str(member_path).startswith(str(destination)):
            raise PolicyRegistryError("Unsafe path detected in bundle.")
    tar.extractall(destination)


def download_bundle(bundle: PolicyBundle, destination: Path) -> Path:
    response = requests.get(bundle.url, timeout=30)
    if response.status_code != 200:
        raise PolicyRegistryError(f"Bundle download failed: {response.status_code}")
    content = response.content

    if bundle.sha256:
        digest = hashlib.sha256(content).hexdigest()
        if digest.lower() != bundle.sha256.lower():
            raise PolicyRegistryError("Bundle checksum mismatch.")

    destination.mkdir(parents=True, exist_ok=True)
    bundle_dir = destination / bundle.bundle_id
    bundle_dir.mkdir(parents=True, exist_ok=True)

    with tarfile.open(fileobj=io.BytesIO(content), mode="r:gz") as tar:
        _safe_extract(tar, bundle_dir)

    return bundle_dir
