#!/usr/bin/env python3
"""Inventory core and contrib archives separately from VLC plugin licensing."""

from __future__ import annotations

import argparse
import hashlib
import os
import subprocess
import sys
from pathlib import Path

from audit_static_modules import (
    GPL_MARKER,
    contains_exact_c_string,
    reject_absolute_symlink_components,
)
from macho_audit import audit_archive_identity, audit_contrib_archive_identity

CORE_PATHS = ("lib/libvlc.a", "lib/libvlccore.a", "lib/vlc/libcompat.a")
FORBIDDEN_CONTRIB_ARCHIVE_TOKENS = ("zvbi",)


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as source:
        for block in iter(lambda: source.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def reject_gpl_marker(path: Path) -> None:
    if contains_exact_c_string(path.read_bytes(), GPL_MARKER):
        raise ValueError(f"GPL VLC marker found in non-module link input: {path}")


def reject_build_paths(path: Path, forbidden_prefix: str) -> None:
    data = path.read_bytes()
    if forbidden_prefix.encode() in data or b"/Users/" in data:
        raise ValueError(f"unmapped build path found in link input: {path}")


def prefix(vlc_root: Path, os_style: str, kind: str, arch: str) -> Path:
    suffix = "OS" if kind == "device" else "Simulator"
    return vlc_root / f"install-{os_style}{suffix}" / arch


def audit(
    vlc_root: Path,
    os_style: str,
    minimum_os: str,
    audit_dir: Path,
    forbidden_prefix: str,
) -> None:
    slices = (("device", "arm64"), ("simulator", "arm64"), ("simulator", "x86_64"))
    reject_absolute_symlink_components(vlc_root)
    core_rows: list[str] = []
    contrib_rows: list[str] = []
    platformless_rows: list[str] = []
    contrib_sets: dict[tuple[str, str], set[str]] = {}

    for kind, arch in slices:
        root = prefix(vlc_root, os_style, kind, arch)
        if root.is_symlink() or not root.is_dir():
            raise ValueError(f"missing or symlinked install prefix: {root}")
        for relative in CORE_PATHS:
            archive = root / relative
            audit_archive_identity(
                archive,
                architecture=arch,
                os_style=os_style,
                kind=kind,
                minimum_os=minimum_os,
            )
            reject_gpl_marker(archive)
            reject_build_paths(archive, forbidden_prefix)
            core_rows.append(f"{kind}\t{arch}\t{relative}\t{sha256(archive)}")

        contrib = root / "contribs/lib"
        if contrib.is_symlink() or not contrib.is_dir():
            raise ValueError(f"missing or symlinked contrib directory: {contrib}")
        entries = sorted(contrib.glob("*.a"))
        names = {entry.name for entry in entries}
        if not names:
            raise ValueError(f"no contrib static archives found in {contrib}")
        forbidden = sorted(
            name
            for name in names
            if any(token in name.lower() for token in FORBIDDEN_CONTRIB_ARCHIVE_TOKENS)
        )
        if forbidden:
            raise ValueError(
                f"forbidden contrib archives found for {kind}/{arch}: {forbidden}"
            )
        contrib_sets[(kind, arch)] = names
        for entry in entries:
            resolved = entry.resolve(strict=True)
            if os.path.commonpath((str(contrib.resolve()), str(resolved))) != str(contrib.resolve()):
                raise ValueError(f"contrib symlink escapes its prefix: {entry} -> {resolved}")
            if not resolved.is_file():
                raise ValueError(f"contrib entry is not a regular file: {entry}")
            declared_members, platformless_members = audit_contrib_archive_identity(
                resolved,
                architecture=arch,
                os_style=os_style,
                kind=kind,
                minimum_os=minimum_os,
            )
            reject_gpl_marker(resolved)
            reject_build_paths(resolved, forbidden_prefix)
            link_target = os.readlink(entry) if entry.is_symlink() else "-"
            contrib_rows.append(
                f"{kind}\t{arch}\t{entry.name}\t{sha256(resolved)}\t{link_target}\t"
                f"{declared_members}\t{len(platformless_members)}"
            )
            platformless_rows.extend(
                f"{kind}\t{arch}\t{entry.name}\t{index}\t{name}"
                for index, name in platformless_members
            )

    reference = contrib_sets[("device", "arm64")]
    for identity, names in contrib_sets.items():
        if names != reference:
            raise ValueError(f"contrib archive set differs for {identity}: {sorted(names ^ reference)}")

    audit_dir.mkdir(parents=True, exist_ok=True)
    (audit_dir / "core-archives.tsv").write_text(
        "slice\tarchitecture\tarchive\tsha256\n"
        + "".join(f"{row}\n" for row in sorted(core_rows)),
        encoding="utf-8",
    )
    (audit_dir / "contrib-archives.tsv").write_text(
        "slice\tarchitecture\tarchive\tsha256\tsymlink_target\t"
        "declared_platform_members\tplatformless_members\n"
        + "".join(f"{row}\n" for row in sorted(contrib_rows)),
        encoding="utf-8",
    )
    (audit_dir / "contrib-platformless-members.tsv").write_text(
        "slice\tarchitecture\tarchive\tmember_index\tmember\n"
        + "".join(f"{row}\n" for row in sorted(platformless_rows)),
        encoding="utf-8",
    )


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--vlc-root", required=True, type=Path)
    parser.add_argument("--os-style", required=True, choices=("iPhone", "AppleTV"))
    parser.add_argument("--minimum-os", required=True)
    parser.add_argument("--audit-dir", required=True, type=Path)
    parser.add_argument("--forbidden-prefix", required=True)
    args = parser.parse_args()
    try:
        audit(
            args.vlc_root.absolute(),
            args.os_style,
            args.minimum_os,
            args.audit_dir.resolve(),
            args.forbidden_prefix,
        )
    except (OSError, ValueError, subprocess.CalledProcessError) as error:
        print(f"ERROR: {error}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
