modules/ApplicationStacks/ApplicationStacks.psm1

<#
    ApplicationStacks.psm1 - evidence-based detection of multi-component application stacks
    (server engine, clients, drivers, DSNs, services, tasks, shares, SQL instances, IIS sites,
    processes) defined in config\application-stacks.json. Read-only: it only reads datasets
    the other collectors already produced.

    Produces: AppStackEvidence (one row per evidence hit, each with an EvidenceId) and
              AppStackSummary (one row per detected stack: roles present, server/client
              versions, host role, and the EvidenceIds behind every conclusion).

    Versions are REPORTED, never interpreted: a server/client version difference raises a
    "vendor validation required" flag, and identical versions are not called compatible.
    Fleet-wide correlation (which server hosts the engine, which RDS hosts carry clients,
    candidate source/target servers) is done by tools\Merge-FleetDiscoveryResults.ps1.
#>


function Get-DiscoveryModuleMetadata {
    [pscustomobject]@{
        ModuleName='ApplicationStacks'; DisplayName='Application Stacks (Sage, Actian, ...)'; Category='Applications'; Version='1.0.0'
        DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false
        RequiresRole=$null; EstimatedImpact='Minimal'; CanRunAsSystem=$true
        ProducesDatasets=@('AppStackEvidence','AppStackSummary')
        ProducesRisks=$true; ProducesFollowUpQuestions=$true; SupportsDeepMode=$true; SupportsComplianceLens=$false
    }
}

function Test-DiscoveryPrerequisites {
    param([object]$Context)
    if ($Context.Config -and $Context.Config.AppStacks -and @($Context.Config.AppStacks.stacks).Count -gt 0) {
        return [pscustomobject]@{ ModuleName='ApplicationStacks'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() }
    }
    [pscustomobject]@{ ModuleName='ApplicationStacks'; CanRun=$false; Status='NotApplicable'; Reason='config\application-stacks.json is missing or defines no stacks.'; Limitations=@() }
}

function Find-AppStackEvidence {
    <#
        Pure: evaluates stack definitions against datasets supplied by -GetRows (a scriptblock
        taking a dataset name and returning its rows). Returns one evidence row per matching
        dataset row per source, numbered EV-0001...
    #>

    param([object[]]$Stacks = @(), [Parameter(Mandatory)][scriptblock]$GetRows)
    $out = [System.Collections.Generic.List[object]]::new()
    $n = 0
    foreach ($st in $Stacks) {
        foreach ($c in @($st.components)) {
            foreach ($src in @($c.sources)) {
                $rows = @(& $GetRows ([string]$src.dataset))
                foreach ($r in $rows) {
                    $v = Get-RowValue -Row $r -Column ([string]$src.field)
                    if ($null -eq $v -or -not ([string]$v -match [string]$src.pattern)) { continue }
                    $n++
                    $ver = ''
                    if ($src.versionField) { $vv = Get-RowValue -Row $r -Column ([string]$src.versionField); if ($vv) { $ver = [string]$vv } }
                    # Item: the matched row's own name (DSN, service, task, instance, share, ...), so a
                    # DSN matched on ApplicationHint is still listed by its DSN name.
                    $item = ''
                    foreach ($f in @('DsnName','InstanceName','TaskName','Name','DisplayName','Path','ProviderName','LocalPort')) { $iv = Get-RowValue -Row $r -Column $f; if ($iv) { $item = [string]$iv; break } }
                    $out.Add([pscustomobject]@{
                        EvidenceId=('EV-{0:D4}' -f $n); Stack=[string]$st.name; Vendor=[string]$st.vendor; Component=[string]$c.component; Role=[string]$c.role
                        Version=$ver; Confidence=$(if ($src.confidence) { [string]$src.confidence } else { 'Possible' })
                        SourceDataset=[string]$src.dataset; SourceField=[string]$src.field; MatchedValue=[string]$v
                        Item=$item; Bitness=[string](Get-RowValue -Row $r -Column 'Bitness')
                    })
                }
            }
        }
    }
    return ,@($out)
}

function Get-AppStackSummary {
    <# Pure: one summary row per stack with evidence. Host role is derived from the strongest server/client evidence. #>
    param([object[]]$Evidence = @(), [object[]]$Stacks = @(), [bool]$IsRdsHost = $false)
    $rows = foreach ($st in $Stacks) {
        $ev = @($Evidence | Where-Object { $_.Stack -eq $st.name })
        if ($ev.Count -eq 0) { continue }
        $server = @($ev | Where-Object { $_.Role -eq 'Server' })
        $client = @($ev | Where-Object { $_.Role -eq 'Client' })
        $sv = @($server | Where-Object { $_.Version } | ForEach-Object { $_.Version } | Select-Object -Unique)
        $cv = @($client | Where-Object { $_.Version } | ForEach-Object { '{0} {1}' -f $_.MatchedValue, $_.Version } | Select-Object -Unique)
        $cvOnly = @($client | Where-Object { $_.Version -and $_.Component -notmatch '(?i)Aatrix' } | ForEach-Object { $_.Version } | Select-Object -Unique)
        $hostRole = if ($server.Count -and $client.Count) { 'Server and client' } elseif ($server.Count) { 'Server' } elseif ($client.Count) { 'Client' } else { 'Related components only' }
        $best = if (@($ev | Where-Object { $_.Role -in @('Server','Client') -and $_.Confidence -eq 'Confirmed' }).Count) { 'Confirmed' }
                elseif (@($ev | Where-Object { $_.Confidence -in @('Confirmed','Likely') }).Count) { 'Likely' } else { 'Possible' }
        $differ = [bool]($sv.Count -gt 0 -and $cvOnly.Count -gt 0 -and @($cvOnly | Where-Object { $sv -notcontains $_ }).Count -gt 0)
        [pscustomobject]@{
            Stack=[string]$st.name; Vendor=[string]$st.vendor; HostRole=$hostRole; Confidence=$best
            RolesPresent=((@($ev | ForEach-Object { $_.Role } | Select-Object -Unique)) -join ', ')
            ServerVersions=($sv -join ', '); ClientVersions=($cv -join '; ')
            LocalServerClientVersionsDiffer=$differ; MultipleServerVersions=[bool]($sv.Count -gt 1)
            IsRdsHost=$IsRdsHost; RdsHostWithClient=[bool]($IsRdsHost -and $client.Count -gt 0)
            EvidenceCount=$ev.Count; EvidenceIds=((@($ev | ForEach-Object { $_.EvidenceId })) -join ', ')
            ValidationNote='Version numbers are reported, not judged: confirm server/client compatibility with the vendor for these exact builds.'
        }
    }
    return ,@($rows)
}

function Invoke-DiscoveryCollection {
    param([object]$Context)
    $stacks = @($Context.Config.AppStacks.stacks)
    $getRows = { param($name) if ($name -and $Context.DataSets.Contains($name)) { @($Context.DataSets[$name].Rows) } else { @() } }
    $evidence = Find-AppStackEvidence -Stacks $stacks -GetRows $getRows
    $isRds = [bool]($Context.DataSets.Contains('RdsDiscovery') -and @($Context.DataSets['RdsDiscovery'].Rows).Count -gt 0)
    $summary = Get-AppStackSummary -Evidence $evidence -Stacks $stacks -IsRdsHost $isRds
    foreach ($s in $summary) {
        Add-DependencyEdge -Context $Context -SourceType 'Application stack' -SourceName $s.Stack -DependencyType 'HostsStackComponents' -Target $Context.ComputerName -Evidence ("{0} evidence record(s): {1}" -f $s.EvidenceCount, $s.EvidenceIds) -Confidence $s.Confidence -SourceDataset 'AppStackSummary' -ProjectImpact 'Vendor Dependency' -ValidationQuestion ("Which server hosts the {0} engine, which machines run its client, and has the vendor confirmed the target version?" -f $s.Stack) | Out-Null
    }
    return ,@{ AppStackEvidence=@($evidence); AppStackSummary=@($summary) }
}

function ConvertTo-DiscoveryDatasets {
    param([object]$Context, $RawData)
    if (-not $RawData) { $RawData = @{} }
    $get = { param($k) if ($RawData[$k]) { @($RawData[$k]) } else { @() } }
    Add-DataSet -Context $Context -Name 'AppStackEvidence' -Description 'Application-stack evidence records (component, role, version, source dataset/field, EvidenceId).' -Rows (& $get 'AppStackEvidence') -Visibility 'Internal' -SourceModule 'ApplicationStacks' | Out-Null
    Add-DataSet -Context $Context -Name 'AppStackSummary'  -Description 'Detected application stacks per server: host role, server/client versions, cited evidence.' -Rows (& $get 'AppStackSummary') -Visibility 'Both' -SourceModule 'ApplicationStacks' | Out-Null
}

function Get-DiscoveryFollowUpQuestions {
    param([object]$Context)
    try {
        if ($Context.DataSets.Contains('AppStackSummary')) {
            foreach ($s in @($Context.DataSets['AppStackSummary'].Rows)) {
                Add-FollowUpQuestion -Context $Context -Category 'Applications' -Module 'ApplicationStacks' -Audience 'Both' -Question ("{0} components were found on this server ({1}). Who supports it, which version is licensed, and has the vendor confirmed the server and client versions for the target environment?" -f $s.Stack, $s.HostRole) | Out-Null
            }
        }
    } catch { }
}

Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Get-DiscoveryFollowUpQuestions','Find-AppStackEvidence','Get-AppStackSummary'