<#
.SYNOPSIS
    Profile this Windows machine and recommend optimal qsv performance settings.

.DESCRIPTION
    Windows (best-effort) counterpart to scripts/qsv-tune.sh, implementing qsv issue
    #2829: inspect the machine's capabilities (RAM, CPU cores, disk type) and the qsv
    binary's compiled-in memory allocator, then emit a tuned set of QSV_* environment
    variables in `.env` format.

    DRY-RUN BY DEFAULT: with no arguments it prints an annotated, ready-to-use `.env`
    block to stdout and writes NOTHING. Use -Write to merge the QSV_* settings into an
    actual `.env` file (idempotently, via sentinel markers, with a .bak backup).

    Allocator tuning (jemalloc/mimalloc) is reported SEPARATELY as environment-set
    snippets, NOT written into `.env`: allocators read their configuration at process
    start, before qsv loads `.env`, so those variables must live in the real environment.

    The recommended values are heuristic starting points - always benchmark against your
    own data shapes (see docs/PERFORMANCE.md).

.PARAMETER Write
    Merge the QSV_* block into the target file instead of printing to stdout.

.PARAMETER Path
    Target .env file for -Write (default: .\.env).

.PARAMETER Force
    With -Write, overwrite the whole file instead of merging just the managed block.

.EXAMPLE
    .\scripts\qsv-tune.ps1
    .\scripts\qsv-tune.ps1 -Write
    .\scripts\qsv-tune.ps1 -Write -Path C:\data\.env
#>
[CmdletBinding()]
param(
    [switch]$Write,
    [string]$Path = ".env",
    [switch]$Force,
    [long]$DebugTotalMemBytes = 0,
    [string]$DebugAllocator = ""
)

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

$MarkStart = "# >>> qsv-tune (generated) >>>"
$MarkEnd = "# <<< qsv-tune <<<"
$GiB = 1073741824

function Format-Bytes([long]$b) {
    if ($b -ge $GiB) { "{0:N1} GiB" -f ($b / $GiB) }
    elseif ($b -ge 1048576) { "{0:N1} MiB" -f ($b / 1048576) }
    else { "$b B" }
}

# qsv's .env loader expects UTF-8/ASCII without a BOM. Set-Content/Add-Content
# default to UTF-16LE (Windows PowerShell 5.1) or UTF-8-with-BOM depending on the
# host, which can corrupt the first key. Always write UTF-8 *without* BOM via .NET.
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
function Resolve-FullPath([string]$p) {
    $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
}
function Write-EnvFile([string]$p, [string]$text) {
    [System.IO.File]::WriteAllText((Resolve-FullPath $p), $text + "`n", $Utf8NoBom)
}

# --- profile the machine -------------------------------------------------
if ($DebugTotalMemBytes -gt 0) {
    $totalMem = $DebugTotalMemBytes
} else {
    $totalMem = [long](Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory
}
# FreePhysicalMemory is in KiB
$availMem = [long]((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory) * 1024
$cpus = [int]$env:NUMBER_OF_PROCESSORS
if ($cpus -lt 1) { $cpus = 1 }

# disk type for the drive backing the current directory
$disk = "unknown"
try {
    $driveLetter = (Get-Item -Path ".").PSDrive.Name
    $part = Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue
    if ($part) {
        $pd = Get-PhysicalDisk -ErrorAction SilentlyContinue |
            Where-Object { $_.DeviceId -eq (Get-Disk -Number $part.DiskNumber).Number }
        if ($pd -and $pd.MediaType) {
            switch -Wildcard ($pd.MediaType.ToString()) {
                "SSD" { $disk = "SSD" }
                "HDD" { $disk = "HDD" }
                default { $disk = "unknown" }
            }
        }
    }
} catch { $disk = "unknown" }

# allocator from `qsv --version`
$alloc = "unknown"
if ($DebugAllocator) {
    $alloc = $DebugAllocator
} elseif (Get-Command qsv -ErrorAction SilentlyContinue) {
    $ver = (& qsv --version) 2>$null
    if ($ver -match "jemalloc") { $alloc = "jemalloc" }
    elseif ($ver -match "mimalloc") { $alloc = "mimalloc" }
    elseif ($ver -match "standard") { $alloc = "standard" }
}

# --- heuristics ----------------------------------------------------------
$maxJobs = $cpus
if ($totalMem -lt (8 * $GiB)) {
    $headroomPct = 30; $memoryCheck = "true"
} elseif ($totalMem -gt (32 * $GiB)) {
    $headroomPct = 10; $memoryCheck = "false"
} else {
    $headroomPct = 20; $memoryCheck = "false"
}
if ($disk -eq "HDD") { $autoIndex = 104857600 } else { $autoIndex = 10485760 }
$bufTuned = ($disk -eq "HDD") -or ($totalMem -ge (32 * $GiB))
$chunkGuideMb = [int](($availMem / 1048576) / $cpus)

# --- build the .env block ------------------------------------------------
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine($MarkStart)
[void]$sb.AppendLine("# Generated by scripts/qsv-tune.ps1 - heuristic starting points, benchmark before relying on them.")
[void]$sb.AppendLine("# Machine profile: Windows | RAM total $(Format-Bytes $totalMem) / avail $(Format-Bytes $availMem) | $cpus logical CPUs | disk $disk | allocator $alloc")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# Use one job per logical CPU for multithreaded commands.")
[void]$sb.AppendLine("QSV_MAX_JOBS = $maxJobs")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# Reserve $headroomPct% of memory before loading a whole file (non-streaming mode).")
[void]$sb.AppendLine("QSV_FREEMEMORY_HEADROOM_PCT = $headroomPct")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# CONSERVATIVE memory check (avail + swap, platform factor) - on when RAM is tight.")
[void]$sb.AppendLine("QSV_MEMORY_CHECK = $memoryCheck")
[void]$sb.AppendLine("")
$idxNote = if ($disk -eq "HDD") { "100 MiB - indexing reads cost more on HDD." } else { "10 MiB - random-access indexing is cheap on SSD." }
[void]$sb.AppendLine("# Auto-create an index for files >= this size (bytes). $idxNote")
[void]$sb.AppendLine("QSV_AUTOINDEX_SIZE = $autoIndex")
[void]$sb.AppendLine("")
if ($bufTuned) {
    $bufNote = if ($disk -eq "HDD") { "help spinning disks." } else { "help with plenty of RAM." }
    [void]$sb.AppendLine("# Larger I/O buffers $bufNote")
    [void]$sb.AppendLine("QSV_RDR_BUFFER_CAPACITY = 1048576")
    [void]$sb.AppendLine("QSV_WTR_BUFFER_CAPACITY = 2097152")
} else {
    [void]$sb.AppendLine("# I/O buffers left at defaults (good for SSD). Uncomment to enlarge.")
    [void]$sb.AppendLine("# QSV_RDR_BUFFER_CAPACITY = 1048576")
    [void]$sb.AppendLine("# QSV_WTR_BUFFER_CAPACITY = 2097152")
}
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# Chunk sizing for parallel stats/frequency. Dynamic (0) is usually best; the")
[void]$sb.AppendLine("# computed avail_mem/jobs figure (~$chunkGuideMb MB) is only a hint if you set a fixed cap.")
[void]$sb.AppendLine("# QSV_STATS_CHUNK_MEMORY_MB = 0")
[void]$sb.AppendLine("# QSV_FREQ_CHUNK_MEMORY_MB = 0")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# Keep qsv's built-in allocator tuning on. Set true only in RSS-constrained")
[void]$sb.AppendLine("# (<4 GiB) environments or for diagnostics.")
[void]$sb.AppendLine("QSV_NO_ALLOC_TUNING = false")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# Skipping the mime-type/format check is faster but removes a safety net. Opt in only")
[void]$sb.AppendLine("# if you trust your inputs (or hit false positives).")
[void]$sb.AppendLine("# QSV_SKIP_FORMAT_CHECK = false")
[void]$sb.Append($MarkEnd)
$envBlock = $sb.ToString()

# --- allocator advice (NOT for .env) -------------------------------------
$advice = @"
# ===========================================================================
# Allocator tuning (advanced, optional)
# ===========================================================================
# IMPORTANT: allocators read their config at PROCESS START, before qsv loads
# `.env`. Set these in your real environment (`setx` or `$env:`), NOT in `.env`.
# They are workload- and platform-sensitive - measure before adopting.
"@
switch ($alloc) {
    "jemalloc" {
        $advice += @"

#
# qsv (jemalloc) already auto-tunes the levers that reliably help its batch workloads
# (background_thread purging + page retention) unless QSV_NO_ALLOC_TUNING=true.
# These extra levers are deliberately left OFF by qsv (mixed results); try only if your
# own benchmarks show a win:
#   # `$env:_RJEM_MALLOC_CONF = "metadata_thp:auto"   # Linux + large-RAM only
#   # `$env:_RJEM_MALLOC_CONF = "percpu_arena:percpu" # rarely helps
# (On builds without the vendored-jemalloc prefix, use MALLOC_CONF instead.)
"@
    }
    "mimalloc" {
        $advice += @"

#
# qsv is using mimalloc. Batch-friendly options to try (measure first):
#   # `$env:MIMALLOC_PURGE_DELAY = "10000"          # hold freed OS pages longer
#   # `$env:MIMALLOC_ALLOW_LARGE_OS_PAGES = "1"     # 2-4 MiB large pages (needs privileges)
#   # `$env:MIMALLOC_RESERVE_HUGE_OS_PAGES = "1"    # reserve 1 GiB huge pages at startup
# See the mimalloc docs (linked in docs/ENVIRONMENT_VARIABLES.md) for the full list.
"@
    }
    "standard" {
        $advice += @"

#
# qsv is using the standard system allocator - no allocator env tuning available.
# For batch/large-file workloads, consider a qsv build with jemalloc or mimalloc.
"@
    }
    default {
        $advice += @"

#
# Could not detect the allocator (is `qsv` on PATH?). Run `qsv --version` and look for
# `jemalloc`, `mimalloc`, or `standard` to know which tuning applies.
"@
    }
}

# --- emit ----------------------------------------------------------------
if (-not $Write) {
    Write-Output $envBlock
    Write-Output ""
    Write-Output $advice
    Write-Information "qsv-tune: dry-run only (nothing written). Use -Write to merge into $Path." -InformationAction Continue
    return
}

if ($Force) {
    if (Test-Path $Path) { Copy-Item $Path "$Path.bak" -Force }
    Write-EnvFile $Path $envBlock
    Write-Information "qsv-tune: wrote $Path (overwrote; backup at $Path.bak)." -InformationAction Continue
} elseif (Test-Path $Path) {
    Copy-Item $Path "$Path.bak" -Force
    $content = Get-Content $Path -Raw
    if ($content -match [regex]::Escape($MarkStart)) {
        # refuse to touch a corrupt/partial block (start marker but no end marker)
        if ($content -notmatch [regex]::Escape($MarkEnd)) {
            Write-Error "qsv-tune: $Path has a start marker but no end marker (partial/corrupt block). Fix it manually or re-run with -Force to overwrite."
            return
        }
        $pattern = "(?s)" + [regex]::Escape($MarkStart) + ".*?" + [regex]::Escape($MarkEnd)
        # MatchEvaluator avoids $-group interpretation in the replacement text
        $content = [regex]::Replace($content, $pattern, { param($m) $envBlock })
        Write-EnvFile $Path $content
        Write-Information "qsv-tune: updated qsv-tune block in $Path (backup at $Path.bak)." -InformationAction Continue
    } else {
        # Rewrite the whole file (Get-Content -Raw decodes any UTF-16/BOM input) so the
        # result is normalized to UTF-8 without BOM, rather than appending UTF-8 bytes
        # onto a possibly UTF-16/BOM file and leaving it mixed-encoded.
        Write-EnvFile $Path ($content.TrimEnd("`r", "`n") + "`n`n" + $envBlock)
        Write-Information "qsv-tune: appended qsv-tune block to $Path (rewritten as UTF-8; backup at $Path.bak)." -InformationAction Continue
    }
} else {
    Write-EnvFile $Path $envBlock
    Write-Information "qsv-tune: created $Path." -InformationAction Continue
}

Write-Information "`nAllocator advice (set in your environment, NOT in .env):" -InformationAction Continue
Write-Output $advice
