param()

$ErrorActionPreference = 'Stop'

$Target = 'both'
$Configuration = 'Release'
$Platform = 'x64'
$SkipTests = $false
$SkipExe = $false

foreach ($arg in $args) {
  switch -Regex ($arg) {
    '^--?target=(.+)$' { $Target = $Matches[1].ToLowerInvariant(); continue }
    '^--?configuration=(.+)$' { $Configuration = $Matches[1]; continue }
    '^--?platform=(.+)$' { $Platform = $Matches[1]; continue }
    '^--?skip-tests$' { $SkipTests = $true; continue }
    '^--?skip-msix$' { if ($Target -eq 'both') { $Target = 'exe' }; continue }
    '^--?skip-exe$' { $SkipExe = $true; continue }
    '^--?no-pause$' { continue }
    default { throw "Unknown build argument: $arg" }
  }
}

if (@('publish', 'exe', 'msix', 'both') -notcontains $Target) {
  throw "Invalid target '$Target'. Use --target=publish|exe|msix|both."
}

$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$Solution = Join-Path $Root 'YMhut.Box.Native.sln'
$Project = Join-Path $Root 'src\box-winUI\YMhut.Box.WinUI.csproj'
$ProjectAssets = Join-Path $Root 'src\box-winUI\Assets'
$NuGetConfig = Join-Path $Root 'NuGet.Config'
$PublishRoot = Join-Path $Root 'build\winui\publish'
$MsixStageRoot = Join-Path $Root 'build\winui\msix-stage'
$BuildLogRoot = Join-Path $Root 'build\winui\logs'
$LatestRoot = Join-Path $Root 'latest'
$OutputRoot = Join-Path $Root 'installer_output'
$OutputUpdateInfoRoot = Join-Path $OutputRoot 'update-info'
$ServerPublicRoot = Join-Path $Root 'server\update\public'
$ServerDownloadRoot = Join-Path $ServerPublicRoot 'downloads'
$ToolStateRoot = Join-Path $Root '.cache\tool_state'
$NuGetRoot = Join-Path $Root '.cache\nuget'
$LocalNuGetFeedRoot = Join-Path $NuGetRoot 'feed'
$AppDataRoot = Join-Path $ToolStateRoot 'appdata'
$HomeRoot = Join-Path $ToolStateRoot 'home'

$PackageIdentityName = 'YMhut.Box'
$PackagePublisher = 'CN=YMhut'
$AppName = 'YMhut Box'
$AppExecutable = 'YMhutBox.exe'
$PfxPassword = 'ymhut-box-local'
$PfxPath = Join-Path $OutputRoot 'certs\YMhutBox.pfx'
$CerPath = Join-Path $OutputRoot 'YMhutBox.cer'

$DownloadBaseUri = if ($env:YMHUT_DOWNLOAD_BASE_URI) { $env:YMHUT_DOWNLOAD_BASE_URI.TrimEnd('/') + '/' } else { 'https://update.ymhut.cn/downloads/' }
$UpdateBaseUri = if ($env:YMHUT_UPDATE_BASE_URI) { $env:YMHUT_UPDATE_BASE_URI.TrimEnd('/') + '/' } else { 'https://update.ymhut.cn/update-info/' }

function New-Directory([string] $Path) {
  New-Item -ItemType Directory -Force -Path $Path | Out-Null
}

function Write-Utf8NoBomFile([string] $Path, [string] $Value) {
  $parent = Split-Path -Parent $Path
  if ($parent) {
    New-Directory $parent
  }

  $encoding = New-Object System.Text.UTF8Encoding -ArgumentList $false
  [IO.File]::WriteAllText($Path, $Value, $encoding)
}

function Reset-DirectoryInsideRepo([string] $Path) {
  $resolvedRoot = [IO.Path]::GetFullPath($Root)
  $resolvedPath = [IO.Path]::GetFullPath($Path)
  if (-not $resolvedPath.StartsWith($resolvedRoot, [StringComparison]::OrdinalIgnoreCase)) {
    throw "Refusing to reset a directory outside the repository: $resolvedPath"
  }

  if (Test-Path -LiteralPath $resolvedPath) {
    Remove-Item -LiteralPath $resolvedPath -Recurse -Force
  }
  New-Directory $resolvedPath
}

function Remove-DirectoryInsideRepo([string] $Path) {
  $resolvedRoot = [IO.Path]::GetFullPath($Root)
  $resolvedPath = [IO.Path]::GetFullPath($Path)
  if (-not $resolvedPath.StartsWith($resolvedRoot, [StringComparison]::OrdinalIgnoreCase)) {
    throw "Refusing to remove a directory outside the repository: $resolvedPath"
  }

  if (Test-Path -LiteralPath $resolvedPath) {
    Remove-Item -LiteralPath $resolvedPath -Recurse -Force
  }
}

function Stop-ProcessesFromDirectory([string] $Directory) {
  if (-not (Test-Path -LiteralPath $Directory)) {
    return
  }

  $resolvedDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
  $stopped = 0
  foreach ($process in Get-Process -ErrorAction SilentlyContinue) {
    $processPath = $null
    try {
      $processPath = $process.Path
    } catch {
      continue
    }

    if ([string]::IsNullOrWhiteSpace($processPath)) {
      continue
    }

    $resolvedProcessPath = [IO.Path]::GetFullPath($processPath)
    if (-not $resolvedProcessPath.StartsWith($resolvedDirectory, [StringComparison]::OrdinalIgnoreCase)) {
      continue
    }

    Write-Host "  Stopping running payload process: $($process.ProcessName) ($($process.Id))"
    Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
    $stopped++
  }

  if ($stopped -gt 0) {
    Start-Sleep -Seconds 2
  }
}

function Invoke-Tool([string] $FilePath, [string[]] $Arguments, [string] $FailureMessage) {
  & $FilePath @Arguments
  if ($LASTEXITCODE -ne 0) {
    throw "$FailureMessage (exit code $LASTEXITCODE)"
  }
}

function Join-ProcessArguments([string[]] $Arguments) {
  $quotedArguments = [Collections.Generic.List[string]]::new()
  foreach ($item in $Arguments) {
    $argument = [string]$item
    if ($argument.Length -eq 0) {
      $quotedArguments.Add('""')
      continue
    }

    if ($argument -notmatch '[\s"]') {
      $quotedArguments.Add($argument)
      continue
    }

    $builder = [Text.StringBuilder]::new()
    [void]$builder.Append('"')
    $backslashes = 0
    foreach ($character in $argument.ToCharArray()) {
      if ($character -eq '\') {
        $backslashes++
        continue
      }

      if ($character -eq '"') {
        [void]$builder.Append(('\' * (($backslashes * 2) + 1)))
        [void]$builder.Append('"')
        $backslashes = 0
        continue
      }

      if ($backslashes -gt 0) {
        [void]$builder.Append(('\' * $backslashes))
        $backslashes = 0
      }
      [void]$builder.Append($character)
    }

    if ($backslashes -gt 0) {
      [void]$builder.Append(('\' * ($backslashes * 2)))
    }
    [void]$builder.Append('"')
    $quotedArguments.Add($builder.ToString())
  }

  $quotedArguments -join ' '
}

function Get-LogTail([string] $Path, [int] $Lines = 24) {
  if (-not (Test-Path -LiteralPath $Path)) {
    return ''
  }

  $text = (Get-Content -LiteralPath $Path -Tail $Lines -ErrorAction SilentlyContinue) -join [Environment]::NewLine
  if ([string]::IsNullOrWhiteSpace($text)) { return '' }
  $text.Trim()
}

function Invoke-ToolQuiet([string] $FilePath, [string[]] $Arguments, [string] $FailureMessage, [string] $SuccessMessage = '') {
  New-Directory $BuildLogRoot
  $toolName = [IO.Path]::GetFileNameWithoutExtension($FilePath)
  if ([string]::IsNullOrWhiteSpace($toolName)) {
    $toolName = ($FilePath -replace '[^A-Za-z0-9_.-]', '_')
  }

  $stamp = [DateTimeOffset]::Now.ToString('yyyyMMdd-HHmmss-fff')
  $stdoutPath = Join-Path $BuildLogRoot "$toolName-$stamp.out.log"
  $stderrPath = Join-Path $BuildLogRoot "$toolName-$stamp.err.log"
  $process = Start-Process `
    -FilePath $FilePath `
    -ArgumentList (Join-ProcessArguments $Arguments) `
    -NoNewWindow `
    -Wait `
    -PassThru `
    -RedirectStandardOutput $stdoutPath `
    -RedirectStandardError $stderrPath
  $exitCode = $process.ExitCode

  if ($exitCode -ne 0) {
    $stdoutTail = Get-LogTail $stdoutPath
    $stderrTail = Get-LogTail $stderrPath
    if ($stdoutTail) {
      Write-Host $stdoutTail
    }
    if ($stderrTail) {
      Write-Host $stderrTail
    }
    throw "$FailureMessage (exit code $exitCode). Logs: $stdoutPath $stderrPath"
  }

  if (-not [string]::IsNullOrWhiteSpace($SuccessMessage)) {
    Write-Host $SuccessMessage
  }
}

function Invoke-DotNet([string[]] $Arguments) {
  Invoke-Tool 'dotnet' $Arguments 'dotnet command failed'
}

function Get-VersionInfo {
  $versionFile = Join-Path $Root 'version.json'
  if (-not (Test-Path -LiteralPath $versionFile)) {
    return [pscustomobject]@{
      Version = '2.0.0'
      Build = '1'
      Channel = 'stable'
      PackageVersion = '2.0.0.1'
    }
  }

  $json = Get-Content -LiteralPath $versionFile -Raw -Encoding UTF8 | ConvertFrom-Json
  $version = if ($json.version) { [string]$json.version } else { '2.0.0' }
  $build = if ($json.build) { ([string]$json.build -replace '[^0-9]', '') } else { '1' }
  if ([string]::IsNullOrWhiteSpace($build)) { $build = '1' }
  $channel = if ($json.channel) { [string]$json.channel } else { 'stable' }

  $parts = @()
  foreach ($part in ($version -split '\.')) {
    $digits = ($part -replace '[^0-9]', '')
    if ($digits) { $parts += [int]$digits }
  }
  while ($parts.Count -lt 3) { $parts += 0 }
  $parts = $parts | Select-Object -First 3
  $packageVersion = (($parts + [int]$build) | ForEach-Object { [Math]::Min($_, 65535) }) -join '.'

  [pscustomobject]@{
    Version = $version
    Build = $build
    Channel = $channel
    PackageVersion = $packageVersion
  }
}

function Find-Executable([string] $Name, [string[]] $Candidates = @(), [string[]] $SearchRoots = @()) {
  $fromPath = Get-Command $Name -ErrorAction SilentlyContinue
  if ($fromPath) { return $fromPath.Source }

  foreach ($candidate in $Candidates) {
    if ($candidate -and (Test-Path -LiteralPath $candidate)) {
      return (Resolve-Path -LiteralPath $candidate).Path
    }
  }

  foreach ($rootPath in ($SearchRoots | Where-Object { $_ } | Select-Object -Unique)) {
    if (-not (Test-Path -LiteralPath $rootPath)) {
      continue
    }

    $tool = Get-ChildItem -LiteralPath $rootPath -Recurse -Filter $Name -File -ErrorAction SilentlyContinue |
      Sort-Object FullName -Descending |
      Select-Object -First 1 -ExpandProperty FullName
    if ($tool) { return $tool }
  }

  return $null
}

function Find-WindowsSdkTool([string] $Name) {
  $roots = @(
    (Join-Path $NuGetRoot 'microsoft.windows.sdk.buildtools'),
    (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'),
    (Join-Path $env:ProgramFiles 'Windows Kits\10\bin'),
    (Join-Path ${env:ProgramFiles(x86)} 'Microsoft SDKs\ClickOnce'),
    (Join-Path $env:ProgramFiles 'Microsoft SDKs\ClickOnce')
  )
  if ($env:WindowsSdkDir) {
    $roots += Join-Path $env:WindowsSdkDir 'bin'
  }

  $candidates = @()
  foreach ($rootPath in ($roots | Where-Object { $_ } | Select-Object -Unique)) {
    if (Test-Path -LiteralPath $rootPath) {
      $candidates += Get-ChildItem -LiteralPath $rootPath -Recurse -Filter $Name -File -ErrorAction SilentlyContinue
    }
  }
  if ($candidates.Count -eq 0) {
    return Find-Executable $Name
  }

  $architecturePreference = switch ($Platform.ToLowerInvariant()) {
    'arm64' { @('\arm64\', '\x64\', '\x86\') }
    'x86' { @('\x86\', '\x64\', '\arm64\') }
    default { @('\x64\', '\amd64\', '\x86\', '\arm64\') }
  }
  foreach ($architecture in $architecturePreference) {
    $tool = $candidates |
      Where-Object { $_.FullName.IndexOf($architecture, [StringComparison]::OrdinalIgnoreCase) -ge 0 } |
      Sort-Object FullName -Descending |
      Select-Object -First 1
    if ($tool) {
      return $tool.FullName
    }
  }

  ($candidates | Sort-Object FullName -Descending | Select-Object -First 1).FullName
}

function Find-InnoSetupCompiler {
  $envCandidates = @()
  if ($env:YMHUT_INNO_SETUP) {
    $envCandidates += if ((Split-Path -Leaf $env:YMHUT_INNO_SETUP) -ieq 'ISCC.exe') {
      $env:YMHUT_INNO_SETUP
    } else {
      Join-Path $env:YMHUT_INNO_SETUP 'ISCC.exe'
    }
  }
  if ($env:INNO_SETUP_HOME) {
    $envCandidates += Join-Path $env:INNO_SETUP_HOME 'ISCC.exe'
  }

  $candidates = @(
    $envCandidates,
    'E:\Inno Setup 7\ISCC.exe',
    (Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 7\ISCC.exe'),
    (Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe'),
    (Join-Path $env:ProgramFiles 'Inno Setup 7\ISCC.exe'),
    (Join-Path $env:ProgramFiles 'Inno Setup 6\ISCC.exe')
  ) | ForEach-Object { $_ }
  Find-Executable 'ISCC.exe' -Candidates $candidates
}

function Resolve-InnoLanguageFile([string] $IsccPath) {
  $compilerRoot = Split-Path -Parent $IsccPath
  $chineseLanguageFile = Join-Path $compilerRoot 'Languages\ChineseSimplified.isl'
  if (Test-Path -LiteralPath $chineseLanguageFile) {
    return (Resolve-Path -LiteralPath $chineseLanguageFile).Path
  }

  $repoLanguageFile = Join-Path $Root 'installer\Languages\ChineseSimplified.isl'
  if (Test-Path -LiteralPath $repoLanguageFile) {
    return (Resolve-Path -LiteralPath $repoLanguageFile).Path
  }

  Write-Warning "ChineseSimplified.isl was not found under '$compilerRoot\Languages'. Falling back to compiler:Default.isl; custom Chinese task/runtime messages remain active."
  return 'compiler:Default.isl'
}

function Save-ScaledLogo {
  param(
    [string] $SourcePath,
    [string] $DestinationPath,
    [int] $Width,
    [int] $Height,
    [int] $Padding,
    [string] $BackgroundHex = '#00FFFFFF',
    [switch] $Wordmark
  )

  Add-Type -AssemblyName System.Drawing
  $source = [Drawing.Image]::FromFile($SourcePath)
  $bitmap = New-Object Drawing.Bitmap($Width, $Height, [Drawing.Imaging.PixelFormat]::Format32bppArgb)
  $graphics = [Drawing.Graphics]::FromImage($bitmap)
  try {
    $graphics.SmoothingMode = [Drawing.Drawing2D.SmoothingMode]::HighQuality
    $graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
    $graphics.CompositingQuality = [Drawing.Drawing2D.CompositingQuality]::HighQuality
    $graphics.TextRenderingHint = [Drawing.Text.TextRenderingHint]::ClearTypeGridFit
    $graphics.Clear([Drawing.ColorTranslator]::FromHtml($BackgroundHex))

    if ($Wordmark) {
      $iconSize = [Math]::Min($Height - ($Padding * 2), [Math]::Min(112, $Width / 3))
      $iconX = $Padding + [Math]::Max(0, ($Height - $iconSize) / 5)
      $iconY = ($Height - $iconSize) / 2
      $graphics.DrawImage($source, [Drawing.RectangleF]::new($iconX, $iconY, $iconSize, $iconSize))

      $titleFont = New-Object Drawing.Font('Segoe UI Semibold', [Math]::Max(18, [Math]::Min(34, $Height / 6)))
      $subFont = New-Object Drawing.Font('Segoe UI', [Math]::Max(10, [Math]::Min(16, $Height / 12)))
      $titleBrush = New-Object Drawing.SolidBrush([Drawing.ColorTranslator]::FromHtml('#111827'))
      $subBrush = New-Object Drawing.SolidBrush([Drawing.ColorTranslator]::FromHtml('#56616A'))
      try {
        $textX = $iconX + $iconSize + $Padding
        $titleY = ($Height / 2) - ($titleFont.Size * 1.15)
        $graphics.DrawString('YMhut Box', $titleFont, $titleBrush, [Drawing.PointF]::new($textX, $titleY))
        $graphics.DrawString('WinUI 3 toolbox', $subFont, $subBrush, [Drawing.PointF]::new($textX + 2, $titleY + ($titleFont.Size * 1.6)))
      } finally {
        $titleFont.Dispose()
        $subFont.Dispose()
        $titleBrush.Dispose()
        $subBrush.Dispose()
      }
    } else {
      $maxWidth = $Width - ($Padding * 2)
      $maxHeight = $Height - ($Padding * 2)
      $scale = [Math]::Min($maxWidth / $source.Width, $maxHeight / $source.Height)
      $drawWidth = $source.Width * $scale
      $drawHeight = $source.Height * $scale
      $drawX = ($Width - $drawWidth) / 2
      $drawY = ($Height - $drawHeight) / 2
      $graphics.DrawImage($source, [Drawing.RectangleF]::new($drawX, $drawY, $drawWidth, $drawHeight))
    }

    New-Directory (Split-Path -Parent $DestinationPath)
    $bitmap.Save($DestinationPath, [Drawing.Imaging.ImageFormat]::Png)
  } finally {
    $graphics.Dispose()
    $bitmap.Dispose()
    $source.Dispose()
  }
}

function Ensure-MsixAssets {
  $sourceIcon = Join-Path $Root 'assets\icons\app_icon.png'
  if (-not (Test-Path -LiteralPath $sourceIcon)) {
    throw "MSIX source icon was not found: $sourceIcon"
  }

  New-Directory $ProjectAssets
  Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'StoreLogo.png') 50 50 5 '#00FFFFFF'
  Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Square44x44Logo.png') 44 44 4 '#00FFFFFF'
  Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Square150x150Logo.png') 150 150 16 '#00FFFFFF'
  Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'LockScreenLogo.png') 70 70 8 '#00FFFFFF'
  Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Wide310x150Logo.png') 310 150 24 '#F5F6F7' -Wordmark
}

function Ensure-LocalDeveloperCertificate {
  New-Directory (Split-Path -Parent $PfxPath)
  if ((Test-Path -LiteralPath $PfxPath) -and (Test-Path -LiteralPath $CerPath)) {
    $secure = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force
    $pfxCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $secure)
    $cerCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath)
    if ($pfxCert.Thumbprint -ne $cerCert.Thumbprint) {
      Export-Certificate -Cert $pfxCert -FilePath $CerPath -Force | Out-Null
    }

    Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\Root' | Out-Null
    Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' | Out-Null
    return
  }

  $secure = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force
  $cert = New-SelfSignedCertificate `
    -Type CodeSigningCert `
    -Subject $PackagePublisher `
    -CertStoreLocation 'Cert:\CurrentUser\My' `
    -KeyExportPolicy Exportable `
    -KeyUsage DigitalSignature `
    -FriendlyName 'YMhut Box WinUI local signing'

  Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $secure | Out-Null
  Export-Certificate -Cert $cert -FilePath $CerPath | Out-Null
  Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\Root' | Out-Null
  Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' | Out-Null
}

function Assert-ArtifactSignatureMatchesCertificate([string] $Path) {
  $cerCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath)
  $signature = Get-AuthenticodeSignature -FilePath $Path
  if (-not $signature.SignerCertificate) {
    throw "Signed artifact does not expose a signer certificate: $Path"
  }

  if ($signature.SignerCertificate.Thumbprint -ne $cerCert.Thumbprint) {
    throw "Signed artifact certificate thumbprint '$($signature.SignerCertificate.Thumbprint)' does not match exported certificate '$($cerCert.Thumbprint)'. Rebuild the package and keep the matching YMhutBox.cer next to it."
  }
}

function Get-WindowsAppRuntimePackageExtensions {
  $projectRoot = Split-Path -Parent $Project
  $objRoot = Join-Path $projectRoot 'obj'
  if (-not (Test-Path -LiteralPath $objRoot)) {
    return ''
  }

  $manifest = Get-ChildItem -LiteralPath $objRoot -Filter 'AppxManifest.xml' -Recurse -ErrorAction SilentlyContinue |
    Where-Object { $_.FullName -like '*\MsixContent\AppxManifest.xml' } |
    Sort-Object LastWriteTime -Descending |
    Select-Object -First 1
  if (-not $manifest) {
    return ''
  }

  [xml]$xml = Get-Content -LiteralPath $manifest.FullName -Raw -Encoding UTF8
  $namespace = [System.Xml.XmlNamespaceManager]::new($xml.NameTable)
  $namespace.AddNamespace('m', 'http://schemas.microsoft.com/appx/manifest/foundation/windows10')
  $extensions = $xml.SelectSingleNode('/m:Package/m:Extensions', $namespace)
  if (-not $extensions) {
    return ''
  }

  $extensions.OuterXml
}

function Write-AppxManifest([string] $TargetDir, [string] $PackageVersion) {
  $packageExtensions = Get-WindowsAppRuntimePackageExtensions
  if ([string]::IsNullOrWhiteSpace($packageExtensions)) {
    throw 'Windows App SDK package extensions were not found. Build output must include MsixContent\AppxManifest.xml so WinUI activatable classes are registered in the MSIX.'
  }

  $manifest = @"
<?xml version="1.0" encoding="utf-8"?>
<Package
    xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
    xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
    xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
    xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
    xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
    IgnorableNamespaces="uap uap5 uap10 rescap">

  <Identity
      Name="$PackageIdentityName"
      Publisher="$PackagePublisher"
      Version="$PackageVersion"
      ProcessorArchitecture="x64" />

  <Properties>
    <DisplayName>$AppName</DisplayName>
    <PublisherDisplayName>YMhut</PublisherDisplayName>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>

  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" />
  </Dependencies>

  <Resources>
    <Resource Language="zh-CN" />
    <Resource Language="en-US" />
  </Resources>

  <Applications>
    <Application Id="App" Executable="$AppExecutable" EntryPoint="Windows.FullTrustApplication" uap10:RuntimeBehavior="packagedClassicApp" uap10:TrustLevel="mediumIL">
      <uap:VisualElements
          DisplayName="$AppName"
          Description="YMhut Box - A multi-functional toolbox"
          Square150x150Logo="Assets\Square150x150Logo.png"
          Square44x44Logo="Assets\Square44x44Logo.png"
          BackgroundColor="transparent">
        <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
      </uap:VisualElements>
      <Extensions>
        <uap5:Extension Category="windows.startupTask">
          <uap5:StartupTask TaskId="YMhutBoxStartupTask" Enabled="false" DisplayName="$AppName" />
        </uap5:Extension>
      </Extensions>
    </Application>
  </Applications>

  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
  </Capabilities>

  $packageExtensions
</Package>
"@
  Set-Content -LiteralPath (Join-Path $TargetDir 'AppxManifest.xml') -Value $manifest -Encoding UTF8
}

function Write-AppInstaller([object] $VersionInfo, [string] $MsixFileName) {
  New-Directory $OutputUpdateInfoRoot
  $appInstallerUri = $UpdateBaseUri + 'winui.appinstaller'
  $msixUri = $DownloadBaseUri + $MsixFileName
  $appInstallerPath = Join-Path $OutputUpdateInfoRoot 'winui.appinstaller'

  $content = @"
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller
    xmlns="http://schemas.microsoft.com/appx/appinstaller/2018"
    Version="$($VersionInfo.PackageVersion)"
    Uri="$appInstallerUri">
  <MainPackage
      Name="$PackageIdentityName"
      Publisher="$PackagePublisher"
      Version="$($VersionInfo.PackageVersion)"
      ProcessorArchitecture="x64"
      Uri="$msixUri" />
  <UpdateSettings>
    <OnLaunch HoursBetweenUpdateChecks="12" ShowPrompt="true" UpdateBlocksActivation="false" />
  </UpdateSettings>
</AppInstaller>
"@
  Set-Content -LiteralPath $appInstallerPath -Value $content -Encoding UTF8
  Copy-Item -LiteralPath $appInstallerPath -Destination (Join-Path $OutputRoot 'winui.appinstaller') -Force

  $localAppInstallerPath = Join-Path $OutputRoot 'winui-local.appinstaller'
  $localAppInstallerUri = [Uri]::new([IO.Path]::GetFullPath($localAppInstallerPath)).AbsoluteUri
  $localMsixUri = [Uri]::new([IO.Path]::GetFullPath((Join-Path $OutputRoot $MsixFileName))).AbsoluteUri
  $localContent = @"
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller
    xmlns="http://schemas.microsoft.com/appx/appinstaller/2018"
    Version="$($VersionInfo.PackageVersion)"
    Uri="$localAppInstallerUri">
  <MainPackage
      Name="$PackageIdentityName"
      Publisher="$PackagePublisher"
      Version="$($VersionInfo.PackageVersion)"
      ProcessorArchitecture="x64"
      Uri="$localMsixUri" />
  <UpdateSettings>
    <OnLaunch HoursBetweenUpdateChecks="0" ShowPrompt="false" UpdateBlocksActivation="false" />
  </UpdateSettings>
</AppInstaller>
"@
  Set-Content -LiteralPath $localAppInstallerPath -Value $localContent -Encoding UTF8
}

function Write-LayoutManifests([string] $Directory) {
  $files = Get-ChildItem -LiteralPath $Directory -Recurse -File |
    ForEach-Object { $_.FullName.Substring($Directory.Length).TrimStart('\', '/') -replace '\\', '/' } |
    Sort-Object

  $configDir = Join-Path $Directory 'config'
  New-Directory $configDir

  $required = @($files | Where-Object { $_ -in @('YMhutBox.exe', 'YMhutBox.dll', 'WebView2Loader.dll') })
  if ($required.Count -eq 0) {
    $required = @('YMhutBox.exe')
  }

  $manifest = [System.Collections.Generic.List[string]]::new()
  if ($versionInfo) {
    $manifest.Add('[Release]')
    $manifest.Add("Version=$($versionInfo.Version)")
    $manifest.Add("Build=$($versionInfo.Build)")
    $manifest.Add("Channel=$($versionInfo.Channel)")
    $manifest.Add("PackageVersion=$($versionInfo.PackageVersion)")
    $manifest.Add('')
  }
  $manifest.Add('[RequiredFiles]')
  foreach ($file in $required) {
    $manifest.Add($file)
  }
  $manifest.Add('')
  $manifest.Add('[Files]')
  foreach ($file in $files) {
    if ($file -ine 'config/install-manifest.ini') {
      $manifest.Add($file)
    }
  }

  Set-Content -LiteralPath (Join-Path $configDir 'install-manifest.ini') -Value $manifest -Encoding UTF8
}

function Get-FileSha256([string] $Path) {
  return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}

function Sync-UpdateServerPackages {
  if (-not (Test-Path -LiteralPath $ServerPublicRoot)) {
    return
  }

  New-Directory $ServerDownloadRoot
  foreach ($legacyFile in @('manifest.json', 'latest-version.json', 'package-manifest.json', 'incremental-manifest.json', 'modules.json')) {
    $legacyPath = Join-Path $ServerPublicRoot $legacyFile
    if (Test-Path -LiteralPath $legacyPath) {
      Remove-Item -LiteralPath $legacyPath -Force
    }
  }

  foreach ($legacyRoot in @(
    (Join-Path $ServerPublicRoot 'packages'),
    (Join-Path $ServerPublicRoot 'tool-packages'),
    (Join-Path $ServerPublicRoot 'incremental'),
    (Join-Path $ServerDownloadRoot 'incremental')
  )) {
    if (Test-Path -LiteralPath $legacyRoot) {
      Remove-Item -LiteralPath $legacyRoot -Recurse -Force
    }
  }

  foreach ($sourceRoot in @($OutputRoot, $OutputUpdateInfoRoot)) {
    if (-not (Test-Path -LiteralPath $sourceRoot)) {
      continue
    }

    Get-ChildItem -LiteralPath $sourceRoot -File -ErrorAction SilentlyContinue |
      Where-Object {
        $_.Name -notmatch '_Light\.' -and (
          $_.Name -match '^YMhut_Box_(WinUI_)?Setup_.+\.(exe|msi)$' -or
          $_.Name -match '^YMhutBox_.+_x64\.msix$' -or
          $_.Name -ieq 'winui.appinstaller'
        )
      } |
      ForEach-Object {
        Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $ServerDownloadRoot $_.Name) -Force
      }
  }

  $updateInfoSource = Join-Path $OutputRoot 'update-info.json'
  if (Test-Path -LiteralPath $updateInfoSource) {
    Copy-Item -LiteralPath $updateInfoSource -Destination (Join-Path $ServerPublicRoot 'update-info.json') -Force
  }

  Write-Host "  Server downloads: $(Join-Path $ServerPublicRoot 'downloads')"
  Write-Host "  Server update info: $(Join-Path $ServerPublicRoot 'update-info.json')"
}
function Write-UpdateCatalogs([object] $VersionInfo) {
  $fullExeName = "YMhut_Box_WinUI_Setup_$($VersionInfo.PackageVersion).exe"
  $msixName = "YMhutBox_$($VersionInfo.PackageVersion)_x64.msix"
  $appInstallerName = 'winui.appinstaller'

  $fullInstallerPath = Join-Path $OutputRoot $fullExeName
  $msixOutputPath = Join-Path $OutputUpdateInfoRoot $msixName
  if (-not (Test-Path -LiteralPath $msixOutputPath)) {
    $msixOutputPath = Join-Path $OutputRoot $msixName
  }
  $appInstallerPath = Join-Path $OutputRoot $appInstallerName

  $fullInstaller = [ordered]@{
    fileName = $fullExeName
    url = $DownloadBaseUri + $fullExeName
    sha256 = if (Test-Path -LiteralPath $fullInstallerPath) { Get-FileSha256 $fullInstallerPath } else { '' }
    size = if (Test-Path -LiteralPath $fullInstallerPath) { (Get-Item -LiteralPath $fullInstallerPath).Length } else { 0 }
    version = $VersionInfo.PackageVersion
  }
  $msix = [ordered]@{
    fileName = $msixName
    url = $DownloadBaseUri + $msixName
    sha256 = if (Test-Path -LiteralPath $msixOutputPath) { Get-FileSha256 $msixOutputPath } else { '' }
    size = if (Test-Path -LiteralPath $msixOutputPath) { (Get-Item -LiteralPath $msixOutputPath).Length } else { 0 }
    version = $VersionInfo.PackageVersion
  }
  $appInstaller = [ordered]@{
    fileName = $appInstallerName
    url = $DownloadBaseUri + $appInstallerName
    sha256 = if (Test-Path -LiteralPath $appInstallerPath) { Get-FileSha256 $appInstallerPath } else { '' }
    size = if (Test-Path -LiteralPath $appInstallerPath) { (Get-Item -LiteralPath $appInstallerPath).Length } else { 0 }
    version = $VersionInfo.PackageVersion
  }

  $manifest = [ordered]@{
    manifestVersion = 5
    latestVersion = $VersionInfo.PackageVersion
    appVersion = $VersionInfo.PackageVersion
    version = $VersionInfo.Version
    build = $VersionInfo.Build
    channel = $VersionInfo.Channel
    latest = [ordered]@{
      version = $VersionInfo.PackageVersion
      fullInstaller = $fullInstaller
      msix = $msix
      appInstaller = $appInstaller
      files = [ordered]@{
        fullInstaller = $fullInstaller
        msix = $msix
        appInstaller = $appInstaller
      }
    }
    messages = [ordered]@{
      updateInfo = 'The official update-info catalog only describes the full offline installer, MSIX, and appinstaller artifacts.'
      distribution = 'The update channel publishes the full offline installer, MSIX, and appinstaller artifacts.'
    }
    createdAt = (Get-Date).ToUniversalTime().ToString('o')
  }

  foreach ($legacyFile in @('manifest.json', 'latest-version.json', 'package-manifest.json', 'incremental-manifest.json')) {
    $legacyPath = Join-Path $OutputRoot $legacyFile
    if (Test-Path -LiteralPath $legacyPath) {
      Remove-Item -LiteralPath $legacyPath -Force
    }
  }

  Write-Utf8NoBomFile (Join-Path $OutputRoot 'update-info.json') ($manifest | ConvertTo-Json -Depth 10)
}
function Compress-ReferenceData([string] $Directory) {
  $assetsRoot = Join-Path $Directory 'Assets'
  $dataRoot = Join-Path $assetsRoot 'data'
  if (-not (Test-Path -LiteralPath $dataRoot)) {
    return
  }

  $jsonFiles = @(Get-ChildItem -LiteralPath $dataRoot -Recurse -Filter '*.json' -File -ErrorAction SilentlyContinue)
  if ($jsonFiles.Count -eq 0) {
    return
  }

  Add-Type -AssemblyName System.IO.Compression
  Add-Type -AssemblyName System.IO.Compression.FileSystem
  Add-Type -AssemblyName System.Security
  $ybinPath = Join-Path $dataRoot 'ymhut-data.ybin'
  if (Test-Path -LiteralPath $ybinPath) {
    Remove-Item -LiteralPath $ybinPath -Force
  }

  $memory = [System.IO.MemoryStream]::new()
  $archive = [System.IO.Compression.ZipArchive]::new($memory, [System.IO.Compression.ZipArchiveMode]::Create, $true)
  try {
    foreach ($file in $jsonFiles) {
      $entryName = $file.FullName.Substring($assetsRoot.Length).TrimStart('\', '/') -replace '\\', '/'
      [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive, $file.FullName, $entryName, [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
    }
  } finally {
    $archive.Dispose()
  }

  $plain = $memory.ToArray()
  $memory.Dispose()
  $sha = [System.Security.Cryptography.SHA256]::Create()
  try {
    $key = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes('YMhut.Box.ReferenceData.YBin.v1'))
  } finally {
    $sha.Dispose()
  }
  $aes = [System.Security.Cryptography.Aes]::Create()
  try {
    $aes.Key = $key
    $aes.GenerateIV()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $encryptor = $aes.CreateEncryptor()
    try {
      $cipher = $encryptor.TransformFinalBlock($plain, 0, $plain.Length)
    } finally {
      $encryptor.Dispose()
    }
    $magic = [Text.Encoding]::ASCII.GetBytes('YMHUTYBIN1')
    $output = [byte[]]::new($magic.Length + $aes.IV.Length + $cipher.Length)
    [Array]::Copy($magic, 0, $output, 0, $magic.Length)
    [Array]::Copy($aes.IV, 0, $output, $magic.Length, $aes.IV.Length)
    [Array]::Copy($cipher, 0, $output, $magic.Length + $aes.IV.Length, $cipher.Length)
    [IO.File]::WriteAllBytes($ybinPath, $output)
  } finally {
    $aes.Dispose()
  }

  foreach ($file in $jsonFiles) {
    Remove-Item -LiteralPath $file.FullName -Force
  }
}

function Compress-SatelliteResourceFolders([string] $Directory) {
  Add-Type -AssemblyName System.IO.Compression
  Add-Type -AssemblyName System.IO.Compression.FileSystem
  $langRoot = Join-Path $Directory 'resources\lang'
  New-Directory $langRoot
  $culturePattern = '^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$'
  foreach ($folder in Get-ChildItem -LiteralPath $Directory -Directory -ErrorAction SilentlyContinue) {
    if ($folder.Name -match $culturePattern -and
        (Test-Path -LiteralPath (Join-Path $folder.FullName 'Microsoft.ui.xaml.dll.mui'))) {
      $binPath = Join-Path $langRoot "$($folder.Name).bin"
      if (Test-Path -LiteralPath $binPath) {
        Remove-Item -LiteralPath $binPath -Force
      }

      $archive = [System.IO.Compression.ZipFile]::Open($binPath, [System.IO.Compression.ZipArchiveMode]::Create)
      try {
        foreach ($file in Get-ChildItem -LiteralPath $folder.FullName -Recurse -File) {
          $entryName = $file.FullName.Substring($folder.FullName.Length).TrimStart('\', '/') -replace '\\', '/'
          [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive, $file.FullName, $entryName, [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
        }
      } finally {
        $archive.Dispose()
      }

      Remove-Item -LiteralPath $folder.FullName -Recurse -Force
    }
  }
}

function Expand-SatelliteResourcePackages([string] $Directory, [switch] $RemovePackages) {
  Add-Type -AssemblyName System.IO.Compression
  Add-Type -AssemblyName System.IO.Compression.FileSystem
  $langRoot = Join-Path $Directory 'resources\lang'
  if (-not (Test-Path -LiteralPath $langRoot)) {
    return
  }

  $culturePattern = '^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$'
  foreach ($package in Get-ChildItem -LiteralPath $langRoot -Filter '*.bin' -File -ErrorAction SilentlyContinue) {
    $culture = [IO.Path]::GetFileNameWithoutExtension($package.Name)
    if ($culture -notmatch $culturePattern) {
      continue
    }

    $target = Join-Path $Directory $culture
    $targetFull = [IO.Path]::GetFullPath($target)
    New-Directory $targetFull

    $archive = [System.IO.Compression.ZipFile]::OpenRead($package.FullName)
    try {
      foreach ($entry in $archive.Entries) {
        $destination = [IO.Path]::GetFullPath((Join-Path $targetFull $entry.FullName))
        if (-not $destination.StartsWith($targetFull, [StringComparison]::OrdinalIgnoreCase)) {
          throw "Refusing to extract language package entry outside target directory: $($entry.FullName)"
        }

        if ($entry.FullName.EndsWith('/') -or $entry.FullName.EndsWith('\')) {
          New-Directory $destination
          continue
        }

        New-Directory (Split-Path -Parent $destination)
        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destination, $true)
      }
    } finally {
      $archive.Dispose()
    }
  }

  if ($RemovePackages) {
    Remove-Item -LiteralPath $langRoot -Recurse -Force
  }
}

function Get-CanonicalCultureName([string] $Name) {
  if ($Name -match '^(?i)zh-cn$') { return 'zh-CN' }
  if ($Name -match '^(?i)en-us$') { return 'en-US' }
  return $Name
}

function Test-CultureDirectoryName([string] $Name) {
  return $Name -match '^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$'
}

function Normalize-LanguageResourceLayout([string] $Directory) {
  $allowedCultures = @('zh-CN', 'en-US')
  $langRoot = Join-Path $Directory 'lang'
  New-Directory $langRoot

  $legacyLangRoot = Join-Path $Directory 'resources\lang'
  if (Test-Path -LiteralPath $legacyLangRoot) {
    Remove-Item -LiteralPath $legacyLangRoot -Recurse -Force
  }

  foreach ($folder in Get-ChildItem -LiteralPath $Directory -Directory -ErrorAction SilentlyContinue) {
    if (-not (Test-CultureDirectoryName $folder.Name)) {
      continue
    }

    $canonical = Get-CanonicalCultureName $folder.Name
    if ($allowedCultures -contains $canonical) {
      $target = Join-Path $langRoot $canonical
      if (Test-Path -LiteralPath $target) {
        Remove-Item -LiteralPath $target -Recurse -Force
      }

      Move-Item -LiteralPath $folder.FullName -Destination $target -Force
      continue
    }

    Remove-Item -LiteralPath $folder.FullName -Recurse -Force
  }

  foreach ($folder in Get-ChildItem -LiteralPath $langRoot -Directory -ErrorAction SilentlyContinue) {
    $canonical = Get-CanonicalCultureName $folder.Name
    if ($allowedCultures -notcontains $canonical) {
      Remove-Item -LiteralPath $folder.FullName -Recurse -Force
      continue
    }

    if ($folder.Name -cne $canonical) {
      $target = Join-Path $langRoot $canonical
      if (Test-Path -LiteralPath $target) {
        Remove-Item -LiteralPath $target -Recurse -Force
      }
      Move-Item -LiteralPath $folder.FullName -Destination $target -Force
    }
  }
}

function Restore-LangResourceLayoutForMsix([string] $Directory) {
  $langRoot = Join-Path $Directory 'lang'
  if (-not (Test-Path -LiteralPath $langRoot)) {
    return
  }

  foreach ($culture in @('zh-CN', 'en-US')) {
    $source = Join-Path $langRoot $culture
    if (-not (Test-Path -LiteralPath $source)) {
      continue
    }

    $target = Join-Path $Directory $culture
    if (Test-Path -LiteralPath $target) {
      Remove-Item -LiteralPath $target -Recurse -Force
    }
    Move-Item -LiteralPath $source -Destination $target -Force
  }

  Remove-Item -LiteralPath $langRoot -Recurse -Force
}

function Normalize-PublishLayout([string] $Directory) {
  Compress-ReferenceData $Directory
  Normalize-AppResourcePri $Directory
  Normalize-LanguageResourceLayout $Directory
  Remove-DevelopmentArtifacts $Directory
  Remove-OldLayoutManifests $Directory
  Remove-RootHelperDuplicates $Directory
  Assert-UnpackagedLanguageResourcesInLang $Directory
  Assert-NoDevelopmentArtifacts $Directory
}

function Assert-UnpackagedLanguageResourcesInLang([string] $Directory) {
  $langRoot = Join-Path $Directory 'resources\lang'
  $languagePackages = @()
  if (Test-Path -LiteralPath $langRoot) {
    $languagePackages = @(Get-ChildItem -LiteralPath $langRoot -Filter '*.bin' -File -ErrorAction SilentlyContinue)
  }

  if ($languagePackages.Count -gt 0) {
    throw "Unpackaged EXE/latest layout must keep WinUI satellite resources expanded; found compressed resources\lang packages in $Directory"
  }

  if (Test-Path -LiteralPath $langRoot) {
    throw "Unpackaged EXE/latest layout must not contain resources\lang; use lang\zh-CN and lang\en-US in $Directory"
  }

  $rootCultureFolders = @(Get-ChildItem -LiteralPath $Directory -Directory -ErrorAction SilentlyContinue |
    Where-Object { Test-CultureDirectoryName $_.Name })
  if ($rootCultureFolders.Count -gt 0) {
    $relativeList = $rootCultureFolders | Select-Object -ExpandProperty Name
    throw "Unpackaged EXE/latest layout must not contain root culture folders: $($relativeList -join ', ')"
  }

  $canonicalLangRoot = Join-Path $Directory 'lang'
  foreach ($culture in @('zh-CN', 'en-US')) {
    $cultureRoot = Join-Path $canonicalLangRoot $culture
    if (-not (Test-Path -LiteralPath $cultureRoot)) {
      throw "Unpackaged EXE/latest layout is missing lang\$culture in $Directory"
    }

    $muiFiles = @(Get-ChildItem -LiteralPath $cultureRoot -Recurse -File -Filter '*.mui' -ErrorAction SilentlyContinue)
    $resourceFiles = @(Get-ChildItem -LiteralPath $cultureRoot -Recurse -File -Filter '*.resources.dll' -ErrorAction SilentlyContinue)
    if (($muiFiles.Count + $resourceFiles.Count) -eq 0) {
      throw "Unpackaged EXE/latest layout lang\$culture contains no satellite resource files in $Directory"
    }
  }

  $unexpectedLangFolders = @(Get-ChildItem -LiteralPath $canonicalLangRoot -Directory -ErrorAction SilentlyContinue |
    Where-Object { @('zh-CN', 'en-US') -notcontains $_.Name })
  if ($unexpectedLangFolders.Count -gt 0) {
    $relativeList = $unexpectedLangFolders | Select-Object -ExpandProperty Name
    throw "Unpackaged EXE/latest layout contains unsupported lang cultures: $($relativeList -join ', ')"
  }
}

function Ensure-RootResourcePri([string] $Directory) {
  $appPriPath = Join-Path $Directory ([IO.Path]::ChangeExtension($AppExecutable, '.pri'))
  if (Test-Path -LiteralPath $appPriPath) {
    Copy-Item -LiteralPath $appPriPath -Destination (Join-Path $Directory 'resources.pri') -Force
  }
}

function Normalize-AppResourcePri([string] $Directory) {
  $appPriPath = Join-Path $Directory ([IO.Path]::ChangeExtension($AppExecutable, '.pri'))
  if (-not (Test-Path -LiteralPath $appPriPath)) {
    return
  }

  $resourcePriPath = Join-Path $Directory 'resources.pri'
  if (Test-Path -LiteralPath $resourcePriPath) {
    Remove-Item -LiteralPath $resourcePriPath -Force
  }
  Move-Item -LiteralPath $appPriPath -Destination $resourcePriPath -Force
}

function Remove-DevelopmentArtifacts([string] $Directory) {
  $patterns = @('*.pdb', '*.winmd', '*.ilk', '*.iobj', '*.ipdb', 'workloads*.json')
  foreach ($pattern in $patterns) {
    Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue |
      ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force }
  }
}

function Remove-OldLayoutManifests([string] $Directory) {
  $oldFiles = @(
    'installer-layout.txt',
    'config\installer-layout.dat',
    'config\installer-required-files.dat',
    'Assets\data\reference-data.dat',
    'release-manifest.json',
    'version.json',
    'Assets\home-globe\textures\solar\solar-texture-manifest.json'
  )
  foreach ($relative in $oldFiles) {
    $path = Join-Path $Directory $relative
    if (Test-Path -LiteralPath $path) {
      Remove-Item -LiteralPath $path -Force
    }
  }
}

function Assert-NoDevelopmentArtifacts([string] $Directory) {
  $blocked = @()
  foreach ($pattern in @('*.pdb', '*.winmd', '*.ilk', '*.iobj', '*.ipdb', 'workloads*.json')) {
    $blocked += @(Get-ChildItem -LiteralPath $Directory -Recurse -File -Filter $pattern -ErrorAction SilentlyContinue)
  }

  $appPriPath = Join-Path $Directory ([IO.Path]::ChangeExtension($AppExecutable, '.pri'))
  if (Test-Path -LiteralPath $appPriPath) {
    $blocked += @(Get-Item -LiteralPath $appPriPath)
  }

  foreach ($relative in @(
    'installer-layout.txt',
    'config\installer-layout.dat',
    'config\installer-required-files.dat',
    'Assets\data\reference-data.dat',
    'release-manifest.json',
    'version.json',
    'Assets\home-globe\textures\solar\solar-texture-manifest.json'
  )) {
    $path = Join-Path $Directory $relative
    if (Test-Path -LiteralPath $path) {
      $blocked += @(Get-Item -LiteralPath $path)
    }
  }

  if ($blocked.Count -gt 0) {
    $relativeList = $blocked |
      Select-Object -ExpandProperty FullName -Unique |
      ForEach-Object { $_.Substring($Directory.Length).TrimStart('\', '/') -replace '\\', '/' }
    throw "Publish layout contains development artifacts: $($relativeList -join ', ')"
  }
}

function Remove-RootHelperDuplicates([string] $Directory) {
  $helperFolders = @(
    @{ Root = 'worker'; Prefix = 'YMhut.Box.Worker' },
    @{ Root = 'plugin-host'; Prefix = 'YMhut.Box.PluginHost' },
    @{ Root = 'download-host'; Prefix = 'YMhut.Box.DownloadHost' }
  )

  foreach ($helper in $helperFolders) {
    $folder = Join-Path $Directory $helper.Root
    if (-not (Test-Path -LiteralPath $folder)) {
      continue
    }

    foreach ($extension in @('.exe', '.dll', '.deps.json', '.runtimeconfig.json')) {
      $path = Join-Path $Directory "$($helper.Prefix)$extension"
      if (Test-Path -LiteralPath $path) {
        Remove-Item -LiteralPath $path -Force
      }
    }
  }
}

function Copy-PublishToLatest {
  Stop-ProcessesFromDirectory $LatestRoot
  Reset-DirectoryInsideRepo $LatestRoot
  Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $LatestRoot -Recurse -Force
  Normalize-PublishLayout $LatestRoot
  Write-LayoutManifests $LatestRoot
}

function Publish-UnpackagedApp([object] $VersionInfo) {
  Reset-DirectoryInsideRepo $PublishRoot
  Invoke-DotNet @(
    'publish', $Project,
    '-c', $Configuration,
    '-r', 'win-x64',
    '--self-contained', 'true',
    '-p:WindowsPackageType=None',
    '-p:PublishSingleFile=false',
    "-p:AssemblyVersion=$($VersionInfo.PackageVersion)",
    "-p:FileVersion=$($VersionInfo.PackageVersion)",
    "-p:InformationalVersion=$($VersionInfo.PackageVersion)",
    '-o', $PublishRoot
  )
  Normalize-PublishLayout $PublishRoot
  Write-LayoutManifests $PublishRoot
  Copy-PublishToLatest
}

function Build-InnoInstaller([object] $VersionInfo, [string] $SignTool, [switch] $AllowMissingCompiler, [string] $PayloadDirectory = $LatestRoot) {
  $iscc = Find-InnoSetupCompiler
  if (-not $iscc) {
    if ($AllowMissingCompiler) {
      Write-Warning 'Inno Setup compiler (ISCC.exe) was not found. Skipping EXE installer generation for this run.'
      return $false
    }

    throw 'Inno Setup compiler (ISCC.exe) was not found. Install Inno Setup 7, then rerun build.bat --target=exe or build.bat --target=both.'
  }

  $iss = Join-Path $Root 'installer\ymhut_box_winui.iss'
  $chineseMessagesFile = Resolve-InnoLanguageFile $iscc
  Invoke-Tool $iscc @(
    "/DMyAppVersion=$($VersionInfo.PackageVersion)",
    "/DMyAppBuild=$($VersionInfo.Build)",
    "/DMyAppChannel=$($VersionInfo.Channel)",
    "/DChineseMessagesFile=$chineseMessagesFile",
    "/DPayloadDir=$PayloadDirectory",
    $iss
  ) 'Inno Setup build failed'

  $setupPath = Join-Path $OutputRoot "YMhut_Box_WinUI_Setup_$($VersionInfo.PackageVersion).exe"
  if ((Test-Path -LiteralPath $setupPath) -and $SignTool) {
    Sign-Artifact $SignTool $setupPath
  }

  return $true
}
function Sign-Artifact([string] $SignTool, [string] $Path) {
  Ensure-LocalDeveloperCertificate
  Invoke-ToolQuiet $SignTool @('sign', '/fd', 'SHA256', '/f', $PfxPath, '/p', $PfxPassword, $Path) "Signing failed for $Path" "  Signed: $Path"
  Assert-ArtifactSignatureMatchesCertificate $Path
}

function Clear-PeCertificateDirectory([string] $Path) {
  if (-not (Test-Path -LiteralPath $Path)) {
    return $false
  }

  $bytes = [IO.File]::ReadAllBytes($Path)
  if ($bytes.Length -lt 0x200 -or
      $bytes[0] -ne 0x4D -or
      $bytes[1] -ne 0x5A) {
    return $false
  }

  $peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
  if ($peOffset -lt 0 -or $peOffset + 0x18 -ge $bytes.Length) {
    return $false
  }

  if ($bytes[$peOffset] -ne 0x50 -or
      $bytes[$peOffset + 1] -ne 0x45 -or
      $bytes[$peOffset + 2] -ne 0 -or
      $bytes[$peOffset + 3] -ne 0) {
    return $false
  }

  $optionalHeaderSize = [BitConverter]::ToUInt16($bytes, $peOffset + 20)
  $optionalHeaderOffset = $peOffset + 24
  if ($optionalHeaderOffset + $optionalHeaderSize -gt $bytes.Length) {
    return $false
  }

  $optionalMagic = [BitConverter]::ToUInt16($bytes, $optionalHeaderOffset)
  $dataDirectoryOffset = switch ($optionalMagic) {
    0x10B { $optionalHeaderOffset + 96 }
    0x20B { $optionalHeaderOffset + 112 }
    default { return $false }
  }

  $certificateDirectoryOffset = $dataDirectoryOffset + (4 * 8)
  if ($certificateDirectoryOffset + 8 -gt $optionalHeaderOffset + $optionalHeaderSize) {
    return $false
  }

  $certificateTableOffset = [BitConverter]::ToUInt32($bytes, $certificateDirectoryOffset)
  $certificateTableSize = [BitConverter]::ToUInt32($bytes, $certificateDirectoryOffset + 4)
  if ($certificateTableOffset -eq 0 -and $certificateTableSize -eq 0) {
    return $false
  }

  for ($i = 0; $i -lt 8; $i++) {
    $bytes[$certificateDirectoryOffset + $i] = 0
  }
  [IO.File]::WriteAllBytes($Path, $bytes)
  return $true
}

function Repair-MsixStageThirdPartyPayloads {
  $knownInvalidPe = Get-ChildItem -LiteralPath (Join-Path $MsixStageRoot 'Tools') -Recurse -Filter 'xiangqi.exe' -File -ErrorAction SilentlyContinue |
    Where-Object { $_.FullName.IndexOf('\XIANGQI\', [StringComparison]::OrdinalIgnoreCase) -ge 0 } |
    Select-Object -First 1
  if ($knownInvalidPe -and (Clear-PeCertificateDirectory $knownInvalidPe.FullName)) {
    Write-Host '  MSIX payload normalized: Tools\...\XIANGQI\xiangqi.exe'
  }
}

function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $SignTool) {
  if (-not $MakeAppx) { throw 'Windows SDK MakeAppx.exe was not found. Install Windows 10/11 SDK.' }
  if (-not $SignTool) { throw 'Windows SDK SignTool.exe was not found. Install Windows 10/11 SDK.' }

  $msixFileName = "YMhutBox_$($VersionInfo.PackageVersion)_x64.msix"
  $msixPath = Join-Path $OutputRoot $msixFileName

  Reset-DirectoryInsideRepo $MsixStageRoot
  Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $MsixStageRoot -Recurse -Force
  Expand-SatelliteResourcePackages $MsixStageRoot -RemovePackages
  Restore-LangResourceLayoutForMsix $MsixStageRoot
  Write-AppxManifest $MsixStageRoot $VersionInfo.PackageVersion

  $appPriPath = Join-Path $MsixStageRoot ([IO.Path]::ChangeExtension($AppExecutable, '.pri'))
  if (Test-Path -LiteralPath $appPriPath) {
    Copy-Item -LiteralPath $appPriPath -Destination (Join-Path $MsixStageRoot 'resources.pri') -Force
    Remove-Item -LiteralPath $appPriPath -Force
  }

  $stageAssets = Join-Path $MsixStageRoot 'Assets'
  New-Directory $stageAssets
  Copy-Item -LiteralPath (Join-Path $ProjectAssets '*') -Destination $stageAssets -Recurse -Force

  Repair-MsixStageThirdPartyPayloads
  Invoke-ToolQuiet $MakeAppx @('pack', '/d', $MsixStageRoot, '/p', $msixPath, '/o') 'MakeAppx packaging failed' "  MSIX package: $msixPath"
  Sign-Artifact $SignTool $msixPath

  New-Directory $OutputUpdateInfoRoot
  Copy-Item -LiteralPath $msixPath -Destination (Join-Path $OutputUpdateInfoRoot $msixFileName) -Force
  Write-AppInstaller $VersionInfo $msixFileName
  Write-MsixInstallHelpers $msixFileName
}

function Write-MsixInstallHelpers([string] $MsixFileName) {
  $psPath = Join-Path $OutputRoot 'Install-YMhutBoxMsix.ps1'
  $cmdPath = Join-Path $OutputRoot 'Install-YMhutBoxMsix.cmd'
  $ps = @"
`$ErrorActionPreference = 'Stop'
`$root = Split-Path -Parent `$MyInvocation.MyCommand.Path
`$certPath = Join-Path `$root 'YMhutBox.cer'
`$msixPath = Join-Path `$root '$MsixFileName'

function Test-Administrator {
  `$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  `$principal = [Security.Principal.WindowsPrincipal]::new(`$identity)
  return `$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

if (-not (Test-Path -LiteralPath `$certPath)) {
  throw "Certificate file was not found: `$certPath"
}
if (-not (Test-Path -LiteralPath `$msixPath)) {
  throw "MSIX package was not found: `$msixPath"
}

`$cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(`$certPath)
`$signature = Get-AuthenticodeSignature -FilePath `$msixPath
if (-not `$signature.SignerCertificate) {
  throw "MSIX signer certificate could not be read: `$msixPath"
}
if (`$signature.SignerCertificate.Thumbprint -ne `$cert.Thumbprint) {
  throw "The MSIX package is signed with `$(`$signature.SignerCertificate.Thumbprint), but YMhutBox.cer is `$(`$cert.Thumbprint). Rebuild the MSIX and install the matching certificate."
}

if (-not (Test-Administrator)) {
  Write-Host 'Administrator permission is required once to trust the local MSIX signing certificate.'
  `$arguments = "-NoProfile -ExecutionPolicy Bypass -File ```"`$PSCommandPath```""
  Start-Process -FilePath powershell.exe -ArgumentList `$arguments -Verb RunAs
  exit
}

Write-Host 'Trusting YMhut Box local signing certificate for this computer...'
foreach (`$store in @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\TrustedPeople', 'Cert:\CurrentUser\Root', 'Cert:\CurrentUser\TrustedPeople')) {
  Import-Certificate -FilePath `$certPath -CertStoreLocation `$store | Out-Null
  if (-not (Get-ChildItem -Path `$store | Where-Object Thumbprint -eq `$cert.Thumbprint)) {
    throw "Certificate was not found in `$store after import. Run this script from an elevated PowerShell window."
  }
}

Write-Host 'Installing YMhut Box MSIX package...'
Add-AppxPackage -Path `$msixPath -ForceUpdateFromAnyVersion
Write-Host 'Done.'
"@
  Set-Content -LiteralPath $psPath -Value $ps -Encoding UTF8

  $cmd = @"
@echo off
setlocal
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Install-YMhutBoxMsix.ps1"
if errorlevel 1 (
  echo.
  echo YMhut Box MSIX installation failed.
  pause
  exit /b 1
)
echo.
echo YMhut Box MSIX installation completed.
pause
"@
  Set-Content -LiteralPath $cmdPath -Value $cmd -Encoding ASCII
}

New-Directory $AppDataRoot
New-Directory $HomeRoot
New-Directory $NuGetRoot
New-Directory $LocalNuGetFeedRoot
New-Directory $OutputRoot
$env:APPDATA = $AppDataRoot
$env:LOCALAPPDATA = $AppDataRoot
$env:HOME = $HomeRoot
$env:DOTNET_CLI_HOME = Join-Path $ToolStateRoot 'dotnet'
$env:NUGET_PACKAGES = $NuGetRoot
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
New-Directory $env:DOTNET_CLI_HOME

$versionInfo = Get-VersionInfo
$makeAppx = Find-WindowsSdkTool 'MakeAppx.exe'
$signTool = Find-WindowsSdkTool 'SignTool.exe'

Write-Host "YMhut Box WinUI build"
Write-Host "  Target: $Target"
Write-Host "  Version: $($versionInfo.Version) build $($versionInfo.Build) ($($versionInfo.PackageVersion))"

Ensure-MsixAssets
Invoke-DotNet @('restore', $Solution, '--configfile', $NuGetConfig, '--ignore-failed-sources')
if (-not $SkipTests) {
  Invoke-DotNet @('test', $Solution, '-c', $Configuration, '--no-restore')
}

Publish-UnpackagedApp $versionInfo

if (($Target -in @('exe', 'both')) -and -not $SkipExe) {
  $exeBuilt = Build-InnoInstaller $versionInfo $signTool -AllowMissingCompiler:($Target -eq 'both')
  if (-not $exeBuilt -and $Target -eq 'exe') {
    throw 'EXE installer generation was requested but Inno Setup compiler was not found.'
  }
}
if ($Target -in @('msix', 'both')) {
  Build-MsixPackage $versionInfo $makeAppx $signTool
}

Write-UpdateCatalogs $versionInfo
Sync-UpdateServerPackages

Write-Host ''
Write-Host 'Build complete.'
Write-Host "  Publish payload: $PublishRoot"
Write-Host "  Latest mirror:   $LatestRoot"
Write-Host "  Update info JSON:$(Join-Path $OutputRoot 'update-info.json')"
Write-Host "  Server downloads:$ServerDownloadRoot"
Write-Host "  Server update info: $(Join-Path $ServerPublicRoot 'update-info.json')"
Write-Host "  Output:          $OutputRoot"
