ANWS.psm1

#
# ActiveNet Workstation Service (ANWS) PowerShell Module
# Version: 2.0.0
# Author: Justin Romanello
#

function Install-ANWS {
    <#
        .SYNOPSIS
            Installs or updates the ActiveNet Workstation Service (ANWS) with official hardening.

        .DESCRIPTION
            Installs the ActiveNet Workstation Service from a specified archive or downloads the latest
            distribution directly from ActiveNet. Configures enterprise hardening per ActiveNet technical
            documentation:
              - Enforces TLS 1.2 download transport.
              - Pre-emptively terminates locking ActiveNet/Java processes.
              - Dynamically locates MSI within nested zip archives.
              - Executes silent MSI installation with verbose transaction logging.
              - Applies Automatic Service Recovery configuration to both 'activenetworkstationservice'
                and 'COMSysApp' (Doc 000152060).
              - Enforces BUILTIN\Users Full Control on the installation directory (Doc 000157572).
              - Configures SDDL restart permissions for Authenticated Users.
              - Validates the localhost port 4444 WebSocket listener (Docs 000080830 & 000087155).

        .PARAMETER Source
            Specifies the path to an ANWS installation zip or installer package. Mandatory when Download is not specified.

        .PARAMETER Download
            When specified, downloads the latest ANWS package directly from ActiveNet.

        .PARAMETER Destination
            Working directory for downloading and extracting installer assets. Defaults to "$env:TEMP\ANWS_Install".

        .PARAMETER CreateShortcut
            When specified, creates a hidden restart batch file and a desktop shortcut ("Restart ActiveNet.lnk") on the Public Desktop.

        .EXAMPLE
            PS> Install-ANWS -Download

        .EXAMPLE
            PS> Install-ANWS -Download -Destination 'C:\Temp' -CreateShortcut

        .EXAMPLE
            PS> Install-ANWS -Source "\\Server\Share\activenetworkstationservice.zip"
    #>

    [CmdletBinding(DefaultParameterSetName='Source')]
    [Alias('Install-ActiveNet')]
    param (      
        [Parameter(ParameterSetName='Source', Mandatory=$true, Position=0)]
        [string]$Source, 

        [Parameter(ParameterSetName='Download', Mandatory=$true)]
        [switch]$Download,
        
        [Parameter(ParameterSetName='Source', Mandatory=$false)]
        [Parameter(ParameterSetName='Download', Mandatory=$false)]
        [string]$Destination = "$env:TEMP\ANWS_Install",

        [Parameter(Mandatory=$false)]
        [Alias('Shortcut')]
        [switch]$CreateShortcut
    )

    $serviceName = 'activenetworkstationservice'
    $displayName = 'ActiveNet Workstation Service'
    $installPath = if (${env:ProgramFiles(x86)}) {
        Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath $displayName
    } else {
        Join-Path -Path $env:ProgramFiles -ChildPath $displayName
    }
    $extractPath = Join-Path -Path $Destination -ChildPath 'Extract'
    $zipPath = Join-Path -Path $Destination -ChildPath "$serviceName.zip"
    $msiLog = Join-Path -Path $env:TEMP -ChildPath 'ANWS_msi_install.log'
    $transcriptLog = Join-Path -Path $env:TEMP -ChildPath "ANWS_Install_Log_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"

    try {
        Start-Transcript -Path $transcriptLog -Append -ErrorAction SilentlyContinue | Out-Null
    } catch {}

    Write-Verbose "=== ActiveNet Workstation Service Modernized Installation ==="
    Write-Verbose "Working Directory: $Destination"
    Write-Verbose "Target Install Path: $installPath"

    try {
        if (-not (Test-Path -Path $Destination)) {
            Write-Verbose "Creating directory '$Destination'..."
            New-Item -ItemType Directory -Path $Destination -Force | Out-Null
        }

        # Step 1: Acquire installer package (Download or Copy)
        if ($Download) {
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12

            $downloadUrl = "https://activenetmaint.active.com/upgrade/$serviceName.zip"
            Write-Verbose "Downloading ANWS package from '$downloadUrl'..."

            $downloadComplete = $false
            $maxRetries = 5
            $retryInterval = 2

            for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
                Write-Verbose "Download attempt $attempt of $maxRetries..."
                try {
                    if ($PSVersionTable.PSVersion.Major -ge 6) {
                        Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -TimeoutSec 120 -ErrorAction Stop
                    } else {
                        Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing -TimeoutSec 120 -ErrorAction Stop
                    }

                    if ((Test-Path -Path $zipPath) -and ((Get-Item -Path $zipPath).Length -gt 0)) {
                        Write-Verbose "Download verified ($((Get-Item -Path $zipPath).Length) bytes)."
                        $downloadComplete = $true
                        break
                    }
                } catch {
                    Write-Verbose "Invoke-WebRequest attempt failed: $($_.Exception.Message)"
                    try {
                        Write-Verbose "Attempting WebClient fallback..."
                        $webClient = New-Object System.Net.WebClient
                        $webClient.DownloadFile($downloadUrl, $zipPath)
                        if ((Test-Path -Path $zipPath) -and ((Get-Item -Path $zipPath).Length -gt 0)) {
                            Write-Verbose "WebClient download verified."
                            $downloadComplete = $true
                            break
                        }
                    } catch {
                        Write-Verbose "WebClient fallback failed: $($_.Exception.Message)"
                    }

                    if ($attempt -lt $maxRetries) {
                        Start-Sleep -Seconds $retryInterval
                    }
                }
            }

            if (-not $downloadComplete) {
                throw "Failed to download ActiveNet package from '$downloadUrl' after $maxRetries attempts."
            }
        } else {
            if (-not (Test-Path -Path $Source)) {
                throw "Specified Source path does not exist: '$Source'"
            }

            $sourceItem = Get-Item -Path $Source
            if ($sourceItem.Extension -ieq '.zip') {
                Write-Verbose "Copying source archive from '$Source' to '$Destination'..."
                Copy-Item -Path $Source -Destination $Destination -Force
                $zipPath = Join-Path -Path $Destination -ChildPath $sourceItem.Name
            } elseif ($sourceItem.Extension -ieq '.msi') {
                Write-Verbose "Source is a direct MSI file."
                $directMsiPath = $sourceItem.FullName
            } else {
                throw "Source file must be a .zip or .msi file. Provided: '$Source'"
            }
        }

        # Step 2: Extraction
        if (-not $directMsiPath) {
            if (-not (Test-Path -Path $zipPath)) {
                throw "Archive file '$zipPath' not found for extraction."
            }

            if (Test-Path -Path $extractPath) {
                Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue
            }
            New-Item -ItemType Directory -Path $extractPath -Force | Out-Null

            Write-Verbose "Extracting installer archive to '$extractPath'..."
            Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force

            $msiFile = Get-ChildItem -Path $extractPath -Filter '*.msi' -Recurse | Select-Object -First 1
            if (-not $msiFile) {
                throw "No MSI package (*.msi) found inside extracted archive at '$extractPath'."
            }
            $targetMsi = $msiFile.FullName
        } else {
            $targetMsi = $directMsiPath
        }

        Write-Verbose "Resolved MSI target: '$targetMsi'"

        # Step 3: Pre-install process & service cleanup
        Write-Verbose "Stopping service '$serviceName' if running..."
        Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue

        Write-Verbose "Terminating potential locking ActiveNet/Java processes..."
        $processNames = @('ActiveNet.ServiceContainer', 'ANWorkstationConfigurationUI', 'java', 'javaw')
        foreach ($pName in $processNames) {
            $foundProcs = Get-Process -Name $pName -ErrorAction SilentlyContinue
            foreach ($proc in $foundProcs) {
                $shouldKill = $false
                if ($pName -in @('java', 'javaw')) {
                    try {
                        $pPath = $proc.MainModule.FileName
                        if ($pPath -and ($pPath -like "*$installPath*" -or $pPath -like "*ActiveNet*")) {
                            $shouldKill = $true
                        }
                    } catch {
                        $shouldKill = $true
                    }
                } else {
                    $shouldKill = $true
                }

                if ($shouldKill) {
                    Write-Verbose "Terminating process '$($proc.ProcessName)' (PID: $($proc.Id))..."
                    Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
                }
            }
        }

        # Step 4: Silent MSI installation
        Write-Verbose "Executing silent MSI installation..."
        $msiArgs = @('/i', "`"$targetMsi`"", '/qn', '/norestart', '/l*v', "`"$msiLog`"")
        $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow

        if ($proc.ExitCode -notin @(0, 3010)) {
            throw "MSI installation failed with exit code $($proc.ExitCode). Review installer log at '$msiLog'."
        }
        Write-Verbose "MSI installation succeeded with exit code $($proc.ExitCode)."

        # Step 5: Official ActiveNet Hardening & Configuration
        # 5a. Automatic Service Recovery Options (Doc 000152060)
        Write-Verbose "Configuring Automatic Service Recovery for '$serviceName'..."
        & sc.exe failure $serviceName reset= 0 actions= restart/60000/restart/60000/restart/60000 2>&1 | Out-Null

        Write-Verbose "Configuring Automatic Service Recovery for COM+ System Application ('COMSysApp')..."
        & sc.exe failure COMSysApp reset= 0 actions= restart/60000/restart/60000/restart/60000 2>&1 | Out-Null

        # 5b. Startup Type Automatic and Service Start
        Write-Verbose "Setting '$serviceName' startup type to Automatic..."
        Set-Service -Name $serviceName -StartupType Automatic -ErrorAction SilentlyContinue

        Write-Verbose "Starting service '$serviceName'..."
        Start-Service -Name $serviceName -ErrorAction SilentlyContinue

        # 5c. Directory Permissions for BUILTIN\Users (Doc 000157572)
        if (Test-Path -Path $installPath) {
            Write-Verbose "Granting Full Control on '$installPath' to BUILTIN\Users (*S-1-5-32-545)..."
            & icacls.exe "$installPath" /grant "*S-1-5-32-545:(OI)(CI)F" /T /C /Q 2>&1 | Out-Null
        }

        # 5d. SDDL Restart Permissions for Authenticated Users
        Write-Verbose "Configuring Authenticated Users restart SDDL permissions..."
        $sddl = 'D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;RPWPCR;;;AU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)'
        & sc.exe sdset $serviceName $sddl 2>&1 | Out-Null

        # 5e. Desktop Shortcut Configuration
        if ($CreateShortcut) {
            Write-Verbose "Creating desktop restart shortcut..."
            Set-ANWSRestart
        }

        # Step 6: Cleanup temporary files
        Write-Verbose "Cleaning up temporary installation assets..."
        if (Test-Path -Path $extractPath) {
            Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue
        }
        if ($zipPath -and (Test-Path -Path $zipPath) -and ($zipPath -like "$Destination*")) {
            Remove-Item -Path $zipPath -Force -ErrorAction SilentlyContinue
        }

        # Step 7: Port 4444 Verification (Docs 000080830 & 000087155)
        Write-Verbose "Verifying localhost port 4444 WebSocket listener..."
        $portListening = $false
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        while ($sw.Elapsed.TotalSeconds -lt 15) {
            try {
                $tcpClient = New-Object System.Net.Sockets.TcpClient
                $asyncResult = $tcpClient.BeginConnect('127.0.0.1', 4444, $null, $null)
                if ($asyncResult.AsyncWaitHandle.WaitOne(1000, $false) -and $tcpClient.Connected) {
                    $tcpClient.EndConnect($asyncResult)
                    $tcpClient.Close()
                    $portListening = $true
                    break
                }
                $tcpClient.Close()
            } catch {}
            Start-Sleep -Milliseconds 500
        }
        $sw.Stop()

        # Step 8: Final Status Reporting
        $svcStatus = (Get-Service -Name $serviceName -ErrorAction SilentlyContinue).Status
        Write-Verbose "ActiveNet Workstation Service Status: $svcStatus | Port 4444 Listening: $portListening"
        Write-Output "ActiveNet Workstation Service installed successfully (Status: $svcStatus, Port 4444: $portListening)."
    } catch {
        Write-Error "ActiveNet Workstation Service installation failed: $($_.Exception.Message)"
        throw $_
    } finally {
        try {
            Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
        } catch {}
    }
}

function Set-ANWSRestart {
    <#
        .SYNOPSIS
            Grants restart permissions to Authenticated Users and creates a desktop restart shortcut.

        .DESCRIPTION
            Configures the ActiveNet Workstation Service (activenetworkstationservice) SDDL security
            descriptor so Authenticated Users can start and stop the service without local administrator
            rights. Generates a hidden batch script (restartanws.bat) and a desktop shortcut (Restart ActiveNet.lnk)
            for ease of use by front-desk and recreation staff.

        .PARAMETER ShortcutLocation
            Directory where the restart batch file and desktop shortcut are placed. Defaults to Public Desktop ($env:PUBLIC\Desktop).

        .EXAMPLE
            PS> Set-ANWSRestart

        .EXAMPLE
            PS> Set-ANWSRestart -ShortcutLocation "C:\Users\Public\Desktop"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false)]
        [string]$ShortcutLocation = "$env:PUBLIC\Desktop"
    )

    $serviceName = 'activenetworkstationservice'
    $sddl = 'D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)(A;;RPWPCR;;;AU)S:(AU;FA;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)'

    try {
        Write-Verbose "Applying SDDL restart permissions to service '$serviceName'..."
        $sdResult = & sc.exe sdset $serviceName $sddl 2>&1
        Write-Verbose "sc.exe sdset output: $sdResult"

        if (-not (Test-Path -Path $ShortcutLocation)) {
            Write-Verbose "Creating directory '$ShortcutLocation'..."
            New-Item -ItemType Directory -Path $ShortcutLocation -Force | Out-Null
        }

        # Create restart batch file
        $batPath = Join-Path -Path $ShortcutLocation -ChildPath 'restartanws.bat'
        $batContent = "@echo off`r`nnet stop $serviceName`r`nnet start $serviceName`r`n"
        Set-Content -Path $batPath -Value $batContent -Encoding ASCII -Force
        
        # Hide the batch file to prevent desktop clutter
        try {
            [System.IO.File]::SetAttributes($batPath, [System.IO.FileAttributes]::Hidden)
        } catch {
            Write-Verbose "Could not set hidden attribute on '$batPath': $($_.Exception.Message)"
        }

        # Create shortcut via WScript.Shell
        $lnkPath = Join-Path -Path $ShortcutLocation -ChildPath 'Restart ActiveNet.lnk'
        Write-Verbose "Creating shortcut at '$lnkPath'..."
        $wshShell = New-Object -ComObject 'WScript.Shell'
        $shortcut = $wshShell.CreateShortcut($lnkPath)
        $shortcut.TargetPath = $batPath
        $shortcut.WorkingDirectory = $ShortcutLocation
        $shortcut.Description = 'Restart ActiveNet Workstation Service'
        $shortcut.IconLocation = "$env:SystemRoot\System32\shell32.dll,238"
        $shortcut.Save()

        Write-Verbose "Restart permissions and desktop shortcut configured successfully at '$lnkPath'."
    } catch {
        Write-Error "Failed to configure ANWS restart permissions or shortcut: $($_.Exception.Message)"
        throw $_
    }
}

function Uninstall-ANWS {
    <#
        .SYNOPSIS
            Uninstalls the ActiveNet Workstation Service with hardened cleanup and corruption recovery.

        .DESCRIPTION
            Natively uninstalls the ActiveNet Workstation Service (ANWS) across Windows PowerShell 5.1
            and PowerShell 7+ without external subprocess dependencies. Detects product registrations via
            both 64-bit and WOW6432Node uninstall registry hives.

            Includes automated self-healing for common MSI Error 1603 conditions by recreating service stubs
            if a prior corrupted uninstallation deleted the Windows service while leaving MSI metadata intact.
            Gracefully stops services, terminates locking Java/ActiveNet processes, executes silent MSI removal,
            and validates full post-uninstallation cleanup of orphaned services, registry keys, directories,
            and desktop shortcuts.

        .EXAMPLE
            PS> Uninstall-ANWS

        .EXAMPLE
            PS> Uninstall-ActiveNet -Verbose
    #>

    [CmdletBinding()]
    [Alias('Uninstall-ActiveNet')]
    param()

    $serviceName = 'activenetworkstationservice'
    $displayName = 'ActiveNet Workstation Service'
    $installPath = if (${env:ProgramFiles(x86)}) {
        Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath $displayName
    } else {
        Join-Path -Path $env:ProgramFiles -ChildPath $displayName
    }
    $msiLog = Join-Path -Path $env:TEMP -ChildPath 'ANWS_msi_uninstall.log'

    Write-Verbose "Beginning uninstallation workflow for $displayName..."

    # Step 1: Detect installation in registry (64-bit and WOW6432Node)
    $regPaths = @(
        'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )

    $installedEntries = Get-ItemProperty -Path $regPaths -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -and ($_.DisplayName -like "*$displayName*" -or $_.DisplayName -like "*ActiveNet Workstation*") }

    $productCodes = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in $installedEntries) {
        Write-Verbose "Detected registry registration: '$($entry.DisplayName)' at $($entry.PSPath)"
        if ($entry.InstallLocation -and (Test-Path -Path $entry.InstallLocation)) {
            $installPath = $entry.InstallLocation
        }

        # Check for GUID product code in PSChildName or UninstallString
        if ($entry.PSChildName -match '^\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}$') {
            if (-not $productCodes.Contains($entry.PSChildName)) {
                $productCodes.Add($entry.PSChildName)
            }
        } elseif ($entry.UninstallString -match '\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}') {
            if (-not $productCodes.Contains($matches[0])) {
                $productCodes.Add($matches[0])
            }
        }
    }

    # Step 2: Handle corrupted state (MSI registered but Windows service missing -> causes MSI Error 1603)
    $serviceExists = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
    if ($productCodes.Count -gt 0 -and -not $serviceExists) {
        Write-Verbose "MSI product registered but Windows service '$serviceName' is missing. Recreating service stub to prevent MSI 1603..."
        $stubBinary = Join-Path -Path $installPath -ChildPath 'ActiveNet.ServiceContainer.exe'
        try {
            New-Service -Name $serviceName -DisplayName $displayName -BinaryPathName "`"$stubBinary`"" -StartupType Disabled -ErrorAction SilentlyContinue | Out-Null
            Write-Verbose "Dummy service stub successfully staged."
        } catch {
            Write-Verbose "Unable to create dummy service stub: $($_.Exception.Message)"
        }
    }

    # Step 3: Pre-uninstall process & service termination
    if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
        Write-Verbose "Stopping service '$serviceName'..."
        Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
    }

    Write-Verbose "Terminating potential locking processes..."
    $processNames = @('ActiveNet.ServiceContainer', 'ANWorkstationConfigurationUI', 'java', 'javaw')
    foreach ($pName in $processNames) {
        $foundProcs = Get-Process -Name $pName -ErrorAction SilentlyContinue
        foreach ($proc in $foundProcs) {
            $shouldKill = $false
            if ($pName -in @('java', 'javaw')) {
                try {
                    $pPath = $proc.MainModule.FileName
                    if ($pPath -and ($pPath -like "*$installPath*" -or $pPath -like "*ActiveNet*")) {
                        $shouldKill = $true
                    }
                } catch {
                    $shouldKill = $true
                }
            } else {
                $shouldKill = $true
            }

            if ($shouldKill) {
                Write-Verbose "Terminating locking process '$($proc.ProcessName)' (PID: $($proc.Id))..."
                Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
            }
        }
    }

    # Step 4: Silent MSI uninstallation
    if ($productCodes.Count -gt 0) {
        foreach ($code in $productCodes) {
            Write-Verbose "Executing MSI uninstallation for ProductCode '$code'..."
            $msiArgs = @('/x', $code, '/qn', '/norestart', '/l*v', "`"$msiLog`"")
            $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow
            Write-Verbose "msiexec /x exited with code $($proc.ExitCode)."
        }
    } else {
        Write-Verbose "No active MSI ProductCode entries detected in registry. Proceeding to post-uninstall cleanup..."
    }

    # Step 5: Post-uninstall double check & purge
    if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) {
        Write-Verbose "Service '$serviceName' still registered post-MSI. Deleting via sc.exe..."
        & sc.exe delete $serviceName 2>&1 | Out-Null
    }

    $serviceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName"
    if (Test-Path -Path $serviceRegPath) {
        Write-Verbose "Purging orphaned service registry key at '$serviceRegPath'..."
        Remove-Item -Path $serviceRegPath -Recurse -Force -ErrorAction SilentlyContinue
    }

    Start-Sleep -Seconds 2
    if (Test-Path -Path $installPath) {
        Write-Verbose "Removing residual installation folder at '$installPath'..."
        Remove-Item -Path $installPath -Recurse -Force -ErrorAction SilentlyContinue
    }

    $targetShortcutDirs = @(
        "$env:PUBLIC\Desktop",
        [Environment]::GetFolderPath('Desktop'),
        [Environment]::GetFolderPath('CommonDesktopDirectory')
    ) | Select-Object -Unique

    $cleanupFiles = @('Restart ActiveNet.lnk', 'restartanws.bat', 'ActiveNet.url')
    foreach ($dir in $targetShortcutDirs) {
        if (Test-Path -Path $dir) {
            foreach ($file in $cleanupFiles) {
                $itemPath = Join-Path -Path $dir -ChildPath $file
                if (Test-Path -Path $itemPath) {
                    Write-Verbose "Removing shortcut/script file at '$itemPath'..."
                    Remove-Item -Path $itemPath -Force -ErrorAction SilentlyContinue
                }
            }
        }
    }

    Write-Verbose "ActiveNet Workstation Service uninstallation and cleanup completed."
}

function Restart-ANWS {
    <#
        .SYNOPSIS
            Restarts the ActiveNet Workstation Service and verifies the port 4444 listener.

        .DESCRIPTION
            Stops the ActiveNet Workstation Service (activenetworkstationservice), terminates any orphaned
            service processes if necessary, starts the service, and verifies that the localhost port 4444
            WebSocket listener is active and accepting connections.

        .PARAMETER TimeoutSeconds
            The maximum time in seconds to wait for the service to start and the port 4444 listener to respond.
            Defaults to 30 seconds.

        .OUTPUTS
            [PSCustomObject] containing ServiceName, Status, Port4444Listening, and Success.

        .EXAMPLE
            PS> Restart-ANWS

        .EXAMPLE
            PS> Restart-ActiveNet -TimeoutSeconds 45
    #>

    [CmdletBinding()]
    [Alias('Restart-ActiveNet')]
    param(
        [Parameter(Mandatory=$false)]
        [int]$TimeoutSeconds = 30
    )

    $serviceName = 'activenetworkstationservice'
    Write-Verbose "Querying status of service '$serviceName'..."

    $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
    if (-not $service) {
        Write-Error "Service '$serviceName' was not found on this system. Verify ActiveNet Workstation Service is installed."
        return [PSCustomObject]@{
            ServiceName       = $serviceName
            Status            = 'NotFound'
            Port4444Listening = $false
            Success           = $false
        }
    }

    # Stop service if running
    if ($service.Status -ne 'Stopped') {
        Write-Verbose "Stopping service '$serviceName'..."
        Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
        
        $stopWait = 0
        while ((Get-Service -Name $serviceName -ErrorAction SilentlyContinue).Status -ne 'Stopped' -and $stopWait -lt 10) {
            Start-Sleep -Seconds 1
            $stopWait++
        }

        if ((Get-Service -Name $serviceName -ErrorAction SilentlyContinue).Status -ne 'Stopped') {
            Write-Verbose "Service failed to stop gracefully within 10s. Forcing process termination..."
            Get-Process -Name 'ActiveNet.ServiceContainer' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
            Start-Sleep -Seconds 2
        }
    }

    # Start service
    Write-Verbose "Starting service '$serviceName'..."
    Start-Service -Name $serviceName -ErrorAction Stop

    # Wait for service status to reach Running
    $serviceStarted = $false
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
        $currentStatus = (Get-Service -Name $serviceName -ErrorAction SilentlyContinue).Status
        if ($currentStatus -eq 'Running') {
            $serviceStarted = $true
            break
        }
        Start-Sleep -Milliseconds 500
    }

    if (-not $serviceStarted) {
        Write-Warning "Service '$serviceName' did not reach 'Running' state within $TimeoutSeconds seconds."
    } else {
        Write-Verbose "Service '$serviceName' is running. Verifying WebSocket listener on localhost:4444..."
    }

    # Validate localhost port 4444 listener
    $portListening = $false
    while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
        try {
            $tcpClient = New-Object System.Net.Sockets.TcpClient
            $asyncResult = $tcpClient.BeginConnect('127.0.0.1', 4444, $null, $null)
            $waitSuccess = $asyncResult.AsyncWaitHandle.WaitOne(1000, $false)
            if ($waitSuccess -and $tcpClient.Connected) {
                $tcpClient.EndConnect($asyncResult)
                $tcpClient.Close()
                $portListening = $true
                break
            }
            $tcpClient.Close()
        } catch {
            # Retry until timeout
        }
        Start-Sleep -Milliseconds 500
    }
    $stopwatch.Stop()

    $finalStatus = (Get-Service -Name $serviceName -ErrorAction SilentlyContinue).Status
    $isSuccess = ($finalStatus -eq 'Running') -and $portListening

    if ($isSuccess) {
        Write-Verbose "ActiveNet Workstation Service restarted successfully and verified on port 4444."
    } else {
        Write-Warning "ActiveNet Workstation Service restart finished with warnings. Running: $($finalStatus -eq 'Running'), Port 4444 Open: $portListening."
    }

    return [PSCustomObject]@{
        ServiceName       = $serviceName
        Status            = [string]$finalStatus
        Port4444Listening = $portListening
        Success           = $isSuccess
    }
}

# Aliases
Set-Alias -Name 'Install-ActiveNet' -Value 'Install-ANWS'
Set-Alias -Name 'Uninstall-ActiveNet' -Value 'Uninstall-ANWS'
Set-Alias -Name 'Restart-ActiveNet' -Value 'Restart-ANWS'

# Export members
Export-ModuleMember -Function @(
    'Install-ANWS',
    'Set-ANWSRestart',
    'Uninstall-ANWS',
    'Restart-ANWS'
) -Alias @(
    'Install-ActiveNet',
    'Uninstall-ActiveNet',
    'Restart-ActiveNet'
)