param(
    [string]$ProjectPath = (Join-Path $PSScriptRoot "..\..\Tornado3_2026Election\Tornado3_2026Election.csproj"),
    [ValidateSet("Debug", "Release")]
    [string]$Configuration = "Release",
    [string]$Platform = "x64",
    [string]$RuntimeIdentifier = "win-x64",
    [ValidatePattern("^\d+\.\d+\.\d+\.\d+$")]
    [string]$PackageVersion,
    [switch]$IncrementPackageRevision,
    [string]$PublicBaseUri = "http://122.34.248.185/msix/",
    [string]$NasHost = "192.168.200.129",
    [int]$NasSshPort = 22,
    [string]$NasUser = $env:NAS_USER,
    [string]$NasRemotePath = "/volume1/web/msix",
    [string]$SshKeyPath = $env:NAS_SSH_KEY,
    [string]$CertificateThumbprint = "E691A33C64DF20A204FFD4F096B9C3EB4B95709C",
    [string]$CertificateFileName = "Comtrophy_MSIX_Signing.cer",
    [string]$InstallCertificateScriptPath = (Join-Path $PSScriptRoot "Install-ComtrophyMsixCertificate.ps1"),
    [switch]$SkipPackageBuild,
    [switch]$NoUpload,
    [switch]$NoVerify
)

$ErrorActionPreference = "Stop"

function Resolve-FullPath {
    param([Parameter(Mandatory)][string]$Path)

    if (Test-Path -LiteralPath $Path) {
        return (Resolve-Path -LiteralPath $Path).Path
    }

    $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
}

function Invoke-Checked {
    param(
        [Parameter(Mandatory)][string]$FilePath,
        [Parameter(Mandatory)][string[]]$Arguments
    )

    Write-Host "> $FilePath $($Arguments -join ' ')"
    & $FilePath @Arguments
    if ($LASTEXITCODE -ne 0) {
        throw "$FilePath failed with exit code $LASTEXITCODE."
    }
}

function Test-IsUnderDirectory {
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][string]$Directory
    )

    $normalizedPath = [System.IO.Path]::GetFullPath($Path).TrimEnd(
        [System.IO.Path]::DirectorySeparatorChar,
        [System.IO.Path]::AltDirectorySeparatorChar
    )
    $normalizedDirectory = [System.IO.Path]::GetFullPath($Directory).TrimEnd(
        [System.IO.Path]::DirectorySeparatorChar,
        [System.IO.Path]::AltDirectorySeparatorChar
    )

    return $normalizedPath.Equals($normalizedDirectory, [System.StringComparison]::OrdinalIgnoreCase) -or
        $normalizedPath.StartsWith(
            $normalizedDirectory + [System.IO.Path]::DirectorySeparatorChar,
            [System.StringComparison]::OrdinalIgnoreCase
        ) -or
        $normalizedPath.StartsWith(
            $normalizedDirectory + [System.IO.Path]::AltDirectorySeparatorChar,
            [System.StringComparison]::OrdinalIgnoreCase
        )
}

function Find-NewestFile {
    param(
        [Parameter(Mandatory)][string]$Root,
        [Parameter(Mandatory)][string]$Filter,
        [DateTime]$NotBefore = [DateTime]::MinValue,
        [string]$ExcludeDirectory
    )

    $resolvedExcludeDirectory = if ($ExcludeDirectory -and (Test-Path -LiteralPath $ExcludeDirectory)) {
        Resolve-FullPath $ExcludeDirectory
    }
    else {
        $null
    }

    Get-ChildItem -Path $Root -Recurse -File -Filter $Filter |
        Where-Object {
            $_.LastWriteTime -ge $NotBefore -and
            (-not $resolvedExcludeDirectory -or -not (Test-IsUnderDirectory -Path $_.FullName -Directory $resolvedExcludeDirectory))
        } |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1
}

function Find-PackageFileByLeafName {
    param(
        [Parameter(Mandatory)][string]$PackageRoot,
        [Parameter(Mandatory)][string]$LeafName,
        [string]$PreferredPathFragment,
        [string]$ExcludeDirectory
    )

    $resolvedExcludeDirectory = if ($ExcludeDirectory -and (Test-Path -LiteralPath $ExcludeDirectory)) {
        Resolve-FullPath $ExcludeDirectory
    }
    else {
        $null
    }

    $candidates = Get-ChildItem -Path $PackageRoot -Recurse -File -Filter $LeafName |
        Where-Object {
            -not $resolvedExcludeDirectory -or
            -not (Test-IsUnderDirectory -Path $_.FullName -Directory $resolvedExcludeDirectory)
        }

    if ($PreferredPathFragment) {
        $preferred = $candidates |
            Where-Object { $_.FullName -like "*$PreferredPathFragment*" } |
            Sort-Object LastWriteTime -Descending |
            Select-Object -First 1
        if ($preferred) {
            return $preferred
        }
    }

    $candidates | Sort-Object LastWriteTime -Descending | Select-Object -First 1
}

function Get-UriLeafName {
    param([Parameter(Mandatory)][string]$Uri)

    try {
        return [System.IO.Path]::GetFileName(([System.Uri]$Uri).AbsolutePath)
    }
    catch {
        return [System.IO.Path]::GetFileName($Uri.Replace("/", "\"))
    }
}

function Ensure-CertificateFile {
    param(
        [Parameter(Mandatory)][string]$CertificatePath,
        [Parameter(Mandatory)][string]$Thumbprint
    )

    if (Test-Path -LiteralPath $CertificatePath) {
        return
    }

    $certificate = Get-ChildItem -Path Cert:\CurrentUser\My |
        Where-Object { $_.Thumbprint -eq $Thumbprint } |
        Select-Object -First 1

    if (-not $certificate) {
        throw "Could not find signing certificate $Thumbprint in Cert:\CurrentUser\My."
    }

    Export-Certificate -Cert $certificate -FilePath $CertificatePath -Force | Out-Null
}

function Get-PackageManifestVersion {
    param([Parameter(Mandatory)][string]$ManifestPath)

    if (-not (Test-Path -LiteralPath $ManifestPath)) {
        throw "Package manifest was not found: $ManifestPath"
    }

    $manifestXml = New-Object System.Xml.XmlDocument
    $manifestXml.Load($ManifestPath)

    $namespaceManager = New-Object System.Xml.XmlNamespaceManager($manifestXml.NameTable)
    $namespaceManager.AddNamespace("pkg", $manifestXml.DocumentElement.NamespaceURI)

    $identityNode = $manifestXml.SelectSingleNode("/pkg:Package/pkg:Identity", $namespaceManager)
    if (-not $identityNode) {
        throw "Identity node was not found in $ManifestPath."
    }

    $identityNode.GetAttribute("Version")
}

function Get-NextPackageRevision {
    param([Parameter(Mandatory)][string]$Version)

    $parts = $Version.Split(".")
    if ($parts.Count -ne 4) {
        throw "Package version must have four parts: $Version"
    }

    $numbers = foreach ($part in $parts) {
        [int]$part
    }
    $numbers[3] += 1

    $numbers -join "."
}

function Set-PackageManifestVersion {
    param(
        [Parameter(Mandatory)][string]$ManifestPath,
        [Parameter(Mandatory)][string]$Version
    )

    if (-not (Test-Path -LiteralPath $ManifestPath)) {
        throw "Package manifest was not found: $ManifestPath"
    }

    $manifestXml = New-Object System.Xml.XmlDocument
    $manifestXml.PreserveWhitespace = $true
    $manifestXml.Load($ManifestPath)

    $namespaceManager = New-Object System.Xml.XmlNamespaceManager($manifestXml.NameTable)
    $namespaceManager.AddNamespace("pkg", $manifestXml.DocumentElement.NamespaceURI)

    $identityNode = $manifestXml.SelectSingleNode("/pkg:Package/pkg:Identity", $namespaceManager)
    if (-not $identityNode) {
        throw "Identity node was not found in $ManifestPath."
    }

    $currentVersion = $identityNode.GetAttribute("Version")
    if ($currentVersion -eq $Version) {
        Write-Host "Package manifest version is already $Version"
        return
    }

    $identityNode.SetAttribute("Version", $Version)
    $manifestXml.Save($ManifestPath)
    Write-Host "Updated package manifest version from $currentVersion to $Version"
}

function Join-PublicUri {
    param(
        [Parameter(Mandatory)][string]$BaseUri,
        [Parameter(Mandatory)][string]$LeafName
    )

    $normalizedBase = $BaseUri
    if (-not $normalizedBase.EndsWith("/")) {
        $normalizedBase += "/"
    }

    "$normalizedBase$LeafName"
}

$projectFullPath = Resolve-FullPath $ProjectPath
$projectDirectory = Split-Path -Parent $projectFullPath
$manifestPath = Join-Path $projectDirectory "Package.appxmanifest"
$packageRoot = Join-Path $projectDirectory "AppPackages"
$stagingRoot = Join-Path $packageRoot "msix-publish-flat"
$certificatePath = Join-Path $stagingRoot $CertificateFileName
$normalizedPublicBaseUri = $PublicBaseUri
if (-not $normalizedPublicBaseUri.EndsWith("/")) {
    $normalizedPublicBaseUri += "/"
}

if ($PackageVersion -and $IncrementPackageRevision) {
    throw "Use either PackageVersion or IncrementPackageRevision, not both."
}

if ($PackageVersion -and $SkipPackageBuild) {
    throw "PackageVersion requires a new package build. Remove -SkipPackageBuild and run again."
}

if ($IncrementPackageRevision -and $SkipPackageBuild) {
    throw "IncrementPackageRevision requires a new package build. Remove -SkipPackageBuild and run again."
}

if ($IncrementPackageRevision) {
    $currentPackageVersion = Get-PackageManifestVersion -ManifestPath $manifestPath
    $PackageVersion = Get-NextPackageRevision -Version $currentPackageVersion
    Write-Host "Auto-incrementing package version from $currentPackageVersion to $PackageVersion"
}

if ($PackageVersion) {
    Set-PackageManifestVersion -ManifestPath $manifestPath -Version $PackageVersion
}

$signingCertificate = Get-ChildItem -Path Cert:\CurrentUser\My |
    Where-Object { $_.Thumbprint -eq $CertificateThumbprint } |
    Select-Object -First 1

if (-not $signingCertificate) {
    throw "Signing certificate $CertificateThumbprint was not found in Cert:\CurrentUser\My."
}

if (-not $signingCertificate.HasPrivateKey) {
    throw "Signing certificate $CertificateThumbprint does not have a private key."
}

$buildStartedAt = Get-Date

if (-not $SkipPackageBuild) {
    $dotnetArgs = @(
        "msbuild",
        $projectFullPath,
        "/restore",
        "/t:Build",
        "/p:Configuration=$Configuration",
        "/p:Platform=$Platform",
        "/p:RuntimeIdentifier=$RuntimeIdentifier",
        "/p:GenerateAppxPackageOnBuild=true",
        "/p:GenerateAppInstallerFile=true",
        "/p:AppxPackageSigningEnabled=true",
        "/p:PackageCertificateThumbprint=$CertificateThumbprint",
        "/p:AppInstallerUri=$normalizedPublicBaseUri",
        "/p:AppxBundle=Never"
    )
    Invoke-Checked -FilePath "dotnet" -Arguments $dotnetArgs
}

if (Test-Path -LiteralPath $stagingRoot) {
    $resolvedStaging = Resolve-FullPath $stagingRoot
    $resolvedPackageRoot = Resolve-FullPath $packageRoot
    if (-not (Test-IsUnderDirectory -Path $resolvedStaging -Directory $resolvedPackageRoot)) {
        throw "Refusing to clean staging path outside package root: $resolvedStaging"
    }
    Get-ChildItem -LiteralPath $stagingRoot -Force | Remove-Item -Recurse -Force
}
else {
    New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null
}

$appInstallerFile = Find-NewestFile -Root $packageRoot -Filter "*_${Platform}.appinstaller" -NotBefore $(if ($SkipPackageBuild) { [DateTime]::MinValue } else { $buildStartedAt.AddMinutes(-2) }) -ExcludeDirectory $stagingRoot
if (-not $appInstallerFile) {
    $appInstallerFile = Find-NewestFile -Root $packageRoot -Filter "*.appinstaller" -ExcludeDirectory $stagingRoot
}

if (-not $appInstallerFile) {
    throw "Could not find an .appinstaller file under $packageRoot."
}

$stagedAppInstallerPath = Join-Path $stagingRoot $appInstallerFile.Name
Copy-Item -LiteralPath $appInstallerFile.FullName -Destination $stagedAppInstallerPath -Force

$appInstallerXml = New-Object System.Xml.XmlDocument
$appInstallerXml.PreserveWhitespace = $true
$appInstallerXml.Load($stagedAppInstallerPath)

$namespaceManager = New-Object System.Xml.XmlNamespaceManager($appInstallerXml.NameTable)
$namespaceManager.AddNamespace("ai", $appInstallerXml.DocumentElement.NamespaceURI)

$mainPackageNode = $appInstallerXml.SelectSingleNode("//ai:MainPackage", $namespaceManager)
if (-not $mainPackageNode) {
    throw "MainPackage node was not found in $($appInstallerFile.FullName)."
}

$mainPackageLeafName = Get-UriLeafName $mainPackageNode.GetAttribute("Uri")
$mainPackageFile = Find-PackageFileByLeafName -PackageRoot $packageRoot -LeafName $mainPackageLeafName -ExcludeDirectory $stagingRoot
if (-not $mainPackageFile) {
    $mainPackageFile = Get-ChildItem -Path $packageRoot -Recurse -File -Filter "*.msix" |
        Where-Object {
            $_.FullName -notlike "*\Dependencies\*" -and
            -not (Test-IsUnderDirectory -Path $_.FullName -Directory $stagingRoot)
        } |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1
}

if (-not $mainPackageFile) {
    throw "Could not find the main .msix package referenced by $($appInstallerFile.FullName)."
}

$signature = Get-AuthenticodeSignature -FilePath $mainPackageFile.FullName
if ($signature.Status -ne "Valid") {
    throw "MSIX signature is not valid: $($signature.StatusMessage)"
}

if ($signature.SignerCertificate.Thumbprint -ne $CertificateThumbprint) {
    throw "MSIX was signed with $($signature.SignerCertificate.Thumbprint), expected $CertificateThumbprint."
}

Copy-Item -LiteralPath $mainPackageFile.FullName -Destination (Join-Path $stagingRoot $mainPackageFile.Name) -Force
$appInstallerXml.DocumentElement.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $appInstallerFile.Name))
$mainPackageNode.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $mainPackageFile.Name))

$dependencyNodes = $appInstallerXml.SelectNodes("//ai:Dependencies/ai:Package", $namespaceManager)
foreach ($dependencyNode in $dependencyNodes) {
    $dependencyLeafName = Get-UriLeafName $dependencyNode.GetAttribute("Uri")
    $architecture = $dependencyNode.GetAttribute("ProcessorArchitecture")
    $preferredFragment = if ($architecture) { "Dependencies\$architecture" } else { $null }
    $dependencyFile = Find-PackageFileByLeafName -PackageRoot $packageRoot -LeafName $dependencyLeafName -PreferredPathFragment $preferredFragment -ExcludeDirectory $stagingRoot

    if (-not $dependencyFile) {
        throw "Could not find dependency package $dependencyLeafName."
    }

    Copy-Item -LiteralPath $dependencyFile.FullName -Destination (Join-Path $stagingRoot $dependencyFile.Name) -Force
    $dependencyNode.SetAttribute("Uri", (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $dependencyFile.Name))
}

$appInstallerXml.Save($stagedAppInstallerPath)

Ensure-CertificateFile -CertificatePath $certificatePath -Thumbprint $CertificateThumbprint

if (Test-Path -LiteralPath $InstallCertificateScriptPath) {
    Copy-Item -LiteralPath $InstallCertificateScriptPath -Destination (Join-Path $stagingRoot (Split-Path -Leaf $InstallCertificateScriptPath)) -Force
}

$stagedFiles = Get-ChildItem -Path $stagingRoot -File | Sort-Object Name

Write-Host ""
Write-Host "Prepared MSIX deployment files:"
$stagedFiles | ForEach-Object {
    Write-Host (" - {0} ({1:N0} bytes)" -f $_.Name, $_.Length)
}

if (-not $NoUpload) {
    if (-not $NasUser) {
        throw "NasUser was not provided. Pass -NasUser <user> or set NAS_USER."
    }

    $sshArgs = @()
    if ($NasSshPort -ne 22) {
        $sshArgs += @("-p", [string]$NasSshPort)
    }
    if ($SshKeyPath) {
        $sshArgs += @("-i", (Resolve-FullPath $SshKeyPath))
    }
    $sshArgs += @("${NasUser}@${NasHost}", "mkdir -p '$NasRemotePath'")
    Invoke-Checked -FilePath "ssh" -Arguments $sshArgs

    $scpArgs = @()
    if ($NasSshPort -ne 22) {
        $scpArgs += @("-P", [string]$NasSshPort)
    }
    if ($SshKeyPath) {
        $scpArgs += @("-i", (Resolve-FullPath $SshKeyPath))
    }
    $scpArgs += $stagedFiles.FullName
    $scpArgs += "${NasUser}@${NasHost}:$NasRemotePath/"
    Invoke-Checked -FilePath "scp" -Arguments $scpArgs
}

if (-not $NoVerify) {
    foreach ($file in $stagedFiles) {
        $uri = Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $file.Name
        try {
            $response = Invoke-WebRequest -Uri $uri -Method Head -UseBasicParsing -TimeoutSec 20
        }
        catch {
            Write-Warning "HEAD failed for $uri. Trying GET."
            $response = Invoke-WebRequest -Uri $uri -UseBasicParsing -TimeoutSec 20
        }

        if ($response.StatusCode -lt 200 -or $response.StatusCode -gt 299) {
            throw "Verification failed for $uri with status $($response.StatusCode)."
        }

        if (-not $NoUpload) {
            $contentLengthHeader = $response.Headers["Content-Length"] | Select-Object -First 1
            if ($contentLengthHeader) {
                $remoteLength = [long]$contentLengthHeader
                if ($remoteLength -ne $file.Length) {
                    throw "Verification failed for $uri. Remote length $remoteLength does not match local length $($file.Length)."
                }
            }
            else {
                Write-Warning "No Content-Length header returned for $uri; status verification only."
            }
        }

        Write-Host "Verified $uri"
    }
}

Write-Host "Staging directory:"
Write-Host $stagingRoot

Write-Host ""
Write-Host "App Installer URL:"
Write-Host (Join-PublicUri -BaseUri $normalizedPublicBaseUri -LeafName $appInstallerFile.Name)
