#requires -Version 7.0

[CmdletBinding()]
param(
    [ValidateSet("Pin", "Baseline", "Candidate")]
    [string]$Mode = "Pin",
    [string]$CandidateVersion = "2.0.0",
    [switch]$KeepTemporaryCopy
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$stableVersion = "1.4.1"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$projectPath = Join-Path $repoRoot "backend\Nexus.Api.csproj"
$testProjectPath = Join-Path $repoRoot "backend-tests\Nexus.Api.Tests.csproj"
$projectHashBefore = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
$temporaryRoot = $null
$dotnetExe = $null

function Get-McpPackageVersion {
    param([Parameter(Mandatory)][string]$Path)

    [xml]$project = Get-Content -LiteralPath $Path -Raw
    $reference = @(
        @($project.Project.ItemGroup.PackageReference) |
            Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
    )
    if ($reference.Count -ne 1) {
        throw "Expected exactly one ModelContextProtocol.AspNetCore reference in $Path."
    }
    return $reference[0].GetAttribute("Version")
}

function Assert-StaticGate {
    $version = Get-McpPackageVersion -Path $projectPath
    if ($version -ne $stableVersion) {
        throw "MCP production pin must remain $stableVersion; found $version."
    }

    $testFiles = @(
        "backend-tests\McpServerConfigurationTests.cs",
        "backend-tests\McpToolsTests.cs",
        "backend-tests\AgentProposalServiceTests.cs"
    )
    foreach ($relativePath in $testFiles) {
        $path = Join-Path $repoRoot $relativePath
        if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
            throw "Required MCP compatibility coverage is missing: $relativePath"
        }
    }

    $toolSource = Get-Content -LiteralPath (Join-Path $repoRoot "backend\Services\NexusMcpTools.cs") -Raw
    foreach ($marker in @(
        "nexus_propose_agent",
        "nexus_get_agent_proposal",
        "UseStructuredContent = true",
        "OutputSchemaType = typeof(AgentProposalToolResult)",
        "Idempotent = true",
        "OpenWorld = false"
    )) {
        if (-not $toolSource.Contains($marker, [StringComparison]::Ordinal)) {
            throw "Required MCP contract marker is missing: $marker"
        }
    }
}

function Resolve-DotNet10 {
    $candidates = @()
    $pathDotnet = Get-Command dotnet -ErrorAction SilentlyContinue
    if ($pathDotnet) {
        $candidates += $pathDotnet.Source
    }
    if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
        $candidates += (Join-Path $env:USERPROFILE ".dotnet\dotnet.exe")
    }

    foreach ($candidate in $candidates | Select-Object -Unique) {
        if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
            continue
        }
        $sdks = & $candidate --list-sdks
        if ($LASTEXITCODE -eq 0 -and ($sdks | Where-Object { $_ -match "^10\." })) {
            return (Resolve-Path -LiteralPath $candidate).Path
        }
    }

    throw ".NET SDK 10 is required for Baseline and Candidate modes. Add it to PATH or install it under USERPROFILE\\.dotnet."
}

function Invoke-DotNetTest {
    param(
        [Parameter(Mandatory)][string]$Project,
        [Parameter(Mandatory)][string]$WorkingDirectory
    )

    Push-Location $WorkingDirectory
    try {
        & $script:dotnetExe test $Project `
            --configuration Release `
            --verbosity minimal
        if ($LASTEXITCODE -ne 0) {
            throw "dotnet test failed for $Project."
        }
    } finally {
        Pop-Location
    }
}

function Copy-SourceTree {
    param(
        [Parameter(Mandatory)][string]$Source,
        [Parameter(Mandatory)][string]$Destination
    )

    New-Item -ItemType Directory -Path $Destination -Force | Out-Null
    $sourceRoot = [System.IO.Path]::GetFullPath($Source)
    Get-ChildItem -LiteralPath $sourceRoot -Recurse -File |
        Where-Object {
            $relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
            $segments = $relative -split "[\\/]"
            -not ($segments -contains "bin" -or $segments -contains "obj")
        } |
        ForEach-Object {
            $relative = [System.IO.Path]::GetRelativePath($sourceRoot, $_.FullName)
            $target = Join-Path $Destination $relative
            $targetDirectory = Split-Path -Parent $target
            New-Item -ItemType Directory -Path $targetDirectory -Force | Out-Null
            Copy-Item -LiteralPath $_.FullName -Destination $target -Force
        }
}

function Set-CandidatePackageVersion {
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][string]$Version
    )

    [xml]$project = Get-Content -LiteralPath $Path -Raw
    $reference = @(
        @($project.Project.ItemGroup.PackageReference) |
            Where-Object { $_.Include -eq "ModelContextProtocol.AspNetCore" }
    )
    if ($reference.Count -ne 1) {
        throw "Candidate copy does not contain exactly one MCP package reference."
    }
    $reference[0].SetAttribute("Version", $Version)
    $project.Save($Path)
}

try {
    Assert-StaticGate
    Write-Host "PASS: MCP production dependency is pinned to $stableVersion and required source/test markers are present."
    Write-Host "Static Pin mode did not execute tests or a live MCP/OpenClaw negotiation."

    if ($Mode -eq "Pin") {
        return
    }

    $dotnetExe = Resolve-DotNet10
    # MSBuild launches helper applications (for example the OpenAPI document
    # generator) through `dotnet`, not through the absolute executable used by
    # this script. Keep those child processes on the same .NET 10 runtime.
    $dotnetRoot = Split-Path -Parent $dotnetExe
    $env:DOTNET_ROOT = $dotnetRoot
    $pathSeparator = [System.IO.Path]::PathSeparator
    if (-not (($env:PATH -split [Regex]::Escape([string]$pathSeparator)) -contains $dotnetRoot)) {
        $env:PATH = "$dotnetRoot$pathSeparator$env:PATH"
    }
    Invoke-DotNetTest -Project $testProjectPath -WorkingDirectory $repoRoot
    Write-Host "PASS: MCP $stableVersion baseline command completed successfully."
    Write-Warning "Environment-gated Docker, Toxiproxy, or live OpenClaw tests may still be skipped; inspect the test summary before treating this as full compatibility evidence."

    if ($Mode -eq "Baseline") {
        return
    }

    if ($CandidateVersion -notmatch "^2\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$") {
        throw "CandidateVersion must be an explicit MCP 2.x package version."
    }

    $temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) "nexus-mcp2-gate-$([Guid]::NewGuid().ToString('N'))"
    New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null
    Copy-SourceTree -Source (Join-Path $repoRoot "backend") -Destination (Join-Path $temporaryRoot "backend")
    Copy-SourceTree -Source (Join-Path $repoRoot "backend-tests") -Destination (Join-Path $temporaryRoot "backend-tests")

    $candidateProject = Join-Path $temporaryRoot "backend\Nexus.Api.csproj"
    $candidateTests = Join-Path $temporaryRoot "backend-tests\Nexus.Api.Tests.csproj"
    Set-CandidatePackageVersion -Path $candidateProject -Version $CandidateVersion

    & $dotnetExe restore $candidateTests --force-evaluate
    if ($LASTEXITCODE -ne 0) {
        throw "MCP $CandidateVersion restore failed in the isolated copy."
    }

    $assetsPath = Join-Path $temporaryRoot "backend\obj\project.assets.json"
    if (-not (Test-Path -LiteralPath $assetsPath -PathType Leaf)) {
        throw "Candidate restore did not create project.assets.json."
    }
    $resolvedMarker = "`"ModelContextProtocol.AspNetCore/$CandidateVersion`""
    $assets = Get-Content -LiteralPath $assetsPath -Raw
    if (-not $assets.Contains($resolvedMarker, [StringComparison]::OrdinalIgnoreCase)) {
        throw "Restore did not resolve the exact requested candidate $CandidateVersion."
    }

    Invoke-DotNetTest -Project $candidateTests -WorkingDirectory $temporaryRoot

    if ($CandidateVersion.Contains("-", [StringComparison]::Ordinal)) {
        Write-Warning "COMPATIBILITY PROBE PASS: $CandidateVersion compiled and passed all backend tests."
        Write-Warning "PROMOTION GATE CLOSED: prerelease packages are not approved for production. The repository remains on $stableVersion."
    } else {
        Write-Host "SDK COMPATIBILITY PROBE PASS: stable candidate $CandidateVersion compiled and passed the backend suite."
        Write-Warning "LIVE OPENCLAW GATE CLOSED: this probe does not test OpenClaw tools/list, client identity, Streamable HTTP, or protocol down-level negotiation."
        Write-Host "The repository was not modified; live isolated OpenClaw compatibility evidence and a separate reviewed dependency change are still required."
    }
} finally {
    $projectHashAfter = (Get-FileHash -LiteralPath $projectPath -Algorithm SHA256).Hash
    if ($projectHashAfter -ne $projectHashBefore) {
        throw "Safety invariant violated: $projectPath changed during the compatibility gate."
    }

    if ($temporaryRoot -and (Test-Path -LiteralPath $temporaryRoot)) {
        if ($KeepTemporaryCopy) {
            Write-Host "Temporary candidate copy retained at $temporaryRoot"
        } else {
            $resolvedTemporaryRoot = [System.IO.Path]::GetFullPath($temporaryRoot)
            $systemTemporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
            if (
                -not $resolvedTemporaryRoot.StartsWith($systemTemporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or
                -not ([System.IO.Path]::GetFileName($resolvedTemporaryRoot)).StartsWith(
                    "nexus-mcp2-gate-",
                    [StringComparison]::Ordinal)
            ) {
                throw "Refusing to remove an unverified temporary path: $resolvedTemporaryRoot"
            }
            Remove-Item -LiteralPath $resolvedTemporaryRoot -Recurse -Force
        }
    }
}
