#!/usr/bin/env pwsh
# Setup-OpenText.ps1 - OpenText HostExplorer 15 SP1 ShopFloor installer + profile
# deployment, callable from both PXE PreInstall and Intune DSC paths.
#
# WHY THIS EXISTS:
#   The vendor-supplied OpenText.exe (Inno Setup wrapper built by WJDT) bundles
#   the install steps but its [Files] section deploys per-user content to
#   {userappdata} - which resolves to SYSTEM's profile under DSC and to a single
#   user under PreInstall. As a result the operator (logging in via Azure AD)
#   never sees the profiles, keymaps, menus, or macros - only the installed
#   binaries. This script replaces OpenText.exe entirely, doing the same install
#   steps via direct msiexec calls AND fanning the per-user content out to:
#     - %ProgramData%\Hummingbird\Connectivity\15.00\Shared\
#     - C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00\
#     - Each existing user profile under C:\Users\
#
# INVOKED BY:
#   - PreInstall: Setup-OpenText.cmd wrapper (because the runner only knows MSI/EXE)
#   - DSC:        Install-OpenText.ps1 downloads the bundled tree from blob, then
#                 invokes this script with -SourceDir <temp dir>
#
# DETECTION:
#   Skips if HKLM:\SOFTWARE\GE\OpenText\Installed = $expectedVersion. Marker is
#   written at the end of a successful run, so the runner / DSC wrapper can
#   no-op on subsequent invocations.

[CmdletBinding()]
param(
    # Override when invoked from a temp dir (DSC path) where the bundled files were
    # just downloaded. Defaults to $PSScriptRoot, but resolved INSIDE the script body
    # below - PowerShell evaluates `param([string]$X = $PSScriptRoot)` at parameter-
    # binding time, when $PSScriptRoot may not yet be populated, so the default winds
    # up as an empty string. Setting it in the body works because $PSScriptRoot is
    # reliably populated by then.
    [string]$SourceDir
)

$ErrorActionPreference = 'Stop'

if (-not $SourceDir) {
    $SourceDir = $PSScriptRoot
}

# --- Inline site-config reader (this script runs from C:\PreInstall\installers\opentext\,
# NOT from C:\Enrollment\shopfloor-setup\, so it can't dot-source Get-PCProfile.ps1) ---
function Get-SiteConfig {
    $configPath = 'C:\Enrollment\site-config.json'
    if (-not (Test-Path $configPath)) { return $null }
    try {
        return Get-Content $configPath -Raw | ConvertFrom-Json
    } catch {
        return $null
    }
}

# --- Logging (set up FIRST so any startup error - missing version.txt, broken
# bundled file, etc. - lands in the log file instead of disappearing into the
# runner's stdout void) ---
$logDir  = 'C:\Logs\PreInstall'
$logFile = Join-Path $logDir 'Setup-OpenText.log'
$msiLog  = Join-Path $logDir 'Setup-OpenText-msi.log'
$mspLog  = Join-Path $logDir 'Setup-OpenText-msp.log'

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

function Write-SetupLog {
    param([string]$Message)
    $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
    Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
    Write-Host $line
}

Write-SetupLog "================================================================"
Write-SetupLog "=== Setup-OpenText.ps1 starting ==="
Write-SetupLog "================================================================"
Write-SetupLog "SourceDir: $SourceDir"
Write-SetupLog "PSScriptRoot: $PSScriptRoot"
Write-SetupLog "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"

# --- Config ---
# Version is read from version.txt next to this script. ONE source of truth: bumping
# version.txt is the only edit needed when shipping a new OpenText build. Setup-
# OpenText.ps1 itself, Install-OpenText.ps1 (DSC wrapper), and the registry marker
# all derive their notion of "expected version" from this file.
$versionFile = Join-Path $SourceDir 'version.txt'
Write-SetupLog "Looking for version.txt at: $versionFile"
if (-not (Test-Path $versionFile)) {
    Write-SetupLog "ERROR: version.txt not found in $SourceDir - cannot determine expected version."
    Write-SetupLog "Directory listing of $SourceDir :"
    if (Test-Path $SourceDir) {
        Get-ChildItem $SourceDir -ErrorAction SilentlyContinue | ForEach-Object {
            Write-SetupLog "  $($_.Name)  $($_.Length) bytes"
        }
    } else {
        Write-SetupLog "  (SourceDir does not exist)"
    }
    exit 1
}
$expectedVersion = (Get-Content -Path $versionFile -Raw -ErrorAction Stop).Trim()
if (-not $expectedVersion) {
    Write-SetupLog "ERROR: version.txt at $versionFile is empty."
    exit 1
}
Write-SetupLog "Expected version (from version.txt): $expectedVersion"

# --- Detection: skip if already deployed at expected version ---
$markerKey = 'HKLM:\SOFTWARE\GE\OpenText'
$markerVal = 'Installed'
if (Test-Path $markerKey) {
    $installed = (Get-ItemProperty -Path $markerKey -Name $markerVal -ErrorAction SilentlyContinue).$markerVal
    if ($installed -eq $expectedVersion) {
        Write-SetupLog "OpenText $expectedVersion already deployed (marker present) - skipping."
        exit 0
    }
    Write-SetupLog "OpenText marker mismatch (found '$installed', expected '$expectedVersion') - re-deploying."
}
else {
    Write-SetupLog "OpenText marker not present - first install."
}

# --- Verify bundled files are where we expect ---
$msiPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.msi'
$cabPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.cab'
$mspPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64_ServicePack1.msp'
$mstPath = Join-Path $SourceDir 'ShopFloorx64.mst'

foreach ($f in @($msiPath, $cabPath, $mspPath, $mstPath)) {
    if (-not (Test-Path $f)) {
        Write-SetupLog "ERROR: required file not found: $f"
        exit 1
    }
}

# --- Step 1: Install the base MSI with the ShopFloor transform ---
# NOTE: We deliberately do NOT pass REBOOT=ReallySuppress here even though we do
# for the VC++ MSIs. OpenText HostExplorer installs shell extensions that hook
# explorer.exe, and the MSI uses Restart Manager to ask explorer to close so the
# in-use shell DLLs can be replaced. With REBOOT=ReallySuppress, RM closes
# explorer.exe but interprets "restart explorer" as a reboot action and refuses
# to relaunch it - leaving the user without a desktop. /norestart on its own
# prevents the actual Windows reboot but lets RM cleanly close-and-relaunch
# explorer mid-install. msiexec still returns 3010 ("reboot would be needed"),
# which we treat as success below.
Write-SetupLog ""
Write-SetupLog "Step 1: Installing OpenTextHostExplorer15x64.msi with ShopFloorx64.mst..."
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }

$msiArgs = "/i `"$msiPath`" TRANSFORMS=`"$mstPath`" /qn /norestart /L*v `"$msiLog`""
Write-SetupLog "  msiexec.exe $msiArgs"

$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName       = 'msiexec.exe'
$psi.Arguments      = $msiArgs
$psi.UseShellExecute = $false
$psi.CreateNoWindow  = $true
$proc = [System.Diagnostics.Process]::Start($psi)
$proc.WaitForExit()
$msiExit = $proc.ExitCode
Write-SetupLog "  msiexec exit code: $msiExit"

if ($msiExit -ne 0 -and $msiExit -ne 3010) {
    Write-SetupLog "ERROR: base MSI install failed (exit $msiExit). See $msiLog"
    exit 1
}
if ($msiExit -eq 3010) {
    Write-SetupLog "  (3010 = reboot needed but suppressed)"
}

# --- Step 2: Apply Service Pack 1 patch ---
# Same Restart Manager rationale as Step 1 - skip REBOOT=ReallySuppress so RM
# can relaunch explorer.exe after replacing the patched shell extension DLLs.
Write-SetupLog ""
Write-SetupLog "Step 2: Applying SP1 patch..."
if (Test-Path $mspLog) { Remove-Item $mspLog -Force -ErrorAction SilentlyContinue }

$mspArgs = "/p `"$mspPath`" /qn /norestart /L*v `"$mspLog`""
Write-SetupLog "  msiexec.exe $mspArgs"

$psi.Arguments = $mspArgs
$proc = [System.Diagnostics.Process]::Start($psi)
$proc.WaitForExit()
$mspExit = $proc.ExitCode
Write-SetupLog "  msiexec exit code: $mspExit"

if ($mspExit -ne 0 -and $mspExit -ne 3010) {
    Write-SetupLog "ERROR: SP1 patch failed (exit $mspExit). See $mspLog"
    exit 1
}

# --- Step 3: Deploy profiles, keymaps, menus, accessories ---
# Source layout (bundled with this script):
#   $SourceDir\Profile\*.hep
#   $SourceDir\Accessories\EB\*.eb*
#   $SourceDir\HostExplorer\Keymap\*.kmv
#   $SourceDir\HostExplorer\Menu\*.hmv
#
# Target layouts (Hummingbird-canonical):
#   %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Profile\
#   %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Accessories\EB\
#   %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Keymap\
#   %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Menu\
#   <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Profile\
#   <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Accessories\EB\
#   <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Keymap\
#   <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Menu\
#
# We deploy to ProgramData\Shared (system-wide fallback), Default User (template
# inherited by every NEW user profile), and every existing user profile (so
# already-created accounts like SupportUser get them immediately).

Write-SetupLog ""
Write-SetupLog "Step 3: Deploying profiles/keymaps/menus/macros..."

# --- Resolve exclude lists from site-config.json (falls back to West Jefferson defaults) ---
$siteConfig = Get-SiteConfig
$profileExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeProfiles) {
    @($siteConfig.opentext.excludeProfiles)
} else {
    @('WJ_Office.hep', 'IBM_qks.hep', 'mmcs.hep')  # West Jefferson defaults
}
$shortcutExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeShortcuts) {
    @($siteConfig.opentext.excludeShortcuts)
} else {
    @('WJ_Office.lnk', 'IBM_qks.lnk', 'mmcs.lnk')
}
if ($siteConfig) {
    Write-SetupLog "Site config loaded - profile excludes: $($profileExcludes -join ', ')"
    Write-SetupLog "Site config loaded - shortcut excludes: $($shortcutExcludes -join ', ')"
} else {
    Write-SetupLog "No site-config.json found - using West Jefferson defaults for excludes"
}

# Map of source subdir -> destination subdir relative to the Hummingbird root.
# Optional Exclude list drops specific filenames from both the source-to-dest
# copy AND from the destination if they were left over from a prior install.
$contentMap = @(
    @{
        Src = 'Profile'
        Dst = 'Profile'
        Exclude = $profileExcludes
    }
    @{ Src = 'Accessories\EB';       Dst = 'Accessories\EB' }
    @{ Src = 'HostExplorer\Keymap';  Dst = 'HostExplorer\Keymap' }
    @{ Src = 'HostExplorer\Menu';    Dst = 'HostExplorer\Menu' }
)

function Copy-HummingbirdContent {
    param(
        [string]$RootDst,    # Hummingbird root, e.g. C:\ProgramData\Hummingbird\Connectivity\15.00\Shared
        [string]$Label
    )
    foreach ($entry in $contentMap) {
        $srcPath = Join-Path $SourceDir $entry.Src
        if (-not (Test-Path $srcPath)) { continue }
        $dstPath = Join-Path $RootDst $entry.Dst
        New-Item -Path $dstPath -ItemType Directory -Force | Out-Null

        # Remove any previously-deployed excluded files from the destination
        # - handles the case where a PC got them from an older install.
        if ($entry.Exclude) {
            foreach ($name in $entry.Exclude) {
                $stale = Join-Path $dstPath $name
                if (Test-Path -LiteralPath $stale) {
                    try {
                        Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
                        Write-SetupLog "  $Label : removed stale $name"
                    } catch {
                        Write-SetupLog "  $Label : failed to remove $stale : $_"
                    }
                }
            }
        }

        $files = Get-ChildItem -Path $srcPath -File -ErrorAction SilentlyContinue
        if ($entry.Exclude) {
            $files = @($files | Where-Object { $entry.Exclude -notcontains $_.Name })
        }
        foreach ($f in $files) {
            Copy-Item -Path $f.FullName -Destination $dstPath -Force
        }
        Write-SetupLog "  $Label : $($entry.Src) -> $dstPath ($($files.Count) files)"
    }
}

# 3a. ProgramData Shared
$sharedRoot = Join-Path $env:ProgramData 'Hummingbird\Connectivity\15.00\Shared'
Copy-HummingbirdContent -RootDst $sharedRoot -Label 'Shared'

# 3b. Default User (template for new user profiles)
$defaultUserRoot = 'C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00'
Copy-HummingbirdContent -RootDst $defaultUserRoot -Label 'Default User'

# 3c. Every existing user profile under C:\Users\
$skipNames = @('Default', 'Default User', 'Public', 'defaultuser0', 'All Users', 'WDAGUtilityAccount')
$userDirs = Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue |
            Where-Object { $skipNames -notcontains $_.Name -and (Test-Path "$($_.FullName)\AppData\Roaming") }

foreach ($u in $userDirs) {
    $userRoot = Join-Path $u.FullName 'AppData\Roaming\Hummingbird\Connectivity\15.00'
    Copy-HummingbirdContent -RootDst $userRoot -Label $u.Name
}

# --- Step 4: Public Desktop shortcuts ---
# Uses $shortcutExcludes (resolved from site-config.json above) to skip
# deploying unwanted .lnk files AND remove any a prior install left behind.
Write-SetupLog ""
Write-SetupLog "Step 4: Deploying public desktop shortcuts..."
$shortcutSrc = Join-Path $SourceDir 'W10shortcuts'
$publicDesktop = 'C:\Users\Public\Desktop'

# Clean up stale copies from prior installs first
foreach ($name in $shortcutExcludes) {
    $stale = Join-Path $publicDesktop $name
    if (Test-Path -LiteralPath $stale) {
        try {
            Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
            Write-SetupLog "  removed stale desktop shortcut: $name"
        } catch {
            Write-SetupLog "  failed to remove stale $stale : $_"
        }
    }
}

if (Test-Path $shortcutSrc) {
    $lnkFiles = Get-ChildItem -Path $shortcutSrc -Filter '*.lnk' -File -ErrorAction SilentlyContinue
    foreach ($l in $lnkFiles) {
        if ($shortcutExcludes -contains $l.Name) {
            Write-SetupLog "  skip (excluded): $($l.Name)"
            continue
        }
        Copy-Item -Path $l.FullName -Destination $publicDesktop -Force
        Write-SetupLog "  $($l.Name) -> $publicDesktop"
    }
}

# --- Step 5: Write registry marker ---
Write-SetupLog ""
Write-SetupLog "Step 5: Writing registry marker..."
if (-not (Test-Path $markerKey)) {
    New-Item -Path $markerKey -Force | Out-Null
}
Set-ItemProperty -Path $markerKey -Name $markerVal -Value $expectedVersion -Force
Set-ItemProperty -Path $markerKey -Name 'InstalledAt' -Value (Get-Date -Format 'o') -Force

Write-SetupLog ""
Write-SetupLog "================================================================"
Write-SetupLog "=== OpenText HostExplorer ShopFloor $expectedVersion deployed ==="
Write-SetupLog "================================================================"
exit 0
