from __future__ import annotations

import ipaddress
import json
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse


class SafetyViolation(RuntimeError):
    pass


@dataclass(frozen=True)
class AssetManifest:
    allowed_hosts: frozenset[str]
    allowed_resource_prefixes: tuple[str, ...]
    synthetic_only: bool
    public_egress: bool

    @classmethod
    def load(cls, path: Path) -> "AssetManifest":
        raw = json.loads(path.read_text(encoding="utf-8"))
        manifest = cls(
            allowed_hosts=frozenset(str(x).lower() for x in raw["allowed_hosts"]),
            allowed_resource_prefixes=tuple(raw["allowed_resource_prefixes"]),
            synthetic_only=bool(raw["synthetic_only"]),
            public_egress=bool(raw["public_egress"]),
        )
        manifest.validate()
        return manifest

    def validate(self) -> None:
        if not self.synthetic_only:
            raise SafetyViolation("Asset manifest must be synthetic-only")
        if self.public_egress:
            raise SafetyViolation("Public egress must remain disabled")
        for host in self.allowed_hosts:
            validate_host_is_non_public(host)

    def assert_resource_allowed(self, resource: str) -> None:
        if not any(resource.startswith(prefix) for prefix in self.allowed_resource_prefixes):
            raise SafetyViolation(f"Resource is outside the owned manifest: {resource}")
        if resource.startswith(("http://", "https://")):
            host = (urlparse(resource).hostname or "").lower()
            if host not in self.allowed_hosts:
                raise SafetyViolation(f"Host is not allowlisted: {host}")
            validate_host_is_non_public(host)


def validate_host_is_non_public(host: str) -> None:
    host = host.strip().lower()
    if host in {"localhost"} or host.endswith((".test", ".invalid", ".example")):
        return
    try:
        address = ipaddress.ip_address(host)
    except ValueError as exc:
        raise SafetyViolation(f"Public or unreserved hostname rejected: {host}") from exc
    if not (address.is_private or address.is_loopback or address.is_link_local):
        raise SafetyViolation(f"Public IP address rejected: {host}")


def assert_sandbox_path(root: Path, requested: Path) -> Path:
    root = root.resolve()
    candidate = (root / requested).resolve() if not requested.is_absolute() else requested.resolve()
    if candidate != root and root not in candidate.parents:
        raise SafetyViolation(f"Path escapes sandbox: {candidate}")
    return candidate


def contains_credential_like_value(value: object) -> bool:
    if isinstance(value, dict):
        return any(
            contains_credential_like_value(k) or contains_credential_like_value(v)
            for k, v in value.items()
        )
    if isinstance(value, (list, tuple, set)):
        return any(contains_credential_like_value(item) for item in value)
    if not isinstance(value, str):
        return False
    lower = value.lower()
    markers = ("sk-proj-", "sk-ant-", "AIza", "authorization: bearer", "api_key=")
    return any(marker.lower() in lower for marker in markers)

