"""Best-effort archive member extraction (filesystem-import aid).

zip/cbz via stdlib zipfile; rar via rarfile (needs an unrar/bsdtar
binary); 7z via py7zr (pure-Python). NEVER raises: on any failure the
context yields an empty list, and the caller still preserves the archive
file itself as a PostAttachment, so nothing is lost.
"""

import logging
import zipfile
from contextlib import contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory

log = logging.getLogger(__name__)

ARCHIVE_EXTS = {".zip", ".cbz", ".rar", ".7z"}

# Magic-byte signatures, so an archive with a mangled / extension-less filename
# is still recognised. Patreon attachment download URLs sanitize to names like
# `01_https___www.patreon.com_media-u_v3_131083093`, whose `Path.suffix` is junk
# (`.com_media-u_v3_131083093`), never `.zip` — an extension-only gate filed
# those as opaque PostAttachments and NEVER extracted them (operator-flagged
# 2026-06-06). Detection is by extension first (cheap), then header sniff.
_RAR_MAGIC = b"Rar!\x1a\x07"
_7Z_MAGIC = b"7z\xbc\xaf\x27\x1c"


def detect_archive_format(path: Path) -> str | None:
    """Return "zip" | "rar" | "7z" for an archive, else None.

    Trusts a known extension first, then falls back to magic-byte sniffing so a
    mis-named or extension-less archive is still handled. (zip covers .cbz too.)
    """
    ext = Path(path).suffix.lower()
    if ext in (".zip", ".cbz"):
        return "zip"
    if ext == ".rar":
        return "rar"
    if ext == ".7z":
        return "7z"
    try:
        if zipfile.is_zipfile(path):
            return "zip"
        with open(path, "rb") as fh:
            head = fh.read(8)
    except OSError:
        return None
    if head.startswith(_RAR_MAGIC):
        return "rar"
    if head.startswith(_7Z_MAGIC):
        return "7z"
    return None


def is_archive(path: Path) -> bool:
    return detect_archive_format(path) is not None


@contextmanager
def extract_archive(path: Path):
    """Context manager yielding a list of (member_name, extracted_path)
    for every regular-file member. Temp dir is removed on exit. Members
    are only ever under the temp dir (libs' safe extractall + rglob from
    base). Fail-soft: any error → yields []."""
    tmp = TemporaryDirectory(prefix="fc_arch_")
    base = Path(tmp.name)
    members: list[tuple[str, Path]] = []
    try:
        try:
            fmt = detect_archive_format(path)
            if fmt == "zip":
                with zipfile.ZipFile(path) as zf:
                    zf.extractall(base)
            elif fmt == "rar":
                import rarfile

                with rarfile.RarFile(path) as rf:
                    rf.extractall(base)
            elif fmt == "7z":
                import py7zr

                with py7zr.SevenZipFile(path, "r") as zf:
                    zf.extractall(base)
            for p in sorted(base.rglob("*")):
                if p.is_file():
                    members.append((str(p.relative_to(base)), p))
        except Exception as exc:
            log.warning("archive extract failed for %s: %s", path, exc)
            members = []
        yield members
    finally:
        tmp.cleanup()
