param(
    [Parameter(Mandatory=$true)][string]$CandidateRoot,
    [Parameter(Mandatory=$true)][string]$OutputRoot,
    [Parameter(Mandatory=$true)][string]$Evidence
)

$ErrorActionPreference = 'Stop'
$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
$candidate = [IO.Path]::GetFullPath($CandidateRoot)
$output = [IO.Path]::GetFullPath($OutputRoot)
$allowedOutput = [IO.Path]::GetFullPath((Join-Path $repo 'Artifacts\V0.12.0')) + '\'
if (-not $output.StartsWith($allowedOutput, [StringComparison]::OrdinalIgnoreCase)) {
    throw 'Promotion staging output must remain under Artifacts/V0.12.0.'
}
if (Test-Path -LiteralPath $output) {
    throw 'Preserve previous evidence: choose a new staging output root.'
}
if ([string]::IsNullOrWhiteSpace($Evidence)) {
    throw 'Explicit user evidence is required.'
}

$manifestPath = Join-Path $candidate 'candidate-manifest.json'
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
if ($manifest.format -ne 'DeepPluckCandidateRun' -or -not $manifest.complete -or $manifest.entries.Count -ne 160) {
    throw 'Expected the complete original 160-candidate run.'
}

$byId = @{}
foreach ($entry in $manifest.entries) {
    if ($byId.ContainsKey([string]$entry.id)) { throw 'Duplicate candidate identity.' }
    $byId[[string]$entry.id] = $entry
}

$events = @()
foreach ($file in Get-ChildItem -LiteralPath (Join-Path $candidate 'reviews') -File -Filter '*.json') {
    $review = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json
    if (-not $byId.ContainsKey([string]$review.id)) { throw "Unknown review candidate: $($review.id)" }
    if ($review.rating -notin @('Keep', 'Maybe', 'Reject')) { throw "Invalid review rating: $($review.id)" }
    $events += [pscustomobject]@{
        File = $file.Name
        ModifiedUtc = $file.LastWriteTimeUtc
        Review = $review
    }
}

$latest = @{}
foreach ($event in ($events | Sort-Object ModifiedUtc, File)) {
    $latest[[string]$event.Review.id] = $event.Review
}

$categoryOrder = @('deep','warm','bright','emotional','atmospheric','arp','poly','experimental')
$categoryFolders = @{
    deep='Deep Plucks'; warm='Warm & Soft'; bright='Bright & Present'; emotional='Emotional'
    atmospheric='Atmospheric'; arp='Arp & Sequence'; poly='Chord & Poly'; experimental='Experimental'
}
$expectedCounts = @{ deep=14; warm=13; bright=12; emotional=15; atmospheric=17; arp=10; poly=19; experimental=16 }
$selected = @($manifest.entries | Where-Object {
    $latest.ContainsKey([string]$_.id) -and $latest[[string]$_.id].rating -eq 'Keep'
})
if ($selected.Count -ne 116) { throw "Expected 116 latest Keep decisions, found $($selected.Count)." }

foreach ($category in $categoryOrder) {
    $actual = @($selected | Where-Object category -eq $category).Count
    if ($actual -ne $expectedCounts[$category]) {
        throw "Unexpected Keep count for ${category}: $actual instead of $($expectedCounts[$category])."
    }
}

[void](New-Item -ItemType Directory -Path $output)
$catalogEntries = @()
foreach ($category in $categoryOrder) {
    foreach ($entry in @($selected | Where-Object category -eq $category | Sort-Object index)) {
        $review = $latest[[string]$entry.id]
        if ([string]$review.sha256 -cne [string]$entry.sha256) { throw "Review hash mismatch: $($entry.name)" }
        $source = [IO.Path]::GetFullPath((Join-Path $candidate ([string]$entry.path -replace '/', '\')))
        if (-not $source.StartsWith($candidate + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'Candidate path escaped.' }
        if (-not (Test-Path -LiteralPath $source)) { throw "Missing candidate: $($entry.name)" }
        if ((Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash -ine [string]$entry.sha256) {
            throw "Candidate changed: $($entry.name)"
        }

        $relative = $categoryFolders[$category] + '/' + [string]$entry.name + '.deeppluckpreset'
        $target = Join-Path $output ($relative -replace '/', '\')
        [void](New-Item -ItemType Directory -Path ([IO.Path]::GetDirectoryName($target)) -Force)
        Copy-Item -LiteralPath $source -Destination $target
        if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash -ine [string]$entry.sha256) {
            throw "Staged copy changed: $($entry.name)"
        }

        $stableSlug = ([string]$entry.name).ToLowerInvariant().Replace('_','-')
        $catalogEntries += [ordered]@{
            id = 'factory12:' + $stableSlug
            category = $category
            name = [string]$entry.name
            contentVersion = '0.12.0'
            path = $relative
            sha256 = [string]$entry.sha256
            approval = [ordered]@{
                by = 'user'
                decision = 'approved'
                evidence = $Evidence
                sha256 = [string]$entry.sha256
            }
        }
    }
}

$legacyEvidence = 'User decision 2026-09-04: remove all four legacy presets from the active Factory browser; preserve factory:0..3 only for compatibility.'
$catalog = [ordered]@{
    format = 'DeepPluckFactoryLibrary'
    schemaVersion = 1
    status = 'approved'
    expectedActiveCount = 116
    selectionProvenance = [ordered]@{
        policy = 'all-latest-Keep'
        candidateManifestSha256 = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash.ToLowerInvariant()
        reviewEventCount = $events.Count
        finalDecisionCount = $latest.Count
        evidence = $Evidence
        naming = 'Candidate identifiers retained as stable V0.12.0 display names by implementation decision.'
    }
    legacyReviews = @(
        [ordered]@{ id='factory:0'; decision='replaced'; evidence=$legacyEvidence }
        [ordered]@{ id='factory:1'; decision='replaced'; evidence=$legacyEvidence }
        [ordered]@{ id='factory:2'; decision='replaced'; evidence=$legacyEvidence }
        [ordered]@{ id='factory:3'; decision='replaced'; evidence=$legacyEvidence }
    )
    entries = $catalogEntries
}

$catalogPath = Join-Path $output 'catalog.json'
[IO.File]::WriteAllText($catalogPath, ($catalog | ConvertTo-Json -Depth 12), [Text.UTF8Encoding]::new($false))
[IO.File]::WriteAllText((Join-Path $output 'promotion-receipt.json'), ([ordered]@{
    format='DeepPluckKeepPromotionReceipt'; schemaVersion=1; selectedCount=$catalogEntries.Count
    catalogSha256=(Get-FileHash -LiteralPath $catalogPath -Algorithm SHA256).Hash.ToLowerInvariant()
    candidateManifestSha256=(Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash.ToLowerInvariant()
    generatedUtc=[DateTime]::UtcNow.ToString('o'); evidence=$Evidence
} | ConvertTo-Json -Depth 6), [Text.UTF8Encoding]::new($false))

Write-Host "Prepared $($catalogEntries.Count) exact Keep-rated files in $output"
