# Prepare TTRPG release: build Win/Linux, copy to release folder.
# Run: prepare-release.cmd
#
# Recommended (Mac first, same version on all platforms):
#   1) Bump version in package.json manually (npm version patch --no-git-tag-version)
#   2) pack:mac on Mac, copy latest-mac.yml + *.zip to release folder
#   3) prepare-release.cmd -AfterMac   (or release-all.cmd)
#
# Options:
#   -AfterMac           do not bump; require Mac files in release folder (default in release-all)
#   -Bump               bump patch before build (Mac can be added later)
#   -Version 1.0.17     set explicit version (with -Bump workflow)
#   -Minor              bump minor (with -Bump)
#   -SkipGit            skip commit/push
#   -SkipLinux          skip Linux build (WSL)
#   -NoBump             do not change package.json (same as -AfterMac without Mac check)

param(
  [string]$Version = '',
  [switch]$Patch,
  [switch]$Minor,
  [switch]$Bump,
  [switch]$AfterMac,
  [switch]$SkipGit,
  [switch]$SkipLinux,
  [switch]$NoBump
)

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

$ToolDir = $PSScriptRoot
$ConfigPath = Join-Path $ToolDir 'release-config.json'

function Write-Step([int]$n, [string]$text) {
  Write-Host ''
  Write-Host "----- Step $n : $text -----" -ForegroundColor Cyan
}

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

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

$FfmpegStaticMirror = 'https://cdn.npmmirror.com/binaries/ffmpeg-static'

function Test-IsNpmCi([string[]]$NpmArgs) {
  return ($NpmArgs.Count -eq 1 -and $NpmArgs[0] -eq 'ci')
}

# Stops Vite/Electron/dev.mjs/TTRPGPlayer that lock native .node files under node_modules
# (typical EPERM unlink on @rolldown/binding-win32-x64-msvc during npm ci).
function Stop-DndPlayerLocks {
  param([string]$ProjectRoot)

  Write-Host '  Stopping processes that may lock node_modules...'
  $killed = [System.Collections.Generic.HashSet[int]]::new()

  foreach ($name in @('TTRPGPlayer', 'electron')) {
    Get-Process -Name $name -ErrorAction SilentlyContinue | ForEach-Object {
      Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
      [void]$killed.Add([int]$_.Id)
    }
  }

  Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
    $cmd = [string]$_.CommandLine
    if (-not $cmd) { return }
    $touches = ($cmd -match [regex]::Escape($ProjectRoot)) -or ($cmd -match 'dnd_player')
    if (-not $touches) { return }
    if ($cmd -match 'dev\.mjs|vite|electron|esbuild|rolldown|tsx|scripts[/\\]dev') {
      Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
      [void]$killed.Add([int]$_.ProcessId)
    }
  }

  foreach ($port in @(5173, 5174)) {
    try {
      Get-NetTCPConnection -LocalPort $port -ErrorAction Stop | ForEach-Object {
        $ownerPid = [int]$_.OwningProcess
        if ($ownerPid -gt 0) {
          Stop-Process -Id $ownerPid -Force -ErrorAction SilentlyContinue
          [void]$killed.Add($ownerPid)
        }
      }
    } catch {
      # NetTCPIP may be unavailable in some shells — ignore
    }
  }

  Start-Sleep -Seconds 2

  $rolldownDir = Join-Path $ProjectRoot 'node_modules\@rolldown\binding-win32-x64-msvc'
  if (Test-Path -LiteralPath $rolldownDir) {
    try {
      Remove-Item -LiteralPath $rolldownDir -Recurse -Force -ErrorAction Stop
      Write-Host '  [OK] removed locked @rolldown/binding-win32-x64-msvc (npm ci will restore)'
    } catch {
      Write-Host "  [--] rolldown binding still locked: $($_.Exception.Message)" -ForegroundColor Yellow
    }
  }

  Write-Host "  Cleared $($killed.Count) process id(s); waiting for file unlock"
  Start-Sleep -Seconds 1
}

function Invoke-NpmRaw {
  param(
    [string[]]$NpmArgs,
    [string]$WorkingDirectory
  )
  $prevEap = $ErrorActionPreference
  $ErrorActionPreference = 'Continue'
  Push-Location $WorkingDirectory
  try {
    & npm @NpmArgs 2>&1 | ForEach-Object {
      if ($_ -is [System.Management.Automation.ErrorRecord]) {
        Write-Host $_.ToString()
      } else {
        Write-Host ([string]$_)
      }
    }
    return $LASTEXITCODE
  } finally {
    Pop-Location
    $ErrorActionPreference = $prevEap
  }
}

function Invoke-NpmCiWithFfmpegMirror {
  param(
    [string]$WorkingDirectory
  )
  $prevMirror = $env:FFMPEG_BINARIES_URL
  $env:FFMPEG_BINARIES_URL = $FfmpegStaticMirror
  try {
    Write-Host "  > npm ci (retry with FFMPEG_BINARIES_URL=$FfmpegStaticMirror)"
    return (Invoke-NpmRaw -NpmArgs @('ci') -WorkingDirectory $WorkingDirectory)
  } finally {
    if ($null -eq $prevMirror) {
      Remove-Item Env:\FFMPEG_BINARIES_URL -ErrorAction SilentlyContinue
    } else {
      $env:FFMPEG_BINARIES_URL = $prevMirror
    }
  }
}

function Invoke-Npm {
  param(
    [string]$Label,
    [string[]]$NpmArgs,
    [string]$WorkingDirectory
  )
  Write-Host "  > $Label"
  $exitCode = Invoke-NpmRaw -NpmArgs $NpmArgs -WorkingDirectory $WorkingDirectory
  if ($exitCode -ne 0 -and (Test-IsNpmCi $NpmArgs)) {
    Write-Host "  [--] npm ci failed (exit $exitCode). Clearing locks and retrying..." -ForegroundColor Yellow
    Stop-DndPlayerLocks $WorkingDirectory
    $exitCode = Invoke-NpmRaw -NpmArgs $NpmArgs -WorkingDirectory $WorkingDirectory
    if ($exitCode -ne 0) {
      Write-Host "  [--] npm ci failed again. Retrying via ffmpeg-static mirror..." -ForegroundColor Yellow
      Stop-DndPlayerLocks $WorkingDirectory
      $exitCode = Invoke-NpmCiWithFfmpegMirror $WorkingDirectory
    }
  }
  if ($exitCode -ne 0) {
    throw "$Label failed (exit $exitCode)"
  }
}

function Convert-ToWslPath([string]$winPath) {
  $full = [System.IO.Path]::GetFullPath($winPath)
  $p = $full -replace '\\', '/'
  if ($p -match '^([A-Za-z]):(.*)$') {
    $drive = $Matches[1].ToLower()
    return "/mnt/$drive$($Matches[2])"
  }
  return $p
}

function Get-WslLinuxBuildCommand([string]$WslProjectPath) {
  # WSL Debian often has node 18 on PATH; scripts/wsl-pack-linux.sh uses nvm + .nvmrc.
  return "cd '$WslProjectPath' && bash scripts/wsl-pack-linux.sh"
}

function Read-PackageVersion([string]$packageJsonPath) {
  $json = Get-Content -LiteralPath $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
  return [string]$json.version
}

function Invoke-NpmVersion {
  param(
    [string]$ProjectRoot,
    [string]$Spec
  )
  Invoke-Npm "npm version $Spec" @('version', $Spec, '--no-git-tag-version', '--allow-same-version') $ProjectRoot
}

function Get-GiteaPushUrl([string]$mcpConfigPath) {
  if (-not (Test-Path -LiteralPath $mcpConfigPath)) {
    return $null
  }
  $raw = Get-Content -LiteralPath $mcpConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
  $token = $raw.mcpServers.'gitea-mailib'.env.GITEA_ACCESS_TOKEN
  if (-not $token) {
    return $null
  }
  return "https://ifontosh:${token}@git.mailib.ru/ifontosh/DndGamePlayer.git"
}

function Invoke-Git {
  param(
    [string]$ProjectRoot,
    [string[]]$GitArgs
  )
  & git -C $ProjectRoot @GitArgs
  if ($LASTEXITCODE -ne 0) {
    throw "git $($GitArgs -join ' ') failed (exit $LASTEXITCODE)"
  }
}

function Get-GitTextOutput {
  param(
    [string]$ProjectRoot,
    [string[]]$GitArgs
  )
  $prevEap = $ErrorActionPreference
  $ErrorActionPreference = 'Continue'
  try {
    $output = & git -C $ProjectRoot @GitArgs 2>&1
    if ($LASTEXITCODE -ne 0) {
      throw "git $($GitArgs -join ' ') failed (exit $LASTEXITCODE)"
    }
    if ($null -eq $output) {
      return ''
    }
    return (@($output | ForEach-Object {
        if ($_ -is [System.Management.Automation.ErrorRecord]) {
          $_.ToString()
        } else {
          [string]$_
        }
      }) -join "`n").Trim()
  } finally {
    $ErrorActionPreference = $prevEap
  }
}

function Write-GitLines($lines) {
  foreach ($line in $lines) {
    if ($line -is [System.Management.Automation.ErrorRecord]) {
      Write-Host $line.ToString()
    } else {
      Write-Host ([string]$line)
    }
  }
}

function Invoke-GitPush {
  param(
    [string]$ProjectRoot,
    [string[]]$PushArgs
  )
  $prevEap = $ErrorActionPreference
  $ErrorActionPreference = 'Continue'
  try {
    $output = & git -C $ProjectRoot push @PushArgs 2>&1
    Write-GitLines $output
    return $LASTEXITCODE
  } finally {
    $ErrorActionPreference = $prevEap
  }
}

function Push-Git([string]$ProjectRoot, [string]$Remote, [string]$McpConfigPath) {
  $branch = Get-GitTextOutput $ProjectRoot @('rev-parse', '--abbrev-ref', 'HEAD')
  Write-Host "  > git push $Remote $branch"
  $exitCode = Invoke-GitPush $ProjectRoot @($Remote, $branch)
  if ($exitCode -eq 0) {
    return
  }
  $pushUrl = Get-GiteaPushUrl $McpConfigPath
  if (-not $pushUrl) {
    throw "git push failed (exit $exitCode) and no Gitea token in mcp config ($McpConfigPath)"
  }
  Write-Host "  > git push via Gitea token URL"
  $exitCode = Invoke-GitPush $ProjectRoot @($pushUrl, "HEAD:${branch}")
  if ($exitCode -ne 0) {
    throw "git push failed (exit $exitCode)"
  }
}

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

function Test-MacReleaseReady {
  param(
    [string]$ReleaseDir,
    [string]$ExpectedVersion
  )
  $macYml = Join-Path $ReleaseDir 'latest-mac.yml'
  if (-not (Test-Path -LiteralPath $macYml)) {
    throw @"
AfterMac: missing latest-mac.yml in $ReleaseDir
  1) Bump version in package.json (now $ExpectedVersion)
  2) npm run pack:mac on Mac
  3) Copy latest-mac.yml + TTRPGPlayer-*.zip into release folder
  4) Run prepare-release.cmd -AfterMac again
"@
  }
  $macVersion = Get-YmlField $macYml 'version'
  if ($macVersion -and $macVersion -ne $ExpectedVersion) {
    throw "AfterMac: latest-mac.yml is v$macVersion but package.json is v$ExpectedVersion. Align versions before building Win/Linux."
  }
  $primary = Get-YmlField $macYml 'path'
  if (-not $primary) {
    throw 'AfterMac: latest-mac.yml has no path: entry'
  }
  $primaryPath = Join-Path $ReleaseDir $primary
  if (-not (Test-Path -LiteralPath $primaryPath)) {
    throw "AfterMac: missing Mac update file $primary (path in latest-mac.yml). Copy zip from Mac build."
  }
  Write-Ok "Mac feed v$ExpectedVersion, primary: $primary"
}

function Copy-ReleaseArtifacts {
  param(
    [string]$BuildReleaseDir,
    [string]$TargetDir
  )

  if (-not (Test-Path -LiteralPath $TargetDir)) {
    New-Item -ItemType Directory -Path $TargetDir | Out-Null
  }

  $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)

  $fixed = @(
    'latest.yml',
    'TTRPGPlayer-Setup.exe',
    'TTRPGPlayer-Setup.exe.blockmap'
  )
  foreach ($n in $fixed) {
    [void]$names.Add($n)
  }

  foreach ($yml in Get-ChildItem -LiteralPath $BuildReleaseDir -Filter 'latest-linux*.yml' -File -ErrorAction SilentlyContinue) {
    [void]$names.Add($yml.Name)
    $content = Get-Content -LiteralPath $yml.FullName -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())
    }
  }

  foreach ($img in Get-ChildItem -LiteralPath $BuildReleaseDir -Filter 'TTRPGPlayer-*.AppImage' -File -ErrorAction SilentlyContinue) {
    [void]$names.Add($img.Name)
  }

  $copied = 0
  foreach ($name in ($names | Sort-Object)) {
    # Always publish Linux AppImage under feed/site name (x64), never leave x86_64 alias.
    if ($name -match 'x86_64' -and $name -match '\.AppImage$') {
      $canonical = $name -replace 'x86_64', 'x64'
      if ($names.Contains($canonical)) {
        continue
      }
      $name = $canonical
    }

    $src = Join-Path $BuildReleaseDir $name
    $destName = $name
    if (-not (Test-Path -LiteralPath $src)) {
      if ($name -match 'x64' -and $name -notmatch 'x86_64') {
        $alt = $name -replace 'x64', 'x86_64'
        $altPath = Join-Path $BuildReleaseDir $alt
        if (Test-Path -LiteralPath $altPath) {
          $src = $altPath
        }
      }
    }
    if (-not (Test-Path -LiteralPath $src)) {
      Write-Host "  [--] skip (not built): $name" -ForegroundColor Yellow
      continue
    }
    $dest = Join-Path $TargetDir $destName
    Copy-Item -LiteralPath $src -Destination $dest -Force
    if ([System.IO.Path]::GetFileName($src) -ne $destName) {
      Write-Ok "copied $([System.IO.Path]::GetFileName($src)) -> $destName"
    } else {
      Write-Ok "copied $destName"
    }
    $copied += 1
  }

  if ($copied -eq 0) {
    throw 'No release artifacts copied - check build output in project release folder'
  }
}

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

$config = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
$projectRoot = [System.IO.Path]::GetFullPath([string]$config.projectRoot)
$releaseDir = [System.IO.Path]::GetFullPath([string]$config.releaseDir)
$gitRemote = [string]$config.gitRemote
$mcpConfig = [string]$config.giteaMcpConfig
$wslDistro = [string]$config.wslDistro

$packageJson = Join-Path $projectRoot 'package.json'
$buildReleaseDir = Join-Path $projectRoot 'release'

if (-not (Test-Path -LiteralPath $packageJson)) {
  throw "package.json not found: $packageJson"
}

Write-Host '=== TTRPG Prepare Release ===' -ForegroundColor Cyan
Write-Host "Project: $projectRoot"
Write-Host "Release folder: $releaseDir"

if ($AfterMac) {
  $NoBump = $true
}
if ($Bump -and $AfterMac) {
  throw 'Use either -Bump or -AfterMac, not both'
}
if ($Bump) {
  $Patch = $true
}

$currentVersion = Read-PackageVersion $packageJson

if ($AfterMac) {
  Write-Step 0 'Mac artifacts check'
  Test-MacReleaseReady $releaseDir $currentVersion
}

# Step 1 - version
if ($AfterMac) {
  Write-Step 1 'Version (unchanged, Mac-first workflow)'
  $newVersion = $currentVersion
  Write-Ok "building Win/Linux at v$newVersion (same as Mac feed)"
} else {
  Write-Step 1 'Bump version'
  $newVersion = $currentVersion
}

if ($AfterMac) {
  # version step done above
} elseif ($NoBump) {
  Write-Ok "version unchanged: $currentVersion"
  $newVersion = $currentVersion
} elseif ($Version) {
  Invoke-NpmVersion $projectRoot $Version.Trim()
  $newVersion = Read-PackageVersion $packageJson
  Write-Ok "version set to $newVersion (was $currentVersion)"
} elseif ($Minor) {
  Invoke-NpmVersion $projectRoot 'minor'
  $newVersion = Read-PackageVersion $packageJson
  Write-Ok "version $currentVersion -> $newVersion (minor)"
} else {
  Invoke-NpmVersion $projectRoot 'patch'
  $newVersion = Read-PackageVersion $packageJson
  Write-Ok "version $currentVersion -> $newVersion (patch)"
}

# Step 2 - git
if ($SkipGit) {
  Write-Step 2 'Git commit and push (skipped)'
} else {
  Write-Step 2 'Git commit and push'
  Invoke-Git $projectRoot @('add', 'package.json')
  if (Test-Path -LiteralPath (Join-Path $projectRoot 'package-lock.json')) {
    Invoke-Git $projectRoot @('add', 'package-lock.json')
  }
  $commitMsg = "chore: release v$newVersion"
  $status = Get-GitTextOutput $projectRoot @('status', '--porcelain')
  if ($status) {
    Invoke-Git $projectRoot @('commit', '-m', $commitMsg)
    Write-Ok "committed: $commitMsg"
  } else {
    Write-Ok 'nothing to commit (version may already be committed)'
  }
  Push-Git $projectRoot $gitRemote $mcpConfig
  Write-Ok 'pushed to remote'
}

# Step 3 - Windows build
Write-Step 3 'Build Windows (npm ci + pack:win)'
Stop-DndPlayerLocks $projectRoot
Invoke-Npm 'npm ci' @('ci') $projectRoot
Invoke-Npm 'npm run pack:win' @('run', 'pack:win') $projectRoot
Write-Ok 'Windows build finished'

# Step 4 - Linux build
if ($SkipLinux) {
  Write-Step 4 'Build Linux (skipped)'
} else {
  Write-Step 4 'Build Linux via WSL (npm ci + pack:linux)'
  $wslProject = Convert-ToWslPath $projectRoot
  $wslCmd = Get-WslLinuxBuildCommand $wslProject
  $wslArgs = @()
  if ($wslDistro) {
    $wslArgs += '-d', $wslDistro
  }
  $wslArgs += '-e', 'bash', '-lc', $wslCmd
  Write-Host "  > wsl $($wslArgs -join ' ')"
  & wsl @wslArgs
  if ($LASTEXITCODE -ne 0) {
    throw "WSL Linux build failed (exit $LASTEXITCODE). Install WSL or use -SkipLinux"
  }
  Write-Ok 'Linux build finished'
}

# Step 5 - copy
Write-Step 5 'Copy artifacts to release folder'
if (-not (Test-Path -LiteralPath $buildReleaseDir)) {
  throw "Build output missing: $buildReleaseDir"
}
Copy-ReleaseArtifacts $buildReleaseDir $releaseDir
Write-Ok "release folder updated: $releaseDir"

Write-Host ''
Write-Host '=== Prepare release done ===' -ForegroundColor Green
Write-Host "Version: $newVersion"
Write-Host ''
Write-Host 'Next steps:' -ForegroundColor Yellow
if ($AfterMac) {
  Write-Host '  Run publish.cmd (release-all continues to publish automatically)'
} else {
  Write-Host '  1) Copy Mac files (latest-mac.yml, TTRPGPlayer-*.zip) from Mac into release folder'
  Write-Host '  2) Run release-all.cmd (default -AfterMac) or publish.cmd'
}
Write-Host ''

exit 0
