param(
  [string]$Version = "",
  [ValidateSet("", "patch", "minor", "major")]
  [string]$Bump = "",
  [string]$OutputRoot = "releases",
  [switch]$SkipTests,
  [switch]$SkipBuild,
  [switch]$PlanOnly,
  [switch]$Force
)

$ErrorActionPreference = "Stop"

$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
$PackageJsonPath = Join-Path $RepoRoot "package.json"
$PackageLockPath = Join-Path $RepoRoot "package-lock.json"
$TauriConfigPath = Join-Path $RepoRoot "src-tauri\tauri.conf.json"
$CargoTomlPath = Join-Path $RepoRoot "src-tauri\Cargo.toml"
$BundleRoot = Join-Path $RepoRoot "src-tauri\target\release\bundle"

function Write-Utf8NoBomFile {
  param(
    [string]$Path,
    [string]$Value
  )

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

function Read-JsonFile {
  param([string]$Path)
  Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json
}

function Write-JsonFile {
  param(
    [string]$Path,
    [object]$Value
  )

  $json = $Value | ConvertTo-Json -Depth 100
  Write-Utf8NoBomFile -Path $Path -Value ($json + [Environment]::NewLine)
}

function Replace-RegexGroup {
  param(
    [string]$Content,
    [string]$Pattern,
    [string]$GroupName,
    [string]$Value,
    [string]$Label
  )

  $match = [regex]::Match($Content, $Pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
  if (-not $match.Success) {
    throw "Cannot find $Label."
  }

  $group = $match.Groups[$GroupName]
  if (-not $group.Success) {
    throw "Cannot find $Label value."
  }

  $Content.Remove($group.Index, $group.Length).Insert($group.Index, $Value)
}

function Get-FirstJsonVersion {
  param(
    [string]$Path,
    [string]$Label
  )

  $content = Get-Content -Raw -LiteralPath $Path
  $match = [regex]::Match($content, '"version"\s*:\s*"(?<value>[^"]+)"')
  if (-not $match.Success) {
    throw "Cannot find version in $Label."
  }
  $match.Groups["value"].Value
}

function Set-FirstJsonVersion {
  param(
    [string]$Path,
    [string]$TargetVersion,
    [string]$Label
  )

  $content = Get-Content -Raw -LiteralPath $Path
  $updated = Replace-RegexGroup `
    -Content $content `
    -Pattern '"version"\s*:\s*"(?<value>[^"]+)"' `
    -GroupName "value" `
    -Value $TargetVersion `
    -Label "version in $Label"
  Write-Utf8NoBomFile -Path $Path -Value $updated
}

function Get-PackageLockVersions {
  $content = Get-Content -Raw -LiteralPath $PackageLockPath
  $topMatch = [regex]::Match(
    $content,
    '^\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"version"\s*:\s*"(?<value>[^"]+)"',
    [System.Text.RegularExpressions.RegexOptions]::Singleline
  )
  if (-not $topMatch.Success) {
    throw "Cannot find top-level version in package-lock.json."
  }

  $rootMatch = [regex]::Match(
    $content,
    '"packages"\s*:\s*\{\s*""\s*:\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"version"\s*:\s*"(?<value>[^"]+)"',
    [System.Text.RegularExpressions.RegexOptions]::Singleline
  )
  if (-not $rootMatch.Success) {
    throw "Cannot find root package version in package-lock.json."
  }

  [ordered]@{
    packageLock = $topMatch.Groups["value"].Value
    packageLockRoot = $rootMatch.Groups["value"].Value
  }
}

function Set-PackageLockVersions {
  param([string]$TargetVersion)

  $content = Get-Content -Raw -LiteralPath $PackageLockPath
  $updated = Replace-RegexGroup `
    -Content $content `
    -Pattern '^\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"version"\s*:\s*"(?<value>[^"]+)"' `
    -GroupName "value" `
    -Value $TargetVersion `
    -Label "top-level version in package-lock.json"
  $updated = Replace-RegexGroup `
    -Content $updated `
    -Pattern '"packages"\s*:\s*\{\s*""\s*:\s*\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"version"\s*:\s*"(?<value>[^"]+)"' `
    -GroupName "value" `
    -Value $TargetVersion `
    -Label "root package version in package-lock.json"
  Write-Utf8NoBomFile -Path $PackageLockPath -Value $updated
}

function Assert-Semver {
  param([string]$Value)
  if ($Value -notmatch "^\d+\.\d+\.\d+$") {
    throw "Version '$Value' is not supported. Use numeric SemVer like 0.1.0."
  }
}

function ConvertTo-VersionParts {
  param([string]$Value)
  Assert-Semver -Value $Value
  $parts = $Value.Split(".")
  [ordered]@{
    major = [int]$parts[0]
    minor = [int]$parts[1]
    patch = [int]$parts[2]
  }
}

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

  $leftParts = ConvertTo-VersionParts -Value $Left
  $rightParts = ConvertTo-VersionParts -Value $Right
  foreach ($part in @("major", "minor", "patch")) {
    if ($leftParts[$part] -gt $rightParts[$part]) { return 1 }
    if ($leftParts[$part] -lt $rightParts[$part]) { return -1 }
  }
  return 0
}

function Get-NextVersion {
  param(
    [string]$Current,
    [string]$Kind
  )

  $parts = ConvertTo-VersionParts -Value $Current
  switch ($Kind) {
    "major" { return "$($parts.major + 1).0.0" }
    "minor" { return "$($parts.major).$($parts.minor + 1).0" }
    "patch" { return "$($parts.major).$($parts.minor).$($parts.patch + 1)" }
    default { throw "Unknown bump kind '$Kind'." }
  }
}

function Get-CargoPackageVersion {
  $content = Get-Content -Raw -LiteralPath $CargoTomlPath
  $packageMatch = [regex]::Match($content, "(?ms)^\[package\]\s*(.*?)(?=^\[|\z)")
  if (-not $packageMatch.Success) {
    throw "Cannot find [package] block in $CargoTomlPath."
  }

  $versionMatch = [regex]::Match($packageMatch.Value, '(?m)^version\s*=\s*"([^"]+)"\s*$')
  if (-not $versionMatch.Success) {
    throw "Cannot find package version in $CargoTomlPath."
  }

  $versionMatch.Groups[1].Value
}

function Get-VersionState {
  $packageLock = Get-PackageLockVersions

  [ordered]@{
    packageJson = [string](Get-FirstJsonVersion -Path $PackageJsonPath -Label "package.json")
    packageLock = [string]$packageLock.packageLock
    packageLockRoot = [string]$packageLock.packageLockRoot
    tauriConfig = [string](Get-FirstJsonVersion -Path $TauriConfigPath -Label "tauri.conf.json")
    cargoToml = [string](Get-CargoPackageVersion)
  }
}

function Get-CurrentVersion {
  $state = Get-VersionState
  $versions = @(@(
    $state.packageJson,
    $state.packageLock,
    $state.packageLockRoot,
    $state.tauriConfig,
    $state.cargoToml
  ) | Select-Object -Unique)

  if ($versions.Count -ne 1) {
    $details = $state.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }
    throw "Version mismatch: $($details -join ', ')."
  }

  Assert-Semver -Value $versions[0]
  $versions[0]
}

function Resolve-TargetVersion {
  param([string]$Current)

  if (-not [string]::IsNullOrWhiteSpace($Version)) {
    Assert-Semver -Value $Version
    return $Version
  }

  if (-not [string]::IsNullOrWhiteSpace($Bump)) {
    return Get-NextVersion -Current $Current -Kind $Bump
  }

  if ($PlanOnly -or -not [Environment]::UserInteractive) {
    return Get-NextVersion -Current $Current -Kind "patch"
  }

  $patch = Get-NextVersion -Current $Current -Kind "patch"
  $minor = Get-NextVersion -Current $Current -Kind "minor"
  $major = Get-NextVersion -Current $Current -Kind "major"

  Write-Host ""
  Write-Host "Current version: $Current"
  Write-Host "Choose release version:"
  Write-Host "  1) patch  $patch"
  Write-Host "  2) minor  $minor"
  Write-Host "  3) major  $major"
  Write-Host "  4) custom"
  Write-Host "  5) keep current $Current"
  $choice = Read-Host "Selection [1]"

  if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" }

  switch ($choice.Trim()) {
    "1" { return $patch }
    "2" { return $minor }
    "3" { return $major }
    "4" {
      $custom = Read-Host "Enter version"
      Assert-Semver -Value $custom
      return $custom
    }
    "5" { return $Current }
    default { throw "Unknown selection '$choice'." }
  }
}

function Set-CargoPackageVersion {
  param([string]$TargetVersion)

  $content = Get-Content -Raw -LiteralPath $CargoTomlPath
  $packageMatch = [regex]::Match($content, "(?ms)^\[package\]\s*(.*?)(?=^\[|\z)")
  if (-not $packageMatch.Success) {
    throw "Cannot find [package] block in $CargoTomlPath."
  }

  $packageBlock = $packageMatch.Value
  $versionMatch = [regex]::Match($packageBlock, '(?m)^version\s*=\s*"(?<value>[^"]+)"\s*$')
  if (-not $versionMatch.Success) {
    throw "Cannot update package version in $CargoTomlPath."
  }

  if ($versionMatch.Groups["value"].Value -eq $TargetVersion) {
    return
  }

  $valueGroup = $versionMatch.Groups["value"]
  $updatedBlock = $packageBlock.Remove($valueGroup.Index, $valueGroup.Length).Insert($valueGroup.Index, $TargetVersion)
  $updatedContent = $content.Remove($packageMatch.Index, $packageMatch.Length).Insert($packageMatch.Index, $updatedBlock)
  Write-Utf8NoBomFile -Path $CargoTomlPath -Value $updatedContent
}

function Set-ManifestVersions {
  param([string]$TargetVersion)

  Set-FirstJsonVersion -Path $PackageJsonPath -TargetVersion $TargetVersion -Label "package.json"
  Set-PackageLockVersions -TargetVersion $TargetVersion
  Set-FirstJsonVersion -Path $TauriConfigPath -TargetVersion $TargetVersion -Label "tauri.conf.json"
  Set-CargoPackageVersion -TargetVersion $TargetVersion
}

function Get-FullPath {
  param([string]$Path)
  [System.IO.Path]::GetFullPath($Path)
}

function Test-IsSubPath {
  param(
    [string]$Parent,
    [string]$Child
  )

  $parentFull = (Get-FullPath -Path $Parent).TrimEnd("\", "/") + [System.IO.Path]::DirectorySeparatorChar
  $childFull = (Get-FullPath -Path $Child).TrimEnd("\", "/") + [System.IO.Path]::DirectorySeparatorChar
  $childFull.StartsWith($parentFull, [System.StringComparison]::OrdinalIgnoreCase)
}

function Get-RelativePath {
  param(
    [string]$BasePath,
    [string]$Path
  )

  $baseUri = [Uri]((Get-FullPath -Path $BasePath).TrimEnd("\", "/") + [System.IO.Path]::DirectorySeparatorChar)
  $pathUri = [Uri](Get-FullPath -Path $Path)
  [Uri]::UnescapeDataString($baseUri.MakeRelativeUri($pathUri).ToString()).Replace("/", "\")
}

function New-ReleaseDirectory {
  param([string]$TargetVersion)

  if ([System.IO.Path]::IsPathRooted($OutputRoot)) {
    $root = Get-FullPath -Path $OutputRoot
  } else {
    $root = Get-FullPath -Path (Join-Path $RepoRoot $OutputRoot)
  }

  $releaseDir = Join-Path $root "proxywarden-v$TargetVersion"

  if (Test-Path -LiteralPath $releaseDir) {
    if (-not (Test-IsSubPath -Parent $root -Child $releaseDir)) {
      throw "Refusing to remove release directory outside OutputRoot: $releaseDir"
    }
    Write-Host "Replacing existing release directory: $releaseDir"
    Remove-Item -LiteralPath $releaseDir -Recurse -Force
  }

  New-Item -ItemType Directory -Path (Join-Path $releaseDir "artifacts") -Force | Out-Null
  $releaseDir
}

function Invoke-NativeCommand {
  param(
    [string]$Name,
    [string]$FilePath,
    [string[]]$Arguments,
    [string]$WorkingDirectory = $RepoRoot
  )

  Write-Host ""
  Write-Host "==> $Name"
  Push-Location $WorkingDirectory
  try {
    & $FilePath @Arguments
    if ($LASTEXITCODE -ne 0) {
      throw "$Name failed with exit code $LASTEXITCODE."
    }
  } finally {
    Pop-Location
  }
}

function Clear-ReleaseBundleOutput {
  if (-not (Test-Path -LiteralPath $BundleRoot)) {
    return
  }

  $targetRoot = Get-FullPath -Path (Join-Path $RepoRoot "src-tauri\target")
  $bundleFull = Get-FullPath -Path $BundleRoot
  if (
    $bundleFull.Equals($targetRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
    -not (Test-IsSubPath -Parent $targetRoot -Child $bundleFull)
  ) {
    throw "Refusing to remove bundle directory outside src-tauri target: $bundleFull"
  }

  Write-Host ""
  Write-Host "Cleaning stale Tauri bundle output: $bundleFull"
  Remove-Item -LiteralPath $bundleFull -Recurse -Force
}

function Invoke-ReleaseBuild {
  if ($SkipBuild) {
    Write-Host ""
    Write-Host "Skipping build because -SkipBuild was provided."
    return
  }

  Invoke-NativeCommand -Name "Frontend build" -FilePath "npm" -Arguments @("run", "build")

  if (-not $SkipTests) {
    Invoke-NativeCommand -Name "Rust tests" -FilePath "cargo" -Arguments @("test") -WorkingDirectory (Join-Path $RepoRoot "src-tauri")
  } else {
    Write-Host ""
    Write-Host "Skipping Rust tests because -SkipTests was provided."
  }

  Clear-ReleaseBundleOutput
  Invoke-NativeCommand -Name "Tauri release build" -FilePath "npm" -Arguments @("run", "tauri", "--", "build")
}

function Get-ArtifactVersionPattern {
  param([string]$TargetVersion)

  "(^|[^0-9A-Za-z])$([regex]::Escape($TargetVersion))([^0-9A-Za-z]|$)"
}

function Copy-ReleaseArtifacts {
  param(
    [string]$ReleaseDir,
    [string]$TargetVersion
  )

  if (-not (Test-Path -LiteralPath $BundleRoot)) {
    throw "Tauri bundle output was not found: $BundleRoot"
  }

  $artifactDir = Join-Path $ReleaseDir "artifacts"
  $allFiles = @(Get-ChildItem -LiteralPath $BundleRoot -Recurse -File |
    Where-Object { $_.Extension -in @(".exe", ".msi", ".zip", ".sig") } |
    Sort-Object FullName)

  if ($allFiles.Count -eq 0) {
    throw "No release artifacts were found under $BundleRoot."
  }

  $versionPattern = Get-ArtifactVersionPattern -TargetVersion $TargetVersion
  $files = @($allFiles | Where-Object { $_.Name -match $versionPattern })
  $ignoredFiles = @($allFiles | Where-Object { $_.Name -notmatch $versionPattern })

  if ($files.Count -eq 0) {
    $found = ($allFiles | ForEach-Object { Get-RelativePath -BasePath $BundleRoot -Path $_.FullName }) -join ", "
    throw "No release artifacts for version $TargetVersion were found under $BundleRoot. Found artifacts: $found"
  }

  if ($ignoredFiles.Count -gt 0) {
    Write-Host ""
    Write-Host "Ignoring bundle artifacts that do not match version ${TargetVersion}:"
    foreach ($ignored in $ignoredFiles) {
      Write-Host ("  - " + (Get-RelativePath -BasePath $BundleRoot -Path $ignored.FullName))
    }
  }

  $copied = @()
  foreach ($file in $files) {
    $relative = Get-RelativePath -BasePath $BundleRoot -Path $file.FullName
    $destination = Join-Path $artifactDir $relative
    New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
    Copy-Item -LiteralPath $file.FullName -Destination $destination -Force
    $copied += Get-Item -LiteralPath $destination
  }

  $copied
}

function Write-Checksums {
  param(
    [string]$ReleaseDir,
    [object[]]$Files
  )

  if ($Files.Count -eq 0) { return $null }

  $artifactDir = Join-Path $ReleaseDir "artifacts"
  $lines = foreach ($file in $Files) {
    $hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256
    $relative = Get-RelativePath -BasePath $artifactDir -Path $file.FullName
    "$($hash.Hash.ToLowerInvariant())  $relative"
  }

  $checksumPath = Join-Path $ReleaseDir "SHA256SUMS.txt"
  Write-Utf8NoBomFile -Path $checksumPath -Value (($lines -join [Environment]::NewLine) + [Environment]::NewLine)
  $checksumPath
}

function Get-GitValue {
  param([string[]]$Arguments)

  try {
    $value = & git @Arguments 2>$null
    if ($LASTEXITCODE -eq 0) {
      return ($value -join [Environment]::NewLine).Trim()
    }
  } catch {}
  return ""
}

function Write-ReleaseMetadata {
  param(
    [string]$ReleaseDir,
    [string]$TargetVersion,
    [object[]]$Artifacts
  )

  $artifactDir = Join-Path $ReleaseDir "artifacts"
  $artifactItems = foreach ($artifact in $Artifacts) {
    [ordered]@{
      path = Get-RelativePath -BasePath $ReleaseDir -Path $artifact.FullName
      sizeBytes = $artifact.Length
      sha256 = (Get-FileHash -LiteralPath $artifact.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
    }
  }

  $manifest = [ordered]@{
    product = "ProxyWarden"
    version = $TargetVersion
    builtAt = (Get-Date).ToString("o")
    source = "local"
    gitCommit = Get-GitValue -Arguments @("rev-parse", "HEAD")
    gitStatus = Get-GitValue -Arguments @("status", "--short")
    artifacts = @($artifactItems)
  }

  Write-JsonFile -Path (Join-Path $ReleaseDir "release-manifest.json") -Value $manifest

  $artifactList = if ($Artifacts.Count -gt 0) {
    ($Artifacts | ForEach-Object {
      "- " + (Get-RelativePath -BasePath $artifactDir -Path $_.FullName)
    }) -join [Environment]::NewLine
  } else {
    "- Build was skipped; no artifacts were copied."
  }

  $notes = @"
# ProxyWarden v$TargetVersion

## Artifacts

$artifactList

## Checksums

See `SHA256SUMS.txt`.

## Release boundary

This release contains the ProxyWarden Control App only. ProxiFyre and Local sing-box remain explicit user-managed components.

"@

  Write-Utf8NoBomFile -Path (Join-Path $ReleaseDir "release-notes.md") -Value $notes
}

function New-PlanResult {
  param(
    [string]$Current,
    [string]$Target
  )

  $outputRootFull = if ([System.IO.Path]::IsPathRooted($OutputRoot)) {
    Get-FullPath -Path $OutputRoot
  } else {
    Get-FullPath -Path (Join-Path $RepoRoot $OutputRoot)
  }

  [ordered]@{
    success = $true
    action = "prepare-release.plan"
    changed = $false
    message = "Release plan is ready."
    details = [ordered]@{
      currentVersion = $Current
      targetVersion = $Target
      releaseDirectory = (Join-Path $outputRootFull "proxywarden-v$Target")
      skipTests = [bool]$SkipTests
      skipBuild = [bool]$SkipBuild
      manifests = @(
        $PackageJsonPath,
        $PackageLockPath,
        $TauriConfigPath,
        $CargoTomlPath
      )
      commands = @(
        "npm run build",
        "cd src-tauri; cargo test",
        "npm run tauri -- build"
      )
    }
  } | ConvertTo-Json -Depth 8
}

try {
  Push-Location $RepoRoot

  $currentVersion = Get-CurrentVersion
  $targetVersion = Resolve-TargetVersion -Current $currentVersion
  Assert-Semver -Value $targetVersion

  if ((Compare-Semver -Left $targetVersion -Right $currentVersion) -lt 0) {
    throw "Target version $targetVersion is lower than current version $currentVersion."
  }

  if ($PlanOnly) {
    New-PlanResult -Current $currentVersion -Target $targetVersion
    exit 0
  }

  Write-Host ""
  Write-Host "Preparing ProxyWarden release $targetVersion..."
  Write-Host "Repository: $RepoRoot"

  Set-ManifestVersions -TargetVersion $targetVersion
  $afterUpdateVersion = Get-CurrentVersion
  if ($afterUpdateVersion -ne $targetVersion) {
    throw "Version update failed. Current version is $afterUpdateVersion."
  }

  Invoke-ReleaseBuild

  $releaseDir = New-ReleaseDirectory -TargetVersion $targetVersion
  $artifacts = @(Copy-ReleaseArtifacts -ReleaseDir $releaseDir -TargetVersion $targetVersion)
  Write-Checksums -ReleaseDir $releaseDir -Files $artifacts | Out-Null
  Write-ReleaseMetadata -ReleaseDir $releaseDir -TargetVersion $targetVersion -Artifacts $artifacts

  Write-Host ""
  Write-Host "Release folder is ready:"
  Write-Host $releaseDir
  Write-Host ""
  Write-Host "Upload the files from the release folder to GitHub release v$targetVersion."
} finally {
  Pop-Location
}
