#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Build and test the Windows port with SQLite wired up (docs/WINDOWS_PORT.md §14.2).

.DESCRIPTION
    Windows has no system SQLite, and GRDB's `GRDBSQLite` is a .systemLibrary whose only
    declared provider is apt — so `sqlite3.h` resolves nowhere and NucleicCore can't build.
    A .systemLibrary is resolved through C header/library search paths rather than the
    SwiftPM module graph, so nothing in Package.swift can fix this: SQLite has to exist on
    disk and be discoverable. This script installs it once via vcpkg and points the compiler
    and linker at it.

    Two mechanisms, deliberately chosen:

      CPATH                 — additive. clang appends it to the header search path without
                              disturbing MSVC/SDK auto-detection, so plain `swift build`
                              works afterwards with no flags at all.
      -Xlinker /LIBPATH:    — passed per-invocation rather than via %LIB%. Setting %LIB%
                              would be actively harmful: clang skips emitting its own
                              -libpath: for the VC, UCRT *and* Windows SDK directories the
                              moment %LIB% is set (clang/lib/Driver/ToolChains/MSVC.cpp,
                              `if (!GetEnv("LIB") || ...)`), so a fresh %LIB% holding only
                              SQLite silently strips the entire platform link path.

.PARAMETER Target
    protocol — the wire layer only (the required CI leg; needs no SQLite).
    core     — NucleicCore, the full Windows manifest. With -Test it builds EVERY declared
               target (nucleic-hostd, NucleicProtocolC, nucleic-smoke) and runs
               NucleicCarbonTests. Both build and test are green on Windows 11 amd64 as of
               2026-07-29, so a failure here is a REGRESSION, not a gap — this leg used to
               be reported as expected-to-fail and that wording is exactly what let a real
               `git` spawn bug read as an items-10+ gap for a day.
    broker   — the C# nucleic-brokerd contract tests (no Swift involved).
    all      — all three, continuing past a failing leg.

.PARAMETER Test
    Run the leg's tests instead of just building it.

.PARAMETER VcpkgRoot
    An existing vcpkg checkout to use. Otherwise the script looks at %VCPKG_INSTALLATION_ROOT%,
    the %LOCALAPPDATA%\vcpkg\vcpkg.path.txt breadcrumb, %LOCALAPPDATA%\Programs\vcpkg,
    %USERPROFILE%\vcpkg and C:\vcpkg, and clones one into %LOCALAPPDATA%\Programs\vcpkg if none
    of those hold a vcpkg.exe.

.PARAMETER Persist
    Also save CPATH to your *user* environment, so future shells and editors resolve
    sqlite3.h without going through this script.

.EXAMPLE
    ./windows/build.ps1 -Target protocol -Test
    ./windows/build.ps1 -Target core -Persist
#>
[CmdletBinding()]
param(
    [ValidateSet('protocol', 'core', 'broker', 'all')]
    [string]$Target = 'protocol',
    [switch]$Test,
    [switch]$Persist,
    [string]$VcpkgRoot,
    [string]$Triplet
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$repo = Split-Path -Parent $PSScriptRoot

function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
function Write-Note($msg) { Write-Host "    $msg" -ForegroundColor DarkGray }

# ---------------------------------------------------------------- SQLite via vcpkg

function Initialize-SQLite {
    if (-not $Triplet) {
        # -static-md links SQLite statically against the DYNAMIC CRT — matching what the
        # Swift toolchain uses, so the CRT agrees and there's no sqlite3.dll to place
        # beside every test binary. `-nucleic` is our overlay triplet (windows/vcpkg-triplets):
        # same thing plus SQLITE_ENABLE_SNAPSHOT, which GRDB's system-SQLite path requires and
        # the stock port compiles out.
        $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
        $script:Triplet = "$arch-windows-static-md-nucleic"
    }
    $overlay = Join-Path $repo 'windows\vcpkg-triplets'

    # `%LOCALAPPDATA%\vcpkg` is NOT a candidate install root, and must never be a clone target:
    # that path is vcpkg's own per-user DATA directory (registries, downloads, and the
    # `vcpkg.path.txt` breadcrumb naming the real root), so it exists — non-empty, no `.git` —
    # on any machine where vcpkg has merely *run* once. Cloning into it fails with "destination
    # path already exists and is not an empty directory". We only ever read it, for that
    # breadcrumb; a clone of our own goes to the conventional per-user install location.
    $breadcrumb = Join-Path $env:LOCALAPPDATA 'vcpkg\vcpkg.path.txt'
    $known = @(
        $VcpkgRoot
        $env:VCPKG_INSTALLATION_ROOT
        $(if (Test-Path $breadcrumb) { (Get-Content $breadcrumb -Raw).Trim() })
        (Join-Path $env:LOCALAPPDATA 'Programs\vcpkg')
        (Join-Path $env:USERPROFILE 'vcpkg')
        'C:\vcpkg'
    )
    $root = $known | Where-Object { $_ -and (Test-Path (Join-Path $_ 'vcpkg.exe')) } | Select-Object -First 1

    if (-not $root) {
        # Nothing usable found. Clone into the per-user install location — unless the caller
        # named a root, in which case honour it (they may be pointing at a fresh directory).
        $root = if ($VcpkgRoot) { $VcpkgRoot } else { Join-Path $env:LOCALAPPDATA 'Programs\vcpkg' }
        if (-not (Test-Path (Join-Path $root '.git'))) {
            # `git clone` refuses a non-empty destination, and the resulting error says nothing
            # about what to do — so diagnose it here, where the remedy is known.
            if ((Test-Path $root) -and (Get-ChildItem $root -Force | Select-Object -First 1)) {
                throw ("$root exists, is not empty, and holds neither vcpkg.exe nor a git " +
                    "checkout. Point the script at your vcpkg with -VcpkgRoot <path>, or " +
                    "remove that directory.")
            }
            Write-Step "Cloning vcpkg to $root (one time)"
            git clone --depth 1 https://github.com/microsoft/vcpkg $root | Out-Host
            if ($LASTEXITCODE -ne 0) { throw "vcpkg clone failed" }
        }
        Write-Step 'Bootstrapping vcpkg (one time)'
        & (Join-Path $root 'bootstrap-vcpkg.bat') -disableMetrics | Out-Host
        if ($LASTEXITCODE -ne 0) { throw "vcpkg bootstrap failed" }
    }
    Write-Note "vcpkg root: $root"

    $prefix = Join-Path $root "installed\$Triplet"
    if (-not (Test-Path (Join-Path $prefix 'include\sqlite3.h'))) {
        Write-Step "Installing sqlite3:$Triplet (one time, a few minutes)"
        & (Join-Path $root 'vcpkg.exe') install "sqlite3:$Triplet" "--overlay-triplets=$overlay" | Out-Host
        if ($LASTEXITCODE -ne 0) { throw "vcpkg install sqlite3:$Triplet failed" }
    }

    $inc = Join-Path $prefix 'include'
    $lib = Join-Path $prefix 'lib'
    if (-not (Test-Path (Join-Path $inc 'sqlite3.h'))) { throw "sqlite3.h missing under $inc" }
    if (-not (Test-Path (Join-Path $lib 'sqlite3.lib'))) { throw "sqlite3.lib missing under $lib" }

    # Additive: clang merges CPATH into the header search path and still auto-detects the
    # MSVC + Windows SDK headers. Contrast %INCLUDE%, which SUPPRESSES that detection.
    if ($env:CPATH -and $env:CPATH -notlike "*$inc*") { $env:CPATH = "$inc;$env:CPATH" }
    elseif (-not $env:CPATH) { $env:CPATH = $inc }

    if ($Persist) {
        $userCPath = [Environment]::GetEnvironmentVariable('CPATH', 'User')
        if (-not $userCPath) { $userCPath = $inc }
        elseif ($userCPath -notlike "*$inc*") { $userCPath = "$inc;$userCPath" }
        [Environment]::SetEnvironmentVariable('CPATH', $userCPath, 'User')
        Write-Note "Persisted CPATH for your user account -> $inc"
        Write-Note 'New shells can now `swift build` NucleicCore with no flags.'
    }

    Write-Note "sqlite3 ($Triplet): $prefix"
    return @{ Include = $inc; Lib = $lib }
}

# ---------------------------------------------------------------- legs

function Invoke-Swift([string[]]$SwiftArgs, [hashtable]$Sql) {
    # Both mechanisms, deliberately redundant. CPATH (set by Initialize-SQLite) is what makes
    # a *bare* `swift build` work later, but passing -Xcc -I here means this script never
    # depends on clang honouring CPATH — if it didn't, the failure would be an inscrutable
    # "'sqlite3.h' file not found" from inside GRDB's module build. -Xlinker only matters
    # when something actually links (tests, executables); a library build needs just the -I.
    #
    # TEMPORARY — /FORCE:MULTIPLE (docs/WINDOWS_PORT.md §14.1.1). swift-crypto's
    # CCryptoBoringSSL and swift-nio-ssl's CNIOBoringSSL both land in every Windows link, and
    # `p_thread_callback_boringssl` (crypto/thread_win.cc) collides between them: BoringSSL's
    # prefix maps are generated by building on a POSIX host, so symbols from the Windows-only
    # translation units are never prefixed. The same pair links clean on Linux, where the
    # equivalent file is thread_pthread.c and IS prefixed. Drop this flag the moment one copy
    # carries the prefix — it is global, so it also downgrades any FUTURE duplicate to a
    # warning. Read the warnings.
    $full = @($SwiftArgs) + @(
        '-Xcc', "-I$($Sql.Include)",
        # `/LIBPATH:`, NOT `-LIBPATH:`. A leading `-L` is claimed by the driver's GNU-style
        # library-path flag, so `-LIBPATH:C:\…\lib` is read as `-L` + `IBPATH:C:\…\lib` and
        # re-emitted to the linker as `-libpath:IBPATH:C:\…\lib` — a directory that exists
        # nowhere, and the only symptom is `lld-link: could not open 'sqlite3.lib'`.
        '-Xlinker', "/LIBPATH:$($Sql.Lib)",
        '-Xlinker', '/FORCE:MULTIPLE')
    Write-Note "swift $($full -join ' ')"
    # Out-Host, not the pipeline: a native command's stdout would otherwise become this
    # function's return value, and the caller's exit-code check would see an array.
    & swift @full | Out-Host
    return $LASTEXITCODE
}

function Build-Protocol([hashtable]$sql) {
    # Narrows the manifest to the wire layer. Read at MANIFEST EVALUATION time, so it has
    # to be in the environment before swift starts, not exported inside the build.
    $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY = '1'
    try {
        Write-Step 'protocol: building NucleicProtocol'
        $code = if ($Test) {
            # SwiftPM 6.3.3 on Windows still schedules index units for test discovery even with
            # `--disable-index-store`, then a clean build dies opening units it never emitted.
            # With indexing enabled, parallel frontends contend for shared SDK/module unit files
            # and Windows reports sharing violations as "permission denied". Serialize test
            # builds until SwiftPM fixes that pair of index-store bugs; ordinary builds stay
            # parallel.
            Invoke-Swift @('test', '-j', '1', '--filter', 'NucleicProtocolTests') $sql
        } else {
            Invoke-Swift @('build', '--target', 'NucleicProtocol') $sql
        }
    } finally {
        Remove-Item Env:\NUCLEIC_WINDOWS_PROTOCOL_ONLY -ErrorAction SilentlyContinue
    }
    return $code
}

function Build-Core([hashtable]$sql) {
    Write-Step 'core: building NucleicCore (full Windows manifest)'
    if ($Test) {
        Write-Note '-Test builds EVERY target (hostd, NucleicProtocolC, smoke) and runs NucleicCarbonTests.'
    }
    # NucleicCore only EXISTS when NUCLEIC_WINDOWS_PROTOCOL_ONLY is absent: with it set,
    # Package.swift narrows the manifest to [NucleicProtocol, NucleicProtocolTests] and this
    # fails with "no target named 'NucleicCore'". CI never sets it for this job, but a local
    # shell that ran the protocol leg by hand keeps it — so clear it just for this build and
    # hand it back afterwards.
    $inherited = $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY
    if ($inherited) {
        Write-Note 'Ignoring inherited NUCLEIC_WINDOWS_PROTOCOL_ONLY — core needs the full manifest.'
        Remove-Item Env:\NUCLEIC_WINDOWS_PROTOCOL_ONLY
    }
    try {
        if ($Test) {
            # See Build-Protocol: clean Windows test builds require indexing + one frontend job.
            return Invoke-Swift @('test', '-j', '1', '--filter', 'NucleicCarbonTests') $sql
        }
        return Invoke-Swift @('build', '--target', 'NucleicCore') $sql
    } finally {
        if ($inherited) { $env:NUCLEIC_WINDOWS_PROTOCOL_ONLY = $inherited }
    }
}

function Build-Broker {
    Write-Step 'broker: dotnet test windows\Nucleic.sln'
    & dotnet test (Join-Path $repo 'windows\Nucleic.sln') --nologo | Out-Host
    if ($LASTEXITCODE -ne 0) { return $LASTEXITCODE }

    # The tests run against FakeWslc, so they never compile Wslc/WslcFacade.cs — the one file
    # that touches the preview SDK, and the one most likely to break when the pin moves. Build
    # it too, or the real facade is unguarded on the only machine that can run it.
    #
    # Build, not test: exercising it needs a live wslc service, which is M1 (a2)'s job.
    Write-Step 'broker: dotnet build -p:UseWslc=true (the real Microsoft.WSL.Containers facade)'
    & dotnet build (Join-Path $repo 'windows\NucleicBroker\NucleicBroker.csproj') `
        -p:UseWslc=true --nologo | Out-Host
    return $LASTEXITCODE
}

# ---------------------------------------------------------------- main

Push-Location $repo
try {
    $results = [ordered]@{}

    if ($Target -in @('protocol', 'core', 'all')) { $sql = Initialize-SQLite }

    switch ($Target) {
        'protocol' { $results['protocol'] = Build-Protocol $sql }
        'core'     { $results['core']     = Build-Core     $sql }
        'broker'   { $results['broker']   = Build-Broker }
        'all' {
            $results['protocol'] = Build-Protocol $sql
            $results['core']     = Build-Core     $sql
            $results['broker']   = Build-Broker
        }
    }

    Write-Host ''
    Write-Step 'Summary'
    # No leg is excused any more. Every one of them has been green on Windows at least once,
    # so a red leg is a regression to look at — not a status quo to scroll past.
    foreach ($k in $results.Keys) {
        $ok = $results[$k] -eq 0
        Write-Host ("    {0,-9} {1}" -f $k, $(if ($ok) { 'PASS' } else { 'FAIL' })) `
            -ForegroundColor $(if ($ok) { 'Green' } else { 'Red' })
    }

    $hard = @($results.Keys | Where-Object { $results[$_] -ne 0 })
    if ($hard.Count -gt 0) { exit 1 }
    exit 0
} finally {
    Pop-Location
}
