"""Helpers for collecting output file metadata without reading file contents."""

from __future__ import annotations

import mimetypes
import os
import stat
import sys
import time
from functools import wraps
from pathlib import Path
from collections.abc import Iterable

from pydantic import BaseModel


for strict in (True, False):
    mimetypes.add_type("application/warc", ".warc", strict=strict)


OUTPUT_FILE_METADATA_SUFFIXES = (".stdout.log", ".stderr.log", ".log", ".pid", ".sh")

EXECUTABLE_BITS = 0o111
BROKEN_SYMLINK_SUFFIX = ".broken-symlink.txt"


def _perf_trace(label):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if os.environ.get("ARCHIVEBOX_PERF_TRACE") != "1":
                return func(*args, **kwargs)
            started_at = time.perf_counter()
            try:
                return func(*args, **kwargs)
            finally:
                elapsed_ms = (time.perf_counter() - started_at) * 1000
                print(f"PERF_TRACE label={label} ms={elapsed_ms:.3f}", file=sys.stderr, flush=True)

        return wrapper

    return decorator


class OutputFile(BaseModel):
    """Metadata for a file emitted by a hook."""

    path: str
    extension: str = ""
    mimetype: str = ""
    size: int = 0


def guess_mimetype(path: str | Path) -> str:
    """Guess a file mimetype from its path without reading the file contents."""
    path_str = path.as_posix() if isinstance(path, Path) else str(path)
    mimetype, encoding = mimetypes.guess_type(path_str, strict=False)
    if mimetype:
        return mimetype

    if encoding == "gzip":
        inner_path = Path(path_str).with_suffix("")
        inner_mimetype, _inner_encoding = mimetypes.guess_type(inner_path.as_posix(), strict=False)
        if inner_mimetype:
            return inner_mimetype
        return "application/gzip"

    return ""


def output_file_from_path(file_path: Path, *, relative_to: Path) -> OutputFile:
    """Build OutputFile metadata from a file path using stat + extension lookup."""
    rel_path = file_path.relative_to(relative_to)
    try:
        size = file_path.lstat().st_size
    except OSError:
        size = 0
    return OutputFile(
        path=str(rel_path),
        extension=file_path.suffix.lower().lstrip("."),
        mimetype=guess_mimetype(rel_path),
        size=size,
    )


@_perf_trace("abx_dl.output_files.scan_output_files")
def scan_output_files(output_dir: Path, file_paths: Iterable[Path] | None = None) -> list[OutputFile]:
    """Collect metadata for real hook output files, excluding process artifacts.

    Also sanitizes the tree as it walks: strips +x from regular files, and
    replaces symlinks whose target escapes ``output_dir`` with a plain-text
    ``{name}.broken-symlink.txt`` sibling holding the original target string.
    Both operations are naturally idempotent.
    """
    if not output_dir.is_dir():
        return []

    try:
        containment_root = output_dir.resolve()
    except OSError:
        containment_root = output_dir

    paths = output_dir.rglob("*") if file_paths is None else file_paths
    output_files = []
    for file_path in paths:
        try:
            stat_result = file_path.lstat()
        except OSError:
            continue
        if stat.S_ISLNK(stat_result.st_mode):
            _neutralize_escaping_symlink(file_path, containment_root)
            continue
        if not stat.S_ISREG(stat_result.st_mode):
            continue
        if stat_result.st_mode & EXECUTABLE_BITS:
            try:
                file_path.chmod(stat_result.st_mode & ~EXECUTABLE_BITS)
            except OSError:
                # Best-effort: if chmod fails (read-only FS, permission denied),
                # leave the bit set rather than failing the whole scan.
                pass
        relative_path = file_path.relative_to(output_dir).as_posix()
        if any(relative_path.endswith(suffix) for suffix in OUTPUT_FILE_METADATA_SUFFIXES):
            continue
        output_files.append(output_file_from_path(file_path, relative_to=output_dir))

    output_files.sort(key=lambda output_file: output_file.path)
    return output_files


def _neutralize_escaping_symlink(link_path: Path, containment_root: Path) -> None:
    """Replace a symlink whose target escapes containment_root with a text record.

    Symlinks that resolve inside containment_root are left alone. Escaping links
    are deleted and a sibling ``{name}.broken-symlink.txt`` holding the original
    target string is written in their place — breaking the traversal while
    preserving the forensic reference.
    """
    try:
        target = os.readlink(link_path)
    except OSError:
        return
    try:
        resolved = link_path.resolve(strict=False)
    except OSError:
        return
    if resolved == containment_root or resolved.is_relative_to(containment_root):
        return

    record_path = link_path.with_name(link_path.name + BROKEN_SYMLINK_SUFFIX)
    try:
        link_path.unlink()
    except OSError:
        return
    try:
        record_path.write_text(str(target) + "\n", encoding="utf-8")
    except OSError:
        # Forensic record is best-effort; the symlink itself has already been
        # removed above, so sanitization has succeeded even if we can't write.
        pass
