param(
    [switch]$Check,
    [switch]$Yes,
    [ValidateSet('stable', 'prerelease')]
    [string]$Channel
)

$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$Root = $PSScriptRoot
$Binary = Join-Path $Root 'zfile\zfile-pro.exe'
$ReleaseBaseUrl = 'https://c.jun6.net/ZFILE-PRO'
$UpdateChannel = if ($Channel) {
    $Channel
} elseif ($env:ZFILE_UPDATE_CHANNEL) {
    $env:ZFILE_UPDATE_CHANNEL
} else {
    'stable'
}
$DefaultManifestUrl = switch ($UpdateChannel) {
    'stable' { "$ReleaseBaseUrl/latest-release.properties" }
    'prerelease' { "$ReleaseBaseUrl/latest-prerelease.properties" }
    default { throw "不支持的更新通道: $UpdateChannel，仅允许 stable 或 prerelease" }
}
$ManifestUrl = if ($env:ZFILE_UPDATE_MANIFEST_URL) {
    $env:ZFILE_UPDATE_MANIFEST_URL
} elseif ($env:ZFILE_UPDATE_METADATA_URL) {
    $env:ZFILE_UPDATE_METADATA_URL
} else {
    $DefaultManifestUrl
}
$SignatureUrl = if ($env:ZFILE_UPDATE_SIGNATURE_URL) {
    $env:ZFILE_UPDATE_SIGNATURE_URL
} else {
    "$ManifestUrl.sig"
}
$AllowCustomUrl = $env:ZFILE_UPDATE_ALLOW_CUSTOM_URL -eq 'true'
$MaxPackageSize = 2GB
$MaxUncompressedSize = 8GB
$MaxFileCount = 200000

function Parse-SemVer {
    param([string]$Value)

    $match = [regex]::Match(
        $Value.Trim(),
        '^[vV]?(?<core>\d+(?:\.\d+)*)(?:-(?<pre>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$'
    )
    if (-not $match.Success) {
        throw "无效版本号: $Value"
    }
    [pscustomobject]@{
        Core = $match.Groups['core'].Value.Split('.')
        Pre  = $match.Groups['pre'].Value.Split('.', [System.StringSplitOptions]::RemoveEmptyEntries)
    }
}

function Compare-PreRelease {
    param([string[]]$Left, [string[]]$Right)

    $length = [Math]::Max($Left.Length, $Right.Length)
    for ($i = 0; $i -lt $length; $i++) {
        if ($i -ge $Left.Length) { return -1 }
        if ($i -ge $Right.Length) { return 1 }
        if ($Left[$i] -ceq $Right[$i]) { continue }

        $leftNumber = [System.Numerics.BigInteger]::Zero
        $rightNumber = [System.Numerics.BigInteger]::Zero
        $leftNumeric = [System.Numerics.BigInteger]::TryParse($Left[$i], [ref]$leftNumber)
        $rightNumeric = [System.Numerics.BigInteger]::TryParse($Right[$i], [ref]$rightNumber)
        if ($leftNumeric -and $rightNumeric) { return $leftNumber.CompareTo($rightNumber) }
        if ($leftNumeric) { return -1 }
        if ($rightNumeric) { return 1 }
        return [string]::CompareOrdinal($Left[$i], $Right[$i])
    }
    return 0
}

function Compare-SemVer {
    param([string]$Left, [string]$Right)

    $leftVersion = Parse-SemVer $Left
    $rightVersion = Parse-SemVer $Right
    $length = [Math]::Max($leftVersion.Core.Length, $rightVersion.Core.Length)
    for ($i = 0; $i -lt $length; $i++) {
        $leftPart = if ($i -lt $leftVersion.Core.Length) {
            [System.Numerics.BigInteger]::Parse($leftVersion.Core[$i])
        } else {
            [System.Numerics.BigInteger]::Zero
        }
        $rightPart = if ($i -lt $rightVersion.Core.Length) {
            [System.Numerics.BigInteger]::Parse($rightVersion.Core[$i])
        } else {
            [System.Numerics.BigInteger]::Zero
        }
        $compared = $leftPart.CompareTo($rightPart)
        if ($compared -ne 0) { return $compared }
    }

    if ($leftVersion.Pre.Length -eq 0 -and $rightVersion.Pre.Length -eq 0) { return 0 }
    if ($leftVersion.Pre.Length -eq 0) { return 1 }
    if ($rightVersion.Pre.Length -eq 0) { return -1 }
    return Compare-PreRelease $leftVersion.Pre $rightVersion.Pre
}

function Read-VerifiedProperties {
    param([string[]]$Lines)

    $properties = @{}
    foreach ($line in $Lines) {
        if ($line -match '^(?<Key>[^=]+)=(?<Value>.*)$') {
            $properties[$Matches.Key] = $Matches.Value.Trim()
        }
    }
    foreach ($key in @('latestVersion', 'url', 'size', 'sha256', 'uncompressedSize', 'fileCount')) {
        if (-not $properties.ContainsKey($key) -or -not $properties[$key]) {
            throw "更新清单校验结果缺少属性: $key"
        }
    }
    return $properties
}

function Test-UpdateArchive {
    param(
        [string]$Archive,
        [long]$ExpectedUncompressedSize,
        [long]$ExpectedFileCount
    )

    Add-Type -AssemblyName System.IO.Compression.FileSystem
    $zip = [IO.Compression.ZipFile]::OpenRead($Archive)
    try {
        if ($zip.Entries.Count -ne $ExpectedFileCount) {
            throw "更新包文件数与签名清单不一致: 期望=$ExpectedFileCount，实际=$($zip.Entries.Count)"
        }
        [long]$actualUncompressedSize = 0
        foreach ($entry in $zip.Entries) {
            $actualUncompressedSize += $entry.Length
            $normalized = $entry.FullName.Replace('\', '/')
            $segments = $normalized.Split('/', [StringSplitOptions]::RemoveEmptyEntries)
            if ([IO.Path]::IsPathRooted($entry.FullName) -or $segments -contains '..') {
                throw "更新包包含不安全路径: $($entry.FullName)"
            }
        }
        if ($actualUncompressedSize -ne $ExpectedUncompressedSize) {
            throw "更新包解压大小与签名清单不一致: 期望=$ExpectedUncompressedSize，实际=$actualUncompressedSize"
        }
    } finally {
        $zip.Dispose()
    }
}

function Get-ZFileProcesses {
    Get-CimInstance Win32_Process -Filter "Name='zfile-pro.exe'" -ErrorAction SilentlyContinue |
        Where-Object { $_.ExecutablePath -and ([IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath($Binary)) }
}

function Start-ZFile {
    Start-Process -FilePath $Binary `
        -ArgumentList '-Dfile.encoding=UTF-8', '-Dsun.stdout.encoding=UTF-8', '-Dsun.stderr.encoding=UTF-8' `
        -WorkingDirectory $Root `
        -WindowStyle Hidden
    Start-Sleep -Seconds 2
    if (-not (Get-ZFileProcesses)) {
        throw '程序启动后立即退出，请查看日志确认原因'
    }
}

function Stop-ZFile {
    $processes = @(Get-ZFileProcesses)
    foreach ($process in $processes) {
        Stop-Process -Id $process.ProcessId -Force
    }
    if ($processes.Count -gt 0) {
        Start-Sleep -Seconds 1
    }
}

function Copy-BackupItem {
    param([string]$BackupDir, [string]$Name)

    $source = Join-Path $Root $Name
    if (Test-Path $source) {
        Copy-Item $source -Destination $BackupDir -Recurse -Force
    }
}

function Restore-ZFile {
    param([string]$BackupDir, [bool]$WasRunning)

    Write-Warning '更新失败，正在恢复旧程序...'
    Stop-ZFile
    foreach ($name in @('zfile', 'static')) {
        $target = Join-Path $Root $name
        Remove-Item $target -Recurse -Force -ErrorAction SilentlyContinue
        $backupItem = Join-Path $BackupDir $name
        if (Test-Path $backupItem) {
            Copy-Item $backupItem -Destination $Root -Recurse -Force
        }
    }
    Get-ChildItem $BackupDir -File -ErrorAction SilentlyContinue |
        Where-Object { $_.Extension -in @('.bat', '.ps1') -or $_.Name -like 'application.properties*' } |
        Copy-Item -Destination $Root -Force
    if ($WasRunning) {
        try {
            Start-ZFile
        } catch {
            Write-Warning "旧程序已恢复，但重新启动失败: $($_.Exception.Message)"
        }
    }
}

if (-not (Test-Path $Binary)) {
    Write-Error "找不到程序文件: $Binary"
    exit 1
}

if (($ManifestUrl -ne $DefaultManifestUrl -or $SignatureUrl -ne "$DefaultManifestUrl.sig") -and
    -not $AllowCustomUrl) {
    Write-Error '检测到自定义更新清单地址，已拒绝使用。如确需使用，请显式设置 ZFILE_UPDATE_ALLOW_CUSTOM_URL=true'
    exit 1
}

$currentVersion = ((& $Binary --version 2>$null | Select-Object -First 1) -as [string]).Trim()
if (-not $currentVersion -or $currentVersion -eq 'unknown') {
    Write-Error '无法读取当前程序版本，当前二进制可能是旧版本或文件不完整'
    exit 1
}

Write-Host '正在检测更新...'
$metadataWorkDir = Join-Path ([IO.Path]::GetTempPath()) ("zfile-update-metadata-" + [guid]::NewGuid().ToString('N'))
$manifestFile = Join-Path $metadataWorkDir 'latest.properties'
$signatureFile = Join-Path $metadataWorkDir 'latest.properties.sig'
New-Item $metadataWorkDir -ItemType Directory -Force | Out-Null
try {
    try {
        Invoke-WebRequest -Uri $ManifestUrl -UseBasicParsing -OutFile $manifestFile
    } catch {
        throw "获取更新清单失败: $ManifestUrl`n$($_.Exception.Message)"
    }
    try {
        Invoke-WebRequest -Uri $SignatureUrl -UseBasicParsing -OutFile $signatureFile
    } catch {
        throw "获取更新清单签名失败: $SignatureUrl`n$($_.Exception.Message)"
    }
    $verifiedOutput = @(& $Binary 'verify-update-manifest' `
        '--manifest' $manifestFile `
        '--signature' $signatureFile `
        '--artifact' 'windows_amd64' 2>&1)
    if ($LASTEXITCODE -ne 0) {
        throw "更新清单签名或平台信息校验失败: $($verifiedOutput -join '; ')"
    }
    $verified = Read-VerifiedProperties $verifiedOutput
    $latestVersion = $verified.latestVersion
    $packageUrl = $verified.url
    [long]$expectedSize = $verified.size
    [long]$expectedUncompressedSize = $verified.uncompressedSize
    [long]$expectedFileCount = $verified.fileCount
    $expectedSha256 = $verified.sha256.ToLowerInvariant()
    if ($expectedSize -gt $MaxPackageSize -or
        $expectedUncompressedSize -gt $MaxUncompressedSize -or
        $expectedFileCount -gt $MaxFileCount) {
        throw "更新包超过安全限制: 下载=$expectedSize，解压=$expectedUncompressedSize，文件数=$expectedFileCount"
    }
    $comparison = Compare-SemVer $latestVersion $currentVersion
} catch {
    Write-Error "检测更新失败: $($_.Exception.Message)"
    exit 1
} finally {
    Remove-Item $metadataWorkDir -Recurse -Force -ErrorAction SilentlyContinue
}

Write-Host "当前版本: $currentVersion"
Write-Host "最新版本: $latestVersion"
Write-Host "更新通道: $UpdateChannel"
if ($comparison -lt 0) {
    Write-Host '当前程序版本高于已发布版本，无需更新'
    exit 0
}
if ($comparison -eq 0) {
    Write-Host '当前已是最新版本'
    exit 0
}

Write-Warning "检测到新版本: $currentVersion -> $latestVersion"
if ($Check) { exit 2 }
if (-not $Yes) {
    $answer = Read-Host '确认下载并更新吗？更新前会备份旧程序 [y/N]'
    if ($answer -notmatch '^(?i:y|yes)$') {
        Write-Host '已取消更新'
        exit 0
    }
}

$workDir = Join-Path ([IO.Path]::GetTempPath()) ("zfile-update-" + [guid]::NewGuid().ToString('N'))
$archive = Join-Path $workDir 'update.zip'
$staging = Join-Path $workDir 'staging'
New-Item $staging -ItemType Directory -Force | Out-Null

try {
    Write-Host "正在下载更新包: $packageUrl"
    Invoke-WebRequest -Uri $packageUrl -UseBasicParsing -OutFile $archive
    $actualSize = (Get-Item $archive).Length
    if ($actualSize -ne $expectedSize) {
        throw "更新包大小校验失败: 期望=$expectedSize，实际=$actualSize"
    }
    $actualSha256 = (Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($actualSha256 -ne $expectedSha256) {
        throw '更新包 SHA-256 校验失败'
    }
    Test-UpdateArchive $archive $expectedUncompressedSize $expectedFileCount
    Expand-Archive -Path $archive -DestinationPath $staging -Force
    $packageBinary = Get-ChildItem $staging -Recurse -File -Filter 'zfile-pro.exe' |
        Where-Object { $_.Directory.Name -eq 'zfile' } |
        Select-Object -First 1
    if (-not $packageBinary) { throw '更新包缺少 zfile\zfile-pro.exe' }
    $packageRoot = $packageBinary.Directory.Parent.FullName
    foreach ($required in @(
        'zfile\zfile-pro.exe',
        '双击我启动.bat',
        '后台启动.bat',
        'start-background.ps1',
        '更新程序.bat',
        'update.ps1'
    )) {
        if (-not (Test-Path (Join-Path $packageRoot $required))) {
            throw "更新包缺少必要文件: $required"
        }
    }

    $wasRunning = @(Get-ZFileProcesses).Count -gt 0
    if ($wasRunning) { Stop-ZFile }

    $safeVersion = $currentVersion -replace '[^0-9A-Za-z._-]', '_'
    $backupDir = Join-Path $Root ("backup\{0}_{1}" -f $safeVersion, (Get-Date -Format 'yyyyMMdd_HHmmss'))
    New-Item $backupDir -ItemType Directory -Force | Out-Null
    try {
        foreach ($name in @('zfile', 'static', 'application.properties', 'application.properties.example')) {
            Copy-BackupItem $backupDir $name
        }
        Get-ChildItem $Root -File |
            Where-Object { $_.Extension -in @('.bat', '.ps1') } |
            Copy-Item -Destination $backupDir -Force
    } catch {
        if ($wasRunning) { Start-ZFile }
        throw "备份旧程序失败: $($_.Exception.Message)"
    }
    Write-Host "旧程序已备份到: $backupDir"

    try {
        foreach ($name in @('zfile', 'static')) {
            $target = Join-Path $Root $name
            Remove-Item $target -Recurse -Force -ErrorAction SilentlyContinue
            $source = Join-Path $packageRoot $name
            if (Test-Path $source) {
                Copy-Item $source -Destination $Root -Recurse -Force
            }
        }
        $packageScripts = @(Get-ChildItem $packageRoot -File |
            Where-Object { $_.Extension -in @('.bat', '.ps1') })
        $packageScriptNames = @($packageScripts | ForEach-Object { $_.Name })
        Get-ChildItem $Root -File |
            Where-Object {
                $_.Extension -in @('.bat', '.ps1') -and
                $_.Name -notin $packageScriptNames
            } |
            Remove-Item -Force
        $packageScripts | Copy-Item -Destination $Root -Force

        $packagePropertiesExample = Join-Path $packageRoot 'application.properties.example'
        if (Test-Path $packagePropertiesExample) {
            Copy-Item $packagePropertiesExample -Destination $Root -Force
        }

        $installedVersion = ((& $Binary --version 2>$null | Select-Object -First 1) -as [string]).Trim()
        if ($installedVersion -ne $latestVersion) {
            throw "更新包版本校验失败: 期望=$latestVersion，实际=$installedVersion"
        }
        if ($wasRunning) { Start-ZFile }
    } catch {
        $reason = $_.Exception.Message
        Restore-ZFile $backupDir $wasRunning
        throw $reason
    }

    Write-Host "更新完成: $currentVersion -> $latestVersion"
    Write-Host "备份目录: $backupDir"
    if (-not $wasRunning) { Write-Host '更新前服务未运行，当前保持停止状态' }
} catch {
    Write-Error "更新失败: $($_.Exception.Message)"
    exit 1
} finally {
    Remove-Item $workDir -Recurse -Force -ErrorAction SilentlyContinue
}
