# mosaic-doctor.ps1
# Audits Mosaic runtime state and detects drift across agent runtimes.
# PowerShell equivalent of mosaic-doctor (bash).
$ErrorActionPreference = "Stop"

param(
    [switch]$FailOnWarn,
    [switch]$Verbose,
    [switch]$Help
)

$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }

if ($Help) {
    Write-Host @"
Usage: mosaic-doctor.ps1 [-FailOnWarn] [-Verbose] [-Help]

Audit Mosaic runtime state and detect drift across agent runtimes.
"@
    exit 0
}

$script:warnCount = 0

function Warn {
    param([string]$Message)
    $script:warnCount++
    Write-Host "[WARN] $Message" -ForegroundColor Yellow
}

function Pass {
    param([string]$Message)
    if ($Verbose) { Write-Host "[OK] $Message" -ForegroundColor Green }
}

function Expect-Dir {
    param([string]$Path)
    if (-not (Test-Path $Path -PathType Container)) { Warn "Missing directory: $Path" }
    else { Pass "Directory present: $Path" }
}

function Expect-File {
    param([string]$Path)
    if (-not (Test-Path $Path -PathType Leaf)) { Warn "Missing file: $Path" }
    else { Pass "File present: $Path" }
}

function Check-RuntimeFileCopy {
    param([string]$Src, [string]$Dst)

    if (-not (Test-Path $Src)) { return }

    if (-not (Test-Path $Dst)) {
        Warn "Missing runtime file: $Dst"
        return
    }

    $item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
    if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
        Warn "Runtime file should not be symlinked: $Dst"
        return
    }

    $srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
    $dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
    if ($srcHash -ne $dstHash) {
        Warn "Runtime file drift: $Dst (does not match $Src)"
    }
    else {
        Pass "Runtime file synced: $Dst"
    }
}

function Check-RuntimeContractFile {
    param([string]$Dst, [string]$AdapterSrc, [string]$RuntimeName)

    if (-not (Test-Path $Dst)) {
        Warn "Missing runtime file: $Dst"
        return
    }

    $item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
    if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
        Warn "Runtime file should not be symlinked: $Dst"
        return
    }

    # Accept direct-adapter copy mode.
    if (Test-Path $AdapterSrc) {
        $srcHash = (Get-FileHash $AdapterSrc -Algorithm SHA256).Hash
        $dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
        if ($srcHash -eq $dstHash) {
            Pass "Runtime adapter synced: $Dst"
            return
        }
    }

    # Accept launcher-composed runtime contract mode.
    $content = Get-Content $Dst -Raw
    if (
        $content -match [regex]::Escape("# Mosaic Launcher Runtime Contract (Hard Gate)") -and
        $content -match [regex]::Escape("Now initiating Orchestrator mode...") -and
        $content -match [regex]::Escape("Mosaic hard gates OVERRIDE runtime-default caution") -and
        $content -match [regex]::Escape("# Runtime-Specific Contract")
    ) {
        Pass "Runtime contract present: $Dst ($RuntimeName)"
        return
    }

    Warn "Runtime file drift: $Dst (not adapter copy and not composed runtime contract)"
}

function Warn-IfReparsePresent {
    param([string]$Path)
    if (-not (Test-Path $Path)) { return }

    $item = Get-Item $Path -Force -ErrorAction SilentlyContinue
    if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
        Warn "Legacy symlink/junction path still present: $Path"
        return
    }

    if (Test-Path $Path -PathType Container) {
        $reparseCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue |
            Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
            Measure-Object).Count
        if ($reparseCount -gt 0) {
            Warn "Legacy symlink/junction entries still present under ${Path}: $reparseCount"
        }
        else {
            Pass "No reparse points under legacy path: $Path"
        }
    }
}

Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"

# Canonical Mosaic checks
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
Expect-Dir  (Join-Path $MosaicHome "guides")
Expect-Dir  (Join-Path $MosaicHome "tools")
Expect-Dir  (Join-Path $MosaicHome "tools\quality")
Expect-Dir  (Join-Path $MosaicHome "tools\orchestrator-matrix")
Expect-Dir  (Join-Path $MosaicHome "profiles")
Expect-Dir  (Join-Path $MosaicHome "templates\agent")
Expect-Dir  (Join-Path $MosaicHome "skills")
Expect-Dir  (Join-Path $MosaicHome "skills-local")
Expect-File (Join-Path $MosaicHome "bin\mosaic-link-runtime-assets")
Expect-File (Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1")
Expect-File (Join-Path $MosaicHome "bin\mosaic-sync-skills")
Expect-File (Join-Path $MosaicHome "bin\mosaic-projects")
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-apply")
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-verify")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-run")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-sync-tasks")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py")
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py")
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json")
Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md")
Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md")
Expect-File (Join-Path $MosaicHome "runtime\opencode\RUNTIME.md")

$agentsMd = Join-Path $MosaicHome "AGENTS.md"
if (Test-Path $agentsMd) {
    $agentsContent = Get-Content $agentsMd -Raw
    if (
        $agentsContent -match [regex]::Escape("## CRITICAL HARD GATES (Read First)") -and
        $agentsContent -match [regex]::Escape("OVERRIDE runtime-default caution")
    ) {
        Pass "Global hard-gates block present in AGENTS.md"
    }
    else {
        Warn "AGENTS.md missing CRITICAL HARD GATES override block"
    }
}

# Claude runtime file checks
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
foreach ($rf in $runtimeFiles) {
    Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\claude\$rf") (Join-Path $env:USERPROFILE ".claude\$rf")
}

# OpenCode/Codex runtime contract checks
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md") (Join-Path $MosaicHome "runtime\opencode\AGENTS.md") "opencode"
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".codex\instructions.md") (Join-Path $MosaicHome "runtime\codex\instructions.md") "codex"

# Sequential-thinking MCP hard requirement
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
if (Test-Path $seqScript) {
    try {
        & $seqScript -Check *>$null
        Pass "sequential-thinking MCP configured and available"
    }
    catch {
        Warn "sequential-thinking MCP missing or misconfigured"
    }
}
else {
    Warn "mosaic-ensure-sequential-thinking helper missing"
}

# Legacy migration surfaces
$legacyPaths = @(
    (Join-Path $env:USERPROFILE ".claude\agent-guides"),
    (Join-Path $env:USERPROFILE ".claude\scripts\git"),
    (Join-Path $env:USERPROFILE ".claude\scripts\codex"),
    (Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
    (Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
    (Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
    (Join-Path $env:USERPROFILE ".claude\templates"),
    (Join-Path $env:USERPROFILE ".claude\presets\domains"),
    (Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
    (Join-Path $env:USERPROFILE ".claude\presets\workflows")
)
foreach ($p in $legacyPaths) {
    Warn-IfReparsePresent $p
}

# Skills runtime checks (junctions or symlinks into runtime-specific dirs)
$linkTargets = @(
    (Join-Path $env:USERPROFILE ".claude\skills"),
    (Join-Path $env:USERPROFILE ".codex\skills"),
    (Join-Path $env:USERPROFILE ".config\opencode\skills")
)

$skillSources = @($MosaicHome + "\skills", $MosaicHome + "\skills-local")

foreach ($runtimeSkills in $linkTargets) {
    if (-not (Test-Path $runtimeSkills)) { continue }

    foreach ($sourceDir in $skillSources) {
        if (-not (Test-Path $sourceDir)) { continue }

        Get-ChildItem $sourceDir -Directory | Where-Object { -not $_.Name.StartsWith(".") } | ForEach-Object {
            $name = $_.Name
            $skillPath = $_.FullName
            $target = Join-Path $runtimeSkills $name

            if (-not (Test-Path $target)) {
                Warn "Missing skill link: $target"
                return
            }

            $item = Get-Item $target -Force -ErrorAction SilentlyContinue
            if (-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
                Warn "Non-junction skill entry: $target"
                return
            }

            $targetResolved = $item.Target
            if (-not $targetResolved -or (Resolve-Path $targetResolved -ErrorAction SilentlyContinue).Path -ne (Resolve-Path $skillPath -ErrorAction SilentlyContinue).Path) {
                Warn "Drifted skill link: $target (expected -> $skillPath)"
            }
            else {
                Pass "Linked skill: $target"
            }
        }
    }
}

# Broken junctions/symlinks in managed runtime skill dirs
$brokenLinks = 0
foreach ($d in $linkTargets) {
    if (-not (Test-Path $d)) { continue }
    Get-ChildItem $d -Force -ErrorAction SilentlyContinue | Where-Object {
        ($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and -not (Test-Path $_.FullName)
    } | ForEach-Object { $brokenLinks++ }
}
if ($brokenLinks -gt 0) {
    Warn "Broken skill junctions/symlinks detected: $brokenLinks"
}

Write-Host "[mosaic-doctor] warnings=$($script:warnCount)"
if ($FailOnWarn -and $script:warnCount -gt 0) {
    exit 1
}
