#!/usr/bin/env bash
# BatchForge+ — Build LGPL-2.1 FFmpeg + ffprobe for bundling
#
# Produces a non-GPL, non-nonfree FFmpeg binary suitable for shipping
# inside a closed-source macOS app under the LGPL-2.1 "executable"
# interpretation (we invoke ffmpeg as a child process, never link to
# its libraries — see b3LAB - BatchForge+__LEGAL.md).
#
# Output:
#   Sources/Resources/bin/ffmpeg
#   Sources/Resources/bin/ffprobe
#
# Strategy:
#   - Build statically against LGPL-only / permissive dependencies
#     (no libx264, no libx265, no libfdk-aac, no libxvid, no GPL filters).
#   - Hardware H.264/HEVC/ProRes encoding stays available via Apple
#     VideoToolbox, which lives in macOS — not in FFmpeg.
#   - drawtext stays available via libfreetype + libharfbuzz.
#
# Usage:
#   bash Scripts/build_ffmpeg_lgpl.sh           # full build (~15–30 min first run)
#   bash Scripts/build_ffmpeg_lgpl.sh --check   # only verify existing binary
#   bash Scripts/build_ffmpeg_lgpl.sh --clean   # wipe build/ffmpeg-build/ and rebuild
#
# Requirements:
#   - macOS with Homebrew
#   - Developer ID Application: Benni Mauz (955RASMBV3) in Keychain
#     (signing pass — same identity used by Scripts/release.sh)

set -euo pipefail

# ──────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────
readonly FFMPEG_VERSION="7.1.2"
# Pinned SHA-256 of https://ffmpeg.org/releases/ffmpeg-7.1.2.tar.xz
# (ffmpeg.org does not publish individual .sha256 files; pin in source for reproducibility).
readonly FFMPEG_SHA256="089bc60fb59d6aecc5d994ff530fd0dcb3ee39aa55867849a2bbc4e555f9c304"

readonly PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
readonly BUILD_ROOT="${PROJECT_ROOT}/build/ffmpeg-build"
readonly SRC_DIR="${BUILD_ROOT}/src"
readonly PREFIX_DIR="${BUILD_ROOT}/prefix"
readonly OUT_BIN_DIR="${PROJECT_ROOT}/Sources/Resources/bin"
readonly OUT_LIB_DIR="${PROJECT_ROOT}/Sources/Resources/lib"

readonly SIGN_IDENTITY="Developer ID Application: Benni Mauz (955RASMBV3)"

# LGPL-compatible Homebrew deps (kept static where possible).
# All packages below are non-GPL: LGPL / BSD / MIT / ISC / Apache.
readonly BREW_DEPS=(
    nasm
    pkg-config
    cmake
    meson
    ninja
    dylibbundler
    freetype
    harfbuzz
    fontconfig
    lame
    opus
    libogg
    libvorbis
    libvpx
    dav1d
    webp
    libass
    snappy
)
# Note: SVT-AV1 intentionally dropped — its 3.x API broke FFmpeg 7.1.2's
# libsvtav1 wrapper. AV1 decode stays available via dav1d (BSD-2). AV1 encode
# can be re-added later via libaom (BSD-2) once API alignment is verified.

# ──────────────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────────────
log()  { printf "\033[1;36m==>\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m!!\033[0m %s\n" "$*" >&2; }
die()  { printf "\033[1;31mXX\033[0m %s\n" "$*" >&2; exit 1; }

require_macos() {
    [[ "$(uname -s)" == "Darwin" ]] || die "macOS only."
}

require_brew() {
    command -v brew >/dev/null 2>&1 \
        || die "Homebrew not found. Install from https://brew.sh first."
}

require_signing_identity() {
    security find-identity -v -p codesigning | grep -Fq "${SIGN_IDENTITY}" \
        || die "Signing identity missing in Keychain: ${SIGN_IDENTITY}"
}

install_brew_deps() {
    log "Installing LGPL-compatible build dependencies (idempotent)…"
    local missing=()
    for dep in "${BREW_DEPS[@]}"; do
        brew list --versions "${dep}" >/dev/null 2>&1 || missing+=("${dep}")
    done
    if (( ${#missing[@]} > 0 )); then
        log "Installing missing: ${missing[*]}"
        brew install "${missing[@]}"
    else
        log "All build dependencies present."
    fi
}

# ──────────────────────────────────────────────────────────────────────
# Verification
# ──────────────────────────────────────────────────────────────────────
verify_binary() {
    local bin="$1"
    [[ -x "${bin}" ]] || die "Binary not executable or missing: ${bin}"

    log "Verifying ${bin}…"
    local cfg
    cfg="$("${bin}" -version 2>&1 | head -100)"

    # Must not contain any GPL-only or nonfree library, or the GPL master flag.
    # We check for `--enable-<lib>` tokens specifically — `--disable-<lib>` is
    # the legitimate way to confirm a forbidden lib was excluded.
    local forbidden=("--enable-libx264" "--enable-libx265" "--enable-libxvid"
                     "--enable-libfdk-aac" "--enable-libvidstab" "--enable-frei0r"
                     "--enable-librubberband" "--enable-libxavs" "--enable-libxavs2"
                     "--enable-libdavs2" "--enable-libuavs3d" "--enable-libtesseract"
                     "--enable-libopencv" "--enable-libsmbclient" "--enable-postproc"
                     "--enable-gpl" "--enable-nonfree")
    for token in "${forbidden[@]}"; do
        if grep -Fq -- "${token}" <<< "${cfg}"; then
            die "Binary contains forbidden GPL/nonfree token: ${token}"
        fi
    done

    # Defensive double-check: matching --disable-<lib> tokens must be present.
    local must_be_disabled=("--disable-libx264" "--disable-libx265" "--disable-libxvid"
                            "--disable-libfdk-aac" "--disable-gpl" "--disable-nonfree")
    for token in "${must_be_disabled[@]}"; do
        if ! grep -Fq -- "${token}" <<< "${cfg}"; then
            die "Binary missing required --disable token: ${token}"
        fi
    done

    # Must contain the features BatchForge+ depends on.
    local required=("--enable-libfreetype" "--enable-libharfbuzz"
                    "--enable-videotoolbox" "--enable-audiotoolbox"
                    "--enable-libmp3lame" "--enable-libopus" "--enable-libvorbis"
                    "--enable-libvpx" "--enable-libdav1d" "--enable-libwebp"
                    "--enable-libsnappy")
    for token in "${required[@]}"; do
        if ! grep -Fq -- "${token}" <<< "${cfg}"; then
            die "Binary missing required feature: ${token}"
        fi
    done

    # HAP encoder must be present (BatchForge+ uses it for VJ workflows).
    "${bin}" -hide_banner -encoders 2>/dev/null | grep -Eq '(^|[[:space:]])hap([[:space:]]|$)' \
        || die "HAP encoder is missing in ${bin} — did libsnappy install fail?"

    # drawtext filter must be available.
    "${bin}" -hide_banner -filters 2>/dev/null | grep -Eq '(^|[[:space:]])drawtext([[:space:]]|$)' \
        || die "drawtext filter is missing in ${bin}"

    # VideoToolbox encoders must be present.
    "${bin}" -hide_banner -encoders 2>/dev/null | grep -q "h264_videotoolbox" \
        || die "h264_videotoolbox encoder is missing in ${bin}"

    log "OK · $(basename "${bin}") · $("${bin}" -version 2>&1 | awk 'NR==1{print $1,$2,$3}')"
}

# ──────────────────────────────────────────────────────────────────────
# Build
# ──────────────────────────────────────────────────────────────────────
fetch_ffmpeg_source() {
    mkdir -p "${SRC_DIR}"
    local archive="${SRC_DIR}/ffmpeg-${FFMPEG_VERSION}.tar.xz"
    local extracted="${SRC_DIR}/ffmpeg-${FFMPEG_VERSION}"

    if [[ -d "${extracted}" ]]; then
        log "FFmpeg source already extracted: ${extracted}"
        return 0
    fi

    if [[ ! -f "${archive}" ]]; then
        log "Downloading FFmpeg ${FFMPEG_VERSION} source…"
        curl -fSL --retry 3 -o "${archive}" \
            "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"
    fi

    log "Verifying tarball integrity against pinned SHA-256…"
    local sha_local
    sha_local="$(shasum -a 256 "${archive}" | awk '{print $1}')"
    if [[ "${sha_local}" != "${FFMPEG_SHA256}" ]]; then
        die "SHA-256 mismatch for ffmpeg-${FFMPEG_VERSION}.tar.xz (got ${sha_local}, expected ${FFMPEG_SHA256})"
    fi
    log "Tarball SHA-256 OK: ${sha_local}"

    log "Extracting source…"
    tar -xJf "${archive}" -C "${SRC_DIR}"
}

configure_ffmpeg() {
    local src="${SRC_DIR}/ffmpeg-${FFMPEG_VERSION}"
    local brew_prefix
    brew_prefix="$(brew --prefix)"

    log "Configuring FFmpeg (LGPL-only, no GPL, no nonfree)…"
    pushd "${src}" >/dev/null

    export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${PKG_CONFIG_PATH:-}"

    ./configure \
        --prefix="${PREFIX_DIR}" \
        --pkg-config-flags="--static" \
        --extra-cflags="-I${brew_prefix}/include" \
        --extra-ldflags="-L${brew_prefix}/lib" \
        --extra-libs="-lpthread -lm" \
        --disable-debug \
        --disable-doc \
        --disable-shared \
        --enable-static \
        --disable-gpl \
        --disable-nonfree \
        --disable-libx264 \
        --disable-libx265 \
        --disable-libxvid \
        --disable-libfdk-aac \
        --disable-libxavs \
        --disable-libxavs2 \
        --disable-libdavs2 \
        --disable-libuavs3d \
        --disable-libopencv \
        --disable-libsmbclient \
        --disable-libtesseract \
        --disable-libvidstab \
        --disable-frei0r \
        --disable-librubberband \
        --disable-postproc \
        --enable-videotoolbox \
        --enable-audiotoolbox \
        --enable-libfreetype \
        --enable-libharfbuzz \
        --enable-libfontconfig \
        --enable-libass \
        --enable-libmp3lame \
        --enable-libopus \
        --enable-libvorbis \
        --enable-libvpx \
        --enable-libdav1d \
        --enable-libwebp \
        --enable-libsnappy \
        --enable-runtime-cpudetect \
        --enable-pthreads \
        --disable-sdl2 \
        --disable-ffplay \
        --disable-libxcb \
        --disable-libxcb-shm \
        --disable-libxcb-shape \
        --disable-libxcb-xfixes \
        --disable-xlib \
        --disable-network \
        --disable-protocol=rtp \
        --disable-protocol=rtmp \
        --enable-protocol=file,pipe,fd,data,subfile,concat,concatf

    popd >/dev/null
}

compile_ffmpeg() {
    local src="${SRC_DIR}/ffmpeg-${FFMPEG_VERSION}"
    local jobs
    jobs="$(sysctl -n hw.ncpu)"
    log "Compiling FFmpeg (-j${jobs})…"
    pushd "${src}" >/dev/null
    make -j"${jobs}" >>"${BUILD_ROOT}/build.log" 2>&1 \
        || { tail -60 "${BUILD_ROOT}/build.log"; die "FFmpeg compile failed — see ${BUILD_ROOT}/build.log"; }
    make install >>"${BUILD_ROOT}/build.log" 2>&1 \
        || { tail -60 "${BUILD_ROOT}/build.log"; die "FFmpeg install failed — see ${BUILD_ROOT}/build.log"; }
    popd >/dev/null
}

stage_binaries() {
    log "Staging binaries into ${OUT_BIN_DIR}…"
    mkdir -p "${OUT_BIN_DIR}"
    install -m 0755 "${PREFIX_DIR}/bin/ffmpeg"  "${OUT_BIN_DIR}/ffmpeg"
    install -m 0755 "${PREFIX_DIR}/bin/ffprobe" "${OUT_BIN_DIR}/ffprobe"
}

bundle_dylibs() {
    log "Bundling non-system dylibs into ${OUT_LIB_DIR}…"
    rm -rf "${OUT_LIB_DIR}"
    mkdir -p "${OUT_LIB_DIR}"

    # dylibbundler rewrites all non-system dylib paths to @loader_path/../lib/
    # so the binary is portable across machines without /opt/homebrew installed.
    for tool in ffmpeg ffprobe; do
        log "  Bundling deps for ${tool}…"
        dylibbundler \
            --overwrite-dir \
            --bundle-deps \
            --create-dir \
            --fix-file "${OUT_BIN_DIR}/${tool}" \
            --dest-dir "${OUT_LIB_DIR}" \
            --install-path "@loader_path/../lib/" \
            >>"${BUILD_ROOT}/dylibbundler.log" 2>&1 \
            || { tail -40 "${BUILD_ROOT}/dylibbundler.log"; die "dylibbundler failed for ${tool}"; }
    done

    # Sanity: no remaining /opt/homebrew or /usr/local references in binaries or dylibs.
    local leaked=0
    while IFS= read -r f; do
        if otool -L "$f" 2>/dev/null | grep -Eq '/opt/homebrew/|/usr/local/'; then
            warn "Dylib still references homebrew/usr-local path: $f"
            otool -L "$f" | grep -E '/opt/homebrew/|/usr/local/' | sed 's/^/    /'
            leaked=1
        fi
    done < <(find "${OUT_BIN_DIR}" "${OUT_LIB_DIR}" -type f \( -name 'ffmpeg' -o -name 'ffprobe' -o -name '*.dylib' \))
    [[ "$leaked" -eq 0 ]] || die "Bundled binaries still depend on non-bundled paths."

    local count
    count="$(find "${OUT_LIB_DIR}" -name '*.dylib' | wc -l | tr -d ' ')"
    log "  Bundled ${count} dylib(s) into ${OUT_LIB_DIR}"
}

sign_binaries() {
    log "Code-signing binaries + dylibs with Developer ID + Hardened Runtime…"

    # Sign dylibs first (inside-out signing order).
    while IFS= read -r dylib; do
        codesign --force --sign "${SIGN_IDENTITY}" \
            --timestamp --options=runtime \
            "${dylib}"
    done < <(find "${OUT_LIB_DIR}" -name '*.dylib' -type f 2>/dev/null)

    # Then the executables themselves.
    for tool in ffmpeg ffprobe; do
        codesign --force --sign "${SIGN_IDENTITY}" \
            --timestamp --options=runtime \
            "${OUT_BIN_DIR}/${tool}"
        codesign -dvv "${OUT_BIN_DIR}/${tool}" 2>&1 | grep -Fq "Authority=${SIGN_IDENTITY}" \
            || die "${tool} not signed correctly"
    done

    # Verify everything is signed with our Team ID.
    while IFS= read -r f; do
        codesign -dvv "$f" 2>&1 | grep -Fq "TeamIdentifier=955RASMBV3" \
            || die "Signature/Team-ID check failed: $f"
    done < <(find "${OUT_BIN_DIR}" "${OUT_LIB_DIR}" -type f \( -name 'ffmpeg' -o -name 'ffprobe' -o -name '*.dylib' \))
}

write_build_config_record() {
    local record="${PROJECT_ROOT}/LICENSES/FFmpeg-BUILD-CONFIG.txt"
    log "Recording build config to ${record}…"
    mkdir -p "${PROJECT_ROOT}/LICENSES"
    {
        echo "BatchForge+ — FFmpeg LGPL Build Record"
        echo "======================================="
        echo ""
        echo "Built: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
        echo "Host:  $(sw_vers -productName) $(sw_vers -productVersion) ($(uname -m))"
        echo "Source: https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"
        echo "Version: ${FFMPEG_VERSION}"
        echo ""
        echo "Configure invocation (verbatim from build_ffmpeg_lgpl.sh):"
        echo "--------------------------------------------------------"
        echo ""
        sed -n '/^configure_ffmpeg()/,/^}/p' "$0" \
            | sed -n '/\.\/configure/,/^$/p' \
            | grep -v '^$' \
            | sed 's/^[[:space:]]*//'
        echo ""
        echo "Linked libraries (verified non-GPL):"
        echo "------------------------------------"
        "${OUT_BIN_DIR}/ffmpeg" -version 2>&1 | head -50
    } > "${record}"
}

# ──────────────────────────────────────────────────────────────────────
# Modes
# ──────────────────────────────────────────────────────────────────────
mode_check() {
    local ffmpeg_bin="${OUT_BIN_DIR}/ffmpeg"
    local ffprobe_bin="${OUT_BIN_DIR}/ffprobe"
    [[ -x "${ffmpeg_bin}" ]] || die "No binary at ${ffmpeg_bin}. Run without --check to build."
    verify_binary "${ffmpeg_bin}"
    verify_binary "${ffprobe_bin}"
    log "Both binaries valid · LGPL-compliant · signed."
    codesign -dvv "${ffmpeg_bin}"  2>&1 | grep -E "Authority|TeamIdentifier" || true
    codesign -dvv "${ffprobe_bin}" 2>&1 | grep -E "Authority|TeamIdentifier" || true
}

mode_clean() {
    log "Wiping ${BUILD_ROOT}…"
    rm -rf "${BUILD_ROOT}"
}

mode_build() {
    mkdir -p "${BUILD_ROOT}"
    : > "${BUILD_ROOT}/build.log"

    require_macos
    require_brew
    require_signing_identity

    install_brew_deps
    fetch_ffmpeg_source
    configure_ffmpeg
    compile_ffmpeg
    stage_binaries
    bundle_dylibs
    sign_binaries
    verify_binary "${OUT_BIN_DIR}/ffmpeg"
    verify_binary "${OUT_BIN_DIR}/ffprobe"
    write_build_config_record

    log "FERTIG."
    log "  ffmpeg  → ${OUT_BIN_DIR}/ffmpeg"
    log "  ffprobe → ${OUT_BIN_DIR}/ffprobe"
    log "  dylibs  → ${OUT_LIB_DIR}/ ($(find "${OUT_LIB_DIR}" -name '*.dylib' 2>/dev/null | wc -l | tr -d ' ') files)"
    log "Build log: ${BUILD_ROOT}/build.log"
}

# ──────────────────────────────────────────────────────────────────────
# Dispatch
# ──────────────────────────────────────────────────────────────────────
case "${1:-build}" in
    build)  mode_build ;;
    --check|check) mode_check ;;
    --clean|clean) mode_clean; mode_build ;;
    -h|--help|help)
        cat <<EOF
Usage: $0 [build|--check|--clean]

  build    (default) Fetch, configure, compile, sign FFmpeg+ffprobe.
  --check  Verify existing Sources/Resources/bin/ffmpeg(probe).
  --clean  Wipe build/ffmpeg-build/ and rebuild from scratch.
EOF
        ;;
    *) die "Unknown argument: $1 — see --help" ;;
esac
