param(
  [string]$Version = (Get-Date -Format "yyyyMMdd-HHmm"),
  [switch]$Publish
)

$ErrorActionPreference = "Stop"

$root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$releaseRoot = Join-Path $root "public\source-release"
$buildRoot = Join-Path $root "tmp\public-source-build"
$stageRoot = Join-Path $buildRoot "stage"
$stageProject = Join-Path $stageRoot "plai-chain-source"
$candidateRoot = Join-Path $buildRoot "candidate"
$zipName = "plai-chain-source-$Version.zip"
$candidateZipPath = Join-Path $candidateRoot $zipName
$candidateManifestPath = Join-Path $candidateRoot "manifest.json"
$candidateHistoryPath = Join-Path $candidateRoot "release-history.json"
$zipPath = Join-Path $releaseRoot $zipName
$latestPath = Join-Path $releaseRoot "plai-chain-source-latest.zip"
$manifestPath = Join-Path $releaseRoot "manifest.json"
$historyPath = Join-Path $releaseRoot "release-history.json"

$includedDirectories = @(
  "config",
  "contracts",
  "deploy",
  "docs",
  "lib",
  "middleware",
  "models",
  "public",
  "routes",
  "scripts",
  "services",
  "views"
)

$includedRootFiles = @(
  ".env.example",
  ".gitignore",
  "ecosystem.qbft.config.js",
  "package-lock.json",
  "package.json",
  "README.md",
  "server.js"
)

$excludedPathPrefixes = @(
  "public\source-release",
  "public\downloads",
  "public\uploads",
  "docs\listing",
  "docs\patent-4-ecosystem-reward-finality"
)

$excludedExtensions = @(
  ".bak",
  ".tmp",
  ".log",
  ".pfx",
  ".pem",
  ".key",
  ".crt",
  ".sqlite",
  ".db",
  ".zip",
  ".7z",
  ".tar",
  ".tgz",
  ".gz",
  ".apk",
  ".aab",
  ".exe",
  ".dll",
  ".jar",
  ".bin"
)

$textExtensions = @(
  ".css",
  ".ejs",
  ".example",
  ".html",
  ".js",
  ".json",
  ".md",
  ".ps1",
  ".svg",
  ".txt",
  ".webmanifest",
  ".xml"
)

function Resolve-RelativePath([string]$fullPath, [string]$basePath = $root) {
  $normalizedBase = [string]$basePath
  if (-not $normalizedBase.EndsWith("\")) {
    $normalizedBase = "$normalizedBase\"
  }
  $baseUri = New-Object System.Uri($normalizedBase)
  $fileUri = New-Object System.Uri($fullPath)
  return [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($fileUri).ToString()).Replace("/", "\")
}

function Is-ExcludedPath([string]$fullPath) {
  $relative = Resolve-RelativePath $fullPath
  foreach ($prefix in $excludedPathPrefixes) {
    if ($relative -eq $prefix -or $relative.StartsWith("$prefix\", [System.StringComparison]::OrdinalIgnoreCase)) {
      return $true
    }
  }

  $name = [System.IO.Path]::GetFileName($fullPath)
  if ($name -match "(?i)\.bak($|[-_.])") {
    return $true
  }
  if ($name -like ".env*" -and $name -ne ".env.example") {
    return $true
  }
  if ($name -match "(?i)^(ledger\.json|npm-debug\.log|yarn-debug\.log|yarn-error\.log|pnpm-debug\.log)$") {
    return $true
  }

  $ext = [System.IO.Path]::GetExtension($fullPath).ToLowerInvariant()
  return $excludedExtensions -contains $ext
}

function Get-SourceFiles {
  $selected = New-Object System.Collections.Generic.List[System.IO.FileInfo]

  foreach ($relative in $includedRootFiles) {
    $absolute = Join-Path $root $relative
    if (-not (Test-Path -LiteralPath $absolute -PathType Leaf)) {
      throw "Required public source file is missing: $relative"
    }
    $selected.Add((Get-Item -LiteralPath $absolute))
  }

  foreach ($relative in $includedDirectories) {
    $absolute = Join-Path $root $relative
    if (-not (Test-Path -LiteralPath $absolute -PathType Container)) {
      throw "Required public source directory is missing: $relative"
    }
    foreach ($file in Get-ChildItem -LiteralPath $absolute -Recurse -File -Force) {
      if (-not (Is-ExcludedPath $file.FullName)) {
        $selected.Add($file)
      }
    }
  }

  return @($selected | Sort-Object FullName -Unique)
}

function Get-LocalSecretValues {
  $envPath = Join-Path $root ".env"
  if (-not (Test-Path -LiteralPath $envPath -PathType Leaf)) {
    return @()
  }

  $values = New-Object System.Collections.Generic.List[string]
  foreach ($line in Get-Content -LiteralPath $envPath -Encoding UTF8) {
    if ($line -notmatch "^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$") {
      continue
    }
    $name = [string]$Matches[1]
    if ($name -notmatch "(?i)(SECRET|PASSWORD|PRIVATE_KEY|MNEMONIC|SEED|API_KEY|CHAIN_KEY_|HMAC|MONGO.*URI|DATABASE.*URL)") {
      continue
    }
    $value = [string]$Matches[2]
    $value = $value.Trim().Trim('"').Trim("'")
    if ($value.Length -ge 8 -and $value -notmatch "(?i)^(change-me|replace-with|example|localhost|127\.0\.0\.1)") {
      $values.Add($value)
    }
  }
  return @($values | Sort-Object -Unique)
}

function Assert-PublicSourceSafe([System.IO.FileInfo[]]$files) {
  $violations = New-Object System.Collections.Generic.List[string]
  $localSecrets = @(Get-LocalSecretValues)
  $forbiddenContentPatterns = @(
    "-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----",
    "(?im)^[ \t]*(AISFOR_CHAIN_KEY_[A-Z0-9_]+|AISFOR_CHAIN_ADMIN_PASSWORD|AISFOR_CHAIN_ADMIN_SESSION_SECRET|AISFOR_CHAIN_WALLET_SSO_SECRET|PLAIS_EVM_VAULT_SECRET|PLAIS_EVM_OPERATOR_PRIVATE_KEY|PLAI_TRON_API_KEY)[ \t]*=[ \t]*(?![ \t]*(?:$|change-me|replace-with|<))[^\r\n#]{8,}[ \t]*$",
    '(?i)"(?:privateKey|validatorPrivateKey|mnemonic|seedPhrase)"\s*:\s*"(?:0x)?[0-9a-f]{64}"'
  )

  foreach ($file in $files) {
    $relative = Resolve-RelativePath $file.FullName
    if (Is-ExcludedPath $file.FullName) {
      $violations.Add("excluded path selected: $relative")
      continue
    }

    if ($relative -match "(?i)(^|\\)(runtime|data|logs|tmp|temp|node_modules|uploads|\.git)(\\|$)") {
      $violations.Add("private/runtime path selected: $relative")
      continue
    }

    $extension = [System.IO.Path]::GetExtension($file.Name).ToLowerInvariant()
    if ($textExtensions -notcontains $extension -and $file.Name -notin @("README.md", ".gitignore")) {
      continue
    }

    $content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
    foreach ($pattern in $forbiddenContentPatterns) {
      if ($content -match $pattern) {
        $violations.Add("forbidden credential material pattern: $relative")
        break
      }
    }
    foreach ($secret in $localSecrets) {
      if ($content.Contains($secret)) {
        $violations.Add("value from local .env found: $relative")
        break
      }
    }
  }

  if ($violations.Count -gt 0) {
    throw "Public source safety audit failed:`n$($violations -join "`n")"
  }
}

function Get-SourceTreeSha256([System.IO.FileInfo[]]$files) {
  $builder = New-Object System.Text.StringBuilder
  foreach ($file in $files | Sort-Object FullName) {
    $relative = Resolve-RelativePath $file.FullName $stageProject
    $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash.ToLowerInvariant()
    [void]$builder.Append($relative.Replace("\", "/"))
    [void]$builder.Append("`t")
    [void]$builder.Append($hash)
    [void]$builder.Append("`n")
  }
  $bytes = [System.Text.Encoding]::UTF8.GetBytes($builder.ToString())
  $sha = [System.Security.Cryptography.SHA256]::Create()
  try {
    return ([System.BitConverter]::ToString($sha.ComputeHash($bytes))).Replace("-", "").ToLowerInvariant()
  } finally {
    $sha.Dispose()
  }
}

New-Item -ItemType Directory -Force -Path $releaseRoot | Out-Null
if (Test-Path -LiteralPath $buildRoot) {
  $resolvedBuildRoot = (Resolve-Path -LiteralPath $buildRoot).Path
  $expectedPrefix = (Join-Path $root "tmp") + "\"
  if (-not $resolvedBuildRoot.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
    throw "Refusing to clean unexpected build path: $resolvedBuildRoot"
  }
  Remove-Item -LiteralPath $resolvedBuildRoot -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $stageProject | Out-Null
New-Item -ItemType Directory -Force -Path $candidateRoot | Out-Null

$sourceFiles = @(Get-SourceFiles)
Assert-PublicSourceSafe $sourceFiles

foreach ($file in $sourceFiles) {
  $relative = Resolve-RelativePath $file.FullName
  $destination = Join-Path $stageProject $relative
  $destinationDir = Split-Path $destination -Parent
  New-Item -ItemType Directory -Force -Path $destinationDir | Out-Null
  Copy-Item -LiteralPath $file.FullName -Destination $destination -Force
}

$readmePublic = Join-Path $stageProject "SOURCE_PUBLIC_NOTICE_KO.md"
Copy-Item -LiteralPath (Join-Path $root "docs\SOURCE_PUBLIC_NOTICE_KO.md") -Destination $readmePublic -Force

$stagedFiles = @(Get-ChildItem -LiteralPath $stageProject -Recurse -File -Force | Sort-Object FullName)

$fileIndex = @(
  foreach ($file in $stagedFiles) {
    [ordered]@{
      path = Resolve-RelativePath $file.FullName $stageProject
      sizeBytes = $file.Length
      modifiedAt = $file.LastWriteTimeUtc.ToString("o")
      sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash.ToLowerInvariant()
    }
  }
)

$topDirectories = @(
  $fileIndex |
    ForEach-Object {
      $parts = $_.path.Split("\")
      if ($parts.Length -gt 1) { $parts[0] } else { "(root)" }
    } |
    Sort-Object -Unique
)

Compress-Archive -Path (Join-Path $stageRoot "plai-chain-source") -DestinationPath $candidateZipPath -Force

$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $candidateZipPath).Hash.ToLowerInvariant()
$fileInfo = Get-Item -LiteralPath $candidateZipPath
$generatedAt = (Get-Date).ToUniversalTime().ToString("o")
$sourceTreeSha256 = Get-SourceTreeSha256 $stagedFiles
$baseGitCommit = (& git -C $root rev-parse HEAD 2>$null | Select-Object -First 1)
if (-not $baseGitCommit) { $baseGitCommit = "unavailable" }
$releaseNotes = @(
  "Latest PLAIS mainnet source snapshot generated with allowlist-based publication rules.",
  "Includes application source, canonical QBFT tooling, deployment scripts, public configuration examples, documentation, and web assets.",
  "Excludes runtime state, validator keys and envelopes, approvals, databases, logs, uploads, backups, legal filing originals, dependencies, and generated artifacts.",
  "Package, source-tree, and per-file SHA256 values are published for integrity verification."
)

$releaseEntry = [ordered]@{
  version = $Version
  package = $zipName
  generatedAt = $generatedAt
  sizeBytes = $fileInfo.Length
  sha256 = $hash
  sourceTreeSha256 = $sourceTreeSha256
  fileCount = $fileIndex.Count
  baseGitCommit = $baseGitCommit
  notes = $releaseNotes
}

$history = @()
if (Test-Path -LiteralPath $historyPath) {
  try {
    $historyRaw = (Get-Content -LiteralPath $historyPath -Raw -Encoding UTF8) -replace "^\uFEFF", ""
    $parsedHistory = $historyRaw | ConvertFrom-Json
    if ($parsedHistory) {
      $history = @($parsedHistory)
    }
  } catch {
    $history = @()
  }
}
$history = @($releaseEntry) + @($history | Where-Object { $_.version -ne $Version } | Select-Object -First 11)

$manifest = [ordered]@{
  ok = $true
  project = "PLAIS Chain"
  network = "PLAIS canonical Besu QBFT mainnet"
  package = $zipName
  latest = "plai-chain-source-latest.zip"
  version = $Version
  generatedAt = $generatedAt
  sizeBytes = $fileInfo.Length
  sha256 = $hash
  latestSha256 = $hash
  sourceTreeSha256 = $sourceTreeSha256
  baseGitCommit = $baseGitCommit
  workingTreeSnapshot = $true
  fileCount = $fileIndex.Count
  directoryCount = $topDirectories.Count
  topDirectories = $topDirectories
  releaseNotes = $releaseNotes
  releases = $history
  fileIndex = $fileIndex
  fileIndexTruncated = $false
  license = @{
    name = "PLAI Chain Public Source Review License"
    summary = "Public review, audit, and transparency use is allowed. Commercial reuse, exchange listing impersonation, private-key extraction attempts, and redistribution with removed notices are not allowed without written permission."
  }
  publicationPolicy = @{
    mode = "allowlist"
    includedDirectories = $includedDirectories
    includedRootFiles = $includedRootFiles
    excludedPathPrefixes = $excludedPathPrefixes
    excludedExtensions = $excludedExtensions
    localEnvironmentValueScan = $true
    credentialPatternScan = $true
  }
  excluded = @{
    directories = @("runtime", "data", "logs", "tmp", "temp", "node_modules", "uploads") + $excludedPathPrefixes
    files = @(".env", "ledger.json", "local credentials and production configuration")
    extensions = $excludedExtensions
    patterns = @(".env.* except .env.example", "*.bak and *.bak-*", "validator keys/envelopes", "approval and runtime state", "private legal/KYB and patent filing originals")
  }
}

$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $candidateManifestPath -Encoding UTF8
$history | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $candidateHistoryPath -Encoding UTF8

Write-Host "PUBLIC_SOURCE_AUDIT_PASSED"
Write-Host "Candidate $candidateZipPath"
Write-Host "Files $($fileIndex.Count)"
Write-Host "SHA256 $hash"
Write-Host "SourceTreeSHA256 $sourceTreeSha256"

if ($Publish.IsPresent) {
  if (Test-Path -LiteralPath $zipPath) {
    throw "A public source release with this version already exists: $zipPath"
  }
  Copy-Item -LiteralPath $candidateZipPath -Destination $zipPath
  Copy-Item -LiteralPath $candidateZipPath -Destination $latestPath -Force
  Copy-Item -LiteralPath $candidateManifestPath -Destination $manifestPath -Force
  Copy-Item -LiteralPath $candidateHistoryPath -Destination $historyPath -Force
  Write-Host "PUBLIC_SOURCE_PUBLISHED"
  Write-Host "Published $zipPath"
}
