#!/usr/bin/env pwsh
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
#Requires -Version 7.0

<#
.SYNOPSIS
    Validates collection manifests for Copilot CLI plugin generation.

.DESCRIPTION
    Reads all .collection.yml files from collections/ and validates structure,
    required fields, artifact path existence, and kind-suffix consistency.

.EXAMPLE
    ./Validate-Collections.ps1
#>

[CmdletBinding()]
param(
    [Parameter()]
    [string]$OutputPath = (Join-Path $PSScriptRoot '../../logs/collection-validation-results.json')
)

$ErrorActionPreference = 'Stop'

Import-Module (Join-Path $PSScriptRoot 'Modules/CollectionHelpers.psm1') -Force
Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force

#region Validation Helpers

function Test-KindSuffix {
    <#
    .SYNOPSIS
        Validates that an item path matches its declared kind suffix.

    .DESCRIPTION
        Checks kind-suffix consistency: agent files end with .agent.md,
        prompt files with .prompt.md, instruction files with .instructions.md,
        and skill items are directories containing a SKILL.md file.

    .PARAMETER Kind
        The declared artifact kind (agent, prompt, instruction, skill).

    .PARAMETER ItemPath
        The relative path from the collection manifest.

    .PARAMETER RepoRoot
        Absolute path to the repository root for skill directory checks.

    .OUTPUTS
        [string] Error message if validation fails, empty string if valid.
    #>
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Kind,

        [Parameter(Mandatory = $true)]
        [string]$ItemPath,

        [Parameter(Mandatory = $true)]
        [string]$RepoRoot
    )

    switch ($Kind) {
        'agent' {
            if ($ItemPath -notmatch '\.agent\.md$') {
                return "kind 'agent' expects *.agent.md but got '$ItemPath'"
            }
        }
        'prompt' {
            if ($ItemPath -notmatch '\.prompt\.md$') {
                return "kind 'prompt' expects *.prompt.md but got '$ItemPath'"
            }
        }
        'instruction' {
            if ($ItemPath -notmatch '\.instructions\.md$') {
                return "kind 'instruction' expects *.instructions.md but got '$ItemPath'"
            }
        }
        'skill' {
            $skillDir = Join-Path -Path $RepoRoot -ChildPath $ItemPath
            $skillFile = Join-Path -Path $skillDir -ChildPath 'SKILL.md'
            if (-not (Test-Path -Path $skillFile)) {
                return "kind 'skill' expects SKILL.md inside '$ItemPath'"
            }
        }
    }

    return ''
}

function Get-CollectionItemKey {
    <#
    .SYNOPSIS
        Builds a stable uniqueness key for collection items.

    .DESCRIPTION
        Uses kind and path to identify the same artifact across collections.

    .PARAMETER Kind
        Artifact kind.

    .PARAMETER ItemPath
        Artifact path.

    .OUTPUTS
        [string] Composite key.
    #>
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Kind,

        [Parameter(Mandatory = $true)]
        [string]$ItemPath
    )

    return "$Kind|$ItemPath"
}

#endregion Validation Helpers

#region Orchestration

function Invoke-CollectionValidation {
    <#
    .SYNOPSIS
        Validates all collection manifests for correctness.

    .DESCRIPTION
        Scans the collections/ directory for .collection.yml files and validates
        each manifest for required fields (id, name, description, items), id
        format, artifact path existence, kind-suffix consistency, and duplicate
        ids across collections.

    .PARAMETER RepoRoot
        Absolute path to the repository root directory.

    .OUTPUTS
        Hashtable with Success bool, ErrorCount int, CollectionCount int, and Results array.
    #>
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$RepoRoot
    )

    $validationResults = [System.Collections.Generic.List[hashtable]]::new()

    function Add-ValidationResult {
        param(
            [Parameter(Mandatory = $true)]
            [string]$Collection,

            [Parameter(Mandatory = $true)]
            [string]$ErrorType,

            [Parameter(Mandatory = $true)]
            [string]$Message,

            [Parameter()]
            [ValidateSet('Error', 'Warning')]
            [string]$Severity = 'Error'
        )

        $validationResults.Add(@{
            Collection = $Collection
            Severity   = $Severity
            ErrorType  = $ErrorType
            Message    = $Message
        })
    }

    $collectionsDir = Join-Path -Path $RepoRoot -ChildPath 'collections'
    $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File

    if ($collectionFiles.Count -eq 0) {
        Write-Host ' WARN No collection manifests found in collections/' -ForegroundColor Yellow
        Add-ValidationResult -Collection 'collections' -ErrorType 'NoCollectionManifests' -Message 'No collection manifests found in collections/' -Severity 'Warning'
        return @{ Success = $true; ErrorCount = 0; CollectionCount = 0; Results = @($validationResults) }
    }

    Write-Host 'Validating collections...'

    $errorCount = 0
    $seenIds = @{}
    $validatedCount = 0
    $allowedMaturities = @('stable', 'preview', 'experimental', 'deprecated', 'removed')
    $canonicalCollectionId = 'hve-core-all'
    $itemOccurrences = @{}

    $knownCollectionIds = @{}
    foreach ($cf in $collectionFiles) {
        $cfId = $cf.Name -replace '\.collection\.yml$', ''
        $knownCollectionIds[$cfId] = $true
    }

    # Sub-domain folders that group artifacts shared across multiple themed collections
    # but are intentionally not collections themselves.
    $sharedSubdomainFolders = @{
        'shared'       = $true
        'rai-planning' = $true
    }

    foreach ($file in $collectionFiles) {
        $baseName = $file.Name -replace '\.collection\.yml$', ''
        $collectionLabel = $baseName
        $companionPath = Join-Path -Path $collectionsDir -ChildPath "$baseName.collection.md"
        if (-not (Test-Path -Path $companionPath)) {
            Write-Host " WARN $($file.Name): missing companion '$baseName.collection.md'" -ForegroundColor Yellow
            Add-ValidationResult -Collection $collectionLabel -ErrorType 'MissingCompanionCollectionMd' -Message "missing companion '$baseName.collection.md'" -Severity 'Warning'
        }

        if (Test-Path -Path $companionPath) {
            $mdContent = Get-Content -Path $companionPath -Raw
            $hasBegin = $mdContent.Contains($CollectionMdBeginMarker)
            $hasEnd = $mdContent.Contains($CollectionMdEndMarker)

            if ($hasBegin -xor $hasEnd) {
                Write-Host "  WARN $($file.Name): $baseName.collection.md has mismatched auto-generation markers" -ForegroundColor Yellow
                Add-ValidationResult -Collection $collectionLabel -ErrorType 'MismatchedAutoGenerationMarkers' -Message "$baseName.collection.md has mismatched auto-generation markers" -Severity 'Warning'
            }

            if ($hasBegin -and $hasEnd) {
                $beginIdx = $mdContent.IndexOf($CollectionMdBeginMarker)
                $endIdx = $mdContent.IndexOf($CollectionMdEndMarker)
                if ($endIdx -le $beginIdx) {
                    Write-Host "  WARN $($file.Name): $baseName.collection.md has markers in wrong order" -ForegroundColor Yellow
                    Add-ValidationResult -Collection $collectionLabel -ErrorType 'CollectionMarkersWrongOrder' -Message "$baseName.collection.md has markers in wrong order" -Severity 'Warning'
                }
            }
        }

        $manifest = Get-CollectionManifest -CollectionPath $file.FullName
        $fileErrors = @()
        $seenItemKeys = @{}

        # Required fields
        $requiredFields = @('id', 'name', 'description', 'items')
        foreach ($field in $requiredFields) {
            if (-not $manifest.ContainsKey($field) -or $null -eq $manifest[$field]) {
                $fileErrors += @{ ErrorType = 'MissingRequiredField'; Message = "missing required field '$field'" }
            }
        }

        # Skip further checks if required fields are absent
        if ($fileErrors.Count -gt 0) {
            foreach ($err in $fileErrors) {
                Write-Host "    x $($file.Name): $($err.Message)" -ForegroundColor Red
                Add-ValidationResult -Collection $collectionLabel -ErrorType $err.ErrorType -Message $err.Message
            }
            $errorCount += $fileErrors.Count
            continue
        }

        $id = $manifest.id
        $collectionLabel = $id

        # Id format
        if ($id -notmatch '^[a-z0-9-]+$') {
            $fileErrors += @{ ErrorType = 'InvalidIdFormat'; Message = "id '$id' must match ^[a-z0-9-]+$" }
        }

        # Duplicate id check
        if ($seenIds.ContainsKey($id)) {
            $fileErrors += @{ ErrorType = 'DuplicateCollectionId'; Message = "duplicate id '$id' (also in $($seenIds[$id]))" }
        }
        else {
            $seenIds[$id] = $file.Name
        }

        # Validate collection-level maturity if present
        if ($manifest.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$manifest.maturity)) {
            $collMaturity = [string]$manifest.maturity
            if ($allowedMaturities -notcontains $collMaturity) {
                $fileErrors += @{ ErrorType = 'InvalidCollectionMaturity'; Message = "invalid collection maturity '$collMaturity' (allowed: $($allowedMaturities -join ', '))" }
            }
        }

        # Validate each item
        $itemCount = $manifest.items.Count
        foreach ($item in $manifest.items) {
            $itemPath = $item.path
            $kind = $item.kind
            $absolutePath = Join-Path -Path $RepoRoot -ChildPath $itemPath
            $itemMaturity = $null
            if ($item.ContainsKey('maturity')) {
                $itemMaturity = [string]$item.maturity
            }
            $effectiveMaturity = Resolve-CollectionItemMaturity -Maturity $itemMaturity

            # Repo-specific path exclusion
            if (Test-HveCoreRepoRelativePath -Path $itemPath) {
                $fileErrors += @{ ErrorType = 'RepoSpecificPath'; Message = "repo-specific path not allowed in collections: $itemPath (root-level artifacts under .github/{type}/ are excluded from distribution)" }
            }

            # Path existence
            if (-not (Test-Path -Path $absolutePath)) {
                $fileErrors += @{ ErrorType = 'PathNotFound'; Message = "path not found: $itemPath" }
            }

            # Kind-suffix consistency
            if ($kind) {
                $suffixError = Test-KindSuffix -Kind $kind -ItemPath $itemPath -RepoRoot $RepoRoot
                if ($suffixError) {
                    $fileErrors += @{ ErrorType = 'MissingSuffix'; Message = $suffixError }
                }
            }
            else {
                $fileErrors += @{ ErrorType = 'MissingItemKind'; Message = "item missing 'kind': $itemPath" }
            }

            if (-not [string]::IsNullOrWhiteSpace($itemMaturity) -and ($allowedMaturities -notcontains $itemMaturity)) {
                $fileErrors += @{ ErrorType = 'InvalidMaturity'; Message = "invalid maturity '$itemMaturity' for item '$itemPath' (allowed: $($allowedMaturities -join ', '))" }
            }

            # Check 2: intra-collection duplicate detection
            if (-not [string]::IsNullOrWhiteSpace($itemPath) -and -not [string]::IsNullOrWhiteSpace($kind)) {
                $dupKey = Get-CollectionItemKey -Kind $kind -ItemPath $itemPath
                if ($seenItemKeys.ContainsKey($dupKey)) {
                    $fileErrors += @{ ErrorType = 'IntraCollectionDuplicate'; Message = "duplicate item '$dupKey' appears more than once in collection '$id'" }
                } else {
                    $seenItemKeys[$dupKey] = $true
                }
            }

            # Check 3: collection-id to folder name consistency
            if ($id -ne 'hve-core-all') {
                $pathSegments = $itemPath -split '[/\\]'
                # Expected pattern: .github/{type}/{collection-id}/{file-or-deeper}
                if ($pathSegments.Count -ge 4 -and $pathSegments[0] -eq '.github') {
                    $folderName = $pathSegments[2]
                    if (-not $sharedSubdomainFolders.ContainsKey($folderName) -and -not $knownCollectionIds.ContainsKey($folderName)) {
                        Write-Host " WARN collection '$id': item folder '$folderName' does not match any known collection ID: $itemPath" -ForegroundColor Yellow
                        Add-ValidationResult -Collection $collectionLabel -ErrorType 'UnknownCollectionFolderReference' -Message "item folder '$folderName' does not match any known collection ID: $itemPath" -Severity 'Warning'
                    }
                }
            }

            if (-not [string]::IsNullOrWhiteSpace($itemPath) -and -not [string]::IsNullOrWhiteSpace($kind)) {
                $itemKey = Get-CollectionItemKey -Kind $kind -ItemPath $itemPath
                if (-not $itemOccurrences.ContainsKey($itemKey)) {
                    $itemOccurrences[$itemKey] = @()
                }

                $itemOccurrences[$itemKey] += @{
                    CollectionId = $id
                    CollectionFile = $file.Name
                    Kind = $kind
                    Path = $itemPath
                    Maturity = $effectiveMaturity
                }
            }

            # Informational log for instruction items
            if ($kind -eq 'instruction') {
                Write-Verbose "  instruction: $itemPath"
            }
        }

        if ($fileErrors.Count -gt 0) {
            Write-Host "  FAIL $id ($itemCount items) - $($fileErrors.Count) error(s)" -ForegroundColor Red
            foreach ($err in $fileErrors) {
                Write-Host "      $($err.Message)" -ForegroundColor Red
                Add-ValidationResult -Collection $collectionLabel -ErrorType $err.ErrorType -Message $err.Message
            }
            $errorCount += $fileErrors.Count
        }
        else {
            Write-Host "  OK $id ($itemCount items)"
        }

        $validatedCount++
    }

    $canonicalManifestFound = ($collectionFiles | Where-Object {
        ($_.Name -replace '\.collection\.yml$', '') -eq $canonicalCollectionId
    }).Count -gt 0
    if (-not $canonicalManifestFound) {
        Write-Host " WARN '$canonicalCollectionId.collection.yml' not found; skipping orphan and cross-collection coverage checks" -ForegroundColor Yellow
        Add-ValidationResult -Collection $canonicalCollectionId -ErrorType 'CanonicalCollectionMissing' -Message "'$canonicalCollectionId.collection.yml' not found; skipping orphan and cross-collection coverage checks" -Severity 'Warning'
    }

    # Duplicate artifact key detection across all collections
    $artifactKeyMap = @{}
    foreach ($itemKey in $itemOccurrences.Keys) {
        $occurrences = $itemOccurrences[$itemKey]
        $first = $occurrences[0]
        $artifactKey = Get-CollectionArtifactKey -Kind $first.Kind -Path $first.Path
        $compositeKey = "$($first.Kind)|$artifactKey"

        if (-not $artifactKeyMap.ContainsKey($compositeKey)) {
            $artifactKeyMap[$compositeKey] = @()
        }
        if ($artifactKeyMap[$compositeKey] -notcontains $first.Path) {
            $artifactKeyMap[$compositeKey] += $first.Path
        }
    }

    foreach ($compositeKey in $artifactKeyMap.Keys) {
        $paths = $artifactKeyMap[$compositeKey]
        if ($paths.Count -gt 1) {
            $kindLabel = ($compositeKey -split '\|')[0]
            $nameLabel = ($compositeKey -split '\|')[1]
            $pathList = ($paths | Sort-Object) -join ', '
            Write-Host "  FAIL duplicate $kindLabel artifact key '$nameLabel' found at distinct paths: $pathList" -ForegroundColor Red
            Add-ValidationResult -Collection 'all-collections' -ErrorType 'DuplicateArtifactKey' -Message "duplicate $kindLabel artifact key '$nameLabel' found at distinct paths: $pathList"
            $errorCount++
        }
    }

    foreach ($itemKey in $itemOccurrences.Keys) {
        $occurrences = $itemOccurrences[$itemKey]
        $canonicalMatches = @($occurrences | Where-Object { $_.CollectionId -eq $canonicalCollectionId })
        $themedMatches    = @($occurrences | Where-Object { $_.CollectionId -ne $canonicalCollectionId })

        # Check 4: item in one or more themed collections but absent from hve-core-all
        # Skip when all themed occurrences are marked maturity:'removed' (intentional tombstone
        # excluded from hve-core-all by Update-HveCoreAllCollection).
        $activeThemedMatches = @($themedMatches | Where-Object { $_.Maturity -ne 'removed' })
        if ($canonicalManifestFound -and $activeThemedMatches.Count -gt 0 -and $canonicalMatches.Count -eq 0) {
            $themedCollections = ($activeThemedMatches | ForEach-Object { $_.CollectionId } | Sort-Object -Unique) -join ', '
            Write-Host "  FAIL item '$itemKey' exists in themed collection(s) [$themedCollections] but is absent from '$canonicalCollectionId'" -ForegroundColor Red
            Add-ValidationResult -Collection $canonicalCollectionId -ErrorType 'ThemedItemMissingFromCanonical' -Message "item '$itemKey' exists in themed collection(s) [$themedCollections] but is absent from '$canonicalCollectionId'"
            $errorCount++
            continue
        }

        # Maturity conflict: only when item appears in canonical AND at least one themed
        if ($canonicalMatches.Count -gt 0 -and $themedMatches.Count -gt 0) {
            $canonical = $canonicalMatches[0]
            foreach ($occurrence in $themedMatches) {
                if ($occurrence.Maturity -ne $canonical.Maturity) {
                    Write-Host "  FAIL maturity conflict for '$itemKey': canonical '$canonicalCollectionId'='$($canonical.Maturity)', '$($occurrence.CollectionId)'='$($occurrence.Maturity)'" -ForegroundColor Red
                    Add-ValidationResult -Collection $occurrence.CollectionId -ErrorType 'MaturityConflict' -Message "maturity conflict for '$itemKey': canonical '$canonicalCollectionId'='$($canonical.Maturity)', '$($occurrence.CollectionId)'='$($occurrence.Maturity)'"
                    $errorCount++
                }
            }
        }
    }

    if ($canonicalManifestFound) {
        # Check 1: Orphan artifact detection
        $onDiskArtifacts = Get-ArtifactFiles -RepoRoot $RepoRoot
        foreach ($artifact in $onDiskArtifacts) {
            $diskKey = Get-CollectionItemKey -Kind $artifact.kind -ItemPath $artifact.path
            $occurrences = if ($itemOccurrences.ContainsKey($diskKey)) { $itemOccurrences[$diskKey] } else { @() }

            $inCanonical = @($occurrences | Where-Object { $_.CollectionId -eq $canonicalCollectionId }).Count -gt 0
            $inThemed    = @($occurrences | Where-Object { $_.CollectionId -ne $canonicalCollectionId }).Count -gt 0

            if (-not $inCanonical) {
                # Skip orphan failure when all themed occurrences are tombstoned (maturity:'removed').
                $themedActive  = @($occurrences | Where-Object { $_.CollectionId -ne $canonicalCollectionId -and $_.Maturity -ne 'removed' }).Count -gt 0
                $themedRemoved = @($occurrences | Where-Object { $_.CollectionId -ne $canonicalCollectionId -and $_.Maturity -eq 'removed' }).Count -gt 0
                if ($themedRemoved -and -not $themedActive) {
                    Write-Verbose "Skipping orphan check for tombstoned item '$diskKey'"
                } else {
                    Write-Host "  FAIL orphan: '$diskKey' is on disk but absent from '$canonicalCollectionId'" -ForegroundColor Red
                    Add-ValidationResult -Collection $canonicalCollectionId -ErrorType 'OrphanArtifact' -Message "'$diskKey' is on disk but absent from '$canonicalCollectionId'"
                    $errorCount++
                }
            } elseif (-not $inThemed) {
                Write-Host " WARN '$diskKey' exists in '$canonicalCollectionId' but is not in any themed collection" -ForegroundColor Yellow
                Add-ValidationResult -Collection $canonicalCollectionId -ErrorType 'CanonicalOnlyArtifact' -Message "'$diskKey' exists in '$canonicalCollectionId' but is not in any themed collection" -Severity 'Warning'
            }
        }
    }

    Write-Host ''
    Write-Host "$validatedCount collections validated, $errorCount errors"

    return @{
        Success         = ($errorCount -eq 0)
        ErrorCount      = $errorCount
        CollectionCount = $validatedCount
        Results         = @($validationResults)
    }
}

function Export-CollectionValidationReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$ValidationResult,

        [Parameter(Mandatory = $true)]
        [string]$OutputPath
    )

    $logsDir = Split-Path -Path $OutputPath -Parent
    if (-not [string]::IsNullOrWhiteSpace($logsDir) -and -not (Test-Path -Path $logsDir)) {
        New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
    }

    $report = @{
        Timestamp        = (Get-Date).ToUniversalTime().ToString('o')
        TotalCollections = $ValidationResult.CollectionCount
        ErrorCount       = $ValidationResult.ErrorCount
        Results          = @($ValidationResult.Results)
    }

    $report | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding utf8
}

#endregion Orchestration

#region Main Execution
if ($MyInvocation.InvocationName -ne '.') {
    try {
        # Verify PowerShell-Yaml module
        if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
            throw "Required module 'PowerShell-Yaml' is not installed."
        }
        Import-Module PowerShell-Yaml -ErrorAction Stop

        # Resolve paths
        $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
        $RepoRoot = (Get-Item "$ScriptDir/../..").FullName

        $result = Invoke-CollectionValidation -RepoRoot $RepoRoot
        Export-CollectionValidationReport -ValidationResult $result -OutputPath $OutputPath

        if (-not $result.Success) {
            throw "Validation failed with $($result.ErrorCount) error(s)."
        }

        exit 0
    }
    catch {
        Write-Error "Collection validation failed: $($_.Exception.Message)"
        Write-CIAnnotation -Message $_.Exception.Message -Level Error
        exit 1
    }
}
#endregion
