src/functions/map-entries.ps1

function New-BuildAllEntry {
    return [ordered]@{ __buildAll = $true }
}

function Test-BuildAllEntry {
    param($Entry)

    return $Entry -is [System.Collections.IDictionary] -and $Entry.__buildAll
}

function Test-IsInvokableBuildEntry {
    param(
        $Entry,
        [ValidateSet('build', 'conf')]$Language
    )

    $reservedKeys = (Get-MapLanguage $Language).reservedKeys

    if (Test-BuildAllEntry $Entry) { return $false }
    if ($Entry -is [scriptblock]) { return $true }
    if ($Entry -is [System.Collections.IDictionary]) {
        if (Test-IsParentEntry $Entry -reservedKeys $reservedKeys) {
            return $false
        }

        return $Entry.exec -is [scriptblock]
    }

    return $false
}

function Get-BuildAllChildren {
    param(
        [System.Collections.IDictionary]$ParentEntry,
        [ValidateSet('build', 'conf')]$Language,
        [string]$ParentKey = '',
        [string]$Separator = '.'
    )

    $reservedKeys = (Get-MapLanguage $Language).reservedKeys
    $result = [ordered]@{}

    foreach ($kvp in $ParentEntry.GetEnumerator()) {
        if ($kvp.Key -in $reservedKeys -or $kvp.Key -eq 'all') {
            continue
        }

        if (!(Test-IsInvokableBuildEntry $kvp.Value -Language $Language)) {
            continue
        }

        $childKey = if ($ParentKey) { "$ParentKey$Separator$($kvp.Key)" } else { $kvp.Key }
        $result[$childKey] = $kvp.Value
    }

    return $result
}

function Resolve-MapEntrySegments {
    param(
        [System.Collections.IDictionary]$Node,
        [string[]]$Segments,
        [string]$Separator,
        [hashtable]$IncludeCache,
        [hashtable]$LoadingIncludes
    )

    $notFound = [pscustomobject]@{ Found = $false; RequiresEnumeration = $false; Value = $null }
    if (!$Node -or !$Segments -or $Segments.Count -eq 0) {
        return $notFound
    }

    $list = if ($Node.list) { $Node.list } else { $Node }
    if ($list -is [scriptblock] -or $list -isnot [System.Collections.IDictionary]) {
        return [pscustomobject]@{ Found = $false; RequiresEnumeration = $true; Value = $null }
    }

    $remainingKey = $Segments -join $Separator
    if ($Segments.Count -gt 1 -and $list.Contains($remainingKey)) {
        return [pscustomobject]@{ Found = $false; RequiresEnumeration = $true; Value = $null }
    }

    $segment = $Segments[0]
    $rest = if ($Segments.Count -gt 1) { $Segments[1..($Segments.Count - 1)] } else { @() }
    $found = $false
    $value = $null

    foreach ($kvp in $list.GetEnumerator()) {
        if ($kvp.Key -eq '#include') {
            if ($kvp.Value -isnot [System.Collections.IDictionary]) {
                continue
            }

            foreach ($include in $kvp.Value.GetEnumerator()) {
                $usePrefix = $include.Value -is [System.Collections.IDictionary] -and $include.Value.prefix -eq $true
                if ($usePrefix -and $include.Key -ne $segment) {
                    continue
                }

                $includedMap = Import-IncludedConfigMap -DirectoryName "$($include.Key)" -BaseDir $Node._baseDir -Cache $IncludeCache -Loading $LoadingIncludes
                if (!$includedMap) {
                    continue
                }

                $includedSegments = if ($usePrefix) { $rest } else { $Segments }
                if ($includedSegments.Count -eq 0) {
                    continue
                }

                $includedResult = Resolve-MapEntrySegments -Node $includedMap -Segments $includedSegments -Separator $Separator -IncludeCache $IncludeCache -LoadingIncludes $LoadingIncludes
                if ($includedResult.RequiresEnumeration) {
                    return $includedResult
                }
                if ($includedResult.Found) {
                    $found = $true
                    $value = $includedResult.Value
                }
            }

            continue
        }

        if ($kvp.Key -ne $segment) {
            continue
        }

        if ($rest.Count -eq 0) {
            $found = $true
            $value = $kvp.Value
            continue
        }

        if ($kvp.Value -is [System.Collections.IDictionary]) {
            $childResult = Resolve-MapEntrySegments -Node $kvp.Value -Segments $rest -Separator $Separator -IncludeCache $IncludeCache -LoadingIncludes $LoadingIncludes
            if ($childResult.RequiresEnumeration) {
                return $childResult
            }
            if ($childResult.Found) {
                $found = $true
                $value = $childResult.Value
            }
        }
    }

    return [pscustomobject]@{ Found = $found; RequiresEnumeration = $false; Value = $value }
}

function Get-MapEntriesFromEntryList {
    param(
        $map,
        $keys,
        [switch][bool]$flatten = $false,
        [switch][bool]$leafsOnly = $false,
        $separator = ".",
        $language = $null
    )

    $results = @()
    $entries = Get-MapEntryList $map -flatten:$flatten -leafsOnly:$leafsOnly -separator:$separator -language $language

    foreach ($key in @($keys)) {
        $found = @($entries.GetEnumerator() | Where-Object { $_.Key -eq $key })
        if ($found.Count -eq 0) { continue }

        $target = $found[0]
        if ((Test-BuildAllEntry $target.Value) -and $language -eq 'build') {
            $parentKey = if ($key -match "^(.*)$([regex]::Escape($separator))all$") { $Matches[1] } else { '' }
            $parentEntry = if ($parentKey) {
                (Get-MapEntriesFromEntryList $map $parentKey -separator $separator -language $language).Value
            }
            else {
                $map
            }

            $children = Get-BuildAllChildren $parentEntry -Language $language -ParentKey $parentKey -Separator $separator
            foreach ($child in $children.GetEnumerator()) {
                $results += [System.Collections.DictionaryEntry]::new($child.Key, $child.Value)
            }
            continue
        }

        $results += $target
    }

    return $results
}

function Get-MapEntry(
    [ValidateScript({
            $_ -is [System.Collections.IDictionary] -or $_ -is [array]
        })]
    $map,
    $key,
    $separator = ".",
    $language = $null,
    [hashtable]$OperationContext
) {
    return (Get-MapEntries $map $key -separator $separator -language $language -OperationContext $OperationContext).Value
}

function Get-MapEntries(
    [ValidateScript({
            $_ -is [System.Collections.IDictionary] -or $_ -is [array]
        })]
    $map,
    $keys,
    [switch][bool]$flatten = $false,
    [switch][bool]$leafsOnly = $false,
    $separator = ".",
    $language = $null,
    [hashtable]$OperationContext
) {
    $results = @()
    $useEnumeration = $flatten -or $leafsOnly -or $map -isnot [System.Collections.IDictionary]
    $includeCache = if ($OperationContext) { $OperationContext.IncludeCache } else { @{} }
    $loadingIncludes = if ($OperationContext) { $OperationContext.LoadingIncludes } else { @{} }

    foreach ($key in @($keys)) {
        if ($useEnumeration) {
            $results += Get-MapEntriesFromEntryList $map $key -flatten:$flatten -leafsOnly:$leafsOnly -separator $separator -language $language
            continue
        }

        $segments = $key -split [regex]::Escape($separator)
        $resolved = Resolve-MapEntrySegments -Node $map -Segments $segments -Separator $separator -IncludeCache $includeCache -LoadingIncludes $loadingIncludes
        if ($resolved.RequiresEnumeration) {
            $results += Get-MapEntriesFromEntryList $map $key -separator $separator -language $language
            continue
        }

        if ($resolved.Found) {
            $results += [System.Collections.DictionaryEntry]::new($key, $resolved.Value)
            continue
        }

        if ($language -eq 'build' -and $segments.Count -gt 1 -and $segments[-1] -eq 'all') {
            $parentKey = if ($key -match "^(.*)$([regex]::Escape($separator))all$") { $Matches[1] } else { '' }
            $parentEntry = if ($parentKey) {
                (Get-MapEntries $map $parentKey -separator $separator -language $language -OperationContext $OperationContext).Value
            }
            else {
                $map
            }

            if ($parentEntry -is [System.Collections.IDictionary] -and -not $parentEntry.Contains('all')) {
                $children = Get-BuildAllChildren $parentEntry -Language $language -ParentKey $parentKey -Separator $separator
                foreach ($child in $children.GetEnumerator()) {
                    $results += [System.Collections.DictionaryEntry]::new($child.Key, $child.Value)
                }
            }
        }
    }

    if (!$results) {
        Write-Verbose "entry '$keys' not found"
    }

    return $results
}

# TODO: key should be a hidden property of $entry
function Get-EntryHasExec {
    param($Entry)

    if ($Entry -isnot [System.Collections.IDictionary]) { return $false }
    $exec = $Entry.exec
    if ($null -eq $exec) { return $false }
    if ($exec -is [array]) { return $exec.Count -gt 0 }
    return $true
}

function Get-EntryCommand(
    [ValidateScript({
            $_ -is [System.Collections.IDictionary] -or $_ -is [array] -or $_ -is [scriptblock]
        })]
    [Parameter(Mandatory = $true)]
    $entry,
    [Parameter(Mandatory = $true)]
    $commandKey
) {
    if (!$entry) { throw "entry is NULL" }
    if ($entry -is [scriptblock]) { return $entry }

    if ($entry -is [System.Collections.IDictionary] -or $entry -is [System.Collections.Hashtable]) {
        if (!$entry.$commandKey) {
            throw "Command '$commandKey' not found"
            return $null
        }
        return $entry.$commandKey
    }

    throw "Entry of type $($entry.GetType().Name) is not supported"
    return $null
}

function Test-IsParentEntry {
    <#
    .SYNOPSIS
        Determines if an entry is a parent container (has nested commands) or a leaf (executable command)
    .PARAMETER Entry
        The map entry to test
    .PARAMETER ReservedKeys
        Array of reserved keys that should be skipped during processing
    .OUTPUTS
        [bool] $true if the entry is a parent container
    #>

    param(
        $Entry,
        [ValidateSet('build', 'conf')]
        $Language = 'build',
        $ReservedKeys
    )

    if (!$PSBoundParameters.ContainsKey('ReservedKeys')) {
        $ReservedKeys = (Get-MapLanguage $Language).reservedKeys
    }

    # If entry is not a hashtable, it's a leaf (scriptblock or other)
    if ($Entry -isnot [System.Collections.IDictionary]) {
        return $false
    }

    # Check for explicit list key (traditional nested structure)
    if ($Entry.list) {
        return $true
    }

    # Check if entry contains nested commands (hashtables or scriptblocks)
    foreach ($subKvp in $Entry.GetEnumerator()) {
        if ($subKvp.Key -in $reservedKeys) {
            continue
        }
        if ($subKvp.Value -is [System.Collections.IDictionary] -or $subKvp.Value -is [scriptblock]) {
            return $true
        }
    }

    return $false
}