<#
.SYNOPSIS
    Query WinMD API metadata from cached JSON files.

.DESCRIPTION
    Reads pre-built JSON cache of WinMD types, members, and namespaces.
    The cache is organized per-package (deduplicated) with project manifests
    that map each project to its referenced packages.

    Supports listing namespaces, types, members, searching, enum value lookup,
    and listing cached projects/packages.

.PARAMETER Action
    The query action to perform:
    - projects    : List cached projects
    - packages    : List packages for a project
    - stats       : Show aggregate statistics for a project
    - namespaces  : List all namespaces (optional -Filter prefix)
    - types       : List types in a namespace (-Namespace required)
    - members     : List members of a type (-TypeName required)
    - search      : Search types and members by name (-Query required)
    - enums       : List enum values (-TypeName required)

.PARAMETER Project
    Project name to query. Auto-selected if only one project is cached.
    Use -Action projects to list available projects.

.PARAMETER Namespace
    Namespace to query types from (used with -Action types).

.PARAMETER TypeName
    Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).

.PARAMETER Query
    Search query string (used with -Action search).

.PARAMETER Filter
    Optional prefix filter for namespaces (used with -Action namespaces).

.PARAMETER CacheDir
    Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
    relative to the workspace root.

.PARAMETER MaxResults
    Maximum number of results to return for search. Defaults to 30.

.EXAMPLE
    .\Invoke-WinMdQuery.ps1 -Action projects
    .\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI
    .\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI
    .\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
    .\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
    .\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
    .\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
    .\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
    [string]$Action,

    [string]$Project,
    [string]$Namespace,
    [string]$TypeName,
    [string]$Query,
    [string]$Filter,
    [string]$CacheDir,
    [int]$MaxResults = 30
)

# ─── Resolve cache directory ─────────────────────────────────────────────────

if (-not $CacheDir) {
    # Convention: skill lives at .github/skills/winmd-api-search/scripts/
    # so workspace root is 4 levels up from $PSScriptRoot.
    $scriptDir = $PSScriptRoot
    $root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
    $CacheDir = Join-Path $root 'Generated Files\winmd-cache'
}

if (-not (Test-Path $CacheDir)) {
    Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
    exit 1
}

# ─── Project resolution helpers ──────────────────────────────────────────────

function Get-CachedProjects {
    $projectsDir = Join-Path $CacheDir 'projects'
    if (-not (Test-Path $projectsDir)) { return @() }
    Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
}

function Resolve-ProjectManifest {
    param([string]$Name)

    $projectsDir = Join-Path $CacheDir 'projects'
    if (-not (Test-Path $projectsDir)) {
        Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
        exit 1
    }

    if ($Name) {
        $path = Join-Path $projectsDir "$Name.json"
        if (-not (Test-Path $path)) {
            # Scan mode appends a hash suffix -- try prefix match
            $matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue)
            if ($matching.Count -eq 1) {
                return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json
            }
            if ($matching.Count -gt 1) {
                $names = ($matching | ForEach-Object { $_.BaseName }) -join ', '
                Write-Error "Multiple projects match '$Name'. Specify the full name: $names"
                exit 1
            }
            $available = (Get-CachedProjects) -join ', '
            Write-Error "Project '$Name' not found. Available: $available"
            exit 1
        }
        return Get-Content $path -Raw | ConvertFrom-Json
    }

    # Auto-select if only one project
    $manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
    if ($manifests.Count -eq 0) {
        Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
        exit 1
    }
    if ($manifests.Count -eq 1) {
        return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json
    }

    $available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
    Write-Error "Multiple projects cached -- use -Project to specify. Available: $available"
    exit 1
}

function Get-PackageCacheDirs {
    param($Manifest)
    $dirs = @()
    foreach ($pkg in $Manifest.packages) {
        $dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version
        if (Test-Path $dir) {
            $dirs += $dir
        }
    }
    return $dirs
}

# ─── Action: projects ────────────────────────────────────────────────────────

function Show-Projects {
    $projects = Get-CachedProjects
    if ($projects.Count -eq 0) {
        Write-Output "No projects cached."
        return
    }
    Write-Output "Cached projects ($($projects.Count)):"
    foreach ($p in $projects) {
        $manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json
        $pkgCount = $manifest.packages.Count
        Write-Output "  $p ($pkgCount package(s))"
    }
}

# ─── Action: packages ────────────────────────────────────────────────────────

function Show-Packages {
    $manifest = Resolve-ProjectManifest -Name $Project
    Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
    foreach ($pkg in $manifest.packages) {
        $metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
        if (Test-Path $metaPath) {
            $meta = Get-Content $metaPath -Raw | ConvertFrom-Json
            Write-Output "  $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members"
        } else {
            Write-Output "  $($pkg.id)@$($pkg.version) -- (cache missing)"
        }
    }
}

# ─── Action: stats ───────────────────────────────────────────────────────────

function Show-Stats {
    $manifest = Resolve-ProjectManifest -Name $Project
    $totalTypes = 0
    $totalMembers = 0
    $totalNamespaces = 0
    $totalWinMd = 0

    foreach ($pkg in $manifest.packages) {
        $metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
        if (Test-Path $metaPath) {
            $meta = Get-Content $metaPath -Raw | ConvertFrom-Json
            $totalTypes += $meta.totalTypes
            $totalMembers += $meta.totalMembers
            $totalNamespaces += $meta.totalNamespaces
            $totalWinMd += $meta.winMdFiles.Count
        }
    }

    Write-Output "WinMD Index Statistics -- $($manifest.projectName)"
    Write-Output "======================================"
    Write-Output "  Packages:   $($manifest.packages.Count)"
    Write-Output "  Namespaces: $totalNamespaces (may overlap across packages)"
    Write-Output "  Types:      $totalTypes"
    Write-Output "  Members:    $totalMembers"
    Write-Output "  WinMD files: $totalWinMd"
}

# ─── Action: namespaces ──────────────────────────────────────────────────────

function Get-Namespaces {
    param([string]$Prefix)
    $manifest = Resolve-ProjectManifest -Name $Project
    $dirs = Get-PackageCacheDirs -Manifest $manifest
    $allNs = @()

    foreach ($dir in $dirs) {
        $nsFile = Join-Path $dir 'namespaces.json'
        if (Test-Path $nsFile) {
            $allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json)
        }
    }

    $allNs = $allNs | Sort-Object -Unique
    if ($Prefix) {
        $allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
    }
    $allNs | ForEach-Object { Write-Output $_ }
}

# ─── Action: types ───────────────────────────────────────────────────────────

function Get-TypesInNamespace {
    param([string]$Ns)
    if (-not $Ns) {
        Write-Error "-Namespace is required for 'types' action."
        exit 1
    }

    $manifest = Resolve-ProjectManifest -Name $Project
    $dirs = Get-PackageCacheDirs -Manifest $manifest
    $safeFile = $Ns.Replace('.', '_') + '.json'
    $found = $false
    $seen = @{}

    foreach ($dir in $dirs) {
        $filePath = Join-Path $dir "types\$safeFile"
        if (-not (Test-Path $filePath)) { continue }
        $found = $true
        $types = Get-Content $filePath -Raw | ConvertFrom-Json
        foreach ($t in $types) {
            if ($seen.ContainsKey($t.fullName)) { continue }
            $seen[$t.fullName] = $true
            Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
        }
    }

    if (-not $found) {
        Write-Error "Namespace not found: $Ns"
        exit 1
    }
}

# ─── Action: members ─────────────────────────────────────────────────────────

function Get-MembersOfType {
    param([string]$FullName)
    if (-not $FullName) {
        Write-Error "-TypeName is required for 'members' action."
        exit 1
    }

    $lastDot = $FullName.LastIndexOf('.')
    if ($lastDot -lt 0) {
        Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName"
        exit 1
    }

    $ns = $FullName.Substring(0, $lastDot)
    $safeFile = $ns.Replace('.', '_') + '.json'

    $manifest = Resolve-ProjectManifest -Name $Project
    $dirs = Get-PackageCacheDirs -Manifest $manifest

    foreach ($dir in $dirs) {
        $filePath = Join-Path $dir "types\$safeFile"
        if (-not (Test-Path $filePath)) { continue }

        $types = Get-Content $filePath -Raw | ConvertFrom-Json
        $type = $types | Where-Object { $_.fullName -eq $FullName }
        if (-not $type) { continue }

        Write-Output "$($type.kind) $($type.fullName)"
        if ($type.baseType) { Write-Output "  Extends: $($type.baseType)" }
        Write-Output ""
        foreach ($m in $type.members) {
            Write-Output "  [$($m.kind)] $($m.signature)"
        }
        return
    }

    Write-Error "Type not found: $FullName"
    exit 1
}

# ─── Action: search ──────────────────────────────────────────────────────────
# Ranks namespaces by best match score on type names and member names.
# Outputs: ranked namespaces with top matching types and the JSON file path.
# The agent can then read the JSON file to inspect all members intelligently.

function Search-WinMd {
    param([string]$SearchQuery, [int]$Max)
    if (-not $SearchQuery) {
        Write-Error "-Query is required for 'search' action."
        exit 1
    }

    $manifest = Resolve-ProjectManifest -Name $Project
    $dirs = Get-PackageCacheDirs -Manifest $manifest

    # Collect: namespace -> { bestScore, matchingTypes[], filePath }
    $nsResults = @{}

    foreach ($dir in $dirs) {
        $nsFile = Join-Path $dir 'namespaces.json'
        if (-not (Test-Path $nsFile)) { continue }
        $nsList = Get-Content $nsFile -Raw | ConvertFrom-Json

        foreach ($n in $nsList) {
            $safeFile = $n.Replace('.', '_') + '.json'
            $filePath = Join-Path $dir "types\$safeFile"
            if (-not (Test-Path $filePath)) { continue }

            $types = Get-Content $filePath -Raw | ConvertFrom-Json
            foreach ($t in $types) {
                $typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery

                # Also search member names for matches
                $bestMemberScore = 0
                $matchingMember = $null
                if ($t.members) {
                    foreach ($m in $t.members) {
                        $memberName = $m.name
                        $mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery
                        if ($mScore -gt $bestMemberScore) {
                            $bestMemberScore = $mScore
                            $matchingMember = $m.signature
                        }
                    }
                }

                $score = [Math]::Max($typeScore, $bestMemberScore)
                if ($score -le 0) { continue }

                if (-not $nsResults.ContainsKey($n)) {
                    $nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() }
                }
                $entry = $nsResults[$n]
                if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
                if ($entry.FilePaths -notcontains $filePath) {
                    $entry.FilePaths += $filePath
                }

                if ($typeScore -ge $bestMemberScore) {
                    $entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore }
                } else {
                    $entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore }
                }
            }
        }
    }

    if ($nsResults.Count -eq 0) {
        Write-Output "No results found for: $SearchQuery"
        return
    }

    $ranked = $nsResults.GetEnumerator() |
        Sort-Object { $_.Value.BestScore } -Descending |
        Select-Object -First $Max

    foreach ($r in $ranked) {
        $ns = $r.Key
        $info = $r.Value
        Write-Output "[$($info.BestScore)] $ns"
        foreach ($fp in $info.FilePaths) {
            Write-Output "    File: $fp"
        }
        # Show top 5 highest-scoring matching types in this namespace
        $info.Types | Sort-Object { $_.Score } -Descending |
            Select-Object -First 5 |
            ForEach-Object { Write-Output "    $($_.Text)" }
        Write-Output ""
    }
}

# ─── Search scoring ──────────────────────────────────────────────────────────
# Simple ranked scoring on type names. Higher = better.
#   100 = exact name    80 = starts-with    60 = substring
#    50 = PascalCase     40 = multi-keyword   20 = fuzzy subsequence

function Get-MatchScore {
    param([string]$Name, [string]$FullName, [string]$Query)

    $q = $Query.Trim()
    if (-not $q) { return 0 }

    if ($Name -eq $q) { return 100 }
    if ($Name -like "$q*") { return 80 }
    if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }

    $initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
    if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }

    $words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
    if ($words.Count -gt 1) {
        $allFound = $true
        foreach ($w in $words) {
            if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
                $allFound = $false
                break
            }
        }
        if ($allFound) { return 40 }
    }

    if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }

    return 0
}

function Test-FuzzySubsequence {
    param([string]$Text, [string]$Pattern)
    $ti = 0
    $tLower = $Text.ToLowerInvariant()
    $pLower = $Pattern.ToLowerInvariant()
    foreach ($ch in $pLower.ToCharArray()) {
        $idx = $tLower.IndexOf($ch, $ti)
        if ($idx -lt 0) { return $false }
        $ti = $idx + 1
    }
    return $true
}

# ─── Action: enums ───────────────────────────────────────────────────────────

function Get-EnumValues {
    param([string]$FullName)
    if (-not $FullName) {
        Write-Error "-TypeName is required for 'enums' action."
        exit 1
    }

    $lastDot = $FullName.LastIndexOf('.')
    if ($lastDot -lt 1) {
        Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName"
        exit 1
    }

    $ns = $FullName.Substring(0, $lastDot)
    $safeFile = $ns.Replace('.', '_') + '.json'

    $manifest = Resolve-ProjectManifest -Name $Project
    $dirs = Get-PackageCacheDirs -Manifest $manifest

    foreach ($dir in $dirs) {
        $filePath = Join-Path $dir "types\$safeFile"
        if (-not (Test-Path $filePath)) { continue }

        $types = Get-Content $filePath -Raw | ConvertFrom-Json
        $type = $types | Where-Object { $_.fullName -eq $FullName }
        if (-not $type) { continue }

        if ($type.kind -ne 'Enum') {
            Write-Error "$FullName is not an Enum (kind: $($type.kind))"
            exit 1
        }
        Write-Output "Enum $($type.fullName)"
        if ($type.enumValues) {
            $type.enumValues | ForEach-Object { Write-Output "  $_" }
        } else {
            Write-Output "  (no values)"
        }
        return
    }

    Write-Error "Type not found: $FullName"
    exit 1
}

# ─── Dispatch ─────────────────────────────────────────────────────────────────

switch ($Action) {
    'projects'   { Show-Projects }
    'packages'   { Show-Packages }
    'stats'      { Show-Stats }
    'namespaces' { Get-Namespaces -Prefix $Filter }
    'types'      { Get-TypesInNamespace -Ns $Namespace }
    'members'    { Get-MembersOfType -FullName $TypeName }
    'search'     { Search-WinMd -SearchQuery $Query -Max $MaxResults }
    'enums'      { Get-EnumValues -FullName $TypeName }
}
