@echo off
setlocal EnableExtensions EnableDelayedExpansion

:: =============================================================================
:: Configuration
:: =============================================================================

set "TEMP_DIR=C:\temp"
set "MSI_PATH=C:\temp\FileWaveClient.msi"
set "UPGRADE_LOG=C:\temp\upgradeClient.log"
set "MSI_LOG=C:\temp\FileWaveClient.log"
set "REG_BACKUP=C:\temp\fwclient.reg"
set "LOCK_DIR=C:\temp\fwclient-upgrade.lock"
set "MSI_VERSION="

set "SERVICE_NAME=FileWaveWinClient"

set "CLIENT_BIN_64=C:\Program Files\FileWave\client\fwcld.exe"
set "CLIENT_BIN_32=C:\Program Files (x86)\FileWave\fwcld.exe"

set "CUSTOMISE_64=C:\Program Files\FileWave\client\customise.bat"
set "CUSTOMISE_32=C:\Program Files (x86)\FileWave\customise.bat"

set "PID_FILE=%ProgramData%\FileWave\FWClient\fwcld.pid"

set "LOG_MAX_BYTES=10485760"

set "START_DELAY_SECONDS=30"
set "LOCK_STALE_MINUTES=20"

set "SERVICE_STOP_ATTEMPTS=40"
set "SERVICE_STOP_WAIT_SECONDS=5"

set "PROCESS_STOP_TIMEOUT_SECONDS=30"
set "PROCESS_STOP_WAIT_SECONDS=2"

set "PID_DELETE_ATTEMPTS=80"
set "PID_DELETE_WAIT_SECONDS=2"

set "MSI_PREFLIGHT_ATTEMPTS=20"
set "MSI_PREFLIGHT_WAIT_SECONDS=15"

set "MSI_INSTALL_ATTEMPTS=5"
set "MSI_1618_ATTEMPTS=20"
set "MSI_1618_WAIT_SECONDS=60"

set "SERVICE_RUNNING_ATTEMPTS=24"
set "SERVICE_RUNNING_WAIT_SECONDS=5"

set "SERVICE_RECOVERY_RESET_SECONDS=86400"
set "SERVICE_RECOVERY_RESTART_DELAY_MS=60000"

:: =============================================================================
:: Main
:: =============================================================================

if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%"

call :trimlog "%MSI_LOG%"
call :trimlog "%UPGRADE_LOG%"

call :acquire_lock
if !ERRORLEVEL! NEQ 0 (
    call :writeout "Could not acquire upgrade lock. Exiting."
    exit /b 4
)

call :writeout "Waiting to start the upgrade process."
call :sleep %START_DELAY_SECONDS%
call :refresh_lock

if not exist "%MSI_PATH%" (
    call :writeout "MSI not found at %MSI_PATH%. Aborting before touching client."
    call :cleanup 10
)

call :read_msi_version
if !ERRORLEVEL! NEQ 0 (
    call :writeout "Could not read ProductVersion from %MSI_PATH%. Aborting before touching client."
    call :cleanup 11
)

call :writeout "Checking Windows Installer availability before stopping client."
call :wait_msi_available %MSI_PREFLIGHT_ATTEMPTS% %MSI_PREFLIGHT_WAIT_SECONDS%
if !ERRORLEVEL! NEQ 0 (
    call :writeout "Windows Installer remained busy before client stop. Aborting without disrupting client."
    call :cleanup 2
)

call :refresh_lock
call :ensure_customise_files

taskkill /F /T /IM fwGUI.exe >NUL 2>&1
call :writeout "Attempted to kill fwGUI.exe, exit code !ERRORLEVEL!."

taskkill /F /T /IM filewavekiosk.exe >NUL 2>&1
call :writeout "Attempted to kill filewavekiosk.exe, exit code !ERRORLEVEL!."

call :writeout "Disabling service auto restart."
sc failure "%SERVICE_NAME%" actions= none reset= 0 >NUL 2>&1
call :writeout "Disable service auto restart, exit code !ERRORLEVEL!."

call :stop_service
call :stop_processes
call :refresh_lock

call :sleep 10

reg export HKLM\SOFTWARE\WOW6432Node\FileWave\WinClient "%REG_BACKUP%" /y >> "%UPGRADE_LOG%" 2>&1
call :writeout "Attempted to export client configuration, exit code !ERRORLEVEL!."

call :delete_pid
if !ERRORLEVEL! NEQ 0 (
    call :writeout "PID deletion failed. Aborting and attempting recovery."
    call :fail 3
)

call :refresh_lock
call :install_client
set "INSTALL_RESULT=!ERRORLEVEL!"

:: Treat MSI success as provisional until the installed product version and service health are confirmed.
if "!INSTALL_RESULT!"=="0" (
    call :verify_installed_version
    if !ERRORLEVEL! NEQ 0 (
        set "INSTALL_RESULT=21"
    )
)

call :refresh_lock
call :post_install_recovery
set "RECOVERY_RESULT=!ERRORLEVEL!"
call :enable_recovery

if "!RECOVERY_RESULT!" NEQ "0" (
    call :writeout "Post-install recovery failed to confirm the client is running, exit code !RECOVERY_RESULT!."
)

if "!INSTALL_RESULT!"=="0" if "!RECOVERY_RESULT!"=="0" (
    call :writeout "Exit 0 - Upgrade done."
    call :cleanup 0
) else (
    if "!INSTALL_RESULT!"=="0" (
        call :writeout "Exit 20 - MSI reported success, but client service did not recover."
        call :cleanup 20
    ) else (
        call :writeout "Exit !INSTALL_RESULT! - Upgrade failed, recovery attempted."
        call :cleanup !INSTALL_RESULT!
    )
)

:: =============================================================================
:: Lock handling
:: =============================================================================

:acquire_lock
mkdir "%LOCK_DIR%" 2>NUL
if !ERRORLEVEL! EQU 0 (
    echo %DATE% %TIME% > "%LOCK_DIR%\created.txt"
    call :writeout "Acquired upgrade lock."
    exit /b 0
)

call :is_lock_stale "%LOCK_DIR%" %LOCK_STALE_MINUTES%
if !ERRORLEVEL! EQU 0 (
    call :writeout "Existing lock is stale. Removing it."
    rmdir /s /q "%LOCK_DIR%" >NUL 2>&1

    mkdir "%LOCK_DIR%" 2>NUL
    if !ERRORLEVEL! EQU 0 (
        echo %DATE% %TIME% > "%LOCK_DIR%\created.txt"
        call :writeout "Acquired upgrade lock after stale lock cleanup."
        exit /b 0
    )
)

call :writeout "Upgrade lock exists and is not stale."
exit /b 1

:is_lock_stale
set "LOCKPATH=%~1"
set "MAXAGE=%~2"

powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$p='%LOCKPATH%'; $max=%MAXAGE%; if (!(Test-Path $p)) { exit 0 }; $hb=Join-Path $p 'heartbeat.txt'; $created=Join-Path $p 'created.txt'; if (Test-Path $hb) { $stamp=(Get-Item $hb).LastWriteTime } elseif (Test-Path $created) { $stamp=(Get-Item $created).LastWriteTime } else { $stamp=(Get-Item $p).LastWriteTime }; $age=((Get-Date)-$stamp).TotalMinutes; if ($age -gt $max) { exit 0 } else { exit 1 }"
exit /b !ERRORLEVEL!

:refresh_lock
if exist "%LOCK_DIR%" (
    echo %DATE% %TIME% > "%LOCK_DIR%\heartbeat.txt"
)
exit /b 0

:: =============================================================================
:: Service and process handling
:: =============================================================================

:stop_service
set /a count=0

:stop_service_loop
set /a count+=1

call :service_state_is 1
if !ERRORLEVEL! EQU 0 (
    call :writeout "%SERVICE_NAME% is STOPPED."
    exit /b 0
)

call :service_state_is 3
if !ERRORLEVEL! EQU 0 (
    call :writeout "%SERVICE_NAME% is STOP_PENDING."
) else (
    sc stop "%SERVICE_NAME%" >NUL 2>&1
    set "STOP_ERR=!ERRORLEVEL!"
    call :writeout "Attempted to stop %SERVICE_NAME%, exit code !STOP_ERR!."

    if "!STOP_ERR!"=="1060" (
        call :writeout "Service does not exist."
        exit /b 0
    )

    if "!STOP_ERR!"=="1062" (
        call :writeout "Service is not active."
        exit /b 0
    )
)

if !count! GEQ %SERVICE_STOP_ATTEMPTS% (
    call :writeout "Tried %SERVICE_STOP_ATTEMPTS% times to stop the service - moving on to process kill."
    exit /b 1
)

call :sleep %SERVICE_STOP_WAIT_SECONDS%
goto stop_service_loop

:stop_processes
call :wait_process_stop fwcld.exe %PROCESS_STOP_TIMEOUT_SECONDS%

tasklist /FI "IMAGENAME eq fwcld.exe" | find /I "fwcld.exe" >NUL
if !ERRORLEVEL! NEQ 1 (
    taskkill /F /T /IM fwcld.exe >NUL 2>&1
    call :writeout "Attempted to kill fwcld.exe, exit code !ERRORLEVEL!."
)

tasklist /FI "IMAGENAME eq fwzmqbroker.exe" | find /I "fwzmqbroker.exe" >NUL
if !ERRORLEVEL! NEQ 1 (
    taskkill /F /T /IM fwzmqbroker.exe >NUL 2>&1
    call :writeout "Attempted to kill fwzmqbroker.exe, exit code !ERRORLEVEL!."
)

exit /b 0

:wait_process_stop
set "PROCESS_NAME=%~1"
set /a timeout=%~2

call :writeout "Waiting for %PROCESS_NAME% to stop."

:wait_process_loop
tasklist /FI "IMAGENAME eq %PROCESS_NAME%" | find /I "%PROCESS_NAME%" >NUL
if !ERRORLEVEL! EQU 1 (
    call :writeout "Process %PROCESS_NAME% has stopped."
    exit /b 0
)

if !timeout! LEQ 0 (
    call :writeout "Timeout reached. %PROCESS_NAME% did not stop."
    exit /b 1
)

call :writeout "Still waiting for %PROCESS_NAME% to stop."
call :sleep %PROCESS_STOP_WAIT_SECONDS%
set /a timeout-=%PROCESS_STOP_WAIT_SECONDS%
goto wait_process_loop

:wait_service_running
set /a svc_count=0

:wait_service_running_loop
set /a svc_count+=1

call :service_state_is 4
if !ERRORLEVEL! EQU 0 (
    call :writeout "%SERVICE_NAME% is RUNNING."
    exit /b 0
)

if !svc_count! GEQ %SERVICE_RUNNING_ATTEMPTS% (
    call :writeout "%SERVICE_NAME% did not reach RUNNING."
    exit /b 1
)

call :sleep %SERVICE_RUNNING_WAIT_SECONDS%
goto wait_service_running_loop

:: =============================================================================
:: MSI handling
:: =============================================================================

:install_client
set /a count=0
set /a busy_count=0
set "NEEDS_SCRUB=0"
set "SCRUB_DONE=0"

:install_client_loop
set /a count+=1
call :sleep 2
call :refresh_lock

call :writeout "Installing the new FileWave Client, attempt !count!."
start /wait msiexec.exe /qn /norestart /l*v+ "%MSI_LOG%" /i "%MSI_PATH%"
set "MSI_EXIT=!ERRORLEVEL!"
call :writeout "Done installing the new FileWave Client, exit code !MSI_EXIT!."

if "!MSI_EXIT!"=="0" exit /b 0

if "!MSI_EXIT!"=="3010" (
    call :writeout "MSI returned 3010: success, reboot required."
    exit /b 0
)

if "!MSI_EXIT!"=="1638" (
    call :writeout "MSI returned 1638: another version already installed."
    exit /b 0
)

if "!MSI_EXIT!"=="1618" (
    set /a busy_count+=1
    if !busy_count! GEQ %MSI_1618_ATTEMPTS% (
        call :writeout "MSI returned 1618 too many times. Giving up and recovering client."
        exit /b 2
    )

    call :writeout "Another MSI transaction is active. Waiting before retry."
    call :sleep %MSI_1618_WAIT_SECONDS%
    call :refresh_lock
    set /a count-=1
    goto install_client_loop
)

:: A vanished original source can be explicit 1612, or a generic 1603 with source errors in the MSI log.
if "!MSI_EXIT!"=="1612" set "NEEDS_SCRUB=1"

if "!MSI_EXIT!"=="1603" (
    call :msi_log_indicates_missing_source
    if !ERRORLEVEL! EQU 0 (
        call :writeout "MSI returned 1603 and log indicates missing original install source."
        set "NEEDS_SCRUB=1"
    )
)

if "!NEEDS_SCRUB!"=="1" (
    if "!SCRUB_DONE!"=="0" (
        call :writeout "MSI source missing or registration broken. Scrubbing FileWave MSI registration before retry."
        call :scrubregistry
        set "SCRUB_DONE=1"
        set "NEEDS_SCRUB=0"
    ) else (
        call :writeout "MSI source issue detected again after registry scrub."
        set "NEEDS_SCRUB=0"
    )
)

if !count! GEQ %MSI_INSTALL_ATTEMPTS% (
    call :writeout "Tried %MSI_INSTALL_ATTEMPTS% times to install the new MSI - considering upgrade failed."
    exit /b 1
)

goto install_client_loop

:wait_msi_available
set /a max_attempts=%~1
set /a wait_seconds=%~2
set /a attempt=0

:wait_msi_available_loop
set /a attempt+=1

call :msi_available
if !ERRORLEVEL! EQU 0 (
    call :writeout "Windows Installer mutex is available."
    exit /b 0
)

call :writeout "Windows Installer is busy, attempt !attempt! of !max_attempts!."

if !attempt! GEQ !max_attempts! exit /b 1

call :sleep !wait_seconds!
call :refresh_lock
goto wait_msi_available_loop

:msi_available
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$created=$false; try { $m = New-Object System.Threading.Mutex($true, 'Global\_MSIExecute', [ref]$created); if ($created) { $m.ReleaseMutex(); $m.Dispose(); exit 0 } else { if ($m) { $m.Dispose() }; exit 1 } } catch { exit 1 }"
exit /b !ERRORLEVEL!

:service_state_is
:: Match the numeric service state from sc.exe; avoids localized state labels and PowerShell in polling loops.
sc query "%SERVICE_NAME%" | findstr /R /C:":[ ]*%~1[ ]" >NUL
exit /b !ERRORLEVEL!

:read_msi_version
:: Read the expected version from the bundled MSI so post-install validation does not rely on the launcher argument.
set "MSI_VERSION="
for /f "usebackq delims=" %%v in (`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$path='%MSI_PATH%'; try { $wi=New-Object -ComObject WindowsInstaller.Installer; $db=$wi.GetType().InvokeMember('OpenDatabase','InvokeMethod',$null,$wi,@($path,0)); $view=$db.OpenView(\"SELECT Value FROM Property WHERE Property = 'ProductVersion'\"); $view.Execute(); $record=$view.Fetch(); if ($record) { $record.StringData(1); exit 0 }; exit 1 } catch { exit 1 }"`) do set "MSI_VERSION=%%v"
if not defined MSI_VERSION exit /b 1
call :writeout "MSI ProductVersion is !MSI_VERSION!."
exit /b 0

:verify_installed_version
set "INSTALLED_VERSION="
for /f "usebackq delims=" %%v in (`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$expected='%MSI_VERSION%'; $v=(Get-Package -Name 'FileWave Client' -ErrorAction SilentlyContinue).Version; if (!$v) { exit 1 }; $v; try { if ([version]$v -eq [version]$expected) { exit 0 } else { exit 2 } } catch { if ($v -eq $expected) { exit 0 } else { exit 2 } }"`) do set "INSTALLED_VERSION=%%v"
if not defined INSTALLED_VERSION (
    call :writeout "Unable to read installed FileWave Client version after MSI success."
    exit /b 1
)

if !ERRORLEVEL! EQU 0 (
    call :writeout "Installed FileWave Client version !INSTALLED_VERSION! matches MSI version."
    exit /b 0
)

call :writeout "Installed FileWave Client version !INSTALLED_VERSION! does not match MSI version !MSI_VERSION!."
exit /b 1

:msi_log_indicates_missing_source
:: Guard the registry scrub so generic 1603 failures are not treated as missing-source failures.
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$p='%MSI_LOG%'; if (!(Test-Path $p)) { exit 1 }; $patterns=@('\b1706\b','no valid source','installation source','source for this product','network resource','SourceList','cannot find.*\.msi','use source','failed to resolve source'); $text=Get-Content -Path $p -Raw -ErrorAction SilentlyContinue; foreach ($pattern in $patterns) { if ($text -match $pattern) { exit 0 } }; exit 1"
exit /b !ERRORLEVEL!

:scrubregistry
(
echo $paths = @^('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'^)
echo $apps = Get-ItemProperty $paths -ErrorAction SilentlyContinue ^| Where-Object { $_.DisplayName -like 'FileWave Client*' }
echo foreach ^($app in $apps^) {
echo     $c = $app.PSChildName -replace '[-{}]',''
echo     if ^($c.Length -eq 32^) {
echo         $pk = -join^($c[7..0]+$c[11..8]+$c[15..12]+$c[17,16,19,18,21,20,23,22,25,24,27,26,29,28,31,30]^)
echo         Remove-Item "HKLM:\SOFTWARE\Classes\Installer\Products\$pk" -Recurse -Force -ErrorAction SilentlyContinue
echo         Remove-Item $app.PSPath -Recurse -Force -ErrorAction SilentlyContinue
echo         Write-Output "Scrubbed old MSI registration for $($app.DisplayName)"
echo     }
echo }
) > "%TEMP_DIR%\scrub.ps1"

powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%TEMP_DIR%\scrub.ps1" >> "%UPGRADE_LOG%" 2>&1
call :writeout "Registry scrub exit code: !ERRORLEVEL!."

del /f /q "%TEMP_DIR%\scrub.ps1" >NUL 2>&1
exit /b 0

:: =============================================================================
:: Post-install recovery
:: =============================================================================

:post_install_recovery
call :client_binary_exists
set "BIN_EXISTS=!ERRORLEVEL!"
set "RECOVERY_STATUS=0"

if "!BIN_EXISTS!"=="0" (
    call :writeout "Starting service."
    sc start "%SERVICE_NAME%" >NUL 2>&1
    call :writeout "sc start exit code: !ERRORLEVEL!."
) else (
    call :writeout "FileWave Client binary not found. Skipping service start."
)

set "FWREG_MISSING=0"
reg query HKLM\Software\WOW6432Node\FileWave\WinClient /v server >NUL 2>&1 || set "FWREG_MISSING=1"

reg query HKLM\Software\WOW6432Node\FileWave\WinClient /v server 2>NUL | find /I "no.server.set" >NUL
set "SERVER_NO_SERVER_SET=!ERRORLEVEL!"

if "!FWREG_MISSING!"=="0" if "!SERVER_NO_SERVER_SET!"=="1" (
    call :writeout "The server address is correct."
) else (
    call :writeout "Fixing server address from backup."

    if "!BIN_EXISTS!"=="0" taskkill /F /T /IM fwcld.exe >NUL 2>&1

    if exist "%REG_BACKUP%" (
        reg import "%REG_BACKUP%" >NUL 2>&1
        call :writeout "Registry import exit code: !ERRORLEVEL!."
    ) else (
        call :writeout "Registry backup missing. Cannot restore server address."
    )

    if "!BIN_EXISTS!"=="0" (
        call :writeout "Restarting service after registry import."
        sc start "%SERVICE_NAME%" >NUL 2>&1
        call :writeout "sc start exit code: !ERRORLEVEL!."
    )
)

if "!BIN_EXISTS!"=="0" (
    call :wait_service_running
    set "RECOVERY_STATUS=!ERRORLEVEL!"
    if "!RECOVERY_STATUS!" NEQ "0" (
        call :writeout "Client binary exists, but %SERVICE_NAME% was not confirmed RUNNING."
    )
    exit /b !RECOVERY_STATUS!
)

exit /b 1

:enable_recovery
call :writeout "Re-enabling service auto restart."
sc config "%SERVICE_NAME%" start= auto >NUL 2>&1
call :writeout "Service automatic start configuration applied, exit code !ERRORLEVEL!."
sc failure "%SERVICE_NAME%" reset= %SERVICE_RECOVERY_RESET_SECONDS% actions= restart/%SERVICE_RECOVERY_RESTART_DELAY_MS%/restart/%SERVICE_RECOVERY_RESTART_DELAY_MS%/restart/%SERVICE_RECOVERY_RESTART_DELAY_MS% >NUL 2>&1
call :writeout "Service auto restart configuration applied, exit code !ERRORLEVEL!."
exit /b 0

:fail
set "FAIL_CODE=%~1"
call :writeout "Failure path entered with exit code %FAIL_CODE%."

call :enable_recovery

call :client_binary_exists
if !ERRORLEVEL! EQU 0 (
    call :writeout "Attempting to start client service during failure recovery."
    sc start "%SERVICE_NAME%" >NUL 2>&1
    call :writeout "Recovery sc start exit code: !ERRORLEVEL!."
    call :wait_service_running
    if !ERRORLEVEL! NEQ 0 (
        call :writeout "Failure recovery could not confirm %SERVICE_NAME% is RUNNING."
    )
)

call :cleanup %FAIL_CODE%

:: =============================================================================
:: Utility functions
:: =============================================================================

:ensure_customise_files
mkdir "C:\Program Files (x86)\FileWave" 2>NUL
if not exist "%CUSTOMISE_32%" type nul > "%CUSTOMISE_32%"

mkdir "C:\Program Files\FileWave" 2>NUL
mkdir "C:\Program Files\FileWave\client" 2>NUL
if not exist "%CUSTOMISE_64%" type nul > "%CUSTOMISE_64%"
exit /b 0

:client_binary_exists
if exist "%CLIENT_BIN_64%" exit /b 0
if exist "%CLIENT_BIN_32%" exit /b 0
exit /b 1

:delete_pid
call :writeout "Delete PID file."
set /a pid_count=0

:delete_pid_loop
set /a pid_count+=1

del /f "%PID_FILE%" >NUL 2>&1
set "DEL_ERR=!ERRORLEVEL!"

if "!DEL_ERR!" NEQ "0" (
    if exist "%PID_FILE%" (
        call :writeout "del returned error !DEL_ERR! on attempt !pid_count!."
    )
)

if "!DEL_ERR!"=="5" (
    call :writeout "Access denied on PID file."
    exit /b 1
)

if not exist "%PID_FILE%" exit /b 0

if !pid_count! GEQ %PID_DELETE_ATTEMPTS% (
    call :writeout "Failed to delete PID file after %PID_DELETE_ATTEMPTS% attempts."
    exit /b 1
)

call :sleep %PID_DELETE_WAIT_SECONDS%
goto delete_pid_loop

:trimlog
set "TRIM_FILE=%~1"
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$p='%TRIM_FILE%'; $max=%LOG_MAX_BYTES%; if (Test-Path $p) { $i=Get-Item $p; if ($i.Length -gt $max) { $b=[System.IO.File]::ReadAllBytes($p); [System.IO.File]::WriteAllBytes($p, $b[($b.Length-$max)..($b.Length-1)]) } }"
exit /b 0

:writeout
set "Message=%~1"
echo "%DATE% %TIME% == %Message%" >> "%UPGRADE_LOG%"
exit /b 0

:sleep
set /a pings=%~1 + 1
"%SystemRoot%\System32\ping.exe" 127.0.0.1 -n !pings! >NUL
exit /b 0

:cleanup
set "EXIT_CODE=%~1"
rmdir /s /q "%LOCK_DIR%" >NUL 2>&1
call :writeout "Exiting with code %EXIT_CODE%."
exit /b %EXIT_CODE%
