src/assess/Import-ScoutConfig.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Load an optional Scout assessment config -- an alternative benchmark,
    rule-selection patterns, and/or per-rule threshold overrides -- from a JSON
    file on disk, falling back to the built-in ALZ reference benchmark whenever
    the path is absent, missing, or unparsable.

.DESCRIPTION
    The PowerShell-native equivalent of the web platform's "config upload":
    instead of an HTTP multipart upload, `-ConfigPath` points at a JSON file.
    The schema mirrors exactly what the assessment engine already consumes --
    nothing new is invented here:

      - `benchmark` is the same shape as benchmarks/alz-reference.json,
                        consumed as-is by Compare-Benchmark (managementGroups,
                        requiredPolicyAssignments, network, ...).
      - `rulePatterns` is the same string[] of glob patterns already accepted
                        by Get-RuleSet -Patterns (and stored as the `Rules`
                        key in manifests/assessments.psd1).
      - `ruleOverrides` is a { ruleId: <value> } or { ruleId: { value, type } }
                        map applied on top of the loaded rule set by
                        Get-RuleSet's -Overrides parameter (patches a rule's
                        assert.value / assert.type after the YAML load).

    Every key is optional and independently overridable, so a config file can
    supply just one of the three without disturbing the others.

    Never throws on a bad file (AB#375): a missing -ConfigPath, a missing
    file, a read failure, a JSON parse failure, or a non-object JSON payload
    all fall back to the built-in default benchmark with a Write-Warning, so
    a broken config degrades to "run with defaults" rather than aborting the
    assessment.

.PARAMETER ConfigPath
    Path to a JSON config file. Optional -- omit to use the built-in default
    benchmark with no rule-pattern or threshold overrides.

.PARAMETER DefaultBenchmarkPath
    Path to the built-in default benchmark JSON (benchmarks/alz-reference.json
    by default). Used as the fallback when -ConfigPath is absent or invalid,
    and as the base benchmark merged under a config file that omits `benchmark`.

.OUTPUTS
    [pscustomobject] with SchemaVersion, Source ('Default' or 'File'),
    ConfigPath (resolved path, or $null), Benchmark, RulePatterns, and
    RuleOverrides.

.NOTES
    Tracks ADO Stories AB#373 (load), AB#374 (save -- see Export-ScoutConfig.ps1),
    AB#375 (hardcoded fallback).
#>

function Import-ScoutConfig {
    [CmdletBinding()]
    param(
        [string] $ConfigPath,
        [string] $DefaultBenchmarkPath = "$PSScriptRoot/benchmarks/alz-reference.json"
    )

    $default = $null
    try {
        $default = Get-Content -LiteralPath $DefaultBenchmarkPath -Raw -ErrorAction Stop |
            ConvertFrom-Json -Depth 100 -ErrorAction Stop
    }
    catch {
        # The built-in default itself being unreadable is extremely unusual, but
        # this must still never throw (AB#375) -- fall back to an empty-but-shaped
        # benchmark object so Compare-Benchmark still runs (every field there is
        # read defensively / treated as optional).
        Write-Warning "Import-ScoutConfig: could not load the built-in default benchmark '$DefaultBenchmarkPath': $($_.Exception.Message). Continuing with an empty benchmark."
        $default = [pscustomobject]@{
            managementGroups          = [pscustomobject]@{ expected = @() }
            requiredPolicyAssignments = @()
            network                   = [pscustomobject]@{}
        }
    }

    $result = [pscustomobject]@{
        SchemaVersion = 1
        Source        = 'Default'
        ConfigPath    = $null
        Benchmark     = $default
        RulePatterns  = $null
        RuleOverrides = @{}
    }

    if (-not $ConfigPath) {
        return $result
    }

    if (-not (Test-Path -LiteralPath $ConfigPath)) {
        Write-Warning "Import-ScoutConfig: config path '$ConfigPath' does not exist -- falling back to the built-in default benchmark."
        return $result
    }

    $raw = $null
    try {
        $raw = Get-Content -LiteralPath $ConfigPath -Raw -ErrorAction Stop
    }
    catch {
        Write-Warning "Import-ScoutConfig: could not read '$ConfigPath': $($_.Exception.Message) -- falling back to the built-in default benchmark."
        return $result
    }

    $parsed = $null
    try {
        $parsed = $raw | ConvertFrom-Json -Depth 100 -ErrorAction Stop
    }
    catch {
        Write-Warning "Import-ScoutConfig: '$ConfigPath' is not valid JSON ($($_.Exception.Message)) -- falling back to the built-in default benchmark."
        return $result
    }

    if ($null -eq $parsed -or $parsed -isnot [System.Management.Automation.PSCustomObject]) {
        Write-Warning "Import-ScoutConfig: '$ConfigPath' did not parse to a JSON object -- falling back to the built-in default benchmark."
        return $result
    }

    $result.Source     = 'File'
    $result.ConfigPath = (Resolve-Path -LiteralPath $ConfigPath).ProviderPath

    if ($parsed.PSObject.Properties['benchmark'] -and $null -ne $parsed.benchmark) {
        $result.Benchmark = $parsed.benchmark
    }

    if ($parsed.PSObject.Properties['rulePatterns'] -and $null -ne $parsed.rulePatterns) {
        $result.RulePatterns = @($parsed.rulePatterns | ForEach-Object { [string]$_ })
    }

    if ($parsed.PSObject.Properties['ruleOverrides'] -and $null -ne $parsed.ruleOverrides) {
        $overrides = @{}
        foreach ($prop in $parsed.ruleOverrides.PSObject.Properties) {
            $overrides[$prop.Name] = $prop.Value
        }
        $result.RuleOverrides = $overrides
    }

    return $result
}