param(
  [Parameter(Mandatory = $true)][string]$InstallRoot,
  [Parameter(Mandatory = $true)][string]$StagingRoot,
  [Parameter(Mandatory = $true)][string]$ManifestPath,
  [Parameter(Mandatory = $true)][string]$PreviousManifestPath,
  [Parameter(Mandatory = $true)][string]$ExecutablePath,
  [Parameter(Mandatory = $true)][string]$LogPath,
  [string]$HandshakePath = "",
  [int]$ParentProcessId = 0
)

$ErrorActionPreference = "Stop"
$backupRoot = Join-Path $StagingRoot "rollback"
$script:backedUpPaths = [System.Collections.Generic.List[string]]::new()
$script:createdPaths = [System.Collections.Generic.List[string]]::new()
$script:previousManifestExisted = $false
$script:applyStarted = $false

function Write-UpdateLog([string]$Message) {
  $timestamp = (Get-Date).ToUniversalTime().ToString("o")
  $lastError = $null
  for ($attempt = 0; $attempt -lt 40; $attempt++) {
    try {
      Add-Content -LiteralPath $LogPath -Value "$timestamp $Message" -Encoding UTF8
      return
    } catch {
      $lastError = $_
      Start-Sleep -Milliseconds 50
    }
  }
  throw $lastError
}

function Test-ManifestPath([string]$Path) {
  if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
  if ($Path.StartsWith("/") -or $Path.StartsWith("\")) { return $false }
  if ($Path.Contains(":")) { return $false }
  foreach ($part in ($Path -replace "\\", "/" -split "/")) {
    if ([string]::IsNullOrWhiteSpace($part) -or $part -eq "..") {
      return $false
    }
  }
  return $true
}

function Join-ManifestPath([string]$Root, [string]$Path) {
  $resolved = $Root
  foreach ($part in ($Path -replace "\\", "/" -split "/")) {
    $resolved = Join-Path $resolved $part
  }
  return $resolved
}

function Backup-InstalledFile([string]$Target, [string]$ManifestPath) {
  if (Test-Path -LiteralPath $Target -PathType Leaf) {
    $backup = Join-ManifestPath $backupRoot $ManifestPath
    New-Item -ItemType Directory -Force -Path (Split-Path -Parent $backup) | Out-Null
    Copy-Item -LiteralPath $Target -Destination $backup -Force
    $script:backedUpPaths.Add($ManifestPath)
  } else {
    $script:createdPaths.Add($ManifestPath)
  }
}

function Restore-PreviousInstallation {
  Write-UpdateLog "Rolling back interrupted update"
  foreach ($path in $script:createdPaths) {
    $target = Join-ManifestPath $InstallRoot $path
    if (Test-Path -LiteralPath $target -PathType Leaf) {
      Remove-Item -LiteralPath $target -Force
    }
  }
  foreach ($path in $script:backedUpPaths) {
    $backup = Join-ManifestPath $backupRoot $path
    $target = Join-ManifestPath $InstallRoot $path
    if (Test-Path -LiteralPath $backup -PathType Leaf) {
      New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null
      Copy-Item -LiteralPath $backup -Destination $target -Force
    }
  }
  $manifestBackup = Join-Path $backupRoot "previous_manifest.json"
  if ($script:previousManifestExisted -and (Test-Path -LiteralPath $manifestBackup)) {
    New-Item -ItemType Directory -Force -Path (Split-Path -Parent $PreviousManifestPath) | Out-Null
    Copy-Item -LiteralPath $manifestBackup -Destination $PreviousManifestPath -Force
  } elseif (Test-Path -LiteralPath $PreviousManifestPath) {
    Remove-Item -LiteralPath $PreviousManifestPath -Force
  }
  Write-UpdateLog "Rollback complete"
}

try {
  New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LogPath) | Out-Null
  if (-not [string]::IsNullOrWhiteSpace($HandshakePath)) {
    New-Item -ItemType Directory -Force -Path (Split-Path -Parent $HandshakePath) | Out-Null
    Set-Content -LiteralPath $HandshakePath -Value "Updater started" -NoNewline -Encoding UTF8
  }
  Write-UpdateLog "Updater started"
  Write-UpdateLog "Starting OPCG Online update"
  Write-UpdateLog "InstallRoot=$InstallRoot"
  Write-UpdateLog "StagingRoot=$StagingRoot"
  Write-UpdateLog "ManifestPath=$ManifestPath"
  Write-UpdateLog "PreviousManifestPath=$PreviousManifestPath"
  Write-UpdateLog "ExecutablePath=$ExecutablePath"
  Write-UpdateLog "HandshakePath=$HandshakePath"
  Write-UpdateLog "ParentProcessId=$ParentProcessId"
  Write-UpdateLog "UpdaterProcessId=$PID"

  $deadline = (Get-Date).AddSeconds(45)
  while ((Get-Date) -lt $deadline) {
    $running = if ($ParentProcessId -gt 0) {
      @(Get-Process -Id $ParentProcessId -ErrorAction SilentlyContinue)
    } else {
      @(Get-Process -Name "opcg_online" -ErrorAction SilentlyContinue | Where-Object {
        try {
          $_.Path -eq $ExecutablePath
        } catch {
          $true
        }
      })
    }
    if ($running.Count -eq 0) { break }
    Start-Sleep -Milliseconds 250
  }

  $manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
  $files = @($manifest.files)
  $filesRoot = Join-Path $StagingRoot "files"
  if (Test-Path -LiteralPath $backupRoot) {
    Remove-Item -LiteralPath $backupRoot -Recurse -Force
  }
  New-Item -ItemType Directory -Force -Path $backupRoot | Out-Null
  if (Test-Path -LiteralPath $PreviousManifestPath -PathType Leaf) {
    $script:previousManifestExisted = $true
    Copy-Item -LiteralPath $PreviousManifestPath -Destination (Join-Path $backupRoot "previous_manifest.json") -Force
  }
  $script:applyStarted = $true

  foreach ($file in $files) {
    $path = [string]$file.path
    if (-not (Test-ManifestPath $path)) {
      throw "Unsafe manifest path: $path"
    }
    $source = Join-ManifestPath $filesRoot $path
    $target = Join-ManifestPath $InstallRoot $path
    if (Test-Path -LiteralPath $source -PathType Leaf) {
      New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null
      Backup-InstalledFile $target $path
      $temporaryTarget = "$target.opcg-new-$PID"
      Copy-Item -LiteralPath $source -Destination $temporaryTarget -Force
      if (Test-Path -LiteralPath $target -PathType Leaf) {
        $replaceBackup = "$target.opcg-replaced-$PID"
        [System.IO.File]::Replace($temporaryTarget, $target, $replaceBackup)
        Remove-Item -LiteralPath $replaceBackup -Force -ErrorAction SilentlyContinue
      } else {
        [System.IO.File]::Move($temporaryTarget, $target)
      }
      Write-UpdateLog "Copied $path"
      continue
    }
    if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
      throw "Manifest file is neither staged nor installed: $path"
    }
    Write-UpdateLog "Kept unchanged $path"
  }

  if (Test-Path -LiteralPath $PreviousManifestPath) {
    $previous = Get-Content -LiteralPath $PreviousManifestPath -Raw | ConvertFrom-Json
    $newPaths = @{}
    foreach ($file in $files) {
      $newPaths[[string]$file.path] = $true
    }
    foreach ($oldFile in @($previous.files)) {
      $oldPath = [string]$oldFile.path
      if (-not (Test-ManifestPath $oldPath)) { continue }
      if ($newPaths.ContainsKey($oldPath)) { continue }
      $obsolete = Join-ManifestPath $InstallRoot $oldPath
      if (Test-Path -LiteralPath $obsolete -PathType Leaf) {
        Backup-InstalledFile $obsolete $oldPath
        Remove-Item -LiteralPath $obsolete -Force
        Write-UpdateLog "Removed obsolete $oldPath"
      }
    }
  }

  New-Item -ItemType Directory -Force -Path (Split-Path -Parent $PreviousManifestPath) | Out-Null
  $temporaryManifest = "$PreviousManifestPath.opcg-new-$PID"
  Copy-Item -LiteralPath $ManifestPath -Destination $temporaryManifest -Force
  if (Test-Path -LiteralPath $PreviousManifestPath -PathType Leaf) {
    $manifestReplaceBackup = "$PreviousManifestPath.opcg-replaced-$PID"
    [System.IO.File]::Replace($temporaryManifest, $PreviousManifestPath, $manifestReplaceBackup)
    Remove-Item -LiteralPath $manifestReplaceBackup -Force -ErrorAction SilentlyContinue
  } else {
    [System.IO.File]::Move($temporaryManifest, $PreviousManifestPath)
  }

  Write-UpdateLog "Update complete, relaunching"
  $relaunch = Start-Process -FilePath $ExecutablePath -WorkingDirectory $InstallRoot -PassThru
  Write-UpdateLog "Relaunched processId=$($relaunch.Id)"
  Start-Sleep -Seconds 5
  if ($relaunch.HasExited) {
    throw "Updated application exited during startup with code $($relaunch.ExitCode)."
  }
  $script:applyStarted = $false
  Remove-Item -LiteralPath $StagingRoot -Recurse -Force -ErrorAction SilentlyContinue
  Write-UpdateLog "Updated application remained healthy after relaunch"
} catch {
  Write-UpdateLog "Update failed: $($_.Exception.Message)"
  Write-UpdateLog "$($_.ScriptStackTrace)"
  if ($script:applyStarted) {
    try {
      Restore-PreviousInstallation
      $restored = Start-Process -FilePath $ExecutablePath -WorkingDirectory $InstallRoot -PassThru
      Write-UpdateLog "Relaunched restored processId=$($restored.Id)"
    } catch {
      Write-UpdateLog "Rollback failed: $($_.Exception.Message)"
    }
  }
  throw
}
