# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

#################################
# Draft-TerminalReleases takes a directory full of Terminal build outputs:
# - zip files
# - preinstallation kits
# - MSIX bundles
# and produces tagged releases with hashes and assets on GitHub.
# It automatically cracks files to get their version numbers, arranges them into branding categories,
# and publishes drafts based on their contents.

#Requires -Version 7.0
#Requires -Modules PSGitHub

[CmdletBinding(SupportsShouldProcess)]
Param(
    [string[]]$Directory,
    [string]$Owner = "microsoft",
    [string]$RepositoryName = "terminal",
    [switch]$DumpOutput = $false
)

$ErrorActionPreference = 'Stop'

$PSDefaultParameterValues['*GitHub*:Owner'] = $Owner
$PSDefaultParameterValues['*GitHub*:RepositoryName'] = $RepositoryName

Enum AssetType {
	Unknown
	ApplicationBundle
	PreinstallKit
	GroupPolicy
	Zip
}

Enum Branding {
	Unknown
	Release
	Preview
	Canary
	Dev
}

$script:tar = "tar"

Class Asset {
	[string]$Name
	[Version]$Version
	[string]$ExpandedVersion

	[AssetType]$Type
	[Branding]$Branding

	[string]$Architecture

	[string]$Path

	[string]$VersionMoniker

	Asset([string]$Path) {
		$this.Path = $Path;
	}

	[void]Load() {
		$local:bundlePath = $this.Path

		$local:sentinelFile = New-TemporaryFile -Confirm:$false -WhatIf:$false
		$local:directory = New-Item -Type Directory "$($local:sentinelFile.FullName)_Package" -Confirm:$false -WhatIf:$false
		Remove-Item $local:sentinelFile -Force -EA:Ignore -Confirm:$false -WhatIf:$false

		$local:ext = [IO.Path]::GetExtension($this.Path)
		$local:filename = [IO.Path]::GetFileName($this.Path)
		If (".zip" -eq $local:ext -and $local:filename -like '*Preinstall*') {
			Write-Verbose "Cracking Preinstall Kit $($this.Path)"
			$local:licenseFile = & $script:tar -t -f $this.Path | Select-String "_License1.xml"
			If (-Not $local:licenseFile) {
				Throw ("$($this.Path) does not appear to be a preinstall kit")
			}
			$local:bundleName = ($local:licenseFile -Split "_")[0] + ".msixbundle"
			Write-Verbose "Found inner bundle $($local:bundleName)"
			& $script:tar -x -f $this.Path -C $local:directory $local:bundleName

			$local:bundlePath = Join-Path $local:directory $local:bundleName
			$this.Type = [AssetType]::PreinstallKit
			$this.Architecture = "all"
		} ElseIf (".zip" -eq $local:ext -and $local:filename -like 'GroupPolicy*') {
			$this.Type = [AssetType]::GroupPolicy
			$this.Architecture = "all"
		} ElseIf (".zip" -eq $local:ext) {
			$this.Type = [AssetType]::Zip
		} ElseIf (".msixbundle" -eq $local:ext) {
			$this.Type = [AssetType]::ApplicationBundle
			$this.Architecture = "all"
		}

		If ($this.Type -In ([AssetType]::ApplicationBundle, [AssetType]::PreinstallKit)) {
			Write-Verbose "Cracking bundle $($local:bundlePath)"
			$local:firstMsixName = & $script:tar -t -f $local:bundlePath |
				Select-String 'Cascadia.*\.msix' |
				Select-Object -First 1 -Expand Line
			& $script:tar -x -f $local:bundlePath -C $local:directory $local:firstMsixName

			Write-Verbose "Found inner msix $($local:firstMsixName)"
			$local:msixPath = Join-Path $local:directory $local:firstMsixName
			& $script:tar -x -v -f $local:msixPath -C $local:directory AppxManifest.xml

			Write-Verbose "Parsing AppxManifest.xml"
			$local:Manifest = [xml](Get-Content (Join-Path $local:directory AppxManifest.xml))
			$this.ParseManifest($local:Manifest)
		} Else {
			If ($this.Type -Ne [AssetType]::GroupPolicy) {
				& $script:tar -x -f $this.Path -C $local:directory --strip-components=1 '*/wt.exe'
				$this.ExpandedVersion = (Get-Item (Join-Path $local:directory wt.exe)).VersionInfo.ProductVersion
			}

			# Zip files just encode everything in their filename. Not great, but workable.
			$this.ParseFilename($local:filename)
		}

		$v = [version]$this.Version
		$this.VersionMoniker = "{0}.{1}" -f ($v.Major, $v.Minor)

		$this.Branding = Switch($this.Name) {
			"Microsoft.WindowsTerminal" { [Branding]::Release }
			"Microsoft.WindowsTerminalPreview" { [Branding]::Preview }
			"Microsoft.WindowsTerminalCanary" { [Branding]::Canary }
			"WindowsTerminalDev" { [Branding]::Dev }
			Default { [Branding]::Unknown }
		}
	}

	[void]ParseManifest([xml]$Manifest) {
		$this.Name = $Manifest.Package.Identity.Name
		$this.Version = $Manifest.Package.Identity.Version
	}

	[void]ParseFilename([string]$filename) {
		$parts = [IO.Path]::GetFileNameWithoutExtension($filename).Split("_")
		$this.Name = $parts[0]
		$this.Version = $parts[1]
		If ($parts.Length -Ge 3) {
			$this.Architecture = $parts[2]
		}
	}

	[string]IdealFilename() {
		$local:bundleName = "{0}_{1}_8wekyb3d8bbwe.msixbundle" -f ($this.Name, $this.Version)

		$local:filename = Switch($this.Type) {
			PreinstallKit {
				"{0}_Windows10_PreinstallKit.zip" -f $local:bundleName
			}
			ApplicationBundle {
				$local:bundleName
			}
			Zip {
				"{0}_{1}_{2}.zip" -f ($this.Name, $this.Version, $this.Architecture)
			}
			GroupPolicy {
				"{0}_{1}.zip" -f ($this.Name, $this.Version)
			}
			Default {
				Throw "Unknown type $($_.Type)"
			}
		}
		return $local:filename
	}

	static [Asset] CreateFromFile([string]$Path) {
		$a = [Asset]::new($Path)
		$a.Load()
		return $a
	}
}

class Release {
	[string]$Name
	[Branding]$Branding
	[Version]$TagVersion
	[Version]$DisplayVersion
	[string]$Branch

	[Asset[]]$Assets

	Release([Asset[]]$a) {
		$this.Assets = $a
		$this.Branding = $a | Where-Object Branding -Ne ([Branding]::Unknown) | Select -Unique -First 1 -Expand Branding
		$this.Name = Switch($this.Branding) {
			Release { "Windows Terminal" }
			Preview { "Windows Terminal Preview" }
			Default { throw "Unknown Branding for release publication $_" }
		}
		$local:minVer = $a.Version | Measure -Minimum | Select-Object -Expand Minimum
		$this.TagVersion = $local:minVer
		$this.DisplayVersion = $local:minVer
		$this.Branch = "release-{0}.{1}" -f ($local:minVer.Major, $local:minVer.Minor)
	}

	[string]TagName() {
		return "v{0}" -f $this.TagVersion
	}
}

enum TaggingType {
	TagAlreadyExists
	TagBranchLatest
	TagCommitSha1
}

class ReleaseConfig {
	[Release]$Release
	[TaggingType]$TaggingType
	[string]$TagTarget # may be null
}

Function Read-ReleaseConfigFromHost([Release]$Release) {
	$choices = @(
		[System.Management.Automation.Host.ChoiceDescription]::new("No New &Tag", "Tag already exists, do not create a new tag"),
		[System.Management.Automation.Host.ChoiceDescription]::new("Tag &Branch"),
		[System.Management.Automation.Host.ChoiceDescription]::new("Tag &Commit")
	)
	
	$existingTagSha1 = & git rev-parse --verify $Release.TagName() 2>$null
	If($existingTagSha1) {
		### If there is already a tag, trust the release engineer.
		Write-Verbose "Found existing tag $($Release.TagName())"
		return [ReleaseConfig]@{
			Release = $Release;
			TaggingType = [TaggingType]::TagAlreadyExists;
			TagTarget = $null;
		}
	}

	$choice = $Host.UI.PromptForChoice("How should I tag $("{0} v{1}" -f ($Release.Name,$Release.DisplayVersion))?", $null, $choices, 0)
	$target = $null
	Switch($choice) {
		0 { }
		1 { $target = $Release.Branch }
		2 { $target = Read-Host "What commit corresponds to $($Release.DisplayVersion)?" }
	}

	Return [ReleaseConfig]@{
		Release = $Release;
		TaggingType = [TaggingType]$choice;
		TagTarget = $target;
	}
}

Function New-ReleaseBody([Release]$Release) {
	$zipAssetVersion = $Release.Assets.ExpandedVersion | ? { $_.Length -Gt 0 } | Select -First 1
	$body = "---`n`n"
	If (-Not [String]::IsNullOrEmpty($zipAssetVersion)) {
		$body += "_Binary files inside the unpackaged distribution archive bear the version number ``$zipAssetVersion``._`n`n"
	}
	$body += "### Asset Hashes`n`n";
	ForEach($a in $Release.Assets) {
		$body += "- {0}`n   - SHA256 ``{1}```n" -f ($a.IdealFilename(), (Get-FileHash $a.Path -Algorithm SHA256 | Select-Object -Expand Hash))
	}
	Return $body
}

# Collect Assets from $Directory, figure out what those assets are
$Assets = Get-ChildItem $Directory -Recurse -Include *.msixbundle, *.zip | ForEach-Object {
	[Asset]::CreateFromFile($_.FullName)
}

$Releases = $Assets | Group VersionMoniker | ForEach-Object {
	[Release]::new($_.Group)
}

$ReleaseConfigs = $Releases | % { Read-ReleaseConfigFromHost $_ }

If($DumpOutput) {
	$ReleaseConfigs
	Return
}

$sentinelFile = New-TemporaryFile -Confirm:$false -WhatIf:$false
$directory = New-Item -Type Directory "$($sentinelFile.FullName)_PackageUploads" -Confirm:$false -WhatIf:$false
Remove-Item $sentinelFile -Force -EA:Ignore -Confirm:$false -WhatIf:$false

ForEach($c in $ReleaseConfigs) {
	$releaseName = "{0} v{1}" -f ($c.Release.Name, $c.Release.DisplayVersion)
	if($PSCmdlet.ShouldProcess("Release $releaseName", "Publish")) {
		Write-Verbose "Preparing release $releaseName"

		$releaseParams = @{
			TagName = $c.Release.TagName();
			Name = $releaseName;
		}

		If($c.TagTarget) {
			Switch($c.TaggingType) {
				[TagCommitSha1] { $releaseParams.CommitSHA = $c.TagTarget }
				[TagBranchLatest] { $releaseParams.Branch = $c.Release.Branch }
			}
			Write-Verbose " - Will create tag $($c.Release.TagName()) to point to $($c.TagTarget)"
		} Else {
			Write-Verbose " - Tag already exists"
		}

		$releaseParams.PreRelease = Switch($c.Release.Branding) {
			Release { $false }
			Preview { $true }
		}

		$releaseParams.Body = New-ReleaseBody $c.Release

		$GitHubRelease = New-GitHubRelease @releaseParams -Draft:$true

		ForEach($a in $c.Release.Assets) {
			$idealPath = (Join-Path $directory $a.IdealFilename())
			Copy-Item $a.Path $idealPath -Confirm:$false
			Write-Verbose "Uploading $idealPath to Release $($GitHubRelease.id)"
			New-GitHubReleaseAsset -ReleaseId $GitHubRelease.id -Path $idealPath
		}
	}
}
