<#
.SYNOPSIS
    Build the LGPL FFmpeg / ffprobe runtime bundled with OnSpec.

.DESCRIPTION
    Maximal-LGPL philosophy. We do NOT use --disable-everything any more;
    instead we let FFmpeg's default LGPL build apply, gated only by three
    non-negotiable license switches, and we link in every BSD / LGPL / MIT /
    ISC external library that is present in the MSYS2 ucrt64 environment.

    The build is verified post-link against the deny-list manifest at
    config/lgpl_build_manifest.json (PR 6) so any accidental GPL / nonfree
    creep fails the build immediately.

.NOTES
    LGPL invariants (sacred; do not relax without a license review):

        --disable-gpl
            Excludes GPL-only components: libx264, libx265, libvidstab,
            libxavs*, libdavs2, libxvid, librubberband, libcdio, libsmbclient,
            and the GPL-only filters (hqdn3d, cropdetect, blackframe, boxblur,
            colormatrix, delogo, eq, fspp, histeq, interlace, kerndeint,
            mcdeint, mpdecimate, nnedi, owdenoise, perspective, phase, pp,
            pp7, pullup, repeatfields, sab, signature, smartblur, spp,
            stereo3d, super2xsai, tinterlace, uspp, vaguedenoiser, mptestsrc,
            cover_rect, find_rect).

        --disable-nonfree
            Excludes nonfree components: libfdk_aac, *_nvenc, *_qsv, *_amf,
            libnpp.

        --disable-network
            Excludes network protocols (HTTP / HTTPS / RTMP / RTSP /
            streaming inputs). OnSpec is a file-based analyzer.

    Philosophy: DENY-list, not ALLOW-list. We trust FFmpeg's default
    component set under the three flags above, and verify after the fact
    that no GPL / nonfree symbol or filter name leaked into the binary.

    Every --enable-libXXX flag below maps to a license-verified BSD / LGPL /
    MIT / ISC external library. See config/lgpl_build_manifest.json for the
    authoritative contract.
#>

param(
    [Parameter(Mandatory = $true)][string]$RepoRoot,
    [Parameter(Mandatory = $true)][string]$WorkRoot,
    [Parameter(Mandatory = $true)][string]$MsysRoot,
    [Parameter(Mandatory = $true)][string]$MpvRepo,
    [Parameter(Mandatory = $true)][string]$MpvCommit,
    [Parameter(Mandatory = $true)][string]$FfmpegRepo,
    [Parameter(Mandatory = $true)][string]$FfmpegRef,
    # Target CPU architecture. "x86_64" (default) builds under the MSYS2
    # UCRT64 environment exactly as before; "aarch64" builds the same
    # maximal-LGPL configuration under the MSYS2 CLANGARM64 environment for
    # native Windows-on-ARM64. Same pins, same external-lib set, same LGPL
    # invariants — only toolchain/arch-specific configure flags differ.
    [ValidateSet("x86_64", "aarch64")][string]$TargetArch = "x86_64"
)

$ErrorActionPreference = "Stop"

function Write-Log([string]$msg) {
    Write-Host "[lgpl-build] $msg"
}

# ---------------------------------------------------------------------------
# Per-arch toolchain profile. The LGPL contract (license switches, external
# lib set, deny-list verification) is arch-independent; only the MSYS2
# environment, compilers, pacman package prefix, and CPU-specific configure
# flags vary. aarch64 uses CLANGARM64 — MSYS2's only Windows-ARM64
# environment (GCC does not target aarch64-w64-mingw32).
# ---------------------------------------------------------------------------

if ($TargetArch -eq "aarch64") {
    $archProfile = @{
        MsysEnvName   = "CLANGARM64"
        EnvSubdir     = "clangarm64"
        EnvPrefix     = "/clangarm64"
        PacmanPrefix  = "mingw-w64-clang-aarch64"
        Cc            = "/clangarm64/bin/clang"
        Cxx           = "/clangarm64/bin/clang++"
        MingwChost    = "aarch64-w64-mingw32"
        ConfigureArch = @(
            "--arch=aarch64",
            # NEON is the aarch64 SIMD baseline; FFmpeg's handwritten ARM
            # asm replaces the x86asm path. No --enable-x86asm here.
            "--enable-neon",
            # armv8-a is the Windows-on-ARM64 hardware floor (the ARM
            # counterpart of the x86-64-v2 floor used on the x64 build).
            "--extra-cflags=`"-O3 -march=armv8-a -mtune=generic -ftree-vectorize`""
        )
        OutDirName    = "out\ffmpeg-aarch64"
    }
} else {
    $archProfile = @{
        MsysEnvName   = "UCRT64"
        EnvSubdir     = "ucrt64"
        EnvPrefix     = "/ucrt64"
        PacmanPrefix  = "mingw-w64-ucrt-x86_64"
        Cc            = "/ucrt64/bin/gcc"
        Cxx           = "/ucrt64/bin/g++"
        MingwChost    = "x86_64-w64-mingw32"
        ConfigureArch = @(
            "--arch=x86_64",
            "--enable-x86asm",
            "--extra-cflags=`"-O3 -march=x86-64-v2 -mtune=generic -ftree-vectorize`""
        )
        OutDirName    = "out\ffmpeg"
    }
}

Write-Log "Target arch: $TargetArch (MSYS2 env: $($archProfile.MsysEnvName))"

function Convert-ToMsysPath([string]$path) {
    $norm = ($path -replace "\\","/")
    if ($norm -match "^([A-Za-z]):/(.+)$") {
        return "/" + $Matches[1].ToLowerInvariant() + "/" + $Matches[2]
    }
    return $norm
}

# ---------------------------------------------------------------------------
# Step 1: mpv-winbuild-cmake checkout (kept for parity with prior pipeline;
# the patch file is a placeholder today and is logged but not required).
# ---------------------------------------------------------------------------

$mpvDir = Join-Path $WorkRoot "mpv-winbuild-cmake"
if (-not (Test-Path $mpvDir)) {
    Write-Log "Cloning mpv-winbuild-cmake..."
    git clone --recursive $MpvRepo $mpvDir | Out-Null
}

Push-Location $mpvDir
try {
    git fetch --all --tags | Out-Null
    git checkout $MpvCommit | Out-Null
    git submodule update --init --recursive | Out-Null
}
finally {
    Pop-Location
}

$patchFile = Join-Path $RepoRoot "scripts\runtime_lgpl\compile-lgpl.patch"
if (Test-Path $patchFile) {
    $patchText = Get-Content -Raw -Encoding UTF8 -Path $patchFile
    Push-Location $mpvDir
    try {
        if ($patchText -match "^\s*diff --git\s+") {
            cmd /c "git apply --check `"$patchFile`" >nul 2>nul"
            if ($LASTEXITCODE -eq 0) {
                Write-Log "Applying LGPL patch."
                git apply $patchFile
            } else {
                Write-Log "LGPL patch present but did not apply cleanly; continuing with upstream defaults."
            }
        } else {
            Write-Log "LGPL patch placeholder detected; skipping patch application."
        }
    }
    finally {
        Pop-Location
    }
}

# ---------------------------------------------------------------------------
# Step 2: FFmpeg source checkout.
# ---------------------------------------------------------------------------

$ffmpegDir = Join-Path $WorkRoot "ffmpeg"
if (-not (Test-Path $ffmpegDir)) {
    Write-Log "Cloning FFmpeg source..."
    git clone $FfmpegRepo $ffmpegDir | Out-Null
}
Push-Location $ffmpegDir
try {
    git fetch --all --tags | Out-Null
    git checkout $FfmpegRef | Out-Null
}
finally {
    Pop-Location
}

# ---------------------------------------------------------------------------
# Step 2b: Patch compat/windows/makedef to support a @<file> response-file
# argument. This works around the Windows / MSYS2 ~32 KB command-line length
# limit hit when building with many enabled external libraries: the
# avcodec-XX.dll def-generation step has to pass ~700 .o files in a single
# argv, which truncates and produces "Object does not exist: libavcodec/f".
# The companion patch to ffbuild/config.mak (Step 5b below) writes the OBJS
# list to a .objs.list file via Make's $(file ...) function and invokes
# makedef with @<that_file>.
# ---------------------------------------------------------------------------

$makedefPath = Join-Path $ffmpegDir "compat\windows\makedef"
if (Test-Path $makedefPath) {
    $makedefText = Get-Content -Raw -Encoding ASCII -Path $makedefPath
    if ($makedefText -notmatch "OnSpec LGPL build patch") {
        Write-Log "Patching compat/windows/makedef for response-file + xargs support."
        $needle1 = @"
vscript=`$1
shift

if [ ! -f "`$vscript" ]; then
    echo "Version script does not exist" >&2
    exit 1
fi

for object in "`$@"; do
"@
        $replacement1 = @"
vscript=`$1
shift

if [ ! -f "`$vscript" ]; then
    echo "Version script does not exist" >&2
    exit 1
fi

# OnSpec LGPL build patch: allow the object list to be supplied via a
# @<file> response file as the first positional argument. Works around
# the Windows / MSYS2 command-line length limit hit when building with
# many enabled external libraries (OBJS expansion exceeds ~32 KB).
if [ `$# -ge 1 ]; then
    case "`$1" in
        @*)
            response_file=`${1#@}
            if [ ! -f "`$response_file" ]; then
                echo "Response file does not exist: `${response_file}" >&2
                exit 1
            fi
            set -f
            set --
            set -- `$(cat "`$response_file")
            set +f
            ;;
    esac
fi

for object in "`$@"; do
"@
        $needle2 = @"
if [ -n "`$AR" ]; then
    `$AR rcs `${libname} `$@ >/dev/null
else
    lib.exe -out:`${libname} `$@ >/dev/null
fi
if [ `$? != 0 ]; then
    echo "Could not create temporary library." >&2
    exit 1
fi
"@
        $replacement2 = @"
if [ -n "`$AR" ]; then
    # OnSpec LGPL build patch: pipe object list through xargs so that ar is
    # invoked in chunks that fit under the Windows / MSYS2 ~32 KB command-line
    # limit. ``ar rcs`` is incremental, so multiple appends into the same
    # archive produce the same result as one giant invocation.
    if ! printf '%s\n' "`$@" | xargs `$AR rcs `${libname} >/dev/null; then
        echo "Could not create temporary library." >&2
        exit 1
    fi
else
    if ! printf '%s\n' "`$@" | xargs lib.exe -out:`${libname} >/dev/null; then
        echo "Could not create temporary library." >&2
        exit 1
    fi
fi
"@
        if ($makedefText.Contains($needle1) -and $makedefText.Contains($needle2)) {
            $patched = $makedefText.Replace($needle1, $replacement1).Replace($needle2, $replacement2)
            [System.IO.File]::WriteAllText($makedefPath, $patched, (New-Object System.Text.ASCIIEncoding))
            Write-Log "compat/windows/makedef patched (response-file + xargs)."
        } else {
            Write-Log "WARN: compat/windows/makedef did not match expected pattern; not patched."
        }
    } else {
        Write-Log "compat/windows/makedef already carries OnSpec response-file patch."
    }
}

# Companion patch to ffbuild/library.mak: route the shared-lib LD step through
# a per-library .ldobjs.list response file via Make's $(file ...) function so
# the linker never sees the full $(filter %.o,$^) expansion on its argv. GNU
# ld supports @<file> natively. Without this, libavcodec linking dies with
# "cannot find libavcodec/evc_par: No such file or directory" (truncation of
# libavcodec/evc_parse.o) due to the same Windows / MSYS2 ~32 KB limit.
$libraryMakPath = Join-Path $ffmpegDir "ffbuild\library.mak"
if (Test-Path $libraryMakPath) {
    $libMakText = Get-Content -Raw -Encoding ASCII -Path $libraryMakPath
    if ($libMakText -match 'lib\$\(NAME\)\.ldobjs\.list') {
        Write-Log "ffbuild/library.mak already carries OnSpec response-file LD patch."
    } else {
        $oldLd = "`t`$`$(LD) `$(SHFLAGS) `$(LDFLAGS) `$(LDSOFLAGS) `$`$(LD_O) `$`$(filter %.o,`$`$^) `$(FFEXTRALIBS)"
        $newLd = "`t`$`$(file >`$(SUBDIR)lib`$(NAME).ldobjs.list,`$`$(filter %.o,`$`$^))`$`$(LD) `$(SHFLAGS) `$(LDFLAGS) `$(LDSOFLAGS) `$`$(LD_O) @`$(SUBDIR)lib`$(NAME).ldobjs.list `$(FFEXTRALIBS)"
        if ($libMakText.Contains($oldLd)) {
            $patched = $libMakText.Replace($oldLd, $newLd)
            [System.IO.File]::WriteAllText($libraryMakPath, $patched, (New-Object System.Text.ASCIIEncoding))
            Write-Log "ffbuild/library.mak LD step patched (response-file mode)."
        } else {
            Write-Log "WARN: ffbuild/library.mak LD recipe did not match expected pattern; not patched."
        }
    }
}

# ---------------------------------------------------------------------------
# Step 2c: Patch libavcodec/libsvtav1.c for SVT-AV1 >= 3.0.0 / 4.0.0 API
# compatibility. MSYS2 ucrt64 ships SVT-AV1 4.x; FFmpeg n7.1.1's binding
# was written against the legacy 2.x API and fails to compile against
# the newer headers with two distinct breakages:
#
#   (a) svt_av1_enc_init_handle() lost its middle "context" argument in
#       SVT-AV1 3.0.0 (upstream commit 988e930c). FFmpeg fixed this in
#       d1ed5c06e3 ("avcodec/libsvtav1: unbreak build with latest svtav1")
#       by gating the call signature behind SVT_AV1_CHECK_VERSION(3, 0, 0).
#
#   (b) EbSvtAv1EncConfiguration.enable_adaptive_quantization was renamed
#       to .aq_mode in SVT-AV1 4.0.0. FFmpeg fixed this in bdc8c505f4
#       ("avcodec/libsvtav1: rename aq_mode for v4.0.0") by gating the
#       field name behind SVT_AV1_CHECK_VERSION(4, 0, 0).
#
# Both fixes landed on FFmpeg master but were not backported to the
# n7.1.1 release tag we pin. We carry them here as in-tree patches so the
# bundled SVT-AV1 4.x encoder links cleanly without bumping FFmpeg.
# Patches are idempotent via the OnSpec marker; on a fresh clone they
# apply, on a re-run they are a no-op.
# ---------------------------------------------------------------------------

$svtPatchPath = Join-Path $ffmpegDir "libavcodec\libsvtav1.c"
if (Test-Path $svtPatchPath) {
    $svtText = Get-Content -Raw -Encoding ASCII -Path $svtPatchPath
    if ($svtText -match "OnSpec LGPL build patch: SVT-AV1") {
        Write-Log "libavcodec/libsvtav1.c already carries OnSpec SVT-AV1 4.x compat patch."
    } else {
        $svtAqOld = @"
    } else if (svt_enc->qp > 0) {
        param->qp                   = svt_enc->qp;
        param->rate_control_mode    = 0;
        param->enable_adaptive_quantization = 0;
    }
"@
        $svtAqNew = @"
    } else if (svt_enc->qp > 0) {
        param->qp                   = svt_enc->qp;
        param->rate_control_mode    = 0;
        /* OnSpec LGPL build patch: SVT-AV1 4.0.0 renamed
         * enable_adaptive_quantization to aq_mode. Mirrors upstream
         * FFmpeg commit bdc8c505f4 ("rename aq_mode for v4.0.0"). */
#if SVT_AV1_CHECK_VERSION(4, 0, 0)
        param->aq_mode = 0;
#else
        param->enable_adaptive_quantization = 0;
#endif
    }
"@
        $svtInitOld = @"
    svt_enc->eos_flag = EOS_NOT_REACHED;

    svt_ret = svt_av1_enc_init_handle(&svt_enc->svt_handle, svt_enc, &svt_enc->enc_params);
"@
        $svtInitNew = @"
    svt_enc->eos_flag = EOS_NOT_REACHED;

    /* OnSpec LGPL build patch: SVT-AV1 3.0.0 dropped the middle
     * "context" argument from svt_av1_enc_init_handle(). Mirrors
     * upstream FFmpeg commit d1ed5c06e3 ("unbreak build with latest
     * svtav1"). */
#if SVT_AV1_CHECK_VERSION(3, 0, 0)
    svt_ret = svt_av1_enc_init_handle(&svt_enc->svt_handle, &svt_enc->enc_params);
#else
    svt_ret = svt_av1_enc_init_handle(&svt_enc->svt_handle, svt_enc, &svt_enc->enc_params);
#endif
"@
        if ($svtText.Contains($svtAqOld) -and $svtText.Contains($svtInitOld)) {
            Write-Log "Patching libavcodec/libsvtav1.c for SVT-AV1 4.x API compatibility."
            $svtPatched = $svtText.Replace($svtAqOld, $svtAqNew).Replace($svtInitOld, $svtInitNew)
            [System.IO.File]::WriteAllText($svtPatchPath, $svtPatched, (New-Object System.Text.ASCIIEncoding))
            Write-Log "libavcodec/libsvtav1.c patched (svt_av1_enc_init_handle arity + aq_mode rename)."
        } else {
            Write-Log "WARN: libavcodec/libsvtav1.c did not match expected pattern; not patched. libsvtav1 may auto-drop at make time."
        }
    }
}

# ---------------------------------------------------------------------------
# Step 3: MSYS2 bash + UCRT64 toolchain pacman install for the three missing
# LGPL-compatible encoder packages.
# ---------------------------------------------------------------------------

$bash = Join-Path $MsysRoot "usr\bin\bash.exe"
if (-not (Test-Path $bash)) {
    throw "MSYS2 bash not found at $bash"
}

# The MSYS2 bootstrap provisions the UCRT64 (x86_64) toolchain. For aarch64
# the CLANGARM64 toolchain must be present too; install it on demand. nasm /
# yasm are x86-only assemblers and intentionally absent here — aarch64 SIMD
# is compiled from FFmpeg's ARM asm by clang itself.
if ($TargetArch -eq "aarch64") {
    $ccPath = Join-Path $MsysRoot "clangarm64\bin\clang.exe"
    if (-not (Test-Path $ccPath)) {
        $toolchainPkgs = @(
            "$($archProfile.PacmanPrefix)-clang",
            "$($archProfile.PacmanPrefix)-cmake",
            "$($archProfile.PacmanPrefix)-make",
            "$($archProfile.PacmanPrefix)-ninja",
            "$($archProfile.PacmanPrefix)-meson",
            "$($archProfile.PacmanPrefix)-pkgconf",
            "$($archProfile.PacmanPrefix)-python",
            "$($archProfile.PacmanPrefix)-vmaf"
        ) -join " "
        Write-Log "Installing CLANGARM64 toolchain packages via pacman..."
        & $bash -lc "pacman -S --noconfirm --needed $toolchainPkgs"
        if ($LASTEXITCODE -ne 0) {
            & $bash -lc "pacman -Sy --noconfirm"
            & $bash -lc "pacman -S --noconfirm --needed $toolchainPkgs"
            if ($LASTEXITCODE -ne 0) {
                throw "pacman install of CLANGARM64 toolchain packages failed after retry."
            }
        }
    }
    if (-not (Test-Path $ccPath)) {
        throw "CLANGARM64 clang not found at $ccPath after toolchain install."
    }
}

# ---------------------------------------------------------------------------
# Step 3b: Ensure the full external-library dependency set for the target arch.
# The MSYS2 bootstrap only provisions the base toolchain; the BSD / LGPL / MIT /
# ISC runtime libraries that FFmpeg links via --enable-libXXX must be present in
# the active environment's pkgconfig tree. These were historically pre-installed
# for UCRT64 (x86_64); installing them here with --needed makes EVERY arch
# self-contained and keeps the external-lib set identical across arches, which
# the Phase 3 capability-parity contract requires (a missing lib silently drops
# a --enable flag in Step 4 and would diverge arch capabilities). Idempotent:
# --needed is a no-op for already-installed packages. The encoder packages
# (openh264, kvazaar, twolame, svt-av1) are handled by their own block below;
# libvmaf ships with the toolchain set above (and the UCRT64 bootstrap).
$pacmanRuntimeLibSuffixes = @(
    "zimg", "dav1d", "libvpx", "opus", "libvorbis", "libtheora", "speex",
    "openjpeg2", "libwebp", "libsoxr", "libmysofa", "libplacebo", "libass",
    "libbluray", "freetype", "harfbuzz", "fontconfig", "libxml2", "lcms2",
    "aom", "rav1e", "lame",
    # LLVM OpenMP runtime (libomp.dll). NOT an FFmpeg --enable-libXXX target, but
    # a *runtime* dependency several of the libraries above load: libsoxr (and
    # some codec libs, e.g. AV1) import libomp.dll. On CLANGARM64 it ships as a
    # separate toolchain package that the MSYS bootstrap does not install by
    # default, so without it swresample/avcodec/avfilter fail to load at runtime
    # (LoadLibrary err 126 -> ffmpeg.exe aborts with 0xC0000135) even though the
    # link and config.h LGPL checks pass. License: Apache-2.0 WITH LLVM-exception
    # (permissive; allowed). The extract step's ldd closure bundles it once present.
    "llvm-openmp"
)
$pacmanRuntimeLibPkgs = $pacmanRuntimeLibSuffixes | ForEach-Object { "$($archProfile.PacmanPrefix)-$_" }
Write-Log "Ensuring $($pacmanRuntimeLibPkgs.Count) external runtime libraries present via pacman --needed..."
$libPacmanCmd = "pacman -S --noconfirm --needed " + ($pacmanRuntimeLibPkgs -join " ")
& $bash -lc $libPacmanCmd
if ($LASTEXITCODE -ne 0) {
    Write-Log "External-lib pacman install failed (exit=$LASTEXITCODE); refreshing db and retrying once."
    & $bash -lc "pacman -Sy --noconfirm"
    & $bash -lc $libPacmanCmd
    if ($LASTEXITCODE -ne 0) {
        # Do not hard-fail here: the per-flag .pc validation in Step 4 drops any
        # genuinely missing library, and the strict deny-list / required-
        # capability verifier (scripts/verify_runtime_lgpl.ps1 -Strict) remains
        # the authoritative gate. A transient mirror/name issue must not block
        # the whole pipeline.
        Write-Log "WARN: external-lib pacman install still failing after retry; Step 4 .pc validation + strict verifier will gate capability parity."
    }
}

$pkgConfigDir = Join-Path $MsysRoot "$($archProfile.EnvSubdir)\lib\pkgconfig"

# (--enable-libXXX flag, pkg-config .pc file under ucrt64/lib/pkgconfig/)
$externalLibs = @(
    @{ Flag = "libzimg";    Pc = "zimg.pc" },
    @{ Flag = "libdav1d";   Pc = "dav1d.pc" },
    @{ Flag = "libvpx";     Pc = "vpx.pc" },
    @{ Flag = "libopus";    Pc = "opus.pc" },
    @{ Flag = "libvorbis";  Pc = "vorbis.pc" },
    @{ Flag = "libtheora";  Pc = "theora.pc" },
    @{ Flag = "libspeex";   Pc = "speex.pc" },
    @{ Flag = "libopenjpeg"; Pc = "libopenjp2.pc" },
    @{ Flag = "libwebp";    Pc = "libwebp.pc" },
    @{ Flag = "libsoxr";    Pc = "soxr.pc" },
    @{ Flag = "libmysofa";  Pc = "libmysofa.pc" },
    @{ Flag = "libplacebo"; Pc = "libplacebo.pc" },
    @{ Flag = "libass";     Pc = "libass.pc" },
    @{ Flag = "libbluray";  Pc = "libbluray.pc" },
    @{ Flag = "libfreetype"; Pc = "freetype2.pc" },
    @{ Flag = "libharfbuzz"; Pc = "harfbuzz.pc" },
    @{ Flag = "fontconfig"; Pc = "fontconfig.pc" },
    @{ Flag = "libxml2";    Pc = "libxml-2.0.pc" },
    @{ Flag = "lcms2";      Pc = "lcms2.pc" },
    @{ Flag = "libaom";     Pc = "aom.pc" },
    @{ Flag = "librav1e";   Pc = "rav1e.pc" },
    @{ Flag = "libsvtav1";  Pc = "SvtAv1Enc.pc" },
    @{ Flag = "libopenh264"; Pc = "openh264.pc" },
    @{ Flag = "libkvazaar"; Pc = "kvazaar.pc" },
    @{ Flag = "libmp3lame"; Pc = "lame.pc" },
    @{ Flag = "libtwolame"; Pc = "twolame.pc" },
    @{ Flag = "libvmaf";    Pc = "libvmaf.pc" }
)

$pacmanEncoderPkgs = @(
    "$($archProfile.PacmanPrefix)-openh264",
    "$($archProfile.PacmanPrefix)-kvazaar",
    "$($archProfile.PacmanPrefix)-twolame",
    "$($archProfile.PacmanPrefix)-svt-av1"
)
$requiredEncoderPcs = @("openh264.pc", "kvazaar.pc", "twolame.pc", "SvtAv1Enc.pc")

$needPacmanInstall = $false
foreach ($pc in $requiredEncoderPcs) {
    if (-not (Test-Path (Join-Path $pkgConfigDir $pc))) {
        $needPacmanInstall = $true
        break
    }
}

if ($needPacmanInstall) {
    Write-Log "Installing missing LGPL encoder packages via pacman: $($pacmanEncoderPkgs -join ', ')"
    $pacmanArgs = $pacmanEncoderPkgs -join " "
    $pacmanCmd = "pacman -S --noconfirm --needed $pacmanArgs"
    & $bash -lc $pacmanCmd
    if ($LASTEXITCODE -ne 0) {
        Write-Log "pacman install failed (exit=$LASTEXITCODE); retrying once after refreshing keyring/db."
        & $bash -lc "pacman -Sy --noconfirm"
        & $bash -lc $pacmanCmd
        if ($LASTEXITCODE -ne 0) {
            throw "pacman install of LGPL encoder packages failed after retry."
        }
    }
} else {
    Write-Log "All LGPL encoder packages (openh264, kvazaar, twolame) already installed."
}

$missingRequired = @()
foreach ($pc in $requiredEncoderPcs) {
    if (-not (Test-Path (Join-Path $pkgConfigDir $pc))) {
        $missingRequired += $pc
    }
}
if ($missingRequired.Count -gt 0) {
    throw "Required encoder pkg-config files missing after pacman install: $($missingRequired -join ', ')"
}

# ---------------------------------------------------------------------------
# Step 4: Validate each --enable-libXXX flag against the actual pkgconfig
# directory. Gracefully drop any lib whose .pc file is missing (with a
# log line), instead of failing the whole build over a single optional
# external library.
# ---------------------------------------------------------------------------

$confirmedLibFlags = New-Object System.Collections.Generic.List[string]
$droppedLibs = New-Object System.Collections.Generic.List[string]
foreach ($entry in $externalLibs) {
    $pcPath = Join-Path $pkgConfigDir $entry.Pc
    if (Test-Path $pcPath) {
        $confirmedLibFlags.Add("--enable-$($entry.Flag)") | Out-Null
    } else {
        $droppedLibs.Add("$($entry.Flag) ($($entry.Pc) missing)") | Out-Null
        Write-Log "WARN: dropping --enable-$($entry.Flag): $($entry.Pc) not present in $pkgConfigDir"
    }
}

if ($droppedLibs.Count -gt 0) {
    Write-Log "Dropped external libs (logged for the capability diff): $($droppedLibs -join '; ')"
}
Write-Log "Linking $($confirmedLibFlags.Count) external libs into FFmpeg build."

# ---------------------------------------------------------------------------
# Step 5: Compose the configure invocation (maximal-LGPL, deny-list).
# ---------------------------------------------------------------------------

$ffmpegDirUnix = Convert-ToMsysPath $ffmpegDir
$outDir = Join-Path $WorkRoot $archProfile.OutDirName
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
$outDirUnix = Convert-ToMsysPath $outDir

$baseConfigureFlags = @(
    "--target-os=mingw32",
    "--cc=$($archProfile.Cc)",
    "--cxx=$($archProfile.Cxx)",
    "--pkg-config=$($archProfile.EnvPrefix)/bin/pkg-config",
    "--prefix='$outDirUnix'",
    "--enable-shared",
    "--disable-static",
    "--enable-ffmpeg",
    "--enable-ffprobe",
    "--disable-ffplay",
    "--disable-doc",
    "--disable-debug",
    "--disable-gpl",
    "--disable-nonfree",
    "--disable-network",
    "--enable-zlib",
    # Performance: belt-and-suspenders optimization / SIMD flags. The
    # Phase 2 perf validation surfaced a measurable slowdown on C-only
    # filters (blockdetect, signalstats) versus a sibling MSYS2 ucrt64
    # ffmpeg build. Handwritten asm and runtime CPU detection are FFmpeg
    # defaults, but we set them explicitly so the build manifest
    # documents the contract. The arch-specific extra-cflags (in
    # $archProfile.ConfigureArch) raise the C-codegen floor — x86-64-v2
    # (SSE4.2) on x64, armv8-a on ARM64 — and re-enable the tree
    # vectoriser that FFmpeg's optflags otherwise disable (handwritten
    # asm beats it for codecs, but for our C-only analyser filters the
    # vectoriser is a net win). -mtune=generic keeps the binary portable
    # across modern Windows hosts.
    "--enable-asm",
    "--enable-optimizations",
    "--enable-runtime-cpudetect"
) + $archProfile.ConfigureArch

$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$bashEnvLines = @(
    'set -eo pipefail',
    "export PATH=$($archProfile.EnvPrefix)/bin:/usr/bin:`$PATH",
    "export MSYSTEM=$($archProfile.MsysEnvName)",
    'export CHERE_INVOKING=1',
    "export MINGW_PREFIX=$($archProfile.EnvPrefix)",
    "export MINGW_CHOST=$($archProfile.MingwChost)",
    "export CC=$($archProfile.Cc)",
    "export CXX=$($archProfile.Cxx)",
    "export PKG_CONFIG=$($archProfile.EnvPrefix)/bin/pkg-config"
)

function Invoke-Configure([System.Collections.Generic.List[string]]$libFlags, [string]$scriptPath, [string]$scriptUnix) {
    $flags = $baseConfigureFlags + $libFlags
    $configureCmd = "./configure " + ($flags -join " ")
    $configureLines = $bashEnvLines + @(
        'echo "[lgpl-build] using compiler: $(command -v "$CC" || true)"',
        "cd '$ffmpegDirUnix'",
        'MAKE_BIN=$(command -v mingw32-make || command -v make)',
        '"$MAKE_BIN" distclean >/dev/null 2>&1 || true',
        $configureCmd
    )
    [System.IO.File]::WriteAllText($scriptPath, ($configureLines -join "`n"), $utf8NoBom)
    return (& $bash -lc "bash '$scriptUnix' 2>&1") | Out-String
}

function Patch-ConfigMak([string]$ffmpegDir) {
    # Companion to the makedef response-file patch in Step 2b. Rewrites the
    # SLIB_CREATE_DEF_CMD line in the configure-generated config.mak so that
    # OBJS is dumped to a per-library .objs.list file via Make's $(file ...)
    # function rather than expanded onto the command line. Without this,
    # libavcodec's def-generation step exceeds the Windows / MSYS2 ~32 KB
    # command-line limit. config.mak is regenerated by configure, so this
    # function must be called after every successful configure invocation.
    $configMak = Join-Path $ffmpegDir "ffbuild\config.mak"
    if (-not (Test-Path $configMak)) {
        Write-Log "WARN: ffbuild/config.mak not found at $configMak; cannot apply def-cmd patch."
        return
    }
    $text = Get-Content -Raw -Encoding ASCII -Path $configMak
    if ($text -match 'lib\$\(NAME\)\.objs\.list') {
        Write-Log "ffbuild/config.mak already carries OnSpec response-file def-cmd patch."
        return
    }
    $oldCmd = 'SLIB_CREATE_DEF_CMD=EXTERN_PREFIX="$(EXTERN_PREFIX)" AR="$(AR_CMD)" NM="$(NM_CMD)" $(SRC_PATH)/compat/windows/makedef $(SUBDIR)lib$(NAME).ver $(OBJS) > $$(@:$(SLIBSUF)=.def)'
    $newCmd = 'SLIB_CREATE_DEF_CMD=$(file >$(SUBDIR)lib$(NAME).objs.list,$(OBJS))EXTERN_PREFIX="$(EXTERN_PREFIX)" AR="$(AR_CMD)" NM="$(NM_CMD)" $(SRC_PATH)/compat/windows/makedef $(SUBDIR)lib$(NAME).ver @$(SUBDIR)lib$(NAME).objs.list > $$(@:$(SLIBSUF)=.def)'
    if (-not $text.Contains($oldCmd)) {
        Write-Log "WARN: ffbuild/config.mak SLIB_CREATE_DEF_CMD line did not match expected pattern; not patched."
        return
    }
    $patched = $text.Replace($oldCmd, $newCmd)
    [System.IO.File]::WriteAllText($configMak, $patched, (New-Object System.Text.ASCIIEncoding))
    Write-Log "ffbuild/config.mak SLIB_CREATE_DEF_CMD patched (response-file mode)."
}

$configureScriptPath = Join-Path $WorkRoot "run_ffmpeg_configure.sh"
$configureScriptUnix = Convert-ToMsysPath $configureScriptPath

Write-Log "Configuring FFmpeg (maximal-LGPL, $($confirmedLibFlags.Count) external libs)..."
$configureOutput = Invoke-Configure $confirmedLibFlags $configureScriptPath $configureScriptUnix
Write-Host $configureOutput

if ($LASTEXITCODE -ne 0) {
    Write-Log "Configure failed; scanning output for libraries that should be dropped..."
    $failedLibPattern = [regex]'ERROR:\s+(lib[A-Za-z0-9_]+|[a-z]+)\s+(?:>=\s*[\d.]+\s+)?not found'
    $matchResults = $failedLibPattern.Matches($configureOutput)
    if ($matchResults.Count -eq 0) {
        Write-Host $configureOutput
        throw "FFmpeg configure failed and no recognizable 'library not found' marker was emitted."
    }
    $libsToDrop = @($matchResults | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
    $newFlags = New-Object System.Collections.Generic.List[string]
    foreach ($flag in $confirmedLibFlags) {
        $libName = $flag -replace '^--enable-', ''
        if ($libsToDrop -contains $libName) {
            $droppedLibs.Add("$libName (configure: not usable in this env)") | Out-Null
            Write-Log "WARN: dropping --enable-$libName after configure failure: see above for upstream reason."
        } else {
            $newFlags.Add($flag) | Out-Null
        }
    }
    if ($newFlags.Count -eq $confirmedLibFlags.Count) {
        throw "FFmpeg configure failed but parsed-drop set $libsToDrop did not match any --enable-libXXX flag."
    }
    $confirmedLibFlags = $newFlags
    Write-Log "Retrying configure with $($confirmedLibFlags.Count) external libs (dropped: $($libsToDrop -join ', '))."
    $configureOutput = Invoke-Configure $confirmedLibFlags $configureScriptPath $configureScriptUnix
    Write-Host $configureOutput
    if ($LASTEXITCODE -ne 0) {
        throw "FFmpeg configure failed on retry after dropping $($libsToDrop -join ', ')."
    }
}

Patch-ConfigMak $ffmpegDir

function Invoke-Make([string]$scriptPath, [string]$scriptUnix) {
    $makeLines = $bashEnvLines + @(
        "cd '$ffmpegDirUnix'",
        'MAKE_BIN=$(command -v mingw32-make || command -v make)',
        '"$MAKE_BIN" -j$(nproc)',
        '"$MAKE_BIN" install'
    )
    [System.IO.File]::WriteAllText($scriptPath, ($makeLines -join "`n"), $utf8NoBom)
    return (& $bash -lc "bash '$scriptUnix' 2>&1") | Out-String
}

$makeScriptPath = Join-Path $WorkRoot "run_ffmpeg_make.sh"
$makeScriptUnix = Convert-ToMsysPath $makeScriptPath

Write-Log "Configure OK. Running make + make install..."
$makeOutput = Invoke-Make $makeScriptPath $makeScriptUnix
Write-Host $makeOutput

if ($LASTEXITCODE -ne 0) {
    Write-Log "Make failed; scanning output for incompatible external-lib bindings to drop and retry..."
    # FFmpeg places each external-lib wrapper at libav{codec,format,filter,util,device}/libXXX.c
    # so a compile error pointing at one of those files maps cleanly to --enable-libXXX.
    $libCompileFailPattern = [regex]'libav(?:codec|format|filter|util|device)/(lib[a-z0-9_]+)\.c:\d+:\d+:\s+error:'
    $matchResults = $libCompileFailPattern.Matches($makeOutput)
    if ($matchResults.Count -eq 0) {
        Write-Host $makeOutput
        throw "FFmpeg make failed and no recognizable 'libXXX.c: error:' marker was emitted; cannot auto-drop."
    }
    $libsToDrop = @($matchResults | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
    $newFlags = New-Object System.Collections.Generic.List[string]
    foreach ($flag in $confirmedLibFlags) {
        $libName = $flag -replace '^--enable-', ''
        if ($libsToDrop -contains $libName) {
            $droppedLibs.Add("$libName (make: source/header API incompatible with FFmpeg $FfmpegRef)") | Out-Null
            Write-Log "WARN: dropping --enable-$libName after make failure: $($flag) source binding rejected the installed library headers."
        } else {
            $newFlags.Add($flag) | Out-Null
        }
    }
    if ($newFlags.Count -eq $confirmedLibFlags.Count) {
        throw "FFmpeg make failed but parsed-drop set $libsToDrop did not match any --enable-libXXX flag."
    }
    $confirmedLibFlags = $newFlags

    Write-Log "Re-running configure with $($confirmedLibFlags.Count) external libs (dropped: $($libsToDrop -join ', '))."
    $configureOutput = Invoke-Configure $confirmedLibFlags $configureScriptPath $configureScriptUnix
    Write-Host $configureOutput
    if ($LASTEXITCODE -ne 0) {
        throw "FFmpeg configure failed on retry after dropping $($libsToDrop -join ', ')."
    }
    Patch-ConfigMak $ffmpegDir
    Write-Log "Configure OK on retry. Running make + make install..."
    $makeOutput = Invoke-Make $makeScriptPath $makeScriptUnix
    Write-Host $makeOutput
    if ($LASTEXITCODE -ne 0) {
        throw "FFmpeg make failed on retry after dropping $($libsToDrop -join ', ')."
    }
}

Write-Log "FFmpeg build complete."

# ---------------------------------------------------------------------------
# Step 6: Build-time LGPL sanity checks.
#   (a) Parse config.h and fail on any GPL/nonfree/network symbol.
#   (b) Probe the just-built ffmpeg.exe for GPL-only filter names and fail
#       if any is present.
# ---------------------------------------------------------------------------

$configHPath = Join-Path $ffmpegDir "config.h"
if (-not (Test-Path $configHPath)) {
    throw "Sanity check failed: $configHPath missing after make install."
}

$forbiddenSymbols = @(
    "CONFIG_GPL",
    "CONFIG_NONFREE",
    "CONFIG_LIBX264",
    "CONFIG_LIBX265",
    "CONFIG_LIBFDK_AAC",
    "CONFIG_NETWORK"
)

$configHText = Get-Content -Raw -Encoding ASCII -Path $configHPath
$leaks = New-Object System.Collections.Generic.List[string]
foreach ($sym in $forbiddenSymbols) {
    if ($configHText -match "(?m)^#define\s+$sym\s+1\b") {
        $leaks.Add($sym) | Out-Null
    }
}
if ($leaks.Count -gt 0) {
    throw "LGPL invariant violated: config.h defines forbidden symbol(s) as 1: $($leaks -join ', ')"
}
Write-Log "config.h LGPL invariants OK (no $($forbiddenSymbols -join ', ') defined as 1)."

$builtFfmpeg = Join-Path $outDir "bin\ffmpeg.exe"
if (-not (Test-Path $builtFfmpeg)) {
    throw "Sanity check failed: built ffmpeg.exe missing at $builtFfmpeg."
}

# The capability invariants below execute the just-built binary. With
# --enable-shared the exe loads its av*/sw* DLLs from its own bin dir, plus the
# transitive external-lib and MSYS2 C/C++ runtime DLLs that live in the active
# environment's bin dir (clangarm64/bin or ucrt64/bin). At this point the
# extract step has not co-located those dependencies yet, so prepend both dirs
# to PATH for the duration of the probes; otherwise the process fails to launch
# with STATUS_DLL_NOT_FOUND. Saved and restored around the probe block.
$savedPathForProbe = $env:PATH
$probeOutBin = Join-Path $outDir "bin"
$probeEnvBin = Join-Path $MsysRoot "$($archProfile.EnvSubdir)\bin"
$env:PATH = "$probeOutBin;$probeEnvBin;$savedPathForProbe"

# An aarch64 binary requires a Windows-on-ARM64 host (x64 hosts cannot emulate
# ARM64); fail with a clear message rather than a cryptic launch error. A DLL
# resolution failure here would surface as a launch error too, so the PATH set
# above must come first.
if ($TargetArch -eq "aarch64") {
    try {
        & $builtFfmpeg -hide_banner -version | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "non-zero exit" }
    } catch {
        $env:PATH = $savedPathForProbe
        throw ("Built aarch64 ffmpeg.exe could not be executed on this host " +
               "($($_.Exception.Message)). This requires a Windows-on-ARM64 host " +
               "with the CLANGARM64 runtime + external-lib DLLs resolvable on PATH " +
               "(checked: $probeOutBin, $probeEnvBin).")
    }
}

$gplOnlyFilters = @(
    "hqdn3d", "cropdetect", "blackframe", "boxblur", "colormatrix",
    "delogo", "eq", "fspp", "histeq", "interlace", "kerndeint",
    "mcdeint", "mpdecimate", "nnedi", "owdenoise", "perspective",
    "phase", "pp", "pp7", "pullup", "repeatfields", "sab",
    "signature", "smartblur", "spp", "stereo3d", "super2xsai",
    "tinterlace", "uspp", "vaguedenoiser", "mptestsrc",
    "cover_rect", "find_rect"
)

$filtersOutput = (& $builtFfmpeg -hide_banner -filters) -join "`n"
$filterLeaks = New-Object System.Collections.Generic.List[string]
foreach ($name in $gplOnlyFilters) {
    if ($filtersOutput -match "(?m)^\s+\S{3}\s+$name\s") {
        $filterLeaks.Add($name) | Out-Null
    }
}
if ($filterLeaks.Count -gt 0) {
    throw "LGPL invariant violated: ffmpeg.exe -filters lists GPL-only filter(s): $($filterLeaks -join ', ')"
}
Write-Log "ffmpeg.exe -filters LGPL invariants OK (no GPL-only filters present)."

function Count-LinesMatching([string]$text, [string]$pattern) {
    return ($text -split "`r?`n" | Where-Object { $_ -match $pattern }).Count
}

$decodersOutput  = (& $builtFfmpeg -hide_banner -decoders)  -join "`n"
$encodersOutput  = (& $builtFfmpeg -hide_banner -encoders)  -join "`n"
$muxersOutput    = (& $builtFfmpeg -hide_banner -muxers)    -join "`n"
$demuxersOutput  = (& $builtFfmpeg -hide_banner -demuxers)  -join "`n"

$decoderCount = Count-LinesMatching $decodersOutput '^\s+[VAS][\.FSXBDP]{5}\s+\S'
$encoderCount = Count-LinesMatching $encodersOutput '^\s+[VAS][\.FSXBDP]{5}\s+\S'
$filterCount  = Count-LinesMatching $filtersOutput  '^\s+[T\.][S\.][C\.]\s+\S+\s+\S+->\S+'
$muxerCount   = Count-LinesMatching $muxersOutput   '^\s+E\s+\S'
$demuxerCount = Count-LinesMatching $demuxersOutput '^\s+D\s+\S'

Write-Log "=========================================================="
Write-Log "LGPL invariants verified."
Write-Log "  decoders  exposed: $decoderCount"
Write-Log "  encoders  exposed: $encoderCount"
Write-Log "  filters   exposed: $filterCount"
Write-Log "  muxers    exposed: $muxerCount"
Write-Log "  demuxers  exposed: $demuxerCount"
Write-Log "  external libs linked: $($confirmedLibFlags.Count) (dropped: $($droppedLibs.Count))"
Write-Log "=========================================================="

# Restore PATH after the capability probes (dependency DLLs were prepended).
$env:PATH = $savedPathForProbe

# ---------------------------------------------------------------------------
# Step 7: Post-build hook for the README codec-doc generator (PR 5.5).
# The generator script is owned by a sibling subagent; if it does not yet
# exist or fails, log a warning and continue (do not fail the build).
# ---------------------------------------------------------------------------

$codecDocScript = Join-Path $RepoRoot "scripts\generate_supported_codecs_doc.py"
if (Test-Path $codecDocScript) {
    Write-Log "Refreshing README auto-generated codec list via $codecDocScript ..."
    Push-Location $RepoRoot
    try {
        & python $codecDocScript --readme README.md
        if ($LASTEXITCODE -ne 0) {
            Write-Log "WARN: codec-doc generator exited with code $LASTEXITCODE; continuing."
        } else {
            Write-Log "README auto-generated codec list refreshed."
        }
    }
    catch {
        Write-Log "WARN: codec-doc generator invocation raised: $($_.Exception.Message); continuing."
    }
    finally {
        Pop-Location
    }
} else {
    Write-Log "Codec-doc generator not present at $codecDocScript yet; skipping README refresh."
}
