# GE-Enforce.ps1 - Unified shopfloor enforcer.
#
# Replaces the per-type enforcers (CMM-Enforce, Keyence-Enforce,
# Machine-Enforce, Common-Enforce, Acrobat-Enforce). Runs as a single SYSTEM
# scheduled task with multiple triggers (at-logon, periodic 5-min, and the
# three shift-change windows).
#
# On each invocation:
#   1. Reads C:\Enrollment\pc-type.txt and pc-subtype.txt.
#   2. Mounts the tsgwp00525 SFLD share using creds from HKLM.
#   3. Processes common\manifest.json (always, with per-entry PCTypes filter).
#   4. Processes <pcType>\manifest.json if it exists (enforces type-specific
#      apps like CMM, Keyence, Display, Keyence, etc.).
#   5. Processes <pcType>-<subType>\manifest.json if it exists
#      (the "standard-machine" case).
#
# Graceful degradation:
#   - pc-type.txt missing -> log + exit 0 (PC is pre-imaging)
#   - SFLD creds missing -> log + exit 0 (Azure DSC hasn't provisioned yet)
#   - Share unreachable   -> log + exit 0 (off-network, retry next cycle)
#   - Per-entry install failure -> log + continue to next entry
#
# Always exits 0 so the scheduled task "last run result" stays clean. Truth
# is in the log: C:\Logs\Shopfloor\enforce-YYYYMMDD.log.

$ErrorActionPreference = 'Continue'

$installRoot = 'C:\Program Files\GE\Shopfloor'
$libPath     = Join-Path $installRoot 'lib\Install-FromManifest.ps1'
$logDir      = 'C:\Logs\Shopfloor'
$logFile     = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd'))
$driveLetter = 'W:'    # distinct from S: (shopfloor share), T:/U: (legacy enforcers)

if (-not (Test-Path $logDir)) {
    New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}

function Write-EnforceLog {
    param([string]$Message, [string]$Level = 'INFO')
    $line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
    Write-Host $line
    Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
}

Write-EnforceLog '================================================================'
Write-EnforceLog "=== GE-Enforce session start (PID $PID, user $env:USERNAME) ==="
Write-EnforceLog '================================================================'

# --- pc-type ---
$pcTypeFile    = 'C:\Enrollment\pc-type.txt'
$pcSubTypeFile = 'C:\Enrollment\pc-subtype.txt'
if (-not (Test-Path $pcTypeFile)) {
    Write-EnforceLog "pc-type.txt not found - PC is pre-imaging, nothing to enforce"
    exit 0
}
$pcType    = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
$pcSubType = if (Test-Path $pcSubTypeFile) {
    (Get-Content -LiteralPath $pcSubTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
} else { '' }
Write-EnforceLog "PCType: $pcType$(if ($pcSubType) { " / $pcSubType" })"

# --- site-config ---
$siteConfigFile = 'C:\Enrollment\site-config.json'
if (-not (Test-Path $siteConfigFile)) {
    Write-EnforceLog "site-config.json not found at $siteConfigFile" 'ERROR'
    exit 0
}
try {
    $siteConfig = Get-Content -LiteralPath $siteConfigFile -Raw | ConvertFrom-Json
} catch {
    Write-EnforceLog "site-config.json parse failed: $_" 'ERROR'
    exit 0
}

$shopfloorShareRoot = $siteConfig.shopfloorShareRoot
if (-not $shopfloorShareRoot) {
    # Fallback: derive from commonAppsSharePath if the new field isn't set.
    $capp = $siteConfig.common.commonAppsSharePath
    if ($capp) {
        # \\...\shared\dt\shopfloor\common\apps -> \\...\shared\dt\shopfloor
        $shopfloorShareRoot = ($capp -replace '\\common\\apps$', '')
    }
}
if (-not $shopfloorShareRoot) {
    Write-EnforceLog 'No shopfloorShareRoot derivable from site-config - nothing to enforce' 'ERROR'
    exit 0
}
Write-EnforceLog "Shopfloor share root: $shopfloorShareRoot"

# --- SFLD credential lookup (written by Azure DSC) ---
function Get-SFLDCredential {
    param([string]$ServerName)
    $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
    if (-not (Test-Path $basePath)) { return $null }
    foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) {
        $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
        if (-not $props -or -not $props.TargetHost) { continue }
        if ($props.TargetHost -eq $ServerName -or
            $props.TargetHost -like "$ServerName.*" -or
            $ServerName -like "$($props.TargetHost).*") {
            return @{
                Username   = $props.Username
                Password   = $props.Password
                TargetHost = $props.TargetHost
                KeyName    = $entry.PSChildName
            }
        }
    }
    return $null
}
$serverName = ($shopfloorShareRoot -replace '^\\\\', '') -split '\\' | Select-Object -First 1
$cred = Get-SFLDCredential -ServerName $serverName
if (-not $cred -or -not $cred.Username -or -not $cred.Password) {
    Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry next cycle"
    exit 0
}
Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))"

# --- Mount ---
& net use $driveLetter /delete /y 2>$null | Out-Null
$netResult = & net use $driveLetter $shopfloorShareRoot /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1
if ($LASTEXITCODE -ne 0) {
    Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" 'WARN'
    Write-EnforceLog 'Share unreachable - probably off-network. Will retry at next cycle.'
    exit 0
}
Write-EnforceLog "Mounted $shopfloorShareRoot as $driveLetter"

try {
    if (-not (Test-Path $libPath)) {
        Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" 'ERROR'
        return
    }

    # --- Process manifests in order: common, per-type, per-type+subtype ---
    $targets = @()

    $commonRoot     = Join-Path $driveLetter 'common'
    $commonManifest = Join-Path $commonRoot 'manifest.json'
    if (Test-Path $commonManifest) {
        $targets += [pscustomobject]@{ Label = 'common'; Manifest = $commonManifest; Root = $commonRoot }
    } else {
        Write-EnforceLog "common\manifest.json missing - skipping common scope" 'WARN'
    }

    $typeDir = $pcType.ToLower()
    if ($typeDir) {
        $typeRoot     = Join-Path $driveLetter $typeDir
        $typeManifest = Join-Path $typeRoot 'manifest.json'
        if (Test-Path $typeManifest) {
            $targets += [pscustomobject]@{ Label = $pcType; Manifest = $typeManifest; Root = $typeRoot }
        } else {
            Write-EnforceLog "$typeDir\manifest.json not on share - no type-specific apps for $pcType"
        }
    }

    if ($pcSubType) {
        $stDir = ($pcType + '-' + $pcSubType).ToLower()
        $stRoot     = Join-Path $driveLetter $stDir
        $stManifest = Join-Path $stRoot 'manifest.json'
        if (Test-Path $stManifest) {
            $targets += [pscustomobject]@{ Label = "$pcType-$pcSubType"; Manifest = $stManifest; Root = $stRoot }
        }
    }

    $scopeResults = @()
    foreach ($t in $targets) {
        Write-EnforceLog "---- Processing scope: $($t.Label) ----"
        Write-EnforceLog "     Manifest: $($t.Manifest)"
        Write-EnforceLog "     Root:     $($t.Root)"
        & $libPath -ManifestPath $t.Manifest -InstallerRoot $t.Root -LogFile $logFile -PCType $pcType -PCSubType $pcSubType
        $rc = $LASTEXITCODE
        Write-EnforceLog "     Install-FromManifest returned $rc"
        $scopeResults += [pscustomobject]@{ Label = $t.Label; ExitCode = $rc }
    }

    # ------------------------------------------------------------------
    # Status write-back to _outputs/logs/<hostname>/status.json on share.
    # Consumed by the management app / fleet dashboard to answer
    # "which PCs checked in, when, and what version did each install."
    # Writes under the mounted drive using SFLD creds; gracefully
    # continues if the share path is not writable.
    # ------------------------------------------------------------------
    try {
        $statusDir  = Join-Path (Join-Path $driveLetter '_outputs') (Join-Path 'logs' $env:COMPUTERNAME)
        if (-not (Test-Path $statusDir)) {
            New-Item -Path $statusDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
        }
        $statusFile = Join-Path $statusDir 'status.json'

        # Walk each processed manifest and pull current DetectionValue per entry.
        $installedVersions = @{}
        foreach ($t in $targets) {
            try {
                $cfg = Get-Content -LiteralPath $t.Manifest -Raw | ConvertFrom-Json
                foreach ($a in $cfg.Applications) {
                    if (-not $a.DetectionPath) { continue }
                    $key = "$($t.Label)/$($a.Name)"
                    $val = $null
                    switch ($a.DetectionMethod) {
                        'Registry' {
                            if ((Test-Path $a.DetectionPath) -and $a.DetectionName) {
                                $p = Get-ItemProperty -Path $a.DetectionPath -Name $a.DetectionName -ErrorAction SilentlyContinue
                                if ($p) { $val = "$($p.$($a.DetectionName))" }
                            }
                        }
                        'FileVersion' {
                            if (Test-Path $a.DetectionPath) {
                                $val = (Get-Item $a.DetectionPath).VersionInfo.FileVersion
                            }
                        }
                        'File'       { if (Test-Path $a.DetectionPath) { $val = 'present' } }
                        'Hash'       { if (Test-Path $a.DetectionPath) { $val = (Get-FileHash -Path $a.DetectionPath -Algorithm SHA256).Hash } }
                        'MarkerFile' { if (Test-Path $a.DetectionPath) { $val = 'marker present' } }
                        'Always'     { $val = 'n/a (Always)' }
                        'pnputil'    { $val = 'pnputil-managed' }
                    }
                    $installedVersions[$key] = $val
                }
            } catch {
                Write-EnforceLog "  status: manifest introspection for $($t.Label) failed: $_" 'WARN'
            }
        }

        $status = [ordered]@{
            hostname          = $env:COMPUTERNAME
            lastCheckIn       = (Get-Date).ToUniversalTime().ToString('o')
            pcType            = $pcType
            pcSubType         = $pcSubType
            enforcerVersion   = '2.0'
            shopfloorShareRoot = $shopfloorShareRoot
            scopesProcessed   = $scopeResults
            installedVersions = $installedVersions
        } | ConvertTo-Json -Depth 6
        Set-Content -Path $statusFile -Value $status -Encoding ascii -ErrorAction Stop
        Write-EnforceLog "Status written to $statusFile"
    } catch {
        Write-EnforceLog "Status write-back failed (share may be read-only for this user): $_" 'WARN'
    }
}
finally {
    & net use $driveLetter /delete /y 2>$null | Out-Null
    Write-EnforceLog "Unmounted $driveLetter"
    Write-EnforceLog '=== GE-Enforce session end ==='
}

exit 0
