# Copy of D:\TTRPG-Release\publish.ps1 - keep in sync when updating the release folder tool.

param(
  [switch]$CheckOnly,
  [switch]$SkipMac
)

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

$ReleaseDir = $PSScriptRoot
$ConfigPath = Join-Path $ReleaseDir 'publish-config.json'

function Expand-ConfigPath([string]$value) {
  if ($value -match '%([^%]+)%') {
    $envName = $Matches[1]
    $envVal = [Environment]::GetEnvironmentVariable($envName)
    if ($null -ne $envVal) {
      return $value.Replace("%$envName%", $envVal)
    }
  }
  return $value
}

function Write-Title([string]$text) {
  Write-Host ''
  Write-Host "=== $text ===" -ForegroundColor Cyan
}

function Write-Ok([string]$text) {
  Write-Host "  [OK] $text" -ForegroundColor Green
}

function Write-Fail([string]$text) {
  Write-Host "  [!!] $text" -ForegroundColor Red
}

function Write-Warn([string]$text) {
  Write-Host "  [--] $text" -ForegroundColor Yellow
}

function Get-YmlVersion([string]$ymlPath) {
  $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8
  $m = [regex]::Match($content, '(?m)^version:\s*(\S+)\s*$')
  if ($m.Success) {
    return $m.Groups[1].Value.Trim()
  }
  return $null
}

function Get-YmlPrimaryPath([string]$ymlPath) {
  $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8
  $m = [regex]::Match($content, '(?m)^path:\s*(\S+)\s*$')
  if ($m.Success) {
    return $m.Groups[1].Value.Trim()
  }
  return $null
}

function Get-YmlReferencedFiles([string]$ymlPath) {
  $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
  $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8
  foreach ($m in [regex]::Matches($content, '(?m)^(?:\s*-\s*)?(?:url|path):\s*(\S+)\s*$')) {
    [void]$names.Add($m.Groups[1].Value.Trim())
  }
  return @($names)
}

function Get-YmlOptionalUrls([string]$ymlPath) {
  $primary = Get-YmlPrimaryPath $ymlPath
  $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
  $content = Get-Content -LiteralPath $ymlPath -Raw -Encoding UTF8
  foreach ($m in [regex]::Matches($content, '(?m)^\s*-\s*url:\s*(\S+)\s*$')) {
    $name = $m.Groups[1].Value.Trim()
    if ($primary -and $name.Equals($primary, [StringComparison]::OrdinalIgnoreCase)) {
      continue
    }
    [void]$names.Add($name)
  }
  return @($names)
}

function Resolve-ReleaseFile([string]$name) {
  $direct = Join-Path $ReleaseDir $name
  if (Test-Path -LiteralPath $direct) {
    return Get-Item -LiteralPath $direct
  }
  if ($name -match 'x64' -and $name -notmatch 'x86_64') {
    $alt = $name -replace 'x64', 'x86_64'
    $altPath = Join-Path $ReleaseDir $alt
    if (Test-Path -LiteralPath $altPath) {
      return Get-Item -LiteralPath $altPath
    }
  }
  if ($name -match 'x86_64') {
    $alt = $name -replace 'x86_64', 'x64'
    $altPath = Join-Path $ReleaseDir $alt
    if (Test-Path -LiteralPath $altPath) {
      return Get-Item -LiteralPath $altPath
    }
  }
  return $null
}

# electron-builder may leave TTRPGPlayer-x86_64.AppImage while yml/site expect x64.
# Prefer the newer x86_64 build and force the feed name before upload.
function Normalize-LinuxAppImageNames {
  $imgs = Get-ChildItem -LiteralPath $ReleaseDir -Filter 'TTRPGPlayer-*.AppImage' -File -ErrorAction SilentlyContinue
  foreach ($img in $imgs) {
    if ($img.Name -notmatch 'x86_64') { continue }
    $toName = $img.Name -replace 'x86_64', 'x64'
    $toPath = Join-Path $ReleaseDir $toName
    if (Test-Path -LiteralPath $toPath) {
      Remove-Item -LiteralPath $toPath -Force
      Write-Warn "removed stale $toName (replaced by $($img.Name))"
    }
    Rename-Item -LiteralPath $img.FullName -NewName $toName
    Write-Ok "normalized $($img.Name) -> $toName"
  }
}

function Add-Upload {
  param(
    [System.Collections.Generic.Dictionary[string, string]]$map,
    [string]$remoteName,
    [System.IO.FileInfo]$file
  )
  if ($map.ContainsKey($remoteName)) {
    $prev = $map[$remoteName]
    if (-not $prev.Equals($file.FullName, [StringComparison]::OrdinalIgnoreCase)) {
      Write-Warn "upload name collision for $remoteName: keeping $($file.Name)"
    }
  }
  $map[$remoteName] = $file.FullName
}

Write-Title 'TTRPG Release Publisher'
Write-Host "Release folder: $ReleaseDir"

if (-not (Test-Path -LiteralPath $ConfigPath)) {
  throw "Missing publish-config.json: $ConfigPath"
}

$config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
$sshKey = Expand-ConfigPath $config.sshKey
$sshTarget = [string]$config.sshTarget
$remoteDir = [string]$config.remoteDir
$feedUrl = [string]$config.feedUrl

if (-not (Test-Path -LiteralPath $sshKey)) {
  throw "SSH key not found: $sshKey"
}

$errors = [System.Collections.Generic.List[string]]::new()
$warnings = [System.Collections.Generic.List[string]]::new()
# remote file name -> local full path (scp must use the feed/site name, not disk alias)
$uploadMap = [System.Collections.Generic.Dictionary[string, string]]::new([StringComparer]::OrdinalIgnoreCase)

Write-Title 'Windows (required)'
$winYml = Join-Path $ReleaseDir 'latest.yml'
if (-not (Test-Path -LiteralPath $winYml)) {
  $errors.Add('Missing latest.yml')
} else {
  Write-Ok 'latest.yml'
  Add-Upload $uploadMap ([System.IO.Path]::GetFileName($winYml)) (Get-Item -LiteralPath $winYml)
  foreach ($name in (Get-YmlReferencedFiles $winYml)) {
    $file = Resolve-ReleaseFile $name
    if ($null -eq $file) {
      $errors.Add("Windows: missing file $name (from latest.yml)")
    } else {
      Write-Ok $file.Name
      Add-Upload $uploadMap $name $file
    }
  }
  $blockmap = Resolve-ReleaseFile 'TTRPGPlayer-Setup.exe.blockmap'
  if ($null -eq $blockmap) {
    $warnings.Add('Missing TTRPGPlayer-Setup.exe.blockmap (recommended)')
  } else {
    Write-Ok $blockmap.Name
    Add-Upload $uploadMap 'TTRPGPlayer-Setup.exe.blockmap' $blockmap
  }
}

Write-Title 'Linux (if latest-linux*.yml present)'
Normalize-LinuxAppImageNames
$linuxYmls = Get-ChildItem -LiteralPath $ReleaseDir -Filter 'latest-linux*.yml' -File -ErrorAction SilentlyContinue
if ($linuxYmls.Count -eq 0) {
  Write-Warn 'No latest-linux*.yml - skipping Linux'
} else {
  foreach ($yml in $linuxYmls) {
    Write-Ok $yml.Name
    Add-Upload $uploadMap $yml.Name $yml
    foreach ($name in (Get-YmlReferencedFiles $yml.FullName)) {
      $file = Resolve-ReleaseFile $name
      if ($null -eq $file) {
        $errors.Add("Linux ($($yml.Name)): missing file $name")
      } else {
        if ($file.Name -ne $name) {
          Write-Warn "$($yml.Name): yml expects $name, disk has $($file.Name) - uploading as $name"
        } else {
          Write-Ok "$($yml.Name) -> $name"
        }
        Add-Upload $uploadMap $name $file
      }
    }
  }
}

Write-Title 'macOS (if latest-mac.yml present)'
$macYml = Join-Path $ReleaseDir 'latest-mac.yml'
if ($SkipMac) {
  Write-Warn 'macOS skipped (-SkipMac)'
} elseif (-not (Test-Path -LiteralPath $macYml)) {
  Write-Warn 'No latest-mac.yml - skipping macOS'
} else {
  Write-Ok 'latest-mac.yml'
  Add-Upload $uploadMap ([System.IO.Path]::GetFileName($macYml)) (Get-Item -LiteralPath $macYml)

  $macVersion = Get-YmlVersion $macYml
  $winYml = Join-Path $ReleaseDir 'latest.yml'
  $winVersion = $null
  if (Test-Path -LiteralPath $winYml) {
    $winVersion = Get-YmlVersion $winYml
  }
  if ($macVersion -and $winVersion -and $macVersion -ne $winVersion) {
    $errors.Add(
      "macOS: latest-mac.yml is v$macVersion but latest.yml is v$winVersion (stale Mac feed). " +
      'On Mac run: npm run pack:mac, then copy latest-mac.yml + TTRPGPlayer-*.zip (+ optional *.dmg). ' +
      'Or publish Win/Linux only: publish.cmd -SkipMac'
    )
  }

  $primaryName = Get-YmlPrimaryPath $macYml
  if (-not $primaryName) {
    $errors.Add('macOS: latest-mac.yml has no path: entry')
  } else {
    $primaryFile = Resolve-ReleaseFile $primaryName
    if ($null -eq $primaryFile) {
      $errors.Add(
        "macOS: missing primary update file $primaryName (path in latest-mac.yml). " +
        'electron-updater on macOS needs the .zip from pack:mac, not only .dmg. Rebuild on Mac or use -SkipMac.'
      )
    } else {
      Write-Ok "primary update: $($primaryFile.Name)"
      Add-Upload $uploadMap $primaryName $primaryFile
    }
  }

  foreach ($name in (Get-YmlOptionalUrls $macYml)) {
    $file = Resolve-ReleaseFile $name
    if ($null -eq $file) {
      $warnings.Add("macOS: optional file missing (not uploaded): $name")
    } else {
      Write-Ok "optional: $($file.Name)"
      Add-Upload $uploadMap $name $file
    }
  }
}

Write-Title 'Summary'
foreach ($w in $warnings) {
  Write-Warn $w
}
if ($errors.Count -gt 0) {
  foreach ($e in $errors) {
    Write-Fail $e
  }
  Write-Host ''
  Write-Host 'Upload cancelled. Fix the release folder and run again.' -ForegroundColor Red
  exit 1
}

Write-Host ''
Write-Host "Files to upload: $($uploadMap.Count)" -ForegroundColor Green
foreach ($remoteName in ($uploadMap.Keys | Sort-Object)) {
  $localName = [System.IO.Path]::GetFileName($uploadMap[$remoteName])
  if ($localName -eq $remoteName) {
    Write-Host "  - $remoteName"
  } else {
    Write-Host "  - $remoteName  (from $localName)"
  }
}

if ($CheckOnly) {
  Write-Host ''
  Write-Host 'CheckOnly: upload skipped.' -ForegroundColor Yellow
  exit 0
}

Write-Title 'Upload'
Write-Host "Target: ${sshTarget}:${remoteDir}"
Write-Host "Feed: $feedUrl"

foreach ($remoteName in ($uploadMap.Keys | Sort-Object)) {
  $localPath = $uploadMap[$remoteName]
  Write-Host "  -> $remoteName"
  & scp -i $sshKey -q $localPath "${sshTarget}:${remoteDir}/$remoteName"
  if ($LASTEXITCODE -ne 0) {
    throw "scp failed for $remoteName (exit $LASTEXITCODE)"
  }
}

Write-Host ''
Write-Host 'Setting www-data ownership on server...'
& ssh -i $sshKey $sshTarget "chown -R www-data:www-data '$remoteDir'"
if ($LASTEXITCODE -ne 0) {
  throw "ssh chown failed (exit $LASTEXITCODE)"
}

Write-Title 'Done'
Write-Host 'Verify:'
Write-Host "  ${feedUrl}latest.yml"
if (Test-Path -LiteralPath (Join-Path $ReleaseDir 'latest-linux.yml')) {
  Write-Host "  ${feedUrl}latest-linux.yml"
}
if (Test-Path -LiteralPath (Join-Path $ReleaseDir 'latest-mac.yml')) {
  Write-Host "  ${feedUrl}latest-mac.yml"
}

exit 0
