#region Safely bypass Restricted Execution Policy 
# Safely bypass Restricted Execution Policy
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
    (Get-ExecutionPolicy) -eq 'Restricted') {

    Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow

    # Rebuild the original argument list as a string to pass through
    $argList = @()
    foreach ($a in $args) {
        if ($a -is [string]) {
            # Quote and escape any existing quotes
            $escaped = $a.Replace('"','`"')
            $argList += "`"$escaped`""
        } else {
            $argList += $a.ToString()
        }
    }
    $argString = $argList -join ' '

    if ($PSCommandPath) {
        # Script saved on disk: re-run same file with same args
        powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`"" $argString
    } else {
        # iwr | iex scenario: re-download SAMY and apply same args
        powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://samy.svstools.ca' -UseBasicParsing | iex } $argString"
    }

    exit
}

# TLS and silent install defaults
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference  = 'None'
#endregion Safely bypass Restricted Execution Policy 

function Invoke-ScriptAutomationMonkey {

<#
.SYNOPSIS
    Main entry point for the Script Automation Monkey (SAMY) deployment tool.

.DESCRIPTION
    Starts the SAMY web UI or runs one of several headless modes for installing,
    cleaning up, or offboarding the SVSMSP toolkit and related stack.

    Headless modes:
      - Silent install of the SVSMSP toolkit (no UI)
      - Toolkit cleanup and removal
      - Full offboarding of the SVSMSP stack
      - Datto RMM integration for:
          * Site discovery and export
          * Site variable push to registry
          * Agent download and installation
          * Saving a copy of the installer
      - Printer and device helpers exposed through the UI endpoints

    When called with no parameters (or via iwr samy.svstools.ca | iex),
    SAMY starts a local HTTP listener, renders the web UI, and executes
    tasks selected in the browser.

.PARAMETER SilentInstall
    Run a default headless install of the SVSMSP toolkit without showing the UI.

.PARAMETER Cleanup
    Remove the SVSMSP toolkit, custom repository, and related deployment
    artifacts without launching the UI.

.PARAMETER Offboard
    Perform a headless offboard of the SVSMSP stack:
    uninstall integrated tools and then clean up the SVSMSP toolkit.

.PARAMETER UseWebhook
    Use the configured webhook to retrieve Datto RMM API credentials instead
    of passing ApiUrl / ApiKey / ApiSecretKey directly.

.PARAMETER WebhookPassword
    Password or token used to authenticate to the Datto credential webhook.

.PARAMETER ApiUrl
    Base URL for the Datto RMM API when using direct API mode.

.PARAMETER ApiKey
    Datto RMM API key when using direct API mode.

.PARAMETER ApiSecretKey
    Datto RMM API secret key when using direct API mode.

.PARAMETER FetchSites
    Fetch the list of Datto RMM sites. Can be combined with SaveSitesList
    and OutputFile to export the site list.

.PARAMETER SaveSitesList
    When used with FetchSites, save the Datto site list to the file
    specified by OutputFile (CSV or JSON).

.PARAMETER OutputFile
    Target file name for the Datto site list when SaveSitesList is used.
    Supports .csv and .json. Default is datto_sites.csv.

.PARAMETER SiteUID
    Datto RMM site UID used when pushing site variables or installing
    the RMM agent for a specific site.

.PARAMETER SiteName
    Friendly Datto RMM site name used for logging only.

.PARAMETER PushSiteVars
    Fetch Datto RMM site variables and push them into the HKLM:\Software\SVS
    Deployment registry key.

.PARAMETER InstallRMM
    Download and run the Datto RMM agent installer for the selected site.

.PARAMETER SaveCopy
    Download and save a copy of the Datto RMM agent installer to disk
    without launching it.

.EXAMPLE
    iwr samy.svstools.ca | iex

    Download and launch SAMY in interactive UI mode on the local machine.

.EXAMPLE
    & ([ScriptBlock]::Create((iwr 'samybeta.svstools.ca').Content )) -SilentInstall
    (iwr samy.svstools.ca -UseBasicParsing).Content | iex
    Invoke-ScriptAutomationMonkey -SilentInstall

    Run a headless install of the SVSMSP toolkit (no UI) from the current session.

.EXAMPLE
    (iwr samy.svstools.ca -UseBasicParsing).Content | iex
    Invoke-ScriptAutomationMonkey `
        -UseWebhook `
        -WebhookPassword 'MySecret' `
        -FetchSites `
        -SaveSitesList `
        -OutputFile 'sites.json'

    Fetch Datto RMM sites via webhook and save them to sites.json.

#>
  
    # ==============================================================
    #  PARAMETERS (DRIVE MODES / PARAMETER SETS)
    #
    #  Default UI:
    #    & (iwr samy.svstools.ca | iex)
    #
    #  Headless modes:
    #    -SilentInstall        Default SVSMSP toolkit install
    #    -Cleanup              Remove toolkit, repo, registry
    #    -Offboard             Full headless offboarding
    #
    #  Datto fetch (sites only):
    #    Webhook:
    #      -UseWebhook -WebhookPassword 'pwd' -FetchSites `
    #      [-SaveSitesList] [-OutputFile 'sites.json']
    #
    #    Direct:
    #      -ApiUrl 'https://api.example.com' `
    #      -ApiKey 'YourApiKey' -ApiSecretKey 'YourSecretKey' `
    #      -FetchSites [-SaveSitesList] [-OutputFile 'sites.json']
    #
    #  Datto install (single site actions):
    #    Webhook:
    #      -UseWebhook -WebhookPassword 'pwd' `
    #      -SiteUID 'site-123' -SiteName 'Acme Corp' `
    #      [-PushSiteVars] [-InstallRMM] [-SaveCopy]
    #
    #    Direct:
    #      -ApiUrl 'https://api.example.com' -ApiKey 'k' -ApiSecretKey 's' `
    #      -SiteUID 'site-123' -SiteName 'Acme Corp' `
    #      [-PushSiteVars] [-InstallRMM] [-SaveCopy]
    # ==============================================================

    [CmdletBinding(
        DefaultParameterSetName = 'UI',
        SupportsShouldProcess   = $true,
        ConfirmImpact           = 'Medium'
    )]
    param(
        # Toolkit-only mode
        [Parameter(Mandatory, ParameterSetName = 'Toolkit')]
        [switch]$SilentInstall,
        
        # Remove Toolkit
        [Parameter(Mandatory, ParameterSetName = 'Cleanup')]
        [switch]$Cleanup,

        # Headless offboarding
        [Parameter(Mandatory, ParameterSetName = 'Offboard')]
        [switch]$Offboard,

        # Datto headless mode (shared switches)

        [Parameter(Mandatory, ParameterSetName = 'DattoFetch')]
        [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
        [switch]$UseWebhook,

        [Parameter(Mandatory, ParameterSetName = 'DattoFetch')]
        [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
        [string]$WebhookPassword,

        [string]$WebhookUrl = $Global:DattoWebhookUrl,
  
        # Only DattoFetch uses these
        [Parameter(ParameterSetName = 'DattoFetch')]
        [switch]$FetchSites,

        [Parameter(ParameterSetName = 'DattoFetch')]
        [switch]$SaveSitesList,

        [Parameter(ParameterSetName = 'DattoFetch')]
        [ValidatePattern('\.csv$|\.json$')]
        [string]$OutputFile = 'datto_sites.csv',

        # Only DattoInstall uses these
        [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
        [string]$SiteUID,

        [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
        [string]$SiteName,

        [Parameter(ParameterSetName = 'DattoInstall')]
        [switch]$PushSiteVars,

        [Parameter(ParameterSetName = 'DattoInstall')]
        [switch]$InstallRMM,

        [Parameter(ParameterSetName = 'DattoInstall')]
        [switch]$SaveCopy
    )
  
    # Global variables
  
    # Listening port for HTTP UI
    $Port = 8082

    # Configurable endpoints
    $Global:DattoWebhookUrl = 'https://bananas.svstools.ca/dattormm'

    # SAMY asset config (change branch or base once and it updates everything)
    $Script:SamyBranch    = 'beta' # 'main' or 'beta'
    $Script:SamyRepoBase  = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch'

    # Top-left corner logo (SVS)
    $Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg"

    # Background SAMY image used in CSS
    $Script:SamyBgLogoUrl  = "$Script:SamyRepoBase/$Script:SamyBranch/SAMY.png"

    $Script:SamyFaviconUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_Favicon.ico"
    $Script:SamyCssUrl     = "$Script:SamyRepoBase/$Script:SamyBranch/samy.css?raw=1"
    $Script:SamyJsUrl      = "$Script:SamyRepoBase/$Script:SamyBranch/samy.js?raw=1"

    # Initialize a global in-memory log cache
    if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
        $Global:LogCache = [System.Collections.ArrayList]::new()
    }

    # SVS Module

    function Initialize-NuGetProvider {
        [CmdletBinding()]
        param()

        # Silent defaults
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        $ProgressPreference = 'SilentlyContinue'
        $ConfirmPreference  = 'None'

        # Pre-create folder if running as SYSTEM
        $provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
        if (-not (Test-Path $provPath)) {
            try {
                New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
                Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent
            } catch {
                Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
            }
        }

        # Ensure PowerShellGet is available
        if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
            try {
                Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
                Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent
            } catch {
                Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
            }
        }

        # Ensure PackageManagement is up-to-date
        $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
        if ($pkgMgmtVersion -lt [Version]"1.3.1") {
            try {
                Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
                Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent
            } catch {
                Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
            }
        }

        # Import modules silently
        Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
        Import-Module PowerShellGet     -Force -ErrorAction SilentlyContinue | Out-Null

        # Trust PSGallery if not already
        $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
        if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
            try {
                Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
                Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent
            } catch {
                Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
            }
        }

        # Ensure NuGet is installed silently
        $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
        if (-not $nuget) {
            try {
                Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
                $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
                Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent
            } catch {
                Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
            }
        } else {
            Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent
        }

        # Final import check
        try {
            Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
        } catch {
            Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
        }
    }

    function Install-SVSMSP {
        param (
            [switch] $Cleanup,
            [switch] $InstallToolkit,
            [Parameter(Mandatory = $false)][array]  $AllModules       = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }),
            [Parameter(Mandatory = $false)][array]  $AllRepositories  = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }),
            [Parameter(Mandatory = $false)][string] $NewModuleName    = "SVSMSP",
            [Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo",
            [Parameter(Mandatory = $false)][string] $NewRepositoryURL  = "http://proget.svstools.ca:8083/nuget/SVS_Repo/"
        )

        function Start-Cleanup {
            Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." Info SVSModule

            # Attempt to uninstall all versions of SVSMSP
            try {
                Uninstall-Module -Name SVSMSP -AllVersions -Force -ErrorAction Stop
                Write-LogHybrid "SVSMSP module uninstalled from system." Success SVSModule -LogToEvent
            }
            catch {
                if ($_.Exception.Message -match 'No match was found') {
                    Write-LogHybrid "No existing SVSMSP module found to uninstall." Warning SVSModule -LogToEvent
                }
                else {
                    Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" Error SVSModule -LogToEvent
                }
            }

            # Remove the custom repository if registered
            if (Get-PSRepository -Name SVS_Repo -ErrorAction SilentlyContinue) {
                try {
                    Unregister-PSRepository -Name SVS_Repo -ErrorAction Stop
                    Write-LogHybrid "SVS_Repo repository unregistered." Success SVSModule -LogToEvent
                }
                catch {
                    Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" Error SVSModule -LogToEvent
                }
            }

            # Remove from current session
            if (Get-Module -Name SVSMSP) {
                try {
                    Remove-Module SVSMSP -Force -ErrorAction Stop
                    Write-LogHybrid "SVSMSP module removed from current session." Success SVSModule -LogToEvent
                }
                catch {
                    Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" Error SVSModule -LogToEvent
                }
            }

            # CSCE cleanup
            $cscePath = 'C:\CSCE'
            if (Test-Path $cscePath) {
                try {
                    Remove-Item -Path $cscePath -Recurse -Force
                    Write-LogHybrid "Deleted '$cscePath' contents." Success SVSModule -LogToEvent
                } catch {
                    Write-LogHybrid "Failed to delete '$cscePath': $($_.Exception.Message)" Warning SVSModule -LogToEvent
                }
            }
        }

        function Remove-SVSDeploymentRegKey {
            $regKey = 'HKLM:\Software\SVS'

            try {
                if (Test-Path $regKey) {
                    Remove-Item -Path $regKey -Recurse -Force
                    Write-LogHybrid "Registry key '$regKey' deleted successfully." Success SVSModule -LogToEvent
                }
                else {
                    Write-LogHybrid "Registry key '$regKey' not found; nothing to delete." Info SVSModule -LogToEvent
                }
            }
            catch {
                Write-LogHybrid "Failed to delete registry key '$regKey': $($_.Exception.Message)" Error SVSModule -LogToEvent
            }
        }

        function Repair-SVSMspEventLogBinding {
            param(
                [string]$EventSource = "SVSMSP_Module",
                [string]$TargetLog   = "SVSMSP Events"
            )

            Write-LogHybrid "Checking Event Log binding for source '$EventSource'..." Info SVSModule -LogToEvent

            try {
                if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
                    Write-LogHybrid "Event source '$EventSource' not found. Nothing to repair." Info SVSModule -LogToEvent
                    return
                }

                $currentLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventSource, '.')
            }
            catch {
                Write-LogHybrid "Failed to query Event Log binding for '$EventSource': $($_.Exception.Message)" Warning SVSModule -LogToEvent
                return
            }

            if (-not $currentLog) {
                Write-LogHybrid "Could not determine current log for event source '$EventSource'. Skipping repair." Warning SVSModule -LogToEvent
                return
            }

            if ($currentLog -eq $TargetLog) {
                Write-LogHybrid "Event source '$EventSource' already bound to '$TargetLog'." Info SVSModule -LogToEvent
                return
            }

            Write-LogHybrid "Rebinding event source '$EventSource' from '$currentLog' to '$TargetLog'..." Warning SVSModule -LogToEvent

            try {
                [System.Diagnostics.EventLog]::DeleteEventSource($EventSource)

                if (-not [System.Diagnostics.EventLog]::Exists($TargetLog)) {
                    New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop
                }
                else {
                    New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop
                }

                Write-LogHybrid "Event source '$EventSource' rebound to '$TargetLog'." Success SVSModule -LogToEvent
            }
            catch {
                Write-LogHybrid "Failed to rebind event source '$EventSource' to log '$TargetLog': $($_.Exception.Message)" Error SVSModule -LogToEvent
            }
        }

        function Start-ToolkitInstallation {
            Initialize-NuGetProvider
            Start-Cleanup

            Write-LogHybrid "Registering repo $NewRepositoryName..." Info SVSModule -LogToEvent
            if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
                Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
            }

            Write-LogHybrid "Installing module $NewModuleName..." Info SVSModule -LogToEvent
            Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force

            Repair-SVSMspEventLogBinding -EventSource "SVSMSP_Module" -TargetLog "SVSMSP Events"

            Write-LogHybrid "Toolkit installation completed." Success SVSModule -LogToEvent
        }

        Write-LogHybrid "Install-SVSMSP called" Info SVSModule -LogToEvent

        if ($Cleanup) {
            Start-Cleanup
            Remove-SVSDeploymentRegKey
            return
        }

        if ($InstallToolkit) {
            Start-ToolkitInstallation
            return
        }

        # Default if no switch passed:
        Start-ToolkitInstallation
    }

    # Write-Log (fallback + hybrid)

    function Write-LogHelper {
        <#
        .SYNOPSIS
            Fallback logging utility with console/file output and Windows Event Log support.
        #>
        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true)]
            [string]$Message,

            [ValidateSet("Info", "Warning", "Error", "Success", "General")]
            [string]$Level = "Info",

            [string]$TaskCategory = "GeneralTask",

            [switch]$LogToEvent = $false,

            [string]$EventSource = "SAMY",

            [string]$EventLog = "SVSMSP Events",

            [int]$CustomEventID,

            [string]$LogFile,

            [switch]$PassThru
        )

        $EventID = if ($CustomEventID) { $CustomEventID } else {
            switch ($Level) {
                "Info"    { 1000 }
                "Warning" { 2000 }
                "Error"   { 3000 }
                "Success" { 4000 }
                default   { 1000 }
            }
        }

        $Color = switch ($Level) {
            "Info"    { "Cyan" }
            "Warning" { "Yellow" }
            "Error"   { "Red" }
            "Success" { "Green" }
            default   { "White" }
        }

        $FormattedMessage = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
        Write-Host $FormattedMessage -ForegroundColor $Color

        if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
            $Global:LogCache = [System.Collections.ArrayList]::new()
        }

        $logEntry = [PSCustomObject]@{
            Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            Level     = $Level
            Message   = $FormattedMessage
        }
        [void]$Global:LogCache.Add($logEntry)

        if ($LogFile) {
            try {
                "$($logEntry.Timestamp) $FormattedMessage" |
                    Out-File -FilePath $LogFile -Append -Encoding UTF8
            }
            catch {
                Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow
            }
        }

        if ($LogToEvent) {

            if (-not $Global:EventSourceInitState) {
                $Global:EventSourceInitState = @{}
            }

            $EntryType = switch ($Level) {
                "Info"    { "Information" }
                "Warning" { "Warning" }
                "Error"   { "Error" }
                "Success" { "Information" }
                default   { "Information" }
            }

            $sourceKey = "$EventLog|$EventSource"

            if (-not $Global:EventSourceInitState.ContainsKey($sourceKey) -or
                -not $Global:EventSourceInitState[$sourceKey]) {

                try {
                    if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {

                        $isAdmin = $false
                        try {
                            $current   = [Security.Principal.WindowsIdentity]::GetCurrent()
                            $principal = New-Object Security.Principal.WindowsPrincipal($current)
                            $isAdmin   = $principal.IsInRole(
                                [Security.Principal.WindowsBuiltInRole]::Administrator
                            )
                        }
                        catch {
                            $isAdmin = $false
                        }

                        if ($isAdmin) {
                            New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
                        }
                        else {
                            $helperScript = @"
if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
    New-EventLog -LogName '$EventLog' -Source '$EventSource'
}
"@

                            $tempPath = [System.IO.Path]::Combine(
                                $env:TEMP,
                                "Init_${EventLog}_$EventSource.ps1".Replace(' ', '_')
                            )

                            $helperScript | Set-Content -Path $tempPath -Encoding UTF8

                            try {
                                $null = Start-Process -FilePath "powershell.exe" `
                                    -ArgumentList "-ExecutionPolicy Bypass -File `"$tempPath`"" `
                                    -Verb RunAs -Wait -PassThru
                            }
                            catch {
                                Write-Host "[Warning] Auto-elevation to create Event Log '$EventLog' / source '$EventSource' failed: $($_.Exception.Message)" -ForegroundColor Yellow
                            }
                            finally {
                                Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
                            }
                        }
                    }

                    if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
                        $Global:EventSourceInitState[$sourceKey] = $true
                    }
                    else {
                        $Global:EventSourceInitState[$sourceKey] = $false
                        Write-Host "[Warning] Event source '$EventSource' does not exist and could not be created. Skipping Event Log write." -ForegroundColor Yellow
                    }
                }
                catch {
                    Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow
                    $Global:EventSourceInitState[$sourceKey] = $false
                }
            }

            if ($Global:EventSourceInitState[$sourceKey]) {
                try {
                    $EventMessage = "TaskCategory: $TaskCategory | Message: $Message"
                    Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventID -Message $EventMessage
                }
                catch {
                    Write-Host "[Warning] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow
                }
            }
        }

        if ($PassThru) {
            return $logEntry
        }
    }
      
    function Write-LogHybrid {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true)]
            [string]$Message,

            [ValidateSet("Info", "Warning", "Error", "Success", "General")]
            [string]$Level = "Info",

            [string]$TaskCategory = "GeneralTask",

            [switch]$LogToEvent,

            [string]$EventSource = "SVSMSP_Module",

            [string]$EventLog    = "SVSMSP Events",

            [int]$CustomEventID,

            [string]$LogFile,

            [switch]$PassThru,

            [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
            [string]$ForegroundColorOverride
        )

        $formatted = "[$Level] [$TaskCategory] $Message"

        $invokeParams = @{
            Message      = $Message
            Level        = $Level
            TaskCategory = $TaskCategory
            LogToEvent   = $LogToEvent
            EventSource  = $EventSource
            EventLog     = $EventLog
        }

        if ($PSBoundParameters.ContainsKey('CustomEventID')) {
            $invokeParams.CustomEventID = $CustomEventID
        }
        if ($PSBoundParameters.ContainsKey('LogFile')) {
            $invokeParams.LogFile = $LogFile
        }
        if ($PassThru) {
            $invokeParams.PassThru = $true
        }

        if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) {
            Write-Host $formatted -ForegroundColor $ForegroundColorOverride

            if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
                Write-Log @invokeParams
            }
            else {
                Write-LogHelper @invokeParams
            }
        }
        else {
            if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
                Write-Log @invokeParams
            }
            else {
                Write-LogHelper @invokeParams
            }
        }
    }

    # Computer rename helper

    function Test-ComputerName {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true)]
            [string]$Name
        )

        if ([string]::IsNullOrWhiteSpace($Name)) { return $false }
        if ($Name.Length -gt 15) { return $false }
        if ($Name -notmatch '^[A-Za-z0-9-]+$') { return $false }
        return $true
    }

    # Task menu definition

    $Global:SamyTasks = @(
        # On-Boarding, left column
        @{ Id='setSVSPowerplan';     Name='setSVSPowerplan';     Label='Set SVS Powerplan';     HandlerFn='Invoke-SetSVSPowerPlan';       Page='onboard'; Column='left' },
        @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Invoke-InstallSVSMSP';         Page='onboard'; Column='left' },
        @{ Id='installCyberQP';      Name='installCyberQP';      Label='Install CyberQP';       HandlerFn='Invoke-InstallCyberQP';        Page='onboard'; Column='left' },
        @{ Id='installHelpDesk';     Name='installHelpDesk';     Label='Install HelpDesk';      HandlerFn='Invoke-InstallHelpDesk';       Page='onboard'; Column='left' },
        @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker';  HandlerFn='Invoke-InstallThreatLocker';   Page='onboard'; Column='left' },
        @{ Id='installRocketCyber';  Name='installRocketCyber';  Label='Install RocketCyber';   HandlerFn='Invoke-InstallRocketCyber';    Page='onboard'; Column='left' },
        @{ Id='installDattoRMM';     Name='installDattoRMM';     Label='Install DattoRMM';      HandlerFn='Invoke-InstallDattoRMM';       Page='onboard'; Column='left'; 
            SubOptions= @(
                @{ Value='inputVar'; Label='Copy Site Variables' },
                @{ Value='rmm';      Label='Install RMM Agent'   },
                @{ Value='exe';      Label='Download Executable' }
            )
        },

        # On-Boarding, right column
        @{ Id='enableBitLocker';       Name='EnableBitLocker';       Label='Enable BitLocker';           HandlerFn='Set-SVSBitLocker';                 Page='onboard'; Column='right' },
        @{ Id='setEdgeDefaultSearch';  Name='setedgedefaultsearch';  Label='Set Edge Default Search';    Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='Invoke-SetEdgeDefaultSearchEngine'; Page='onboard'; Column='right' },
  
        # Off-Boarding
        @{ Id='offUninstallCyberQP';      Name='offUninstallCyberQP';      Label='Uninstall CyberQP';           HandlerFn='Invoke-UninstallCyberQP';      Page='offboard' },
        @{ Id='offUninstallHelpDesk';     Name='offUninstallHelpDesk';     Label='Uninstall HelpDesk';          HandlerFn='Invoke-UninstallHelpDesk';     Page='offboard' },
        @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker';      HandlerFn='Invoke-UninstallThreatLocker'; Page='offboard' },
        @{ Id='offUninstallRocketCyber';  Name='offUninstallRocketCyber';  Label='Uninstall RocketCyber';       HandlerFn='Invoke-UninstallRocketCyber';  Page='offboard' },
        @{ Id='offCleanupSVSMSPModule';   Name='offCleanupSVSMSPModule';   Label='Cleanup SVSMSP Toolkit';      HandlerFn='Invoke-CleanupSVSMSP';         Page='offboard' },

        # Tweaks
        @{ Id='disableAnimations';     Name='disableAnimations';     Label='Disable Animations';           HandlerFn='Disable-Animations';    Page='tweaks' },

        # SVS Apps
        @{ Id='wingetLastpass';        Name='wingetLastpass';        Label='LastPass Desktop App';         HandlerFn='Install-WingetLastPass'; Page='SVSApps' },
        @{ Id='wingetChrome';          Name='wingetChrome';          Label='Google Chrome';                 HandlerFn='Invoke-InstallChrome';  Page='SVSApps' },
        @{ Id='wingetAcrobat';         Name='wingetAcrobat';         Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Invoke-InstallAcrobat'; Page='SVSApps' }
    )

    Write-LogHybrid "Tasks by page: onboard=$(
        ($Global:SamyTasks | Where-Object Page -eq 'onboard').Count
    ) offboard=$(
        ($Global:SamyTasks | Where-Object Page -eq 'offboard').Count
    ) tweaks=$(
        ($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count
    ) apps=$(
        ($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count
    )" Info UI -LogToEvent

    function Publish-Checkboxes {
        param(
            [Parameter(Mandatory)][string]$Page,
            [string]$Column
        )

        $tasks = $Global:SamyTasks | Where-Object Page -EQ $Page

        if (-not [string]::IsNullOrEmpty($Column)) {
            $tasks = $tasks | Where-Object Column -EQ $Column
        }

        (
            $tasks |
            ForEach-Object {
                $taskId   = $_.Id
                $tooltip  = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
                    " title='$($_.Tooltip)'"
                } else { '' }

                $html = "<label$tooltip><input type='checkbox' id='$taskId' name='$($_.Name)' data-column='$Column'> $($_.Label)</label>"

                if ($_.SubOptions) {
                    $subHtml = (
                        $_.SubOptions |
                        ForEach-Object {
                            "<label style='margin-left:20px; display:block;'>
                            <input type='checkbox' class='sub-option-$taskId' name='$($_.Value)' value='$($_.Value)'> $($_.Label)
                            </label>"
                        }
                    ) -join "`n"

                    $html += @"
<div id='${taskId}OptionsContainer' style='display:none; margin-top:4px;'>
$subHtml
</div>
"@
                }

                $html
            }
        ) -join "`n"
    }

    function Get-ModuleVersionHtml {
        $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1

        $branchDisplay = switch ($Script:SamyBranch.ToLower()) {
            'main' { 'Main / Stable' }
            'beta' { 'Beta' }
            default { $Script:SamyBranch }
        }

        if ($mod) {
            return "<div style='color:#bbb; font-size:0.9em; margin-top:1em;'>
        Module Version: $($mod.Version)<br>
        UI Branch: $branchDisplay
        </div>"
        }

        return "<div style='color:#f66;'>SVSMSP_Module not found</div>"
    }

    # Server helpers

    function Get-NextFreePort {
        param([int]$Start = $Port)
        for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
            $l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
            try { 
                $l.Start(); 
                $l.Stop(); 
                return $p 
            } catch {}
        }
        throw "No free TCP port available."
    }

    function Start-Server {
        $Global:Listener = [System.Net.HttpListener]::new()
        $primaryPrefix  = "http://localhost:$Port/"
        $wildcardPrefix = "http://+:$Port/"

        try {
            $Global:Listener.Prefixes.Add($primaryPrefix)
            $Global:Listener.Start()
            Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
        }
        catch [System.Net.HttpListenerException] {
            if ($_.Exception.ErrorCode -eq 5) {
                Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL..." Warning Server -LogToEvent
                try {
                    $user = "$env:USERDOMAIN\$env:USERNAME"
                    if (-not $user.Trim()) { $user = $env:USERNAME }
                    Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
                    $Global:Listener = [System.Net.HttpListener]::new()
                    $Global:Listener.Prefixes.Add($wildcardPrefix)
                    $Global:Listener.Start()
                    Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
                } catch {
                    Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
                    return
                }
            }
            elseif ($_.Exception.NativeErrorCode -in 32,183) {
                $old = $Port
                $Port = Get-NextFreePort -Start ($Port + 1)
                $Global:Listener = [System.Net.HttpListener]::new()
                $primaryPrefix = "http://localhost:$Port/"
                $Global:Listener.Prefixes.Add($primaryPrefix)
                $Global:Listener.Start()
                Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
            }
            else {
                Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
                return
            }
        }

        try {
            while ($Global:Listener.IsListening) {
                $ctx = $Global:Listener.GetContext()
                try {
                    Dispatch-Request $ctx
                } catch {
                    Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
                }
            }
        }
        finally {
            $Global:Listener.Close()
            Write-LogHybrid "Listener closed." Info Server -LogToEvent
        }
    }

    # UI HTML helpers

    function Get-RemoteText {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true)][string]$Url
        )

        try {
            $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop
            return $resp.Content
        }
        catch {
            Write-LogHybrid "Get-RemoteText failed for ${Url}: $($_.Exception.Message)" Warning UI -LogToEvent
            return ""
        }
    }

    function Get-UIHtml {
        param([string]$Page = 'onboard')
        if (-not $Page) { $Page = 'onboard' }

        $onboardLeft  = Publish-Checkboxes -Page 'onboard' -Column 'left'
        $onboardRight = Publish-Checkboxes -Page 'onboard' -Column 'right'
        $offboard     = Publish-Checkboxes -Page 'offboard'  -Column ''
        $tweaks       = Publish-Checkboxes -Page 'tweaks'    -Column ''
        $apps         = Publish-Checkboxes -Page 'SVSApps'   -Column ''

        $tasksJsAll = (
            $Global:SamyTasks | ForEach-Object {
                "    { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
            }
        ) -join ",`n"

        $branchDisplay = switch ($Script:SamyBranch.ToLower()) {
            'main' { 'Main / Stable' }
            'beta' { 'Beta' }
            default { $Script:SamyBranch }
        }

        $cssContent = Get-RemoteText -Url $Script:SamyCssUrl
        $jsContent  = Get-RemoteText -Url $Script:SamyJsUrl

        if ($cssContent) {
            $pattern     = 'background-image:\s*url\("SAMY\.png"\);?'
            $replacement = "background-image: url('$Script:SamyBgLogoUrl');"
            $cssContent  = [regex]::Replace($cssContent, $pattern, $replacement)
        }

        $htmlTemplate = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Script Automation Monkey</title>
<link rel="icon" href="$Script:SamyFaviconUrl">

<style>
$cssContent
</style>
</head>
<body>
<div class="logo-container">
  <div class="logo-left">
    <img src="$Script:SamyTopLogoUrl" alt="SVS Logo">
    {{moduleVersion}}
  </div>

  <div id="tagline" class="tagline">
    Script Automation Monkey (Yeah!)
  </div>

  <div id="samyHint" class="samy-hint"></div>
</div>

<div class="container">
  <div class="sidebar">
    <button class="tab-button" data-tab="onboardTab">On-Boarding</button>
    <button class="tab-button" data-tab="offboardTab">Off-Boarding</button>
    <button class="tab-button" data-tab="tweaksTab">Tweaks</button>
    <button class="tab-button" data-tab="SVSAppsTab">SVS APPs</button>
    <button class="tab-button" data-tab="devicesTab">Devices</button>

    <div id="status-box" style="margin-top: 1em; font-family: monospace;"></div>
  </div>
  <div class="content">
    <div id="onboardTab" class="tab-content">
      <h2>On-Boarding</h2>
      <h3 class="subtitle">This new deployment method ensures everything is successfully deployed with greater ease!</h3>

      <div class="columns-container">
        <div class="checkbox-group column">
          <h3>SVSMSP Stack</h3>
          <label><input type="checkbox" id="selectAllLeftCheckbox" onclick="toggleColumn('left')"> Select All</label>
          {{onboardLeftColumn}}
        </div>
        <div class="checkbox-group column">
          <h3>Optional</h3>
          <label><input type="checkbox" id="selectAllRightCheckbox" onclick="toggleColumn('right')"> Select All</label>
          {{onboardRightColumn}}

          <div class="option">
            <label>
              <input type="checkbox" id="chkRenameComputer" data-column="right">
              Rename Computer
            </label>
          </div>

          <div id="renameComputerBlock" style="display:none; margin-left: 24px; margin-top: 6px;">
            <label for="txtNewComputerName">New computer name:</label>
            <input type="text" id="txtNewComputerName" placeholder="e.g. CORP-LAP-123" />
            <small style="display:block; margin-top:4px;">
              (Max 15 chars; letters, numbers, and hyphens only.)
            </small>
          </div>
        </div>
      </div>

      <div id="PasswordContainer" style="display:none; margin-bottom:1em;">
        <label for="Password">Enter Password:</label>
        <div style="display:flex; gap:5px;">
          <input type="password" id="Password" placeholder="Enter Password" style="flex:1;" />
          <button onclick="fetchSites()" class="go-button">GO!</button>
        </div>
      </div>

      <div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
        <label for="dattoDropdown">Select a Datto RMM site:</label>
        <select id="dattoDropdown" style="width:100%;">
          <option disabled selected>Fetching sites...</option>
        </select>
      </div>
        
    </div>

    <div id="offboardTab" class="tab-content">
      <h2>Off-Boarding</h2>
      <div class="columns-container">
        <div class="checkbox-group column">
          <h3>Remove Stack</h3>
          <label>
            <input type="checkbox" id="offboardSelectAll" onclick="toggleOffboardAll()">
            Select All
          </label>
          {{offboardCheckboxes}}
        </div>
      </div>
    </div>

    <div id="tweaksTab" class="tab-content">
      <h2>Tweaks</h2>
      <div class="columns-container">
        <div class="checkbox-group column">
          <h3>Tweaks</h3>
          {{tweaksCheckboxes}}
        </div>
      </div>    
    </div>

    <div id="SVSAppsTab" class="tab-content">
      <h2>SVS APPs</h2>
      <div class="columns-container">
        <div class="checkbox-group column">
          <h3>Applications</h3>
          {{appsCheckboxes}}
        </div>
      </div>
    </div>

    <div id="devicesTab" class="tab-content">
      <h2>Devices</h2>
      <h3 class="subtitle">Manage printers and other client devices.</h3>

      <div id="printerPasswordContainer" style="margin-bottom:1em;">
        <label for="PrinterPassword">Enter Printer Password:</label>
        <div style="display:flex; gap:5px;">
          <input type="password"
                 id="PrinterPassword"
                 placeholder="Enter printer password"
                 style="flex:1;" />
          <button onclick="fetchPrinters()" class="go-button">Get Printers</button>
        </div>
      </div>

      <div id="printerClientContainer" style="display:none; margin-bottom:1em;">
        <label for="printerClientDropdown">Select Client:</label>
        <select id="printerClientDropdown" style="width:100%;">
          <option disabled selected>Fetch printers first...</option>
        </select>
      </div>

      <div id="printerListContainer" style="display:none; margin-bottom:1em;">
        <label>Printers for selected client:</label>
        <small style="display:block; margin-bottom:4px; opacity:0.8;">
          Check the printers to install, and mark one as "Make default" (optional).
        </small>
        
        <div id="printerCheckboxContainer"
             style="max-height:200px; overflow-y:auto; border:1px solid #444; padding:6px; border-radius:4px;">
        </div>

        <button id="installPrintersButton"
                class="go-button"
                style="margin-top:8px;"
                onclick="installSelectedPrinters()">
          Install Selected Printers
        </button>
      </div>
    </div>
  </div>
</div>

<script>
  window.SAMY_TASKS = [
{{tasksJsAll}}
  ];

  window.SAMY_DEFAULT_PAGE = "{{defaultPage}}";
</script>

<script>
$jsContent
</script>

<div class="fixed-buttons">
  <button class="exit-button" onclick="endSession()">Exit</button>
  <button class="run-button"  onclick="triggerInstall()">Run Selected</button>
</div>

</body>
</html>
"@

        $html = $htmlTemplate
        $html = $html.Replace('{{moduleVersion}}', (Get-ModuleVersionHtml))
        $html = $html.Replace('{{onboardLeftColumn}}',  $onboardLeft)
        $html = $html.Replace('{{onboardRightColumn}}', $onboardRight)
        $html = $html.Replace('{{offboardCheckboxes}}', $offboard)
        $html = $html.Replace('{{tweaksCheckboxes}}',   $tweaks)
        $html = $html.Replace('{{appsCheckboxes}}',     $apps)
        $html = $html.Replace('{{tasksJsAll}}',         $tasksJsAll)
        $html = $html.Replace('{{defaultPage}}',        $Page)

        return $html
    }

    # HTTP responder helpers

    function Send-Text {
        param($Context, $Text)
        if (-not $Context -or -not $Context.Response) {
            return
        }
        $bytes = [Text.Encoding]::UTF8.GetBytes($Text)
        $Context.Response.ContentType     = 'text/plain'
        $Context.Response.ContentLength64 = $bytes.Length
        $Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
        $Context.Response.OutputStream.Close()
    }

    function Send-HTML {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $true)][object] $Context,
            [Parameter(Mandatory = $true)][string] $Html
        )
        if (-not $Context -or -not $Context.Response) {
            return
        }
        $bytes = [Text.Encoding]::UTF8.GetBytes($Html)
        $Context.Response.ContentType     = 'text/html'
        $Context.Response.ContentLength64 = $bytes.Length
        $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
        $Context.Response.OutputStream.Close()
    }

    function Send-JSON {
        [CmdletBinding()]
        param(
            $Context,
            $Object
        )

        if (-not $Context -or -not $Context.Response) {
            return
        }

        try {
            if ($null -eq $Object) {
                Write-LogHybrid "Send-JSON called with `$null object; returning empty JSON array." Warning Printers -LogToEvent
                $json = '[]'
            }
            else {
                try {
                    $json = $Object | ConvertTo-Json -Depth 5 -ErrorAction Stop
                }
                catch {
                    Write-LogHybrid "Send-JSON serialization failed: $($_.Exception.Message); returning empty JSON array." Error Printers -LogToEvent
                    $json = '[]'
                }
            }

            $json = [string]$json

            $bytes = [Text.Encoding]::UTF8.GetBytes($json)
            $Context.Response.ContentType     = 'application/json'
            $Context.Response.ContentLength64 = $bytes.Length
            $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
            $Context.Response.OutputStream.Close()
        }
        catch {
            Write-LogHybrid "Send-JSON fatal error: $($_.Exception.Message)" Error Printers -LogToEvent
            try {
                $fallback = '[]'
                $bytes = [Text.Encoding]::UTF8.GetBytes($fallback)
                $Context.Response.ContentType     = 'application/json'
                $Context.Response.ContentLength64 = $bytes.Length
                $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
                $Context.Response.OutputStream.Close()
            }
            catch {
            }
        }
    }

    function Invoke-TasksCompleted {
        param($Context)

        Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent
        Send-Text $Context "Tasks completion acknowledged."
    }

    # Datto handlers (HTTP side)

    function Invoke-FetchSites {
        param($Context)

        try {
            $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
            $pw  = (ConvertFrom-Json $raw).password

            $Global:WebhookPassword = $pw

            $sites = Install-DattoRMM `
                -UseWebhook `
                -WebhookPassword $pw `
                -FetchSites `
                -SaveSitesList:$SaveSitesList `
                -OutputFile $OutputFile

            Send-JSON $Context $sites
        }
        catch {
            Write-LogHybrid "Invoke-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
            $Context.Response.StatusCode = 500
            Send-Text $Context "Internal server error fetching sites."
        }
    }

    # Onboarding handlers

    function Invoke-SetSVSPowerPlan {
        param($Context)

        Set-SVSPowerPlan

        Write-LogHybrid "PowerPlan set" Success OnBoard
        Send-Text  $Context "PowerPlan applied"
    }

    function Invoke-InstallSVSMSP {
        param($Context)
        Write-LogHybrid "HTTP trigger: Invoke-InstallSVSMSP" Info OnBoard
        try {
            Install-SVSMSP -InstallToolkit
            Send-Text $Context "SVSMSP Module installed/updated."
        } catch {
            Write-LogHybrid "Error in Install-SVSMSP: $_" Error OnBoard
            Send-Text $Context "ERROR: $_"
        }
    }

    function Invoke-InstallCyberQP {
        param($Context)

        Install-CyberQP

        Write-LogHybrid "CyberQP installed" Success OnBoard
        Send-Text $Context "CyberQP installed"
    }

    function Invoke-InstallThreatLocker {
        param($Context)

        Install-ThreatLocker

        Write-LogHybrid "ThreatLocker installed" Success OnBoard
        Send-Text $Context "ThreatLocker installed"
    }

    function Invoke-InstallRocketCyber {
        param($Context)

        Install-RocketCyber

        Write-LogHybrid "RocketCyber installed" Success OnBoard
        Send-Text $Context "RocketCyber installed"
    }

    function Invoke-InstallHelpDesk {
        param($Context)

        Install-svsHelpDesk

        Write-LogHybrid "SVS HelpDesk installed" Success OnBoard
        Send-Text $Context "SVS HelpDesk installed"
    }

    function Invoke-SetEdgeDefaultSearchEngine {
        param($Context)

        try {
            Write-LogHybrid "Configuring Edge default search provider" Info OnBoard
            set-EdgeDefaultSearchEngine
            Write-LogHybrid "Edge default search set to Google" Success OnBoard
            Send-Text $Context "Edge default search provider configured."
        } catch {
            Write-LogHybrid "Failed to set Edge default search: $($_.Exception.Message)" Error OnBoard
            Send-Text $Context "ERROR: $($_.Exception.Message)"
        }
    }

    function Invoke-RenameComputer {
        param($Context)

        try {
            if ($Context.Request.HttpMethod -ne 'POST') {
                $Context.Response.StatusCode = 405
                Send-Text $Context 'Use POST'
                return
            }

            $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
            if (-not $rawBody) {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Missing request body.'
                return
            }

            try {
                $body = $rawBody | ConvertFrom-Json
            } catch {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Invalid JSON body.'
                return
            }

            $newName = $body.newName

            if (-not (Test-ComputerName -Name $newName)) {
                Write-LogHybrid "RenameComputer: invalid computer name '$newName'." Error OnBoard -LogToEvent
                $Context.Response.StatusCode = 400
                Send-JSON $Context @{
                    Success = $false
                    Error   = "Invalid computer name. Must be 1-15 characters and use only letters, numbers, and hyphens."
                }
                return
            }

            Write-LogHybrid "RenameComputer: renaming computer to '$newName'." Info OnBoard -LogToEvent

            try {
                Rename-Computer -NewName $newName -Force -ErrorAction Stop
            } catch {
                Write-LogHybrid "RenameComputer: rename failed: $($_.Exception.Message)" Error OnBoard -LogToEvent
                $Context.Response.StatusCode = 500
                Send-JSON $Context @{
                    Success = $false
                    Error   = $_.Exception.Message
                }
                return
            }

            Write-LogHybrid "RenameComputer: rename complete, reboot required for new name to apply." Success OnBoard -LogToEvent

            Send-JSON $Context @{
                Success = $true
                NewName = $newName
                Note    = "Rename successful. A reboot is required for the new name to take effect."
            }
        } catch {
            Write-LogHybrid "Invoke-RenameComputer fatal error: $($_.Exception.Message)" Error OnBoard -LogToEvent
            $Context.Response.StatusCode = 500
            Send-Text $Context "Internal error during computer rename."
        }
    }

    function Invoke-InstallDattoRMM {
        param($Context)

        try {
            if ($Context.Request.HttpMethod -ne 'POST') {
                $Context.Response.StatusCode = 405
                Send-Text $Context 'Use POST'
                return
            }

            $body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
            $data = ConvertFrom-Json $body

            Install-DattoRMM `
                -UseWebhook `
                -WebhookPassword $Global:WebhookPassword `
                -SiteUID      $data.UID `
                -SiteName     $data.Name `
                -PushSiteVars:($data.checkedValues -contains 'inputVar') `
                -InstallRMM:   ($data.checkedValues -contains 'rmm') `
                -SaveCopy:     ($data.checkedValues -contains 'exe')

            Send-Text $Context "Triggered DattoRMM for $($data.Name)"
        }
        catch {
            Write-LogHybrid "Invoke-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
            $Context.Response.StatusCode = 500
            Send-Text $Context "Internal server error during DattoRMM install."
        }
    }

    # App handlers

    function Invoke-InstallChrome { 
        param($Context)
        try {
            winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
            Write-LogHybrid "Installed Google Chrome via winget" Success SVSApps -LogToEvent
            Send-Text $Context "Chrome installed"
        } catch {
            Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
            Send-Text $Context "ERROR: $($_.Exception.Message)"
        }
    }

    function Invoke-InstallAcrobat { 
        param($Context)
        try {
            winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
            Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" Success SVSApps -LogToEvent
            Send-Text $Context "Acrobat Reader installed"
        } catch {
            Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
            Send-Text $Context "ERROR: $($_.Exception.Message)"
        }
    }

    # Offboarding handlers

    function Invoke-UninstallCyberQP {
        param($Context)

        try {
            if (Get-Command Uninstall-CyberQP -ErrorAction Stop) {
                Uninstall-CyberQP
                Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent
                if ($Context) { Send-Text $Context "CyberQP uninstalled." }
            } else {
                throw "Uninstall-CyberQP cmdlet not found in SVSMSP toolkit."
            }
        }
        catch {
            Write-LogHybrid "Error uninstalling CyberQP: $($_.Exception.Message)" Error OffBoard -LogToEvent
            if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
        }
    }

    function Invoke-UninstallHelpDesk {
        param($Context)

        try {
            if (Get-Command Uninstall-HelpDesk -ErrorAction Stop) {
                Uninstall-HelpDesk
                Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent
                if ($Context) { Send-Text $Context "SVS HelpDesk uninstalled." }
            } else {
                throw "Uninstall-HelpDesk cmdlet not found in SVSMSP toolkit."
            }
        }
        catch {
            Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent
            if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
        }
    }

    function Invoke-UninstallThreatLocker {
        param($Context)

        try {
            if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) {
                Uninstall-ThreatLocker
                Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent
                if ($Context) { Send-Text $Context "ThreatLocker uninstalled." }
            } else {
                throw "Uninstall-ThreatLocker cmdlet not found in SVSMSP toolkit."
            }
        }
        catch {
            Write-LogHybrid "Error uninstalling ThreatLocker: $($_.Exception.Message)" Error OffBoard -LogToEvent
            if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
        }
    }

    function Invoke-UninstallRocketCyber {
        param($Context)

        try {
            if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) {
                Uninstall-RocketCyber
                Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent
                if ($Context) { Send-Text $Context "RocketCyber uninstalled." }
            } else {
                throw "Uninstall-RocketCyber cmdlet not found in SVSMSP toolkit."
            }
        }
        catch {
            Write-LogHybrid "Error uninstalling RocketCyber: $($_.Exception.Message)" Error OffBoard -LogToEvent
            if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
        }
    }

    function Invoke-CleanupSVSMSP {
        param($Context)

        try {
            if (Get-Command Install-SVSMSP -ErrorAction Stop) {
                Install-SVSMSP -Cleanup

                Write-LogHybrid "SVSMSP toolkit cleanup completed (module, repo, registry)." Success OffBoard -LogToEvent
                if ($Context) { Send-Text $Context "SVSMSP toolkit cleanup completed." }
            } else {
                throw "Install-SVSMSP function not found in current session."
            }
        }
        catch {
            Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent
            if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
        }
    }

    # Printer handlers and core

    function Get-SamyDriverRootFolder {
        [CmdletBinding()]
        param()

        $root = Join-Path $env:ProgramData 'SVS\Samy\Drivers'

        if (-not (Test-Path $root)) {
            try {
                New-Item -Path $root -ItemType Directory -Force | Out-Null
                Write-LogHybrid "Created driver root folder '$root'." Info Printers -LogToEvent
            } catch {
                Write-LogHybrid "Failed to create driver root folder '$root': $($_.Exception.Message)" Warning Printers -LogToEvent
            }
        }

        return $root
    }

    function Get-SamyDriverFolderForProfile {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)][pscustomobject]$Profile
        )

        $root = Get-SamyDriverRootFolder

        if ($Profile.PSObject.Properties.Name -contains 'DriverFolderName' -and $Profile.DriverFolderName) {
            $folderName = $Profile.DriverFolderName
        } else {
            $folderName = "$($Profile.ClientCode)_$($Profile.ProfileName)"
        }

        $dest = Join-Path $root $folderName

        if (-not (Test-Path $dest)) {
            try {
                New-Item -Path $dest -ItemType Directory -Force | Out-Null
                Write-LogHybrid "Created driver folder '$dest'." Info Printers -LogToEvent
            } catch {
                Write-LogHybrid "Failed to create driver folder '$dest': $($_.Exception.Message)" Warning Printers -LogToEvent
            }
        }

        return $dest
    }

    function Get-SamyDriverPackageUrl {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)][pscustomobject]$Profile
        )

        if ($Profile.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) {
            return $Profile.DriverPackageUrl
        }

        if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
            return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
        }

        return $null
    }

    function Get-SamyClientListFromServer {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)][string]$Uri,
            [Parameter(Mandatory)][string]$Password
        )

        try {
            Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent

            $headers = @{
                SAMYPW = $Password
            }

            $resp = Invoke-RestMethod -Uri $Uri `
                                      -Method Get `
                                      -Headers $headers `
                                      -ContentType 'application/json' `
                                      -ErrorAction Stop

            if (-not $resp) {
                Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent
                return @()
            }

            if ($resp -is [System.Collections.IEnumerable] -and -not ($resp -is [string])) {
                return @($resp)
            } else {
                return ,$resp
            }
        }
        catch {
            Write-LogHybrid ("Get-SamyClientListFromServer failed for {0}: {1}" -f $Uri, $_.Exception.Message) Error Printers -LogToEvent
            return @()
        }
    }

    function Invoke-GetPrinters {
        param($Context)

        try {
            if ($Context.Request.HttpMethod -ne 'POST') {
                $Context.Response.StatusCode = 405
                Send-Text $Context 'Use POST'
                return
            }

            $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
            if (-not $rawBody) {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Missing request body.'
                return
            }

            try {
                $body = $rawBody | ConvertFrom-Json
            } catch {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Invalid JSON body.'
                return
            }

            $password = $body.password
            if (-not $password) {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Password is required.'
                return
            }

            $uri = 'https://bananas.svstools.ca/getprinters'
            Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent

            $printers = Get-SamyClientListFromServer -Uri $uri -Password $password

            if ($null -eq $printers) {
                Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent
                $printers = @()
            }

            try {
                Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty
            }
            catch {
                Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent
            }

            Send-JSON $Context $printers
        }
        catch {
            Write-LogHybrid "Invoke-GetPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
            $Context.Response.StatusCode = 500
            Send-Text $Context "Internal server error fetching printers."
        }
    }

    function Invoke-InstallPrinters {
        param($Context)

        try {
            if ($Context.Request.HttpMethod -ne 'POST') {
                $Context.Response.StatusCode = 405
                Send-Text $Context 'Use POST'
                return
            }

            $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
            if (-not $rawBody) {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Missing request body.'
                return
            }

            try {
                $body = $rawBody | ConvertFrom-Json
            } catch {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'Invalid JSON body.'
                return
            }

            $printers = $body.printers
            if (-not $printers -or $printers.Count -eq 0) {
                $Context.Response.StatusCode = 400
                Send-Text $Context 'No printers specified.'
                return
            }

            Write-LogHybrid "Received request to install $($printers.Count) printers." Info Printers -LogToEvent

            $successCount = 0
            $failures     = @()

            foreach ($p in $printers) {
                $clientCode   = $p.ClientCode
                $profileName  = $p.ProfileName
                $setDefault   = $false

                if ($p.PSObject.Properties.Name -contains 'SetAsDefault' -and $p.SetAsDefault) {
                    $setDefault = $true
                }

                if (-not $clientCode -or -not $profileName) {
                    $msg = "Skipping printer entry because ClientCode or ProfileName is missing."
                    Write-LogHybrid $msg Warning Printers -LogToEvent
                    $failures += $msg
                    continue
                }

                $summary = "ClientCode=$clientCode ProfileName=$profileName DisplayName=$($p.DisplayName) Location=$($p.Location) SetAsDefault=$setDefault"
                Write-LogHybrid "Installing printer ($summary)" Info Printers -LogToEvent

                try {
                    Invoke-SamyPrinterInstall `
                        -ClientCode  $clientCode `
                        -ProfileName $profileName `
                        -SetAsDefault:$setDefault `
                        -WhatIf

                    $successCount++
                }
                catch {
                    $errMsg = "Failed to install printer ($summary): $($_.Exception.Message)"
                    Write-LogHybrid $errMsg Error Printers -LogToEvent
                    $failures += $errMsg
                }
            }

            $result = @{
                SuccessCount = $successCount
                FailureCount = $failures.Count
                Failures     = $failures
                Message      = "Printer install (WHATIF) processed. Check SAMY logs for detail."
            }

            Send-JSON $Context $result
        }
        catch {
            Write-LogHybrid "Invoke-InstallPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
            $Context.Response.StatusCode = 500
            Send-Text $Context "Internal server error installing printers."
        }
    }

    function Get-SamyPrinterLocalConfigPath {
        [CmdletBinding()]
        param()

        $configDir = Join-Path $env:ProgramData 'SVS\Samy\Printers'

        if (-not (Test-Path $configDir)) {
            try {
                New-Item -Path $configDir -ItemType Directory -Force | Out-Null
                Write-LogHybrid "Created printer config folder at '$configDir'." Info Printers -LogToEvent
            }
            catch {
                Write-LogHybrid "Failed to create printer config folder '$configDir': $($_.Exception.Message)" Warning Printers -LogToEvent
            }
        }

        return (Join-Path $configDir 'printers.json')
    }

    function Get-SamyPrinterConfigFromFile {
        [CmdletBinding()]
        param()

        $path = Get-SamyPrinterLocalConfigPath

        if (-not (Test-Path $path)) {
            throw "Local printer config file not found at '$path'. Create or update printers.json first."
        }

        $json = Get-Content -Path $path -Raw -ErrorAction Stop
        $profiles = $json | ConvertFrom-Json

        if (-not $profiles) {
            throw "Printer config file '$path' is empty or invalid JSON."
        }

        return $profiles
    }

    $Script:Samy_PrinterProfiles = $null

    function Get-SamyPrinterProfiles {
        [CmdletBinding()]
        param(
            [string]$ClientCode
        )

        if (-not $Script:Samy_PrinterProfiles) {
            $Script:Samy_PrinterProfiles = Get-SamyPrinterConfigFromFile
        }

        $result = $Script:Samy_PrinterProfiles

        if ($PSBoundParameters.ContainsKey('ClientCode') -and $ClientCode) {
            $result = $result | Where-Object { $_.ClientCode -eq $ClientCode }
        }

        return $result
    }

    function Get-SamyPrinterProfile {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)][string]$ClientCode,
            [Parameter(Mandatory)][string]$ProfileName
        )

        $profiles = Get-SamyPrinterProfiles -ClientCode $ClientCode
        $match    = $profiles | Where-Object { $_.ProfileName -eq $ProfileName }

        if (-not $match) {
            throw "No printer profile found for ClientCode '$ClientCode' and ProfileName '$ProfileName'."
        }

        if ($match.Count -gt 1) {
            throw "Multiple printer profiles found for ClientCode '$ClientCode' and ProfileName '$ProfileName'. De-duplicate in printers.json."
        }

        return $match
    }

    function Ensure-SamyPrinterDriver {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [pscustomobject]$Profile
        )

        $driverName = $Profile.DriverName
        if (-not $driverName) {
            throw "Profile '$($Profile.ProfileName)' has no DriverName defined in printer config."
        }

        $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue
        if ($existingDriver) {
            Write-LogHybrid "Printer driver '$driverName' already installed." Info Printers -LogToEvent
            return
        }

        Write-LogHybrid "Printer driver '$driverName' not found. Preparing to install." Info Printers -LogToEvent

        $localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile

        $infPath = $null
        if ($Profile.PSObject.Properties.Name -contains 'DriverInfPath' -and $Profile.DriverInfPath) {
            if (Test-Path $Profile.DriverInfPath) {
                $infPath = $Profile.DriverInfPath
                Write-LogHybrid "Using existing INF path '$infPath' for driver '$driverName'." Info Printers -LogToEvent
            } else {
                Write-LogHybrid "Configured DriverInfPath '$($Profile.DriverInfPath)' does not exist, will try repo download." Warning Printers -LogToEvent
            }
        }

        $packageDownloaded = $false

        if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
            $driverPackageUrl = "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
            $localZip         = Join-Path $localDriverRoot "package.zip"

            Write-LogHybrid "Attempting to download driver package from $driverPackageUrl." Info Printers -LogToEvent

            try {
                Invoke-WebRequest -Uri $driverPackageUrl -OutFile $localZip -UseBasicParsing -ErrorAction Stop
                Write-LogHybrid "Downloaded driver package from $driverPackageUrl to $localZip." Success Printers -LogToEvent
                $packageDownloaded = $true
            }
            catch [System.Net.WebException] {
                $response   = $_.Exception.Response
                $statusCode = $null
                if ($response -and $response.StatusCode) {
                    $statusCode = [int]$response.StatusCode
                }

                if ($statusCode -eq 404) {
                    Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent
                }
                else {
                    Write-LogHybrid "Driver package download failed ($statusCode) from $driverPackageUrl: $($_.Exception.Message)" Error Printers -LogToEvent
                    throw "Failed to download driver package from $driverPackageUrl: $($_.Exception.Message)"
                }
            }
            catch {
                Write-LogHybrid "Driver package download failed from $driverPackageUrl: $($_.Exception.Message)" Error Printers -LogToEvent
                throw "Failed to download driver package from $driverPackageUrl: $($_.Exception.Message)"
            }
        }
        else {
            Write-LogHybrid "No DriverPackagePath defined for '$($Profile.DisplayName)'; will rely on local INF." Info Printers -LogToEvent
        }

        if ($packageDownloaded) {
            try {
                Expand-Archive -Path $localZip -DestinationPath $localDriverRoot -Force
                Write-LogHybrid "Expanded driver package to '$localDriverRoot'." Info Printers -LogToEvent
            }
            catch {
                Write-LogHybrid "Failed to expand driver package '$localZip': $($_.Exception.Message)" Error Printers -LogToEvent
                throw "Failed to expand driver package '$localZip': $($_.Exception.Message)"
            }

            if (-not $infPath) {
                if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) {

                    $candidateInf = Join-Path $localDriverRoot $Profile.DriverInfName
                    if (Test-Path $candidateInf) {
                        $infPath = $candidateInf
                        Write-LogHybrid "Resolved INF from package as '$infPath' using DriverInfName '$($Profile.DriverInfName)' at root." Info Printers -LogToEvent
                    }
                    else {
                        Write-LogHybrid "Expected INF '$candidateInf' not found at root; searching recursively..." Warning Printers -LogToEvent

                        $found = Get-ChildItem -Path $localDriverRoot -Recurse -Filter $Profile.DriverInfName -File -ErrorAction SilentlyContinue |
                                 Select-Object -First 1

                        if ($found) {
                            $infPath = $found.FullName
                            Write-LogHybrid "Resolved INF from package as '$infPath' (found by recursive search for '$($Profile.DriverInfName)')." Info Printers -LogToEvent
                        }
                        else {
                            Write-LogHybrid "Could not find any '$($Profile.DriverInfName)' under '$localDriverRoot' after expanding package." Error Printers -LogToEvent
                        }
                    }
                }
                else {
                    Write-LogHybrid "DriverInfName not defined for profile '$($Profile.ProfileName)'; cannot auto-resolve INF from expanded package." Warning Printers -LogToEvent
                }
            }
        }

        if (-not $infPath -or -not (Test-Path $infPath)) {
            throw "Driver '$driverName' is not installed and no valid DriverInfPath or usable driver package is available for profile '$($Profile.ProfileName)'."
        }

        Write-LogHybrid "Installing printer driver '$driverName' from '$infPath'." Info Printers -LogToEvent

        $pnputilCmd = "pnputil.exe /add-driver `"$infPath`" /install"
        Write-LogHybrid "Running: $pnputilCmd" Info Printers -LogToEvent

        $pnputilOutput = & pnputil.exe /add-driver "$infPath" /install 2>&1
        $exitCode      = $LASTEXITCODE

        Write-LogHybrid "pnputil exit code: $exitCode. Output:`n$pnputilOutput" Info Printers -LogToEvent

        if ($exitCode -ne 0) {
            throw "pnputil failed with exit code $exitCode installing '$driverName' from '$infPath'."
        }

        try {
            Write-LogHybrid "Calling Add-PrinterDriver -Name '$driverName'." Info Printers -LogToEvent
            Add-PrinterDriver -Name $driverName -ErrorAction Stop
        }
        catch {
            Write-LogHybrid "Add-PrinterDriver failed for '$driverName' using '$infPath': $($_.Exception.Message)" Error Printers -LogToEvent
            throw "Add-PrinterDriver failed for '$driverName': $($_.Exception.Message)"
        }

        Start-Sleep -Seconds 2

        $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue

        if (-not $existingDriver) {
            $sharpNames = (Get-PrinterDriver -ErrorAction SilentlyContinue |
                           Where-Object Name -like 'SHARP*' |
                           Select-Object -ExpandProperty Name) -join ', '

            if (-not $sharpNames) { $sharpNames = '(none)' }

            Write-LogHybrid "After pnputil/Add-PrinterDriver, driver '$driverName' not found. Existing SHARP drivers: $sharpNames" Warning Printers -LogToEvent
            throw "Failed to find printer driver '$driverName' after Add-PrinterDriver."
        }

        Write-LogHybrid "Printer driver '$driverName' installed and detected successfully." Success Printers -LogToEvent
    }

    function Install-SamyTcpIpPrinter {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [pscustomobject]$Profile,

            [switch]$SetAsDefault
        )

        $portName    = $Profile.Address
        $printerName = $Profile.DisplayName

        if (-not $portName) {
            throw "TCP/IP printer profile '$($Profile.ProfileName)' is missing Address in printer config."
        }

        if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {
            Write-Verbose "Creating TCP/IP port '$portName'."
            Add-PrinterPort -Name $portName -PrinterHostAddress $Profile.Address
        }
        else {
            Write-Verbose "TCP/IP port '$portName' already exists."
        }

        $existingPrinter = Get-Printer -Name $printerName -ErrorAction SilentlyContinue
        if ($existingPrinter) {
            Write-Verbose "Printer '$printerName' already exists. Skipping creation."
        }
        else {
            Write-Verbose "Creating printer '$printerName' on port '$portName' using driver '$($Profile.DriverName)'."
            Add-Printer -Name $printerName -PortName $portName -DriverName $Profile.DriverName
        }

        if ($SetAsDefault -or $Profile.IsDefault) {
            Write-Verbose "Setting '$printerName' as default printer."
            (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
        }
    }

    function Install-SamySharedPrinter {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [pscustomobject]$Profile,

            [switch]$SetAsDefault
        )

        if (-not $Profile.PrintServer -or -not $Profile.ShareName) {
            throw "Shared printer profile '$($Profile.ProfileName)' is missing PrintServer or ShareName in printer config."
        }

        $connectionName = "\\$($Profile.PrintServer)\$($Profile.ShareName)"

        $existing = Get-Printer -ErrorAction SilentlyContinue |
            Where-Object {
                $_.Name -eq $Profile.DisplayName -or
                $_.ShareName -eq $Profile.ShareName
            }

        if ($existing) {
            Write-Verbose "Shared printer '$($Profile.DisplayName)' already connected as '$($existing.Name)'."
            $printerName = $existing.Name
        }
        else {
            Write-Verbose "Adding shared printer connection '$connectionName'."
            Add-Printer -ConnectionName $connectionName

            $printerName = (Get-Printer |
                Where-Object { $_.Name -like "*$($Profile.ShareName)*" } |
                Select-Object -First 1
            ).Name
        }

        if ($SetAsDefault -or $Profile.IsDefault) {
            Write-Verbose "Setting '$printerName' as default printer."
            (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
        }
    }

    function Invoke-SamyPrinterInstall {
        [CmdletBinding(SupportsShouldProcess = $true)]
        param(
            [Parameter(Mandatory)]
            [string]$ClientCode,

            [Parameter(Mandatory)]
            [string]$ProfileName,

            [switch]$SetAsDefault
        )

        try {
            $profile    = Get-SamyPrinterProfile -ClientCode $ClientCode -ProfileName $ProfileName
            $targetName = $profile.DisplayName

            if ($PSCmdlet.ShouldProcess($targetName, "Install printer")) {

                Write-LogHybrid "Installing printer profile '$($profile.ProfileName)' for client '$ClientCode' (WhatIf=$WhatIfPreference)." Info Printers -LogToEvent

                Ensure-SamyPrinterDriver -Profile $profile

                switch ($profile.Type) {
                    'TcpIp' {
                        Install-SamyTcpIpPrinter -Profile $profile -SetAsDefault:$SetAsDefault
                    }
                    'Shared' {
                        Install-SamySharedPrinter -Profile $profile -SetAsDefault:$SetAsDefault
                    }
                    default {
                        throw "Unsupported printer Type '$($profile.Type)' in profile '$($profile.ProfileName)'. Use 'TcpIp' or 'Shared'."
                    }
                }

                Write-LogHybrid "Installed printer '$($profile.DisplayName)' (Client=$ClientCode Profile=$ProfileName)." Info Printers -LogToEvent
            }
        }
        catch {
            Write-LogHybrid (
                "Printer install failed for Client={0} Profile={1}: {2}" -f $ClientCode, $ProfileName, $_.Exception.Message
            ) Error Printers -LogToEvent
            throw
        }
    }

    function Update-SamyPrinterConfig {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory)]
            [object]$PrinterProfiles,

            [switch]$SkipIfEmpty
        )

        $path = Get-SamyPrinterLocalConfigPath

        $profilesArray = @($PrinterProfiles)

        if ($SkipIfEmpty -and ($null -eq $PrinterProfiles -or $profilesArray.Count -eq 0)) {
            Write-LogHybrid "Update-SamyPrinterConfig: no printer profiles returned; keeping existing printers.json." Warning Printers -LogToEvent
            return
        }

        if ($profilesArray.Count -eq 0) {
            Write-LogHybrid "Update-SamyPrinterConfig: zero profiles; writing empty printers.json." Warning Printers -LogToEvent
        }

        try {
            $profilesArray | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
            Write-LogHybrid "Saved $($profilesArray.Count) printer profiles to '$path'." Success Printers -LogToEvent

            $Script:Samy_PrinterProfiles = $null
        }
        catch {
            Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent
        }
    }

    # Install-DattoRMM core function

    function Install-DattoRMM {
        <#
        .SYNOPSIS
            Installs/configures the Datto RMM agent and handles sites/variables.
        #>
        [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
        param (
            [switch]$UseWebhook,
            [string]$WebhookPassword,
            [string]$WebhookUrl    = $Global:DattoWebhookUrl,
            [string]$ApiUrl,
            [string]$ApiKey,
            [string]$ApiSecretKey,
            [switch]$FetchSites,
            [switch]$SaveSitesList,
            [string]$OutputFile      = 'datto_sites.csv',
            [switch]$PushSiteVars,
            [switch]$InstallRMM,
            [switch]$SaveCopy,
            [string]$SiteUID,
            [string]$SiteName
        )

        if ($SaveSitesList -and -not $FetchSites) {
            Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent
            return
        }

        if ($UseWebhook) {
            if (-not $WebhookPassword) {
                Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent
                return
            }
            try {
                $resp = Invoke-RestMethod -Uri $WebhookUrl `
                                          -Headers @{ SAMYPW = $WebhookPassword } `
                                          -Method GET
                $ApiUrl       = $resp.ApiUrl
                $ApiKey       = $resp.ApiKey
                $ApiSecretKey = $resp.ApiSecretKey
                Write-LogHybrid "Webhook credentials fetched." Success DattoRMM -LogToEvent
            } catch {
                Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent
                return
            }
        }

        if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
            Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent
            return
        }

        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        try {
            $publicCred = New-Object System.Management.Automation.PSCredential(
                'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
            )
            $tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
                                           -Credential $publicCred `
                                           -Method Post `
                                           -ContentType 'application/x-www-form-urlencoded' `
                                           -Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
            $token = $tokenResp.access_token
            Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent
        } catch {
            Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
            return
        }

        $headers = @{ Authorization = "Bearer $token" }

        if ($FetchSites) {
            try {
                $sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
                $siteList = $sitesResp.sites | Sort-Object name | ForEach-Object {
                    [PSCustomObject]@{ Name = $_.name; UID = $_.uid }
                }
                Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM -LogToEvent

                if ($SaveSitesList) {
                    $desktop = [Environment]::GetFolderPath('Desktop')
                    $path    = Join-Path $desktop $OutputFile
                    $ext     = [IO.Path]::GetExtension($OutputFile).ToLower()
                    if ($ext -eq '.json') {
                        $siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8
                    } else {
                        $siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
                    }
                    Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM -LogToEvent
                }

                return $siteList
            } catch {
                Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent
                return @()
            }
        }

        if ($PushSiteVars) {
            try {
                $varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
                Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent
            } catch {
                Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
            }
            $regPath = "HKLM:\Software\SVS\Deployment"
            foreach ($v in $varsResp.variables) {
                try {
                    if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
                    New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
                    Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent
                } catch {
                    Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
                }
            }
        }

        if ($InstallRMM) {
            if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
                try {
                    $dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
                    $tmp   = "$env:TEMP\AgentInstall.exe"
                    Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
                    Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent
                    Start-Process -FilePath $tmp -NoNewWindow
                    Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent
                } catch {
                    Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
                }
            }
        }

        if ($SaveCopy) {
            try {
                $dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
                $path  = "C:\Temp\AgentInstall.exe"
                if (-not (Test-Path 'C:\Temp')) { New-Item -Path 'C:\Temp' -ItemType Directory | Out-Null }
                Invoke-WebRequest -Uri $dlUrl -OutFile $path -UseBasicParsing
                Write-LogHybrid "Saved installer copy to $path." Info DattoRMM -LogToEvent
            } catch {
                Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
            }
        }

        if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
            Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM -LogToEvent
        }
    }

    # Dispatch-Request

    function Dispatch-Request {
        param($Context)

        $path = $Context.Request.Url.AbsolutePath.TrimStart('/')

        if ($path -eq 'quit') {
            Write-LogHybrid "Shutdown requested" Info Server -LogToEvent
            Send-Text $Context "Server shutting down."
            $Global:Listener.Stop()
            return
        }

        if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') {
            Invoke-TasksCompleted $Context
            return
        }

        if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
            Invoke-FetchSites $Context
            return
        }

        if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') {
            Invoke-RenameComputer $Context
            return
        }

        if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getprinters') {
            Invoke-GetPrinters $Context
            return
        }

        if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'installprinters') {
            Invoke-InstallPrinters $Context
            return
        }

        if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps', 'devices')) {
            $page = if ($path -eq '') { 'onboard' } else { $path }
            $html = Get-UIHtml -Page $page
            Send-HTML $Context $html
            return
        }
          
        $task = $Global:SamyTasks | Where-Object Name -EQ $path
        if ($task) {
            & $task.HandlerFn $Context
            return
        }

        $Context.Response.StatusCode = 404
        Send-Text $Context '404 - Not Found'
    }

    # MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI)

    Write-LogHybrid "Invoke-ScriptAutomationMonkey starting (ParameterSet=$($PSCmdlet.ParameterSetName))" Info Bootstrap -LogToEvent

    switch ($PSCmdlet.ParameterSetName) {

        'Toolkit' {
            Write-LogHybrid "Running in Toolkit mode (-SilentInstall)." Info SVSModule -LogToEvent

            Install-SVSMSP -InstallToolkit

            Write-LogHybrid "Toolkit mode completed." Success SVSModule -LogToEvent
            return
        }

        'Cleanup' {
            Write-LogHybrid "Running in Cleanup mode (-Cleanup)." Info SVSModule -LogToEvent

            Install-SVSMSP -Cleanup

            Write-LogHybrid "Cleanup mode completed." Success SVSModule -LogToEvent
            return
        }

        'Offboard' {
            Write-LogHybrid "Running in headless Offboard mode (-Offboard)." Info OffBoard -LogToEvent

            try { Invoke-UninstallCyberQP      -Context $null } catch { Write-LogHybrid "Headless offboard: CyberQP uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
            try { Invoke-UninstallHelpDesk     -Context $null } catch { Write-LogHybrid "Headless offboard: HelpDesk uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
            try { Invoke-UninstallThreatLocker -Context $null } catch { Write-LogHybrid "Headless offboard: ThreatLocker uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
            try { Invoke-UninstallRocketCyber  -Context $null } catch { Write-LogHybrid "Headless offboard: RocketCyber uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
            try { Invoke-CleanupSVSMSP         -Context $null } catch { Write-LogHybrid "Headless offboard: SVSMSP cleanup failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }

            Write-LogHybrid "Headless Offboard mode completed." Success OffBoard -LogToEvent
            return
        }

        'DattoFetch' {
            Write-LogHybrid "Running in DattoFetch mode (fetch sites)." Info DattoRMM -LogToEvent

            $sites = Install-DattoRMM `
                -UseWebhook `
                -WebhookPassword $WebhookPassword `
                -FetchSites `
                -SaveSitesList:$SaveSitesList `
                -OutputFile $OutputFile

            if ($sites) {
                Write-LogHybrid "DattoFetch returned $($sites.Count) sites." Success DattoRMM -LogToEvent

                try {
                    $sites | Sort-Object Name | Format-Table Name, UID -AutoSize
                } catch {
                    Write-LogHybrid "Failed to render Datto site table to console: $($_.Exception.Message)" Warning DattoRMM -LogToEvent
                }
            } else {
                Write-LogHybrid "DattoFetch returned no sites." Warning DattoRMM -LogToEvent
            }

            return
        }

        'DattoInstall' {
            Write-LogHybrid "Running in DattoInstall mode for site '$SiteName' ($SiteUID)." Info DattoRMM -LogToEvent

            Install-DattoRMM `
                -UseWebhook `
                -WebhookPassword $WebhookPassword `
                -SiteUID $SiteUID `
                -SiteName $SiteName `
                -PushSiteVars:$PushSiteVars `
                -InstallRMM:$InstallRMM `
                -SaveCopy:$SaveCopy

            Write-LogHybrid "DattoInstall mode completed for site '$SiteName'." Success DattoRMM -LogToEvent
            return
        }

        default {
            Write-LogHybrid "Starting Script Automation Monkey UI on port $Port." Info UI -LogToEvent

            try {
                Start-Process "http://localhost:$Port/" | Out-Null
            } catch {
                Write-LogHybrid "Failed to launch browser: $($_.Exception.Message)" Warning UI -LogToEvent
            }

            Start-Server

            Write-LogHybrid "Script Automation Monkey UI stopped (listener exited)." Info UI -LogToEvent
        }
    }

} # end function Invoke-ScriptAutomationMonkey

if ($MyInvocation.InvocationName -ne '.') {
    Invoke-ScriptAutomationMonkey @args
}
