from __future__ import annotations

import re
import stat
import zipfile
from pathlib import PurePosixPath
from pathlib import Path
from typing import Any


MAX_ZIP_FILE_COUNT = 15000
MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES = 2 * 1024 * 1024 * 1024
MAX_ZIP_MEMBER_UNCOMPRESSED_BYTES = 512 * 1024 * 1024
MAX_ZIP_MEMBER_NAME_LENGTH = 512
MAX_ZIP_COMPRESSION_RATIO = 200
ZIP_BOMB_RATIO_MIN_BYTES = 10 * 1024 * 1024
ZIP_SIGNATURE_SAMPLE_BYTES = 65536
COMPRESSION_RATIO_EXEMPT_EXTENSIONS = {
    ".texturemips",
}

DANGEROUS_EXTENSIONS = {
    ".app",
    ".apk",
    ".bat",
    ".bash",
    ".binpkg",
    ".cmd",
    ".com",
    ".cpl",
    ".dll",
    ".dylib",
    ".elf",
    ".exe",
    ".fish",
    ".gadget",
    ".hta",
    ".jar",
    ".js",
    ".jse",
    ".lnk",
    ".msi",
    ".msp",
    ".pif",
    ".pl",
    ".ps1",
    ".psm1",
    ".py",
    ".pyc",
    ".pyo",
    ".pyw",
    ".rb",
    ".reg",
    ".scr",
    ".sh",
    ".so",
    ".url",
    ".vb",
    ".vbe",
    ".vbs",
    ".wsf",
    ".wsh",
    ".zsh",
}

NESTED_ARCHIVE_EXTENSIONS = {
    ".7z",
    ".bz2",
    ".gz",
    ".iso",
    ".lz",
    ".lzma",
    ".rar",
    ".tar",
    ".tgz",
    ".txz",
    ".xz",
    ".zip",
    ".zipx",
}

ALLOWED_EXTENSIONS = {
    ".afdesign",
    ".afphoto",
    ".ai",
    ".actor",
    ".blend",
    ".cardesign",
    ".carfinalstate",
    ".carfinalstatewithconsumable",
    ".compatibletyres",
    ".compatiblerim",
    ".csv",
    ".csp",
    ".dds",
    ".design",
    ".driverfinalstate",
    ".eps",
    ".exr",
    ".glb",
    ".gltf",
    ".hdr",
    ".indd",
    ".jpg",
    ".jpeg",
    ".json",
    ".loc",
    ".material",
    ".md",
    ".mechanicalcarpreset",
    ".mesh",
    ".mtl",
    ".obj",
    ".ora",
    ".png",
    ".psb",
    ".psd",
    ".rimmesh",
    ".scene",
    ".texture",
    ".texturemips",
    ".txt",
    ".xcf",
    ".kra",
    ".svg",
    ".tif",
    ".tiff",
    ".tyretextures",
    ".visualcarpreset",
    ".webp",
}

WINDOWS_DRIVE_RE = re.compile(r"^[A-Za-z]:")

TEXT_EXTENSIONS = {
    ".cardesign",
    ".csv",
    ".gltf",
    ".json",
    ".loc",
    ".md",
    ".mtl",
    ".obj",
    ".txt",
}

STRICT_BINARY_SIGNATURES = {
    ".dds": (b"DDS ",),
    ".glb": (b"glTF",),
    ".jpg": (b"\xFF\xD8\xFF",),
    ".jpeg": (b"\xFF\xD8\xFF",),
    ".png": (b"\x89PNG\r\n\x1A\n",),
}

BLOCKED_MAGIC_SIGNATURES = (
    ("Windows executable", (b"MZ",)),
    ("ELF executable", (b"\x7FELF",)),
    ("Mach-O executable", (b"\xFE\xED\xFA\xCE", b"\xFE\xED\xFA\xCF", b"\xCE\xFA\xED\xFE", b"\xCF\xFA\xED\xFE")),
    ("ZIP archive", (b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08")),
    ("RAR archive", (b"Rar!\x1A\x07\x00", b"Rar!\x1A\x07\x01\x00")),
    ("7z archive", (b"7z\xBC\xAF\x27\x1C",)),
    ("GZip archive", (b"\x1F\x8B",)),
    ("BZip2 archive", (b"BZh",)),
    ("XZ archive", (b"\xFD7zXZ\x00",)),
)

BLOCKED_TEXT_PREFIXES = (
    "<!doctype html",
    "<html",
    "<script",
    "@echo off",
    "powershell",
    "#!/bin/sh",
    "#!/bin/bash",
    "#!/usr/bin/env sh",
    "#!/usr/bin/env bash",
    "set wshshell",
)


def _zip_member_mode(info: zipfile.ZipInfo) -> int:
    return (info.external_attr >> 16) & 0xFFFF


def _is_zip_symlink(info: zipfile.ZipInfo) -> bool:
    mode = _zip_member_mode(info)
    return bool(mode and stat.S_ISLNK(mode))


def _is_zip_special_file(info: zipfile.ZipInfo) -> bool:
    mode = _zip_member_mode(info)
    if not mode:
        return False
    return stat.S_ISCHR(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode) or stat.S_ISSOCK(mode)


def _normalized_member_name(name: str, *, max_member_name_length: int = MAX_ZIP_MEMBER_NAME_LENGTH) -> str:
    raw = str(name or "").replace("\\", "/")
    if "\x00" in raw:
        raise ValueError(f"Unsafe ZIP member path contains a null byte: {name}")
    if len(raw) > max_member_name_length:
        raise ValueError(f"ZIP member path is too long: {name}")
    if raw.startswith("/") or raw.startswith("//") or WINDOWS_DRIVE_RE.match(raw):
        raise ValueError(f"Unsafe absolute ZIP member path: {name}")

    pure = PurePosixPath(raw)
    parts = tuple(part for part in pure.parts if part not in {"", "."})
    if not parts:
        return ""
    if any(part == ".." for part in parts):
        raise ValueError(f"Unsafe ZIP member path traversal: {name}")
    if WINDOWS_DRIVE_RE.match(parts[0]):
        raise ValueError(f"Unsafe Windows drive ZIP member path: {name}")
    return "/".join(parts)


def _member_suffix(member: str) -> str:
    return PurePosixPath(member).suffix.lower()


def _read_member_sample(archive: zipfile.ZipFile, info: zipfile.ZipInfo) -> bytes:
    sample_size = min(int(info.file_size or 0), ZIP_SIGNATURE_SAMPLE_BYTES)
    if sample_size <= 0:
        return b""
    with archive.open(info, "r") as handle:
        return handle.read(sample_size)


def _looks_like_tar(sample: bytes) -> bool:
    return len(sample) >= 265 and sample[257:262] == b"ustar"


def _looks_like_iso(sample: bytes) -> bool:
    return len(sample) >= 32774 and sample[32769:32774] == b"CD001"


def _validate_member_signature(suffix: str, sample: bytes, filename: str) -> None:
    if not sample:
        return

    for label, signatures in BLOCKED_MAGIC_SIGNATURES:
        if any(sample.startswith(signature) for signature in signatures):
            raise ValueError(f"Blocked disguised {label} in ZIP member: {filename}")
    if _looks_like_tar(sample):
        raise ValueError(f"Blocked disguised TAR archive in ZIP member: {filename}")
    if _looks_like_iso(sample):
        raise ValueError(f"Blocked disguised ISO image in ZIP member: {filename}")

    expected = STRICT_BINARY_SIGNATURES.get(suffix)
    if expected and not any(sample.startswith(signature) for signature in expected):
        raise ValueError(f"ZIP member content does not match its file type: {filename}")

    if suffix in TEXT_EXTENSIONS:
        if b"\x00" in sample:
            raise ValueError(f"ZIP text member contains binary data: {filename}")
        try:
            text = sample.decode("utf-8-sig")
        except UnicodeDecodeError as exc:
            raise ValueError(f"ZIP text member is not valid UTF-8: {filename}") from exc
        normalized = text.lstrip().lower()
        if any(normalized.startswith(prefix) for prefix in BLOCKED_TEXT_PREFIXES):
            raise ValueError(f"Blocked script-like ZIP text member: {filename}")


def validate_zip_archive(
    archive: zipfile.ZipFile,
    *,
    required_manifest: str | None = None,
    allowed_extensions: set[str] | None = None,
    allow_nested_archives: bool = False,
    max_file_count: int = MAX_ZIP_FILE_COUNT,
    max_total_uncompressed_bytes: int = MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES,
    max_member_uncompressed_bytes: int = MAX_ZIP_MEMBER_UNCOMPRESSED_BYTES,
    max_member_name_length: int = MAX_ZIP_MEMBER_NAME_LENGTH,
    max_compression_ratio: int = MAX_ZIP_COMPRESSION_RATIO,
) -> dict[str, Any]:
    allowed = {ext.lower() for ext in (allowed_extensions or ALLOWED_EXTENSIONS)}
    total_uncompressed = 0
    file_count = 0
    seen: set[str] = set()
    manifest_found = required_manifest is None

    for info in archive.infolist():
        normalized = _normalized_member_name(info.filename, max_member_name_length=max_member_name_length)
        if not normalized:
            continue

        key = normalized.lower()
        if key in seen:
            raise ValueError(f"Duplicate ZIP member path is not allowed: {info.filename}")
        seen.add(key)

        if required_manifest and key == _normalized_member_name(required_manifest, max_member_name_length=max_member_name_length).lower():
            manifest_found = True

        if info.flag_bits & 0x1:
            raise ValueError("Encrypted ZIP packages are not supported")
        if _is_zip_symlink(info):
            raise ValueError(f"ZIP symlinks are not supported: {info.filename}")
        if _is_zip_special_file(info):
            raise ValueError(f"ZIP special files are not supported: {info.filename}")
        if info.is_dir():
            continue

        suffix = _member_suffix(normalized)
        if suffix in DANGEROUS_EXTENSIONS:
            raise ValueError(f"Blocked dangerous ZIP file type: {info.filename}")
        if not allow_nested_archives and suffix in NESTED_ARCHIVE_EXTENSIONS:
            raise ValueError(f"Nested archives are not allowed in imported packages: {info.filename}")
        if suffix not in allowed:
            raise ValueError(f"Unsupported ZIP file type for LiveryLab import: {info.filename}")

        file_count += 1
        if file_count > max_file_count:
            raise ValueError(f"ZIP package contains too many files: {file_count}")

        file_size = int(info.file_size or 0)
        compress_size = int(info.compress_size or 0)
        if file_size > max_member_uncompressed_bytes:
            raise ValueError(f"ZIP member is too large: {info.filename}")
        total_uncompressed += file_size
        if total_uncompressed > max_total_uncompressed_bytes:
            raise ValueError("ZIP package is too large after decompression")

        ratio = file_size / max(compress_size, 1)
        if suffix not in COMPRESSION_RATIO_EXEMPT_EXTENSIONS and file_size >= ZIP_BOMB_RATIO_MIN_BYTES and ratio > max_compression_ratio:
            raise ValueError(f"Suspicious ZIP compression ratio for: {info.filename}")

        sample = _read_member_sample(archive, info)
        _validate_member_signature(suffix, sample, info.filename)

    if not manifest_found and required_manifest:
        raise FileNotFoundError(f"{required_manifest} not found in package ZIP")

    return {
        "fileCount": file_count,
        "totalUncompressedBytes": total_uncompressed,
        "requiredManifest": required_manifest or "",
    }


def validate_zip_path(
    path: str,
    *,
    required_manifest: str | None = None,
    allowed_extensions: set[str] | None = None,
    allow_nested_archives: bool = False,
    max_file_count: int = MAX_ZIP_FILE_COUNT,
    max_total_uncompressed_bytes: int = MAX_ZIP_TOTAL_UNCOMPRESSED_BYTES,
    max_member_uncompressed_bytes: int = MAX_ZIP_MEMBER_UNCOMPRESSED_BYTES,
    max_member_name_length: int = MAX_ZIP_MEMBER_NAME_LENGTH,
    max_compression_ratio: int = MAX_ZIP_COMPRESSION_RATIO,
) -> dict[str, Any]:
    with zipfile.ZipFile(path, "r") as archive:
        return validate_zip_archive(
            archive,
            required_manifest=required_manifest,
            allowed_extensions=allowed_extensions,
            allow_nested_archives=allow_nested_archives,
            max_file_count=max_file_count,
            max_total_uncompressed_bytes=max_total_uncompressed_bytes,
            max_member_uncompressed_bytes=max_member_uncompressed_bytes,
            max_member_name_length=max_member_name_length,
            max_compression_ratio=max_compression_ratio,
        )


def assert_target_within_roots(
    target: str | Path,
    allowed_roots: list[str | Path] | tuple[str | Path, ...],
    *,
    label: str = "import target",
) -> Path:
    target_path = Path(target).expanduser()
    target_resolved = target_path.resolve(strict=False)
    for root in allowed_roots:
        root_path = Path(root).expanduser()
        root_resolved = root_path.resolve(strict=False)
        try:
            target_resolved.relative_to(root_resolved)
            return target_path
        except ValueError:
            continue
    roots_text = ", ".join(str(Path(root).resolve(strict=False)) for root in allowed_roots)
    raise ValueError(f"Unsafe {label} outside allowed LiveryLab roots: {target_resolved} (allowed: {roots_text})")


def _relative_file_parts(package_root: Path, path: Path) -> tuple[str, ...]:
    root_resolved = package_root.resolve(strict=False)
    target_resolved = path.resolve(strict=False)
    try:
        rel = target_resolved.relative_to(root_resolved)
    except ValueError as exc:
        raise ValueError(f"Package file escapes package root: {path}") from exc
    parts = tuple(part for part in rel.parts if part not in {"", "."})
    if not parts or any(part == ".." for part in parts):
        raise ValueError(f"Unsafe package file path: {path}")
    return parts


def validate_package_root_structure(
    package_root: str | Path,
    *,
    allowed_top_level: set[str],
    required_files: set[str] | None = None,
    label: str = "package",
) -> dict[str, Any]:
    root = Path(package_root).expanduser().resolve(strict=False)
    if not root.exists() or not root.is_dir():
        raise FileNotFoundError(f"{label} folder not found: {root}")

    allowed = {str(item).replace("\\", "/").strip("/").lower() for item in allowed_top_level if str(item).strip()}
    required = {str(item).replace("\\", "/").strip("/").lower() for item in (required_files or set()) if str(item).strip()}
    seen_files: set[str] = set()
    file_count = 0

    for path in sorted(root.rglob("*"), key=lambda item: item.as_posix().lower()):
        if path.is_symlink():
            raise ValueError(f"{label} contains unsupported symlink: {path}")
        if not path.is_file():
            continue
        parts = _relative_file_parts(root, path)
        rel_key = "/".join(parts).lower()
        top_key = parts[0].lower()
        if top_key not in allowed:
            raise ValueError(f"{label} contains unsupported top-level entry: {parts[0]}")
        seen_files.add(rel_key)
        file_count += 1

    missing = sorted(required - seen_files)
    if missing:
        raise FileNotFoundError(f"{label} is missing required file(s): {', '.join(missing)}")

    return {"fileCount": file_count, "allowedTopLevel": sorted(allowed), "requiredFiles": sorted(required)}


def validate_archive_top_level_structure(
    archive: zipfile.ZipFile,
    *,
    allowed_top_level: set[str],
    required_files: set[str] | None = None,
    label: str = "package",
) -> dict[str, Any]:
    allowed = {str(item).replace("\\", "/").strip("/").lower() for item in allowed_top_level if str(item).strip()}
    required = {str(item).replace("\\", "/").strip("/").lower() for item in (required_files or set()) if str(item).strip()}
    seen_files: set[str] = set()
    file_count = 0

    for info in archive.infolist():
        normalized = _normalized_member_name(info.filename)
        if not normalized or info.is_dir():
            continue
        parts = tuple(part for part in normalized.split("/") if part)
        if not parts:
            continue
        top_key = parts[0].lower()
        if top_key not in allowed:
            raise ValueError(f"{label} contains unsupported top-level entry: {parts[0]}")
        seen_files.add(normalized.lower())
        file_count += 1

    missing = sorted(required - seen_files)
    if missing:
        raise FileNotFoundError(f"{label} is missing required file(s): {', '.join(missing)}")

    return {"fileCount": file_count, "allowedTopLevel": sorted(allowed), "requiredFiles": sorted(required)}


def validate_legacy_skin_system_payload(
    package_root: str | Path,
    *,
    label: str = "package",
) -> dict[str, Any]:
    """Allow only legacy raw SavedCars inside a skin package's system folder."""
    root = Path(package_root).expanduser().resolve(strict=False)
    system_root = root / "system"
    if not system_root.exists():
        return {"fileCount": 0, "files": []}
    if not system_root.is_dir():
        raise ValueError(f"{label} contains unsupported legacy system entry: system")

    files: list[str] = []
    for path in sorted(system_root.rglob("*"), key=lambda item: item.as_posix().lower()):
        if not path.is_file():
            continue
        parts = _relative_file_parts(root, path)
        allowed = (
            len(parts) == 3
            and parts[0].lower() == "system"
            and parts[1].lower() == "savedcars"
            and Path(parts[2]).suffix.lower() == ".carfinalstate"
        )
        if not allowed:
            rel = "/".join(parts)
            raise ValueError(f"{label} contains unsupported legacy system entry: {rel}")
        files.append("/".join(parts))
    return {"fileCount": len(files), "files": files}
