param([string]$GameDirectory=$PSScriptRoot,[string]$LocalFeed,[switch]$Rollback)
$ErrorActionPreference='Stop';$utf8=New-Object Text.UTF8Encoding($false);$mutex=$null;$locked=$false
function Digest([byte[]]$Bytes){$a=[Security.Cryptography.SHA256]::Create();try{return ([BitConverter]::ToString($a.ComputeHash($Bytes))).Replace('-','').ToLowerInvariant()}finally{$a.Dispose()}}
function Write-Atomic([string]$Path,[byte[]]$Bytes){
 [IO.Directory]::CreateDirectory((Split-Path $Path))|Out-Null
 $temp=$Path+'.'+[Guid]::NewGuid().ToString('N').Substring(0,8)+'.tmp';$swap=$temp+'.old'
 try{[IO.File]::WriteAllBytes($temp,$Bytes);if(Test-Path -LiteralPath $Path){[IO.File]::Replace($temp,$Path,$swap)}else{[IO.File]::Move($temp,$Path)}}finally{foreach($p in @($temp,$swap)){if(Test-Path -LiteralPath $p){Remove-Item -LiteralPath $p}}}
}
function Save-Json($Path,$Data){Write-Atomic $Path ($utf8.GetBytes(($Data|ConvertTo-Json -Depth 20)))}
try{
 $game=[IO.Path]::GetFullPath($GameDirectory).TrimEnd('\');$updaterRoot=Join-Path $game 'irongrave-updater'
 $stateFile=Join-Path $updaterRoot 'state.json';$transactions=Join-Path $updaterRoot 'transactions'
 if(!(Test-Path -LiteralPath (Join-Path $game 'mods') -PathType Container)){throw 'Choose the Minecraft folder containing mods.'}
 if(@(Get-CimInstance Win32_Process|Where-Object { $_.Name -in @('java.exe','javaw.exe') -and $_.CommandLine -like ('*'+$game+'*') -and ($_.CommandLine -like '*--gameDir*' -or $_.CommandLine -like '*prismlauncher*' -or $_.CommandLine -like '*multimc*') }).Count){throw 'Minecraft is already using this instance. Close it before updating or restoring.'}
 $mutex=New-Object Threading.Mutex($false,('Local\IronGraveUpdater-'+(Digest ($utf8.GetBytes($game.ToLowerInvariant())))));$locked=$mutex.WaitOne(0)
 if(!$locked){throw 'Another updater is running for this instance.'}
 function Target([string]$Relative){
  if($Relative -notmatch '^(mods/[^/]+\.jar|kubejs/(assets|data|client_scripts|server_scripts|startup_scripts)/.+|defaultconfigs/.+)$' -or $Relative.Contains('..') -or $Relative -match '[\\:*?"<>|]' -or $Relative -match '[. ](/|$)' -or $Relative -match '(^|/)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|/|$)'){throw "Unmanaged or unsafe release path: $Relative"}
  $p=[IO.Path]::GetFullPath((Join-Path $game $Relative));if(!$p.StartsWith($game+'\',[StringComparison]::OrdinalIgnoreCase)){throw 'Path escapes the instance.'}
  $walk=$p;while($walk -and $walk.Length -ge $game.Length){if((Test-Path -LiteralPath $walk) -and ((Get-Item -LiteralPath $walk).Attributes -band [IO.FileAttributes]::ReparsePoint)){throw 'Linked paths are not supported.'};$walk=Split-Path $walk -Parent};return $p
 }
 function Read-State{if(Test-Path -LiteralPath $stateFile){return (Get-Content -LiteralPath $stateFile -Raw|ConvertFrom-Json)};return [pscustomobject]@{release='existing-pack';files=@()}}
 $migrationModule=Join-Path $PSScriptRoot 'Config-Migrations.ps1';if(Test-Path -LiteralPath $migrationModule){. $migrationModule}
 function Valid-Current{
  $state=Read-State;foreach($f in $state.files){$p=Target $f.path;if(!(Test-Path -LiteralPath $p -PathType Leaf) -or (Digest ([IO.File]::ReadAllBytes($p))) -ne $f.sha256){return $false}};return $true
 }
 function Restore-Journal([string]$Journal){
  $t=Get-Content -LiteralPath $Journal -Raw|ConvertFrom-Json;$folder=Split-Path $Journal
  foreach($f in $t.operations){
   if($f.config){$p=Config-Target $f.path;$backupBytes=[IO.File]::ReadAllBytes((Join-Path $folder $f.backup));if((Digest $backupBytes) -ne $f.before){throw 'Config backup integrity check failed.'};$currentBytes=[IO.File]::ReadAllBytes($p);$restoreBytes=if((Digest $currentBytes) -eq $f.after){$backupBytes}else{Restore-ConfigBytes $f};$f|Add-Member NoteProperty restoredBytes ([byte[]]$restoreBytes) -Force;continue}
   $p=Target $f.path;$now=if(Test-Path -LiteralPath $p -PathType Leaf){Digest ([IO.File]::ReadAllBytes($p))}else{$null}
   if($now -and $now -ne $f.after -and $now -ne $f.before){throw "A file changed after the update: $($f.path). Backup retained; restore refused."}
   if($f.present -and (Digest ([IO.File]::ReadAllBytes((Join-Path $folder $f.backup)))) -ne $f.before){throw 'Backup integrity check failed.'}
  }
  foreach($f in $t.operations){if($f.config){$p=Config-Target $f.path;if((Digest ([IO.File]::ReadAllBytes($p))) -ne (Digest $f.restoredBytes)){Write-Atomic $p $f.restoredBytes};$f.PSObject.Properties.Remove('restoredBytes');continue};$p=Target $f.path;if($f.present){$now=if(Test-Path -LiteralPath $p -PathType Leaf){Digest ([IO.File]::ReadAllBytes($p))}else{$null};if($now -ne $f.before){Write-Atomic $p ([IO.File]::ReadAllBytes((Join-Path $folder $f.backup)))}}elseif(Test-Path -LiteralPath $p){Remove-Item -LiteralPath $p}}
  if($t.oldStatePresent){Write-Atomic $stateFile ([IO.File]::ReadAllBytes((Join-Path $folder 'old-state.json')))}elseif(Test-Path -LiteralPath $stateFile){Remove-Item -LiteralPath $stateFile}
  $t.status='Restored';Save-Json $Journal $t;Write-Host 'Previous managed files restored.'
 }
 $journals=@(Get-ChildItem -LiteralPath $transactions -Filter transaction.json -Recurse -ErrorAction SilentlyContinue|Sort-Object LastWriteTime -Descending)
 foreach($j in $journals){$t=Get-Content -LiteralPath $j.FullName -Raw|ConvertFrom-Json;if($t.status -eq 'Pending'){Write-Host 'Recovering an interrupted update...';Restore-Journal $j.FullName}}
 if($Rollback){
  $chosen=$null;foreach($j in $journals){$t=Get-Content -LiteralPath $j.FullName -Raw|ConvertFrom-Json;if($t.status -eq 'Applied'){$chosen=$j.FullName;break}}
  if(!$chosen){throw 'No applied update is available to restore.'};Restore-Journal $chosen
  # Pause until deliberately resumed: prelaunch must not reinstall the rejected release.
  Set-Content -LiteralPath (Join-Path $updaterRoot 'PAUSED.txt') -Value 'Updates paused after manual rollback.';exit 0
 }
 if(Test-Path -LiteralPath (Join-Path $updaterRoot 'PAUSED.txt')){if(!(Valid-Current)){throw 'Updates paused and managed files are incomplete. Restore or repair before launch.'};Write-Host 'Updates paused after rollback; launching the preserved release.';exit 0}
 $config=Get-Content -LiteralPath (Join-Path $updaterRoot 'updater.json') -Raw|ConvertFrom-Json
 if(!$LocalFeed -and $config.localFeed){$LocalFeed=Join-Path $updaterRoot $config.localFeed}
 if($LocalFeed){$feed=[IO.Path]::GetFullPath($LocalFeed)}else{
  $base=[uri]$config.baseUrl;if($base.Scheme -ne 'https' -or $base.UserInfo){throw 'The online feed requires an HTTPS URL without credentials.'}
 }
 function Fetch([string]$Location,[long]$Limit){
  if($LocalFeed){
   if($Location -match '^https://'){throw 'Local test feeds cannot download external URLs.'}
   $p=[IO.Path]::GetFullPath((Join-Path $feed $Location));if(!$p.StartsWith($feed.TrimEnd('\')+'\',[StringComparison]::OrdinalIgnoreCase)){throw 'Feed path escapes the test folder.'}
   if((Get-Item -LiteralPath $p).Length -gt $Limit){throw 'Download exceeds allowed size.'};return ,[IO.File]::ReadAllBytes($p)
  }
  $url=[uri]::new($base,$Location);if($url.Scheme -ne 'https' -or $url.UserInfo){throw 'Unsafe download URL.'}
  [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12
  $response=$null
  for($attempt=0;$attempt -lt 2;$attempt++){
   $req=[Net.HttpWebRequest]::Create($url);$req.Timeout=if($attempt -eq 0){5000}else{15000};$req.ReadWriteTimeout=30000;$req.AllowAutoRedirect=$false;$req.UserAgent='IronGrave-Updater/0.2'
   try{$response=$req.GetResponse();break}catch [Net.WebException]{$req.Abort();if($attempt -eq 1 -or $_.Exception.Status -notin @([Net.WebExceptionStatus]::Timeout,[Net.WebExceptionStatus]::ConnectFailure)){throw}}
  }
  try{
   if([int]$response.StatusCode -ne 200 -or $response.ContentLength -gt $Limit){throw 'Unexpected response or oversized download.'}
   $stream=$response.GetResponseStream();$memory=New-Object IO.MemoryStream;$buffer=New-Object byte[] 65536
   try{while(($count=$stream.Read($buffer,0,$buffer.Length)) -gt 0){if($memory.Length+$count -gt $Limit){throw 'Download exceeds expected size.'};$memory.Write($buffer,0,$count)};return ,$memory.ToArray()}finally{$memory.Dispose();$stream.Dispose()}
  }finally{$response.Dispose()}
 }
 try{
  Write-Host ('IronGrave: checking for updates. Installed: '+(Read-State).release)
  $raw=Fetch 'latest.json' 10485760;$signature=$utf8.GetString((Fetch 'latest.sig' 16384)).Trim()
  $rsa=New-Object Security.Cryptography.RSACryptoServiceProvider
  try{$rsa.FromXmlString([IO.File]::ReadAllText((Join-Path $updaterRoot 'release-public-key.xml')));if(!$rsa.VerifyData($raw,'SHA256',[Convert]::FromBase64String($signature))){throw 'Release signature check failed.'}}finally{$rsa.Dispose()}
  $release=$utf8.GetString($raw)|ConvertFrom-Json
  if($release.schema -notin @(1,2) -or $release.minecraft -ne '1.21.1' -or $release.loader -ne 'neoforge-21.1.233' -or !$release.release){throw 'Unsupported release or loader version.'}
  if($release.schema -eq 1 -and @($release.configMigrations).Count -and $release.configMigrations){throw 'Config migrations require schema 2.'}
  $old=Read-State;$oldByPath=@{};foreach($f in $old.files){Target $f.path|Out-Null;$oldByPath[$f.path]=$f}
  $seen=@{};$changes=@();$newFiles=@()
  foreach($f in $release.files){
   $p=Target $f.path
   if($seen.ContainsKey($f.path) -or $f.sha256 -notmatch '^[0-9a-f]{64}$' -or $f.size -lt 0 -or $f.size -gt 536870912 -or !$f.url){throw 'Invalid or duplicate release file.'};$seen[$f.path]=$true
   $present=Test-Path -LiteralPath $p -PathType Leaf;$now=if($present){Digest ([IO.File]::ReadAllBytes($p))}else{$null}
   $newFiles+=@{path=$f.path;sha256=$f.sha256;size=$f.size}
   if($now -eq $f.sha256){continue}
   if($present -and !$oldByPath.ContainsKey($f.path)){throw "Unmanaged file differs: $($f.path). It was not overwritten."}
   if($present -and $oldByPath.ContainsKey($f.path) -and $now -ne $oldByPath[$f.path].sha256){throw "Managed file changed locally: $($f.path). Restore its original or use a clean test copy."}
   $changes+=@{path=$f.path;target=$p;present=$present;before=$now;after=$f.sha256;file=$f;remove=$false}
  }
  foreach($f in $old.files){if(!$seen.ContainsKey($f.path)){
   $p=Target $f.path;$present=Test-Path -LiteralPath $p -PathType Leaf;$now=if($present){Digest ([IO.File]::ReadAllBytes($p))}else{$null}
   if($present -and $now -ne $f.sha256){throw "Obsolete managed file changed locally: $($f.path). Removal refused."}
   $changes+=@{path=$f.path;target=$p;present=$present;before=$now;after=$null;remove=$true}
  }}
  $migrationIds=@($old.configMigrations|Where-Object {$_});$seenMigration=@{};$configSeen=@{};$newMigration=$false
  foreach($migration in @($release.configMigrations|Where-Object {$_})){
   if(!(Test-Path -LiteralPath $migrationModule) -or $migration.id -notmatch '^[a-zA-Z0-9_-]{1,64}$' -or $seenMigration.ContainsKey($migration.id)){throw 'Invalid or unsupported config migration.'};$seenMigration[$migration.id]=$true
   if($migration.id -in $migrationIds){continue}
   if(!@($migration.files).Count -or @($migration.files).Count -gt 10){throw 'Invalid config migration size.'}
   foreach($file in $migration.files){if($configSeen.ContainsKey($file.path)){throw 'Duplicate config migration target.'};$configSeen[$file.path]=$true;$change=Prepare-Config $file;if($change.before -ne $change.after){$changes+=$change}}
   $migrationIds+=$migration.id;$newMigration=$true
  }
  if(!$changes.Count -and !$newMigration -and $old.release -eq $release.release){Write-Host 'Already up to date.';exit 0}
  $needed=[long]16777216
  foreach($c in $changes){if(!$c.remove){if($c.config){$needed+=([long]$c.bytes.Length*2)}else{$needed+=([long]$c.file.size*2)}};if($c.present){$needed+=(Get-Item -LiteralPath $c.target).Length};if(($c.target.Length+22) -ge 260){throw 'Managed file path is too long. Move the instance to a shorter Prism path.'}}
  $drive=New-Object IO.DriveInfo([IO.Path]::GetPathRoot($game));if($drive.AvailableFreeSpace -lt $needed){throw 'Not enough free disk space to stage and back up this update.'}
  $folder=Join-Path $transactions ([Guid]::NewGuid().ToString('N').Substring(0,12));if(($folder.Length+44) -ge 260){throw 'Updater backup path is too long. Move the instance to a shorter Prism path.'};[IO.Directory]::CreateDirectory($folder)|Out-Null
  $operations=@();$index=0
  foreach($c in $changes){
   $c.staged=Join-Path $folder ($index.ToString()+'.download')
   if(!$c.remove){if($c.config){Write-Host ('Preparing selected settings in '+$c.path);$bytes=$c.bytes}else{Write-Host ('Downloading '+$c.path);$bytes=Fetch $c.file.url $c.file.size;if($bytes.Length -ne $c.file.size -or (Digest $bytes) -ne $c.after){throw 'Downloaded file failed size/hash verification.'}};[IO.File]::WriteAllBytes($c.staged,$bytes)}
   $backup=$index.ToString()+'.backup';if($c.present){$bytes=[IO.File]::ReadAllBytes($c.target);if((Digest $bytes) -ne $c.before){throw 'File changed during download.'};[IO.File]::WriteAllBytes((Join-Path $folder $backup),$bytes)}
   $operations+=@{path=$c.path;present=$c.present;before=$c.before;after=$c.after;backup=$backup;config=[bool]$c.config;fields=$c.fields};$index++
  }
  $oldStatePresent=Test-Path -LiteralPath $stateFile;if($oldStatePresent){[IO.File]::WriteAllBytes((Join-Path $folder 'old-state.json'),[IO.File]::ReadAllBytes($stateFile))}
  $t=@{status='Pending';release=$release.release;oldStatePresent=$oldStatePresent;operations=$operations};$journal=Join-Path $folder 'transaction.json';Save-Json $journal $t
  try{
   foreach($c in $changes){$now=if(Test-Path -LiteralPath $c.target -PathType Leaf){Digest ([IO.File]::ReadAllBytes($c.target))}else{$null};if($now -ne $c.before){throw 'File changed before commit.'};if($c.remove){if($c.present){Remove-Item -LiteralPath $c.target}}else{Write-Atomic $c.target ([IO.File]::ReadAllBytes($c.staged))}}
   Save-Json $stateFile @{schema=1;release=$release.release;files=$newFiles;configMigrations=$migrationIds};$t.status='Applied';Save-Json $journal $t
  }catch{Restore-Journal $journal;throw}
  Write-Host ('Installed '+$release.release+'. '+$release.notes);exit 0
 }catch{
  if(Valid-Current){Write-Host ('Update unavailable: '+$_.Exception.Message);Write-Host 'Previous managed files are intact; continuing with the installed pack.';exit 0}
  throw
 }
}catch{Write-Host ('LAUNCH BLOCKED: '+$_.Exception.Message);exit 1}
finally{if($locked){$mutex.ReleaseMutex()};if($mutex){$mutex.Dispose()}}
