#Requires -Version 5.1

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$false)]
    [ValidateSet("arm64", "x64")]
    [string]$Platform = "x64",
    
    [Parameter(Mandatory=$false)]
    [ValidateSet("Debug", "Release")]
    [string]$Configuration = "Release",

    [switch]$Clean,
    [switch]$ForceCert,
    [switch]$NoSign,
    [switch]$CIBuild
)

# PowerToys sparse packaging helper.
# Generates a sparse MSIX (no payload) that grants package identity to selected Win32 components.
# Multiple applications (PowerOCR, Settings UI, etc.) can share this single sparse identity.

$ErrorActionPreference = 'Stop'

$isCIBuild = $false
if ($CIBuild.IsPresent) {
    $isCIBuild = $true
} elseif ($env:CIBuild) {
    $isCIBuild = $env:CIBuild -ieq 'true'
}

$currentPublisherHint = $script:Config.CertSubject

# Configuration constants - centralized management
$script:Config = @{
    IdentityName   = "Microsoft.PowerToys.SparseApp"
    SparseMsixName = "PowerToysSparse.msix"
    CertPrefix     = "PowerToysSparse"
    CertSubject    = 'CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US'
    CertValidMonths = 12
}

#region Helper Functions

function Find-WindowsSDKTool {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ToolName,
        
        [Parameter(Mandatory=$false)]
        [string]$Architecture = "x64"
    )
    
    # Simple fallback: check common Windows SDK locations
    $commonPaths = @(
        "${env:ProgramFiles}\Windows Kits\10\bin\*\$Architecture\$ToolName",
        "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\$Architecture\$ToolName",
        "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x86\$ToolName"  # SignTool fallback
    )
    
    foreach ($pattern in $commonPaths) {
        $found = Get-ChildItem $pattern -ErrorAction SilentlyContinue | 
                 Sort-Object Name -Descending | 
                 Select-Object -First 1
        if ($found) {
            Write-BuildLog "Found $ToolName at: $($found.FullName)" -Level Info
            return $found.FullName
        }
    }
    
    throw "$ToolName not found. Please ensure Windows SDK is installed."
}

function Test-CertificateValidity {
    param([string]$ThumbprintFile)
    
    if (-not (Test-Path $ThumbprintFile)) { return $false }
    
    try {
        $thumb = (Get-Content $ThumbprintFile -Raw).Trim()
        if (-not $thumb) { return $false }
        $cert = Get-Item "cert:\CurrentUser\My\$thumb" -ErrorAction Stop
        return $cert.HasPrivateKey -and $cert.NotAfter -gt (Get-Date)
    } catch {
        return $false
    }
}

function Write-BuildLog {
    param([string]$Message, [string]$Level = "Info")
    
    $colors = @{ Error = "Red"; Warning = "Yellow"; Success = "Green"; Info = "Cyan" }
    $color = if ($colors.ContainsKey($Level)) { $colors[$Level] } else { "White" }
    
    Write-Host "[$(Get-Date -f 'HH:mm:ss')] $Message" -ForegroundColor $color
}

function Stop-FileProcesses {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$FilePath
    )
    
    # This function is kept for compatibility but simplified since 
    # the staging directory approach resolves the file lock issues
    Write-Verbose "File process check for: $FilePath"
}

#endregion

# Environment diagnostics for troubleshooting
Write-BuildLog "Starting PackageIdentity build process..." -Level Info
Write-BuildLog "PowerShell Version: $($PSVersionTable.PSVersion)" -Level Info
try {
    $execPolicy = Get-ExecutionPolicy
    Write-BuildLog "Execution Policy: $execPolicy" -Level Info
} catch {
    Write-BuildLog "Execution Policy: Unable to determine (MSBuild environment)" -Level Info
}
Write-BuildLog "Current User: $env:USERNAME" -Level Info
Write-BuildLog "Build Platform: $Platform, Configuration: $Configuration" -Level Info

# Check for Visual Studio environment
if ($env:VSINSTALLDIR) {
    Write-BuildLog "Running in Visual Studio environment: $env:VSINSTALLDIR" -Level Info
}

# Ensure certificate provider is available
try {
    # Force load certificate provider for MSBuild environment
    if (-not (Get-PSProvider -PSProvider Certificate -ErrorAction SilentlyContinue)) {
        Write-BuildLog "Loading certificate provider..." -Level Warning
        Import-Module Microsoft.PowerShell.Security -Force
    }
    if (-not (Test-Path 'Cert:\CurrentUser')) {
        Write-BuildLog "Certificate drive not available, attempting to initialize..." -Level Warning
        Import-Module PKI -ErrorAction SilentlyContinue
        # Try to access the certificate store to force initialization
        Get-ChildItem "Cert:\CurrentUser\My" -ErrorAction SilentlyContinue | Out-Null
    }
} catch {
    Write-BuildLog ("Note: Certificate provider setup may need manual configuration: {0}" -f $_) -Level Warning
}

# Project root folder (now set to current script folder for local builds)
$ProjectRoot = $PSScriptRoot
$UserFolder = Join-Path $ProjectRoot '.user'
if (-not (Test-Path $UserFolder)) { New-Item -ItemType Directory -Path $UserFolder | Out-Null }

# Certificate file paths using configuration
$prefix = $script:Config.CertPrefix
$CertThumbFile, $CertCerFile = @('.thumbprint', '.cer') | 
    ForEach-Object { Join-Path $UserFolder "$prefix.certificate.sample$_" }

# Clean option: remove bin/obj and uninstall existing sparse package if present
if ($Clean) {
    Write-BuildLog "Cleaning build artifacts..." -Level Info
    'bin','obj' | ForEach-Object { 
        $target = Join-Path $ProjectRoot $_
        if (Test-Path $target) { Remove-Item $target -Recurse -Force }
    }
    Write-BuildLog "Attempting to remove existing sparse package (best effort)" -Level Info
    try { Get-AppxPackage -Name $script:Config.IdentityName | Remove-AppxPackage } catch {}
}

# Force certificate regeneration if requested
if ($ForceCert -and (Test-Path $UserFolder)) {
    Write-BuildLog "ForceCert specified: removing existing certificate artifacts..." -Level Warning
    Remove-Item $UserFolder -Recurse -Force
    New-Item -ItemType Directory -Path $UserFolder | Out-Null
}

# Ensure dev cert (development only; not for production use) - skip if NoSign specified
$needNewCert = -not $NoSign -and (-not (Test-Path $CertThumbFile) -or $ForceCert -or -not (Test-CertificateValidity -ThumbprintFile $CertThumbFile))

if ($needNewCert) {
    Write-BuildLog "Generating development certificate (prefix=$($script:Config.CertPrefix))..." -Level Info

    # Clear stale files in the certificate cache
    if (Test-Path $UserFolder) {
        Get-ChildItem -Path $UserFolder | ForEach-Object {
            if ($_.PSIsContainer) {
                Remove-Item $_.FullName -Recurse -Force
            } else {
                Remove-Item $_.FullName -Force
            }
        }
    }
    if (-not (Test-Path $UserFolder)) {
        New-Item -ItemType Directory -Path $UserFolder | Out-Null
    }

    $now = Get-Date
    $expiration = $now.AddMonths($script:Config.CertValidMonths)
    # Subject MUST match <Identity Publisher="..."> inside AppxManifest.xml
    $friendlyName = "PowerToys Dev Sparse Cert Create=$now"
    $keyFriendly = "PowerToys Dev Sparse Key Create=$now"

    $certStore = 'cert:\CurrentUser\My'
    $ekuOid = '2.5.29.37'
    $ekuValue = '1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.10.3.13'
    $eku = "$ekuOid={text}$ekuValue"

    $cert = New-SelfSignedCertificate -CertStoreLocation $certStore `
        -NotAfter $expiration `
        -Subject $script:Config.CertSubject `
        -FriendlyName $friendlyName `
        -KeyFriendlyName $keyFriendly `
        -KeyDescription $keyFriendly `
        -TextExtension $eku

    # Export certificate files
    Set-Content -Path $CertThumbFile -Value $cert.Thumbprint -Force
    Export-Certificate -Cert $cert -FilePath $CertCerFile -Force | Out-Null
}

# Determine output directory - using PowerToys standard structure
# Navigate to PowerToys root (two levels up from src/PackageIdentity)
$PowerToysRoot = Split-Path (Split-Path $ProjectRoot -Parent) -Parent
$outDir = Join-Path $PowerToysRoot "$Platform\$Configuration"

if (-not (Test-Path $outDir)) {
    Write-BuildLog "Creating output directory: $outDir" -Level Info
    New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}

# PackageIdentity folder (this script location) containing the sparse manifest and assets
$sparseDir = $PSScriptRoot
$manifestPath = Join-Path $sparseDir 'AppxManifest.xml'
if (-not (Test-Path $manifestPath)) { throw "Missing AppxManifest.xml in PackageIdentity folder: $manifestPath" }

$versionPropsPath = Join-Path $PowerToysRoot 'src\Version.props'
$targetManifestVersion = $null
$versionCandidate = $null
if (Test-Path $versionPropsPath) {
    try {
        [xml]$propsXml = Get-Content -Path $versionPropsPath -Raw
        $versionCandidate = $propsXml.Project.PropertyGroup.Version
    } catch {
        Write-BuildLog ("Unable to read version from {0}: {1}" -f $versionPropsPath, $_) -Level Warning
    }
} else {
    Write-BuildLog "Version.props not found at $versionPropsPath; manifest version will remain unchanged." -Level Warning
}

if ($versionCandidate) {
    $targetManifestVersion = $versionCandidate.Trim()
    if (($targetManifestVersion -split '\.').Count -lt 4) {
        $targetManifestVersion = "$targetManifestVersion.0"
    }
    Write-BuildLog "Using sparse package version from Version.props: $targetManifestVersion" -Level Info
} else {
    Write-BuildLog "No version value provided; manifest version will remain unchanged." -Level Info
}

# Find MakeAppx.exe from Windows SDK
try {
    $hostSdkArchitecture = if ([System.Environment]::Is64BitProcess) { 'x64' } else { 'x86' }
    $makeAppxPath = Find-WindowsSDKTool -ToolName "makeappx.exe" -Architecture $hostSdkArchitecture
} catch {
    Write-Error "MakeAppx.exe not found. Please ensure Windows SDK is installed."
    exit 1
}

# Pack sparse MSIX from PackageIdentity folder
$msixPath = Join-Path $outDir $script:Config.SparseMsixName

# Clean up existing MSIX file
if (Test-Path $msixPath) {
    Write-BuildLog "Removing existing MSIX file..." -Level Info
    try {
        Remove-Item $msixPath -Force -ErrorAction Stop
        Write-BuildLog "Successfully removed existing MSIX file" -Level Success
    } catch {
    Write-BuildLog ("Warning: Could not remove existing MSIX file: {0}" -f $_) -Level Warning
    }
}

# Create a clean staging directory to avoid file lock issues
$stagingDir = Join-Path $outDir "staging"
if (Test-Path $stagingDir) {
    Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $stagingDir -Force | Out-Null

try {
    Write-BuildLog "Creating clean staging directory for packaging..." -Level Info
    
    # Copy only essential files to staging directory to avoid file locks
    $essentialFiles = @(
        "AppxManifest.xml"
        "Images\*"
    )
    
    foreach ($filePattern in $essentialFiles) {
        $sourcePath = Join-Path $sparseDir $filePattern
        $relativePath = $filePattern
        
        if ($filePattern.Contains('\')) {
            $targetDir = Join-Path $stagingDir (Split-Path $relativePath -Parent)
            if (-not (Test-Path $targetDir)) {
                New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
            }
        }
        
        if ($filePattern.EndsWith('\*')) {
            # Copy directory contents
            $sourceDir = $sourcePath.TrimEnd('\*')
            $targetDir = Join-Path $stagingDir (Split-Path $relativePath.TrimEnd('\*') -Parent)
            if (Test-Path $sourceDir) {
                Copy-Item -Path "$sourceDir\*" -Destination $targetDir -Force -ErrorAction SilentlyContinue
            }
        } else {
            # Copy single file
            $targetPath = Join-Path $stagingDir $relativePath
            if (Test-Path $sourcePath) {
                Copy-Item -Path $sourcePath -Destination $targetPath -Force -ErrorAction SilentlyContinue
            }
        }
    }
    
    # Ensure publisher matches the dev certificate for local builds
    $manifestStagingPath = Join-Path $stagingDir 'AppxManifest.xml'
    $shouldUseDevPublisher = -not $isCIBuild
    if (Test-Path $manifestStagingPath) {
        try {
            [xml]$manifestXml = Get-Content -Path $manifestStagingPath -Raw
            $identityNode = $manifestXml.Package.Identity
            $manifestChanged = $false
            if ($identityNode) {
                $currentPublisherHint = $identityNode.Publisher
            }

            if ($identityNode) {
                if ($targetManifestVersion -and $identityNode.Version -ne $targetManifestVersion) {
                    Write-BuildLog "Updating manifest version to $targetManifestVersion" -Level Info
                    $identityNode.SetAttribute('Version', $targetManifestVersion)
                    $manifestChanged = $true
                }

                if ($shouldUseDevPublisher -and $identityNode.Publisher -ne $script:Config.CertSubject) {
                    Write-BuildLog "Updating manifest publisher for local build" -Level Warning
                    $identityNode.SetAttribute('Publisher', $script:Config.CertSubject)
                    $manifestChanged = $true
                }
                $currentPublisherHint = $identityNode.Publisher
            }

            if ($manifestChanged) {
                $manifestXml.Save($manifestStagingPath)
            }
        } catch {
            Write-BuildLog ("Unable to adjust manifest metadata: {0}" -f $_) -Level Warning
        }
    }

    Write-BuildLog "Staging directory prepared with essential files only" -Level Success
    
    # Pack MSIX using staging directory
    Write-BuildLog "Packing sparse MSIX ($($script:Config.SparseMsixName)) from staging -> $msixPath" -Level Info
    
    & $makeAppxPath pack /d $stagingDir /p $msixPath /nv /o
    
    if ($LASTEXITCODE -eq 0 -and (Test-Path $msixPath)) {
        Write-BuildLog "MSIX packaging completed successfully" -Level Success
    } else {
        Write-BuildLog "MakeAppx failed with exit code $LASTEXITCODE" -Level Error
        exit 1
    }
} finally {
    # Clean up staging directory
    if (Test-Path $stagingDir) {
        try {
            Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
            Write-BuildLog "Cleaned up staging directory" -Level Info
        } catch {
            Write-BuildLog ("Warning: Could not clean up staging directory: {0}" -f $_) -Level Warning
        }
    }
}

# Sign package (skip if NoSign specified for CI scenarios)
if ($NoSign) {
    Write-BuildLog "Skipping signing (NoSign specified for CI build)" -Level Warning
} else {
    # Use certificate thumbprint for signing (safer, no password)
    $certThumbprint = (Get-Content -Path $CertThumbFile -Raw).Trim()
    try {
        $signToolPath = Find-WindowsSDKTool -ToolName "signtool.exe"
    } catch {
        Write-Error "SignTool.exe not found. Please ensure Windows SDK is installed."
        exit 1
    }
    Write-BuildLog "Signing sparse MSIX using cert thumbprint $certThumbprint..." -Level Info
    & $signToolPath sign /fd SHA256 /sha1 $certThumbprint $msixPath
    if ($LASTEXITCODE -ne 0) {
        Write-Warning "SignTool failed (exit $LASTEXITCODE). Ensure the certificate is in CurrentUser\\My and try -ForceCert if needed."
        exit $LASTEXITCODE
    }
}

$publisherHintFile = Join-Path $UserFolder "$($script:Config.CertPrefix).publisher.txt"
try {
    Set-Content -Path $publisherHintFile -Value $currentPublisherHint -Force -NoNewline
} catch {
    Write-BuildLog ("Unable to write publisher hint: {0}" -f $_) -Level Warning
}

Write-BuildLog "`nPackage created: $msixPath" -Level Success

if ($NoSign) {
    Write-BuildLog "UNSIGNED package created for CI build. Sign before deployment." -Level Warning
} else {
    Write-BuildLog "Install the dev certificate (once): $CertCerFile" -Level Info
    Write-BuildLog "Identity Name: $($script:Config.IdentityName)" -Level Info
}

Write-BuildLog "Register sparse package:" -Level Info
Write-BuildLog "  Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$outDir`"" -Level Warning
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$outDir`" -ForceApplicationShutdown" -Level Warning
