<#
.SYNOPSIS
  Reference orchestrator: source a GE-Enforce manifest from shopdb, run the
  UNCHANGED engine against it, and report the result back to shopdb.

  This is a thin wrapper around Install-FromManifest.ps1 (the engine). It is a
  reference a site adapts into its GE-Enforce.ps1 flow; it is not the live
  dispatcher. The engine, detection, self-heal, and SMB payload resolution are
  untouched - only the source of the manifest JSON moves from a share file to
  shopdb, plus a result report.

.PARAMETER Scope
  The imaging pc-type / scope name (e.g. gea-shopfloor-cmm), same value the
  dispatcher already resolves from C:\Enrollment\pc-type.txt.

.PARAMETER EnginePath
  Path to Install-FromManifest.ps1 (the engine lib, >= 2.6).

.PARAMETER ShareManifestPath
  The current on-share manifest for this scope. In shadow mode the engine runs
  against THIS (unchanged behavior) and shopdb is only compared + reported. Once
  cut over, omit it and the engine runs against the shopdb-sourced manifest.

.PARAMETER ShadowMode
  Fetch + compare + report, but install from the share (no behavior change).

.PARAMETER IncludeCommon
  Also fetch the fleet-wide CommonScope and merge it into -Scope, so the PC
  enforces its own scope entries PLUS common's (on a Name conflict the -Scope
  entry wins). OFF by default: a scope is enforced ALONE (self-sufficient).
  Displays are self-sufficient and do NOT set this. Only a share-less non-display
  PC that genuinely needs the fleet-wide common entries over HTTPS turns it on.

.PARAMETER CommonScope
  Name of the fleet-wide scope merged when -IncludeCommon is set (default
  'common'). Ignored without -IncludeCommon.

.NOTES
  Fail-safe: any error exits 0 so a bad web app never blocks or breaks a PC.
  Config comes from HKLM:\SOFTWARE\GE\ShopDB (BaseUrl, ApiToken) - see the psm1.
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string]$Scope,
    [Parameter(Mandatory)] [string]$EnginePath,
    [string]$ShareManifestPath,
    [switch]$ShadowMode,
    [switch]$IncludeCommon,
    [string]$CommonScope = 'common',
    [string]$BaseUrl,
    [string]$ApiToken,
    [string]$LogFile = "C:\Logs\Shopfloor\shopdb-enforce-$(Get-Date -Format yyyyMMdd).log"
)

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
    try {
        $logDir = Split-Path -Parent $LogFile
        if ($logDir -and -not (Test-Path $logDir)) {
            New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
        }
        Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue
    } catch {}
    Write-Host $line
}

function Write-ShopdbEventLog {
    <#
      Write a Windows Application event-log entry under source 'ShopdbEnforce'.
      Used to make an otherwise silent fail-safe (no manifest and an empty cache)
      observable to whoever watches the display. Best-effort: registering the
      source needs admin, which the SYSTEM scheduled task has; any failure is
      swallowed so it can never break the fail-safe.
    #>
    param([string]$Message,
          [string]$EntryType = 'Warning',
          [int]$EventId = 1001)
    $source = 'ShopdbEnforce'
    try {
        if (-not [System.Diagnostics.EventLog]::SourceExists($source)) {
            New-EventLog -LogName Application -Source $source -ErrorAction Stop
        }
        Write-EventLog -LogName Application -Source $source -EntryType $EntryType `
            -EventId $EventId -Message $Message -ErrorAction Stop
    } catch {}
}

try {
    Import-Module (Join-Path $PSScriptRoot 'ShopdbEnforceClient.psm1') -Force

    $config = Get-ShopdbConfig -BaseUrl $BaseUrl -ApiToken $ApiToken
    if (-not $config) {
        Write-Log 'No shopdb BaseUrl configured yet - retry next cycle.' 'WARN'
        exit 0
    }

    $sync = Sync-ShopdbManifest -Scope $Scope -Config $config
    if (-not $sync.Path) {
        # Fail-safe stays (exit 0), but a fresh display with an empty cache would
        # otherwise enforce nothing SILENTLY. Surface it: a Windows event-log
        # entry plus a best-effort report ping so it shows in Enforcement Reports.
        $reason = if ($sync.Error) { $sync.Error } else { 'shopdb unreachable and no cached manifest' }
        Write-Log "No manifest available for $Scope ($reason)." 'WARN'
        Write-ShopdbEventLog -Message ("GE-Enforce could not fetch a manifest for scope '$Scope' and has no cached copy; nothing was enforced this cycle. Reason: $reason") -EntryType 'Error' -EventId 1001
        try {
            $failReport = New-ShopdbReport -Scope $Scope -AppliedVersion 0 -Summary @{
                Installed = 0; Skipped = 0; Failed = 1; Filtered = 0; EnforcerVersion = '2.6'
                Results = @(@{ Name = '(manifest-fetch)'; Action = 'failed'; Message = $reason })
            }
            if (Send-ShopdbReport -Config $config -Report $failReport) {
                Write-Log 'Reported empty-cache fetch failure to shopdb.'
            }
        } catch {}
        exit 0
    }
    Write-Log "Manifest for $Scope from $($sync.Source) (v$($sync.Version))."

    # Shadow mode: compare shopdb vs the share, but install from the share.
    if ($ShadowMode -and $ShareManifestPath -and (Test-Path $ShareManifestPath)) {
        $diff = Compare-ShopdbShadow -ShopdbManifestPath $sync.Path -ShareManifestPath $ShareManifestPath
        if ($diff.Same) {
            Write-Log 'Shadow: shopdb manifest matches the share.'
        } else {
            Write-Log ("Shadow DIFF: shopdb-only=[{0}] share-only=[{1}] orderDiff={2}" -f `
                ($diff.ShopdbOnly -join ','), ($diff.ShareOnly -join ','), $diff.OrderDiff) 'WARN'
        }
    }

    # Which manifest the engine actually runs against.
    if ($ShadowMode -and $ShareManifestPath) {
        # Shadow: install from the share exactly as today (no payload resolve,
        # no common merge - the share already carries its own common scope).
        #
        # GUARDED, because the share is a TRANSIENT mount. GE-Enforce.ps1 maps it
        # for the length of its cycle and unmounts at the end, so a caller that
        # runs on its own schedule finds the drive gone. Handing the engine a
        # path on a dead drive returned nothing usable and the summary zero-filled
        # to 0 installed / 0 skipped / 0 failed - a silent nothing that reads
        # exactly like a healthy no-op. Shadow must run INSIDE the enforce cycle;
        # this makes the alternative loud instead of invisible.
        if (-not (Test-Path -LiteralPath $ShareManifestPath)) {
            $reason = "share manifest not reachable at $ShareManifestPath " +
                      '(is the share still mounted? shadow must run inside the ' +
                      'enforce cycle)'
            Write-Log $reason 'WARN'
            Write-ShopdbEventLog -Message ("GE-Enforce shadow could not reach the share manifest for scope '$Scope': $reason") -EntryType 'Error' -EventId 1002
            try {
                $failReport = New-ShopdbReport -Scope $Scope -AppliedVersion 0 -Summary @{
                    Installed = 0; Skipped = 0; Failed = 1; Filtered = 0; EnforcerVersion = '2.6'
                    Results = @(@{ Name = '(share-manifest)'; Action = 'failed'; Message = $reason })
                }
                Send-ShopdbReport -Config $config -Report $failReport | Out-Null
            } catch {}
            exit 0
        }
        $manifestToRun = $ShareManifestPath
    } else {
        # Optional common-scope inheritance (OFF by default; displays are
        # self-sufficient). Only when -IncludeCommon is set do we fetch the
        # fleet-wide common scope (best-effort, same fail-safe cache) and merge
        # it in with the pctype winning on conflict. Skipped when this run IS
        # the common scope.
        $manifestToMerge = $sync.Path
        if ($IncludeCommon -and $CommonScope -and ($CommonScope -ine $Scope)) {
            $commonSync = Sync-ShopdbManifest -Scope $CommonScope -Config $config
            if ($commonSync.Path) {
                $manifestToMerge = Merge-ShopdbManifests -PrimaryManifestPath $sync.Path -CommonManifestPath $commonSync.Path
                if ($manifestToMerge -ne $sync.Path) {
                    Write-Log "Merged common scope '$CommonScope' (from $($commonSync.Source), v$($commonSync.Version)) into $Scope."
                }
            } else {
                Write-Log "Common scope '$CommonScope' unavailable (no fetch, no cache) - enforcing $Scope alone." 'WARN'
            }
        }

        # Cutover: stage any http/inline payloads to local files and rewrite the
        # manifest to point at them, so the UNCHANGED engine installs from local
        # (no SMB needed for share-less PCs).
        $manifestToRun = Resolve-ShopdbPayloads -ManifestPath $manifestToMerge -Config $config
        if ($manifestToRun -ne $manifestToMerge) {
            Write-Log "Resolved http/inline payloads to local files: $manifestToRun"
        }
    }

    # --- INTEGRATION POINT ---------------------------------------------------
    # Run the engine. EXPECTED ENGINE CONTRACT: Install-FromManifest.ps1 returns
    # a summary object (hashtable or PSCustomObject) carrying integer counts
    #   Installed / Skipped / Failed / Filtered
    # a string EnforcerVersion, and a Results list of per-entry outcomes
    #   @{ Name; Action; SelfHealed; ExitCode; Message }.
    # The engine may not honor that yet: it might return $null, a bare return
    # code, or emit several objects. ConvertTo-ShopdbSummary adapts whatever it
    # returns into a well-formed summary hashtable so the report stage always
    # gets clean input (we do NOT assume the engine was fixed).
    # The engine requires -InstallerRoot (base for any relative Source/Installer
    # path) and -LogFile. Shadow runs off the share, so relative paths resolve
    # against the share scope dir. Cutover rewrites payloads to ABSOLUTE local
    # paths, so InstallerRoot is only a harmless fallback base (the payload cache).
    if ($ShadowMode -and $ShareManifestPath) {
        $installerRoot = Split-Path -Parent $ShareManifestPath
    } else {
        $installerRoot = Join-Path (Split-Path -Parent $manifestToRun) 'payloads'
    }
    if ($installerRoot -and -not (Test-Path $installerRoot)) {
        New-Item -ItemType Directory -Path $installerRoot -Force -ErrorAction SilentlyContinue | Out-Null
    }
    $engineLog = $LogFile -replace '\.log$', '-engine.log'

    Write-Log "Running engine against $manifestToRun"
    $engineResult = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope `
        -InstallerRoot $installerRoot -LogFile $engineLog
    $summary = ConvertTo-ShopdbSummary -EngineResult $engineResult

    # Report the result (best-effort).
    $appliedVersion = 0
    if ($sync.Version) { [int]::TryParse($sync.Version, [ref]$appliedVersion) | Out-Null }
    $report = New-ShopdbReport -Scope $Scope -AppliedVersion $appliedVersion -Summary $summary
    if (Send-ShopdbReport -Config $config -Report $report) {
        Write-Log "Reported: installed=$($summary.Installed) skipped=$($summary.Skipped) failed=$($summary.Failed)."
    } else {
        Write-Log 'Report POST failed (non-fatal).' 'WARN'
    }
} catch {
    Write-Log "Unhandled error (non-fatal): $($_.Exception.Message)" 'ERROR'
}
exit 0
