#requires -Version 5
<#
.SYNOPSIS
    Generates manifest.json deterministically from pack.lock.json.

.DESCRIPTION
    The lockfile (pack.lock.json) is the source of truth for every mod's exact
    version, URL, SHA-1, and size. Running this script does not change versions.

    To intentionally bump versions, use Update-Pack.ps1 with the -Refresh flag.
    To see what new versions are available, use Check-Updates.ps1.

    Workflow:
      1. Edit pack.lock.json (manually or via Update-Pack.ps1 -Refresh)
      2. Run Build-Pack.ps1 -OutputPath ...\manifest.json
      3. Update the server to match
      4. Deploy manifest to your hosted URL

.PARAMETER OutputPath
    Where to write manifest.json.

.PARAMETER LocalPackSource
    Optional folder containing local override files (configs, resourcepacks, etc.).

.PARAMETER SelfHostBaseUrl
    Public URL prefix for self-hosted files (only used when LocalPackSource is set).
#>

[CmdletBinding()]
param(
    [string]$OutputPath        = ".\manifest.json",
    [string]$LocalPackSource   = "",
    [string]$SelfHostBaseUrl   = "https://bns.sijbers.uk/pack/files",
    # Optional -- point at a published launcher .exe to embed launcherVersion + launcherUrl
    # in the manifest. The launcher displays a "newer version available" banner when
    # its embedded version is lower than this. Skip entirely to leave both fields out.
    [string]$LauncherExePath   = "",
    [string]$LauncherPublicUrl = "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe",
    # Skip the auto-invoke of Build-Tweaks.ps1 (only touch this if you're debugging).
    [switch]$SkipTweaks
)

$ErrorActionPreference = 'Stop'
$ProgressPreference    = 'SilentlyContinue'

$here     = Split-Path -Parent $MyInvocation.MyCommand.Path
$lockPath = Join-Path $here '..\pack\pack.lock.json'

# Build any data-only tweak jars first, then fold the resulting overrides
# folder into the manifest. Set -SkipTweaks to bypass. The "overrides" name
# matches CurseForge/Modrinth modpack conventions: files that override or
# augment the standard mod set, hosted by us.
$tweaksRoot    = Join-Path $here '..\pack\tweaks'
$overridesRoot = Join-Path $here '..\pack\overrides'
if (-not $SkipTweaks -and (Test-Path $tweaksRoot)) {
    & (Join-Path $here 'Build-Tweaks.ps1') -TweaksRoot $tweaksRoot -OutputRoot (Join-Path $overridesRoot 'mods')
}
if (-not $LocalPackSource -and (Test-Path $overridesRoot)) {
    $LocalPackSource = $overridesRoot
}

if (-not (Test-Path $lockPath)) {
    throw "pack.lock.json not found at $lockPath. Run Update-Pack.ps1 -Refresh first to bootstrap it."
}

$lock = Get-Content $lockPath -Raw | ConvertFrom-Json

Write-Host ""
Write-Host "Building manifest from pack.lock.json:"
Write-Host ("  Pack:     {0} v{1}" -f $lock.name, $lock.version)
Write-Host ("  MC:       {0} ({1} {2})" -f $lock.minecraft, $lock.loader.type, $lock.loader.version)
Write-Host ("  Locked:   {0}" -f $lock.lockedAt)
Write-Host ("  Mods:     {0}" -f $lock.mods.Count)
Write-Host ""

$files = @()
$totalBytes = 0L

foreach ($mod in $lock.mods) {
    # `side` is emitted only when explicitly set in the lockfile -- a missing or
    # null lockfile side means "both", and we keep the manifest clean by omitting
    # the field rather than serialising "both" everywhere.
    $entry = [ordered]@{
        path = $mod.path
        url  = $mod.url
        sha1 = $mod.sha1
        size = $mod.size
    }
    $hasSide = ($mod.PSObject.Properties.Name -contains 'side') -and $mod.side -and $mod.side -ne 'both'
    if ($hasSide) { $entry.side = $mod.side }
    $files += [pscustomobject]$entry
    $totalBytes += $mod.size
    $sideTag = if ($hasSide) { "  [$($mod.side)]" } else { '' }
    Write-Host ("  [{0}] {1,-26} {2,-22} {3,8:N0} KB{4}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB), $sideTag)
}

# Local overrides (configs, custom files not on Modrinth/CurseForge)
if ($LocalPackSource -and (Test-Path $LocalPackSource)) {
    Write-Host ""
    Write-Host "Adding local overrides from $LocalPackSource..."
    $managedRoots = @('mods', 'config', 'resourcepacks', 'shaderpacks', 'kubejs', 'defaultconfigs')
    $base = $SelfHostBaseUrl.TrimEnd('/')
    $sourceFull = (Resolve-Path $LocalPackSource).Path.TrimEnd('\','/')
    foreach ($root in $managedRoots) {
        $rootDir = Join-Path $LocalPackSource $root
        if (-not (Test-Path $rootDir)) { continue }
        Get-ChildItem -Path $rootDir -Recurse -File | ForEach-Object {
            $rel = $_.FullName.Substring($sourceFull.Length).TrimStart('\','/') -replace '\\','/'
            $sha1 = (Get-FileHash -Algorithm SHA1 -Path $_.FullName).Hash.ToLowerInvariant()

            # Tweak jars (built by Build-Tweaks.ps1 into pack/overrides/mods/)
            # are pure data-only NeoForge mods -- worldgen modifiers, recipe
            # overrides, etc. They typically depend on server-only libraries
            # (Lithostitched, etc.) and have no client-side rendering. Default
            # them to side="server" so the client doesn't try to load them
            # without their server-only deps -> the loader otherwise refuses to
            # start with "Mod X requires Y or above" errors. If a future tweak
            # is genuinely client-relevant, override by editing this rule.
            $entry = [ordered]@{
                path = $rel
                url  = "$base/$rel"
                sha1 = $sha1
                size = $_.Length
            }
            if ($root -eq 'mods') { $entry.side = 'server' }

            $files += [pscustomobject]$entry
            $totalBytes += $_.Length
            $sideTag = if ($entry.side) { "  [$($entry.side)]" } else { '' }
            Write-Host ("  [local]   {0}{1}" -f $rel, $sideTag)
        }
    }
}

$manifest = [ordered]@{
    name      = $lock.name
    version   = $lock.version
    minecraft = [ordered]@{ version = $lock.minecraft }
    loader    = [ordered]@{ type = $lock.loader.type; version = $lock.loader.version }
    files     = $files
}

# Optional: copy a defaultServer block through from the lockfile so the launcher
# can pre-populate friends' multiplayer list. Lockfile schema:
#   "defaultServer": { "name": "Brass & Sigil", "ip": "bns.sijbers.uk" }
if ($lock.PSObject.Properties.Name -contains "defaultServer" -and $lock.defaultServer) {
    $manifest.defaultServer = [ordered]@{
        name = $lock.defaultServer.name
        ip   = $lock.defaultServer.ip
    }
    Write-Host ("  Default server: {0} ({1})" -f $lock.defaultServer.name, $lock.defaultServer.ip)
}

# Optional: shader pack to enable by default on fresh installs. Lockfile schema:
#   "defaultShader": "ComplementaryReimagined_r5.7.1.zip"
if ($lock.PSObject.Properties.Name -contains "defaultShader" -and $lock.defaultShader) {
    $manifest.defaultShader = $lock.defaultShader
    Write-Host ("  Default shader: {0}" -f $lock.defaultShader)
}

# Optional: public URL of the brass-sigil-server panel. Used by the launcher
# to send friend-side whitelist requests. Lockfile schema:
#   "panelUrl": "https://bns-admin.sijbers.uk"
if ($lock.PSObject.Properties.Name -contains "panelUrl" -and $lock.panelUrl) {
    $manifest.panelUrl = $lock.panelUrl
    Write-Host ("  Panel URL: {0}" -f $lock.panelUrl)
}

# Optional launcher metadata: when -LauncherExePath is supplied, read the exe's
# FileVersion (set via <Version> in ModpackLauncher.csproj) and embed it. The
# launcher compares its assembly version to this value and shows an upgrade
# banner pointing at LauncherPublicUrl when older.
if ($LauncherExePath) {
    if (-not (Test-Path $LauncherExePath)) {
        throw "LauncherExePath '$LauncherExePath' does not exist."
    }
    $launcherFile = Get-Item $LauncherExePath
    $launcherVersion = $launcherFile.VersionInfo.FileVersion
    $versionSource   = 'PE FileVersion'

    # Linux PowerShell can't read PE FileVersion from a Windows .exe. When that
    # happens, fall back to launcher/ModpackLauncher.csproj's <Version> (the
    # source of truth that the Windows compile bakes into FileVersion anyway).
    # Append ".0" so callers get the same 4-part form as the PE metadata.
    if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
        $csprojPath = Join-Path $here '..\launcher\ModpackLauncher.csproj'
        if (Test-Path $csprojPath) {
            [xml]$csproj = Get-Content $csprojPath
            $csprojVersion = ($csproj.Project.PropertyGroup.Version | Where-Object { $_ }) | Select-Object -First 1
            if ($csprojVersion) {
                $launcherVersion = if ($csprojVersion -match '^\d+\.\d+\.\d+$') { "$csprojVersion.0" } else { $csprojVersion }
                $versionSource = "csproj <Version> (fallback)"
            }
        }
    }
    if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
        throw "Launcher exe has no FileVersion and csproj <Version> couldn't be read either -- set <Version> in ModpackLauncher.csproj and republish."
    }
    # FileVersion is the four-component form (e.g. "0.1.0.0" for csproj <Version>0.1.0</Version>).
    # Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous
    # comparisons between trimmed and untrimmed forms.
    $manifest.launcherVersion = $launcherVersion
    $manifest.launcherUrl     = $LauncherPublicUrl

    Write-Host ""
    Write-Host ("Launcher metadata embedded:")
    Write-Host ("  Version:  {0}  ({1})" -f $launcherVersion, $versionSource)
    Write-Host ("  Url:      {0}" -f $LauncherPublicUrl)
    Write-Host ("  Source:   {0}" -f $launcherFile.FullName)
}

$json = $manifest | ConvertTo-Json -Depth 10
$outDir = Split-Path -Parent $OutputPath
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
[System.IO.File]::WriteAllText($OutputPath, $json, [System.Text.UTF8Encoding]::new($false))

Write-Host ""
Write-Host "Manifest written: $OutputPath"
Write-Host ("  Files:    {0}" -f $files.Count)
Write-Host ("  Total:    {0:N1} MB (download size)" -f ($totalBytes / 1MB))
