Common/Import-FrameworkDefinitions.ps1

function Import-FrameworkDefinitions {
    <#
    .SYNOPSIS
        Loads all framework definition JSON files and returns an ordered array.
    .DESCRIPTION
        Scans the specified directory for *.json files, parses each as a framework
        definition, applies defaults for missing fields, and returns an array of
        hashtables sorted by displayOrder. Invalid JSON files are skipped with a
        warning.
    .PARAMETER FrameworksPath
        Directory containing framework JSON files (e.g., controls/frameworks/).
    .OUTPUTS
        System.Collections.Hashtable[]
        Each hashtable contains: frameworkId, label, description, css, totalControls,
        displayOrder, scoringMethod, profiles, filterFamily.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FrameworksPath
    )

    if (-not (Test-Path -Path $FrameworksPath -PathType Container)) {
        Write-Warning "Framework definitions directory not found: $FrameworksPath"
        return @()
    }

    $jsonFiles = Get-ChildItem -Path $FrameworksPath -Filter '*.json' -File -ErrorAction SilentlyContinue
    if (-not $jsonFiles -or @($jsonFiles).Count -eq 0) {
        Write-Warning "No framework JSON files found in: $FrameworksPath"
        return @()
    }

    # Prefix-to-filter-family mapping
    $prefixMap = @{
        'cis'       = 'CIS'
        'nist'      = 'NIST'
        'iso'       = 'ISO'
        'stig'      = 'STIG'
        'pci'       = 'PCI'
        'cmmc'      = 'CMMC'
        'hipaa'     = 'HIPAA'
        'cisa'      = 'CISA'
        'soc'       = 'SOC2'
        'fedramp'   = 'FedRAMP'
        'essential' = 'Essential8'
        'mitre'     = 'MITRE'
    }

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

    foreach ($file in $jsonFiles) {
        try {
            $raw = Get-Content -Path $file.FullName -Raw -ErrorAction Stop
            $def = $raw | ConvertFrom-Json -ErrorAction Stop
        }
        catch {
            Write-Warning "Skipping invalid framework JSON: $($file.Name) - $_"
            continue
        }

        if (-not $def.frameworkId -or -not $def.label) {
            Write-Warning "Skipping framework JSON missing frameworkId or label: $($file.Name)"
            continue
        }

        # Extract scoring method and profiles from the scoring object
        $scoringMethod = 'control-coverage'
        $profiles = $null
        if ($def.scoring -and $def.scoring.method) {
            $scoringMethod = $def.scoring.method
        }
        if ($def.scoring -and $def.scoring.profiles) {
            $profiles = @{}
            foreach ($prop in $def.scoring.profiles.PSObject.Properties) {
                $profileData = @{}
                if ($prop.Value.controlCount) {
                    $profileData['controlCount'] = [int]$prop.Value.controlCount
                }
                if ($prop.Value.label) {
                    $profileData['label'] = $prop.Value.label
                }
                if ($prop.Value.css) {
                    $profileData['css'] = $prop.Value.css
                }
                $profiles[$prop.Name] = $profileData
            }
        }

        # Derive filterFamily from frameworkId prefix (longest prefix first to avoid
        # 'cis' matching before 'cisa')
        $fwId = $def.frameworkId
        $filterFamily = ''
        foreach ($prefix in ($prefixMap.Keys | Sort-Object -Property Length -Descending)) {
            if ($fwId.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                $filterFamily = $prefixMap[$prefix]
                break
            }
        }
        if (-not $filterFamily) {
            # Fallback: uppercase the first segment before the first hyphen
            $filterFamily = ($fwId -split '-')[0].ToUpper()
        }

        # Preserve raw scoring sub-structures for catalog rendering
        $scoringData = @{}
        if ($def.scoring) {
            foreach ($prop in $def.scoring.PSObject.Properties) {
                $scoringData[$prop.Name] = $prop.Value
            }
        }

        # Preserve top-level structural keys outside scoring (strategies, controls, etc.)
        # Convert PSCustomObjects to hashtables so callers can use .Keys
        $extraKeys = @('strategies', 'controls', 'sections', 'nonAutomatableCriteria', 'licensingProfiles', 'groupBy')
        $extraData = @{}
        foreach ($key in $extraKeys) {
            if ($def.PSObject.Properties.Name -contains $key) {
                $val = $def.$key
                if ($val -is [System.Management.Automation.PSCustomObject]) {
                    $ht = @{}
                    foreach ($p in $val.PSObject.Properties) { $ht[$p.Name] = $p.Value }
                    $extraData[$key] = $ht
                }
                else {
                    $extraData[$key] = $val
                }
            }
        }

        $frameworks.Add(@{
            frameworkId   = $fwId
            label         = [string]$def.label
            description   = if ($def.description) { [string]$def.description } else { '' }
            css           = if ($def.css) { [string]$def.css } else { 'fw-default' }
            totalControls = if ($def.totalControls) { [int]$def.totalControls } else { 0 }
            displayOrder  = if ($null -ne $def.displayOrder) { [int]$def.displayOrder } else { 999 }
            scoringMethod = $scoringMethod
            profiles      = $profiles
            filterFamily  = $filterFamily
            scoringData   = $scoringData
            extraData     = $extraData
        })
    }

    # Sort by displayOrder, then by frameworkId for stable ordering
    $sorted = @($frameworks | Sort-Object -Property { $_.displayOrder }, { $_.frameworkId })
    # Comma operator prevents PowerShell from unwrapping single-element arrays
    return , $sorted
}