@echo off
REM ============================================================================
REM  DO NOT EDIT anything in this batch prologue (every line above the marker
REM  line further down). It must stay BYTE-IDENTICAL across every release so the
REM  script can safely replace ITSELF mid-run (self-update): cmd only ever reads
REM  this constant prologue, while the PowerShell body is loaded fully into
REM  memory before it runs. Only ever change the PowerShell body.
REM ============================================================================
setlocal EnableExtensions
title JoshPack Updater
set "JP_DIR=%~dp0"
set "JP_SELF=%~f0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$f=[IO.File]::ReadAllText($env:JP_SELF); iex $f.Substring($f.IndexOf([char]35+'PSBODY'))"
echo.
pause
endlocal
exit /b
#PSBODY
# ============================================================================
#  JoshPack auto-updater / installer
#  Put this .bat in your R.E.P.O.  BepInEx\plugins  folder and double-click it.
#  It first makes sure IT is the latest updater, then finds the latest JoshPack
#  on files.snapzfs.com by version number, removes the old copy, and installs
#  the new one. No admin rights, and no version pointer to keep in sync.
#
#  Reusing this for another game/mod? Only the four $cfg lines below change.
#  (Never edit the batch prologue above the marker line -- see the note up top.)
# ============================================================================
$ErrorActionPreference = 'Stop'
try { [Net.ServicePointManager]::SecurityProtocol =
        [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}

# ---- per-mod configuration --------------------------------------------------
$cfgBase      = 'https://files.snapzfs.com'   # site root
$cfgFolder    = 'Game_Stuff/REPO_Game_Files' # subfolder holding this mod's zips (auto-located if it moves)
$cfgModPrefix = 'JoshPack'                    # zips are named <prefix>-v<version>.zip
$cfgModFolder = 'JoshPack'                    # folder INSIDE the zip to install
# -----------------------------------------------------------------------------

$plugins = $env:JP_DIR
if (-not $plugins) { $plugins = (Get-Location).Path }
$plugins = $plugins.TrimEnd('\')

# ---- finding the mod on the server -----------------------------------------
# $cfgFolder above is only the LAST KNOWN home. If the download folders ever get
# reorganised again, this script walks the site's directory listings and finds
# wherever the zips went, so a move can never strand anybody. The walk only runs
# when the known folder comes up empty, so the normal case still costs one request.
$cfgMaxDepth = 4      # how many folder levels deep the search will go
$cfgMaxFetch = 250    # hard cap on listings fetched, so a search can never run away

function Get-DirUrl([string]$folder) {
    if ([string]::IsNullOrWhiteSpace($folder)) { return $cfgBase.TrimEnd('/') + '/' }
    # Escape each path SEGMENT separately. EscapeDataString over the whole string would
    # turn the '/' of a nested folder into %2F, which nginx reads as a literal character
    # rather than a separator, and the request 404s.
    $esc = ($folder.Trim('/') -split '/' | ForEach-Object { [Uri]::EscapeDataString($_) }) -join '/'
    return $cfgBase.TrimEnd('/') + '/' + $esc + '/'
}

$zipRx = [regex]('(?i)' + [regex]::Escape($cfgModPrefix) + '-v(\d+(?:\.\d+){1,3})\.zip')

# Highest <prefix>-v<version>.zip named anywhere in one directory listing, or $null.
function Get-BestVersion([string]$html) {
    $best = $null
    foreach ($m in $zipRx.Matches($html)) {
        $v = $null
        if ([version]::TryParse($m.Groups[1].Value, [ref]$v)) {
            if (($null -eq $best) -or ($v -gt $best)) { $best = $v }
        }
    }
    return $best
}

function Find-ModFolder {
    # Breadth-first walk of the autoindex listings.
    #
    # It deliberately walks the WHOLE site instead of stopping at the first hit. A
    # forgotten copy left behind in an old folder would otherwise win simply for being
    # shallower in the tree, and quietly install a stale version. Highest version wins,
    # wherever it lives.
    $queue = New-Object System.Collections.Queue
    $queue.Enqueue(@{ Path = ''; Depth = 0 })
    $seen = New-Object 'System.Collections.Generic.HashSet[string]'
    [void]$seen.Add('')
    $fetches = 0
    $bestVer = $null; $bestUrl = $null; $bestHtml = $null

    while (($queue.Count -gt 0) -and ($fetches -lt $cfgMaxFetch)) {
        $node = $queue.Dequeue()
        $url  = Get-DirUrl $node.Path
        try { $html = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 20).Content }
        catch { continue }
        $fetches++

        $v = Get-BestVersion $html
        if (($null -ne $v) -and (($null -eq $bestVer) -or ($v -gt $bestVer))) {
            $bestVer = $v; $bestUrl = $url; $bestHtml = $html
        }
        if ($node.Depth -ge $cfgMaxDepth) { continue }

        # Only a RELATIVE href ending in '/' is a subdirectory in this listing. Excluding
        # ':' drops absolute http(s) links, and a leading '/' or '.' drops site-root links
        # and the '../' parent entry -- all of which come from the injected page header,
        # not from the file list.
        foreach ($m in [regex]::Matches($html, '(?i)<a\s+href="([^":?#]+/)"')) {
            $href = $m.Groups[1].Value
            if ($href.StartsWith('/') -or $href.StartsWith('.')) { continue }
            $child = (($node.Path.Trim('/') + '/' + [Uri]::UnescapeDataString($href).Trim('/'))).Trim('/')
            if ($seen.Add($child)) { $queue.Enqueue(@{ Path = $child; Depth = $node.Depth + 1 }) }
        }
    }
    if ($bestUrl) { return @{ Url = $bestUrl; Html = $bestHtml; Version = $bestVer; Fetches = $fetches } }
    return $null
}

# Confirm the configured folder, or go find where the files went. Returns the listing
# HTML alongside the URL so the download step below does not have to fetch it twice.
function Resolve-ModLocation {
    $url = Get-DirUrl $cfgFolder
    try {
        $html = (Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 20).Content
        if ($null -ne (Get-BestVersion $html)) { return @{ Url = $url; Html = $html } }
    } catch {}

    Write-Host ''
    Write-Host ($cfgModPrefix + ' is not where this script expected to find it:') -ForegroundColor Yellow
    Write-Host ('  ' + $url) -ForegroundColor Yellow
    Write-Host 'Searching the file server for where it moved to ...' -ForegroundColor Yellow
    $found = Find-ModFolder
    if (-not $found) {
        Write-Host 'No copy found anywhere on the server.' -ForegroundColor Yellow
        return $null
    }
    Write-Host ('Found it: ' + $found.Url) -ForegroundColor Green
    return $found
}

# Provisional -- Resolve-ModLocation confirms or corrects this before anything uses it.
$dirUrl  = Get-DirUrl $cfgFolder
$dirHtml = $null

Write-Host ''
Write-Host '========================================' -ForegroundColor Cyan
Write-Host ('           ' + $cfgModPrefix + '  Updater') -ForegroundColor Cyan
Write-Host '========================================' -ForegroundColor Cyan
Write-Host ('Install folder: ' + $plugins)
Write-Host ''

# Work out where the mod actually lives BEFORE the self-update check below. That check
# downloads this script from the same folder -- so if the folder had moved and we had not
# looked first, self-update would 404 and the script could never repair itself.
$loc = Resolve-ModLocation
if ($loc) { $dirUrl = $loc.Url; $dirHtml = $loc.Html }
# ---- SELF-UPDATE: if the server's copy of THIS .bat differs, refresh it in place and stop. ----
# Safe mid-run: the PowerShell body is already in memory, and cmd only reads the frozen prologue
# above (byte-identical across releases), so the current run always finishes cleanly on old bytes.
$self = $env:JP_SELF
if ($self -and (Test-Path -LiteralPath $self)) {
    try {
        $selfName = [IO.Path]::GetFileName($self)
        Write-Host 'Making sure the updater script itself is current ...'
        $tmpBat = Join-Path $env:TEMP ($selfName + '.' + [guid]::NewGuid().ToString('N') + '.new')
        Invoke-WebRequest -Uri ($dirUrl + [Uri]::EscapeDataString($selfName)) -OutFile $tmpBat -UseBasicParsing
        $remoteHash = (Get-FileHash -LiteralPath $tmpBat -Algorithm SHA256).Hash
        $localHash  = (Get-FileHash -LiteralPath $self   -Algorithm SHA256).Hash
        if ($remoteHash -ne $localHash) {
            # Overwrite this file's bytes in place, granting share so cmd's open handle stays valid.
            $newBytes = [IO.File]::ReadAllBytes($tmpBat)
            $fs = [IO.File]::Open($self, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::ReadWrite)
            try { $fs.Write($newBytes, 0, $newBytes.Length) } finally { $fs.Dispose() }
            Remove-Item -LiteralPath $tmpBat -Force -ErrorAction SilentlyContinue
            Write-Host ''
            Write-Host '***************************************************************' -ForegroundColor Yellow
            Write-Host '  The updater script had a NEWER version on the server.'         -ForegroundColor Yellow
            Write-Host '  It has been refreshed in place. Please RUN THIS .bat AGAIN'     -ForegroundColor Yellow
            Write-Host '  to continue with the updated script.'                           -ForegroundColor Yellow
            Write-Host '***************************************************************' -ForegroundColor Yellow
            [Environment]::Exit(0)
        }
        Remove-Item -LiteralPath $tmpBat -Force -ErrorAction SilentlyContinue
        Write-Host 'Updater script is current.' -ForegroundColor Green
    }
    catch {
        Write-Host ('(Could not check for an updater update: ' + $_.Exception.Message + ' -- continuing.)') -ForegroundColor DarkGray
    }
}

try {
    # 1. Read the folder listing and pick the HIGHEST <prefix>-v<version>.zip (no pointer file).
    Write-Host ('Checking ' + $dirUrl + ' for the latest ' + $cfgModPrefix + ' ...')
    # Resolve-ModLocation already fetched this listing; only re-fetch if it did not.
    $html = $dirHtml
    if (-not $html) { $html = (Invoke-WebRequest -Uri $dirUrl -UseBasicParsing).Content }
    $rx = [regex]('(?i)' + [regex]::Escape($cfgModPrefix) + '-v(\d+(?:\.\d+){1,3})\.zip')
    $best = $null; $name = $null
    foreach ($m in $rx.Matches($html)) {
        $v = $null
        if ([version]::TryParse($m.Groups[1].Value, [ref]$v)) {
            if (($null -eq $best) -or ($v -gt $best)) { $best = $v; $name = $m.Groups[0].Value }
        }
    }
    if (-not $name) { throw ('No ' + $cfgModPrefix + '-v*.zip found at ' + $dirUrl) }
    Write-Host ('Latest release: ' + $name) -ForegroundColor Green

    # 2. Download it to a fresh temp folder.
    $tmp = Join-Path $env:TEMP ($cfgModPrefix + 'Update_' + [guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $tmp | Out-Null
    $zip = Join-Path $tmp $name
    Write-Host 'Downloading ...'
    Invoke-WebRequest -Uri ($dirUrl + [Uri]::EscapeDataString($name)) -OutFile $zip -UseBasicParsing

    # 3. Extract it.
    Write-Host 'Extracting ...'
    $ext = Join-Path $tmp 'x'
    Expand-Archive -LiteralPath $zip -DestinationPath $ext -Force

    # 4. Find the mod folder inside the zip (top level, or nested just in case).
    $src = Join-Path $ext $cfgModFolder
    if (-not (Test-Path $src)) {
        $found = Get-ChildItem -Path $ext -Recurse -Directory -Filter $cfgModFolder | Select-Object -First 1
        if ($found) { $src = $found.FullName }
    }
    if (-not (Test-Path $src)) { throw ("Couldn't find a '" + $cfgModFolder + "' folder inside " + $name + '.') }

    # 5. Remove ANY existing install in this plugins folder (only now, so a failed download above
    #    never leaves you without a working copy).
    $dest = Join-Path $plugins $cfgModFolder
    if (Test-Path $dest) {
        Write-Host ('Removing the currently installed ' + $cfgModFolder + ' ...')
        Remove-Item -LiteralPath $dest -Recurse -Force
    }

    # 6. Install the new one.
    Write-Host 'Installing ...'
    Copy-Item -LiteralPath $src -Destination $dest -Recurse -Force

    # 7. Clean up.
    Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue

    Write-Host ''
    Write-Host ('SUCCESS - ' + $name + ' installed.') -ForegroundColor Green
    Write-Host ('  -> ' + $dest)
    Write-Host 'Now launch R.E.P.O. via Thunderstore Mod Manager ("Start modded").'
}
catch {
    Write-Host ''
    Write-Host ('UPDATE FAILED: ' + $_.Exception.Message) -ForegroundColor Red
    Write-Host 'Nothing was changed. Check your internet connection, and make sure this .bat is'
    Write-Host 'sitting in your  ...\BepInEx\plugins  folder.'
}
