<#
.SYNOPSIS
  Client-side helpers for sourcing GE-Enforce manifests from shopdb and reporting
  results back. Site-neutral reference kit - deploy alongside GE-Enforce; do NOT
  hardcode any site here.

  This module does NOT replace Install-FromManifest.ps1 (the engine). It only
  changes where the manifest JSON comes from (shopdb HTTP instead of a share
  file) and adds a result report. The engine, detection, self-heal, and SMB
  payload resolution stay exactly as they are.

  Resilience mirrors GE-Enforce: any failure is non-fatal (fail-safe). If shopdb
  is unreachable the client enforces from the last-known-good cached manifest and
  a PC is never left unmanaged because the web app is down.

  Config (params override registry): HKLM:\SOFTWARE\GE\ShopDB
    BaseUrl   e.g. https://shopdb.example.geaerospace.net
    ApiToken  a geenforce.fetch (+ geenforce.report) managed service token,
              provisioned the same way as SFLD creds (Azure DSC).
#>

Set-StrictMode -Version Latest

function Get-ShopdbConfig {
    param([string]$BaseUrl, [string]$ApiToken)
    $regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
    if ((-not $BaseUrl -or -not $ApiToken) -and (Test-Path $regPath)) {
        $props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
        if (-not $BaseUrl -and $props.BaseUrl) { $BaseUrl = $props.BaseUrl }
        if (-not $ApiToken -and $props.ApiToken) { $ApiToken = $props.ApiToken }
    }
    if (-not $BaseUrl -or -not $ApiToken) { return $null }
    return @{ BaseUrl = $BaseUrl.TrimEnd('/'); ApiToken = $ApiToken }
}

function Sync-ShopdbManifest {
    <#
      Fetch the current published manifest for a scope into a local cache, using
      an ETag so an unchanged manifest is a cheap 304. On any network error, fall
      back to the last-known-good cached copy. Returns:
        @{ Path; Version; Source }  where Source is
        'shopdb' | 'cache-304' | 'cache-lastgood' | $null (nothing available)
    #>
    param(
        [Parameter(Mandatory)] [string]$Scope,
        [Parameter(Mandatory)] [hashtable]$Config,
        [string]$CacheDir = 'C:\ProgramData\ShopDB\geenforce'
    )
    if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null }
    $manifestPath = Join-Path $CacheDir "$Scope.json"
    $etagPath     = Join-Path $CacheDir "$Scope.etag"
    $headers = @{ 'X-API-Key' = $Config.ApiToken }
    if (Test-Path $etagPath) { $headers['If-None-Match'] = (Get-Content -LiteralPath $etagPath -Raw).Trim() }

    $uri = "$($Config.BaseUrl)/api/geenforce/manifest?pctype=$([uri]::EscapeDataString($Scope))"
    try {
        $response = Invoke-WebRequest -Uri $uri -Headers $headers -UseBasicParsing `
            -TimeoutSec 30 -ErrorAction Stop
        if ($response.StatusCode -eq 200) {
            [System.IO.File]::WriteAllText($manifestPath, $response.Content)
            # PowerShell 7 returns header values as string arrays; 5.1 as scalars.
            # @(...)[0] yields a clean scalar in both.
            $etag = @($response.Headers['ETag'])[0]
            if ($etag) {
                Set-Content -LiteralPath $etagPath -Value $etag -NoNewline
            }
            $version = @($response.Headers['X-Manifest-Version'])[0]
            if ($version) {
                Set-Content -LiteralPath (Join-Path $CacheDir "$Scope.version") -Value $version -NoNewline
            }
            return @{ Path = $manifestPath; Version = $version; Source = 'shopdb' }
        }
    } catch {
        $status = $null
        if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
        if ($status -eq 304 -and (Test-Path $manifestPath)) {
            return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
        }
        # Network/other failure: fall back to last-known-good.
        if (Test-Path $manifestPath) {
            return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-lastgood' }
        }
        return @{ Path = $null; Version = $null; Source = $null }
    }
    # 304 without exception (some PS versions) -> use cache.
    if (Test-Path $manifestPath) {
        return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
    }
    return @{ Path = $null; Version = $null; Source = $null }
}

function Read-CachedVersion {
    param([string]$CacheDir, [string]$Scope)
    $verPath = Join-Path $CacheDir "$Scope.version"
    if (Test-Path $verPath) { return (Get-Content -LiteralPath $verPath -Raw).Trim() }
    return $null
}

function Compare-ShopdbShadow {
    <#
      Shadow-mode comparison: do the shopdb manifest and the on-share manifest
      select the same ordered entry names? Returns @{ Same; ShopdbOnly; ShareOnly;
      OrderDiff }. Behavioral, not byte, comparison.
    #>
    param([Parameter(Mandatory)][string]$ShopdbManifestPath,
          [Parameter(Mandatory)][string]$ShareManifestPath)
    $shopdb = (Get-Content -LiteralPath $ShopdbManifestPath -Raw | ConvertFrom-Json)
    $share  = (Get-Content -LiteralPath $ShareManifestPath  -Raw | ConvertFrom-Json)
    $shopdbNames = @($shopdb.Applications | ForEach-Object { $_.Name })
    $shareNames  = @($share.Applications  | ForEach-Object { $_.Name })
    return @{
        Same       = (($shopdbNames -join '|') -eq ($shareNames -join '|'))
        ShopdbOnly = @($shopdbNames | Where-Object { $_ -notin $shareNames })
        ShareOnly  = @($shareNames  | Where-Object { $_ -notin $shopdbNames })
        OrderDiff  = (($shopdbNames -join '|') -ne ($shareNames -join '|'))
    }
}

function Send-ShopdbReport {
    <#
      POST an enforcement report to shopdb. Best-effort: never throws, returns
      $true on success. Report is a hashtable matching POST /api/geenforce/report.
    #>
    param([Parameter(Mandatory)][hashtable]$Config,
          [Parameter(Mandatory)][hashtable]$Report)
    try {
        $body = ($Report | ConvertTo-Json -Depth 6)
        Invoke-RestMethod -Uri "$($Config.BaseUrl)/api/geenforce/report" `
            -Method Post -Headers @{ 'X-API-Key' = $Config.ApiToken } `
            -ContentType 'application/json' -Body $body -TimeoutSec 30 -ErrorAction Stop | Out-Null
        return $true
    } catch {
        return $false
    }
}

function New-ShopdbReport {
    <#
      Build a report payload from an engine summary. `Summary` is expected to
      carry Installed/Skipped/Failed/Filtered counts and a Results list of
      @{ Name; Action; SelfHealed; ExitCode; Message }. Shape the engine's own
      per-entry outcomes into this at the call site.
    #>
    param([string]$Hostname = $env:COMPUTERNAME,
          [Parameter(Mandatory)][string]$Scope,
          [int]$AppliedVersion,
          [Parameter(Mandatory)][hashtable]$Summary)
    return @{
        hostname        = $Hostname
        scopename       = $Scope
        appliedversion  = $AppliedVersion
        enforcerversion = $Summary.EnforcerVersion
        counts = @{
            installed = [int]$Summary.Installed
            skipped   = [int]$Summary.Skipped
            failed    = [int]$Summary.Failed
            filtered  = [int]$Summary.Filtered
        }
        results = @($Summary.Results)
    }
}

Export-ModuleMember -Function Get-ShopdbConfig, Sync-ShopdbManifest, `
    Compare-ShopdbShadow, Send-ShopdbReport, New-ShopdbReport, Read-CachedVersion
