# Run this script from a PowerShell prompt with administrator privileges
#Requires -Version 5.1
<#PSScriptInfo

    .VERSION 1.0
    .GUID frdepo42-50a9-872d-b26a-488f4e0e9344
    .AUTHOR Sage X3 R&D
    .COMPANYNAME Sage
    .COPYRIGHT (c) Copyright SAGE 2006-2025. All Rights Reserved.
    .TAGS MS Windows
    .LICENSEURI
    .PROJECTURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDSCRIPTS 
    .EXTERNALSCRIPTDEPENDENCIES Powershell-5.1 / PWSH 7.2
    .RELEASENOTES
    .DESCRIPTION 
    Install / Uninstall X3-Services   
    - Create MS Windows Service
    - Check Service running
    - Check Yml config files

    Usage: 
    install-tools.ps1 -Action "Install"
    install-tools.ps1 -Action "Uninstall"

    #>   
param (
    [Parameter(Mandatory = $true)]$Action, 
    [Parameter(Mandatory = $false)] [string] $InstallPath,
    [Parameter(Mandatory = $false)] [string] $LogPath,
    [Parameter(Mandatory = $false)] [string] $UserName,
    [Parameter(Mandatory = $false)] [string] $Password, # This password can be encoded to base64 with the prefix: base64:XXXXXX
    [Parameter(Mandatory = $false)] [string] $ServiceDisplayName,
    [Parameter(Mandatory = $false)] [string] $ServiceDescription
)

function Add-To-Log {
    [CmdletBinding()]
    <#
        .SYNOPSIS 
            Log routine that will accept an input Message, log it to a file and indicate if the Message represents an error or not. 
        .PARAMETER Message 
            Some type of sentence describing what you want to log to the log file
        .PARAMETER file
            The file you want to append the sentence to
        .EXAMPLE 
            Informational Usage:
            Add-To-Log -Message "Command Message executed: $cmd" -file $logName  -LogMessageType Info -WriteToOutput $true
        .EXAMPLE 
            Error Usage: 
            Add-To-Log -Message "$($PSItem.Exception.Message) $(Get-CRLF)" -file $logName  -LogMessageType Error -WriteToOutput $true
    #>
    param(
        [Parameter(Mandatory = $true)][AllowEmptyString()][string] $Message,
        [Parameter(Mandatory = $false)][string] $File,
        [Parameter(Mandatory = $true)][LogMessageType]$logMessageType,
        [Parameter(Mandatory = $false)][System.Boolean]$WriteToOutput = $false
    )

    if ( [string]::IsNullOrEmpty($File)) { 
        $File = $logFileName
    }
    
    Add-Content -Value "$((Get-Date).ToString("yyyy-MM-dd HH:mm:ss")) $logMessageType $($Message) " -Path $File -ErrorAction Continue

    if ( $WriteToOutput -eq $true ) {

        switch ($logMessageType) {
            Warning {
                Write-Warning $Message            
            }
            Error {
                Write-Error $Message
            }
            Debug {
                Write-Debug $Message
                Wrtie-Verbose $Message
            }
            Info {
                Write-Output $Message
            }
            Default {
                Write-Output $Message
            }
        }
    }
}
function Install-ConfigFile {
    [CmdletBinding()]

    param (
        [Parameter(Mandatory = $true)] [string] $SourceFileName,
        [Parameter(Mandatory = $true)] [string] $DestFileName
    )

    if (Test-Path -Path $DestFileName) {

        Add-To-Log -Message "'$DestFileName' already exists. File unchanged." -LogMessageType Info -WriteToOutput $true

    }
    else {

        if (Test-Path -Path $SourceFileName) {
            Add-To-Log -Message "Creating '$DestFileName' (from '$SourceFileName')" -LogMessageType Info -WriteToOutput $true
            Rename-Item -Path $SourceFileName -NewName $DestFileName -Force
            Add-To-Log -Message "'$SourceFileName' renamed '$DestFileName'" -LogMessageType Info -WriteToOutput $false
        }
        else {
            Add-To-Log -Message "'$SourceFileName' doesn't exist. Copy failed." -LogMessageType Info -WriteToOutput $true
        }     
    }
}

# Count number of spaces at the beginning of a string (yml fields)
#
function CountSpaces {
    param (
        [Parameter(Mandatory = $true)] [string] $StringValue     
    )
    [int]$blanks = $argName.Length - $argName.TrimStart().Length
    return $blanks
}

#
# Read Yml file. Return HashTable of properties
# Ex: C:\Sage\X3Services\xtrem-security.yml, C:\Sage\X3Services\xtrem-config.yml
# 
function Read-Config-File {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [string] $ConfigFile     
    )
    [hashtable]$properties = @{}

    if (-not (Test-Path -Path $ConfigFile)) {
        Add-To-Log -Message "Read-Config-File $($ConfigFile): Doesn't exist" -LogMessageType Error -WriteToOutput $true
        throw "An error has occurred: Configuration file $ConfigFile doesn't exist"
    }

    $reader = New-Object -TypeName System.IO.StreamReader -ArgumentList $ConfigFile
    $i = 0
    $sectionArray = New-Object System.Collections.ArrayList
    $lastBlankCount = 0
    while ($null -ne ($currentLine = $reader.ReadLine())) {
        $MessagePair = $currentLine.Split(":", 2)
        $argName = $($MessagePair[0])
        $argValue = $($MessagePair[1])
        if ( (-not [string]::IsNullOrEmpty( $argName )) -and (-not $argName.StartsWith("#")) ) {
    
            # Section entry
            if ( ([string]::IsNullOrEmpty($argValue))) {
                
                $blankCount = CountSpaces -StringValue $argName
                if ($blankCount -gt $lastBlankCount) {
                    $sectionArray.Add($argName)
                }
                else {
                    # case "syracuse:"
                    if ($blankCount -eq 0) {
                        $sectionArray.Clear()
                        $sectionArray.Add($argName)
                    } 
                    # case "   domains:"
                    else {
                        if ($sectionArray.Length -gt 0) {
                            $sectionArray.RemoveAt($sectionArray.Length - 1)
                        }
                        $sectionArray.Add($argName)
                    }
                }
            }
            # case "clientId: ${syracuse.clientid}"
            else {
                $argName = $argName.Trim()
                $argValue = $argValue.Trim()
                $argValue = $argValue.Replace("\\", "\")  

                [string]$mykey = ""
                foreach ($section in $sectionArray) {
                    $mykey += "$($section.trim())"
                    $mykey += "."    
                }
                $mykey += "$($argName)"
                $properties["$mykey"] = "$argValue"
            }
        }
        $i++
    }
    $reader.Close()
    $reader.Dispose() 

    Add-To-Log -Message "Read-Config-File $ConfigFile - $i entries read" -LogMessageType Info -WriteToOutput $true
    return $properties
} 

function PrintHeader {
    [CmdletBinding()]
    param()

    Add-To-Log -Message  ("Action: $($Action)" ) -LogMessageType Info -WriteToOutput $true
    Add-To-Log -Message  ("OsName: $([System.Environment]::OSVersion.VersionString)" ) -LogMessageType Info -WriteToOutput $true
    Add-To-Log -Message  ("Platform: $([System.Environment]::OSVersion.Platform)" ) -LogMessageType Info -WriteToOutput $false
    Add-To-Log -Message  ("PowerShell: $($PSVersionTable.PSVersion)" ) -LogMessageType Info -WriteToOutput $true
    Add-To-Log -Message  ("HostName: $([System.Net.Dns]::GetHostName())" )  -LogMessageType Info -WriteToOutput $true
    Add-To-Log -Message  ("InstallationPath: $InstallPath" )  -LogMessageType Info -WriteToOutput $true
    Add-To-Log -Message  ("LogFile: $logFileName" )  -LogMessageType Info -WriteToOutput $true

}

function ConvertTo-Yaml {
    param (
        [Parameter(Mandatory = $true)]
        [object]$InputObject,        
        [int]$IndentLevel = 0
    )

    $yaml = ""
    $indent = "  " * $IndentLevel

    if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
        $properties = $InputObject  | Get-Member -MemberType Properties
        foreach ($property in  $properties) {

            $val = $InputObject.$($property.Name)
            if ($property.MemberType -eq "NoteProperty") {

                if ($val -is [System.Management.Automation.PSCustomObject]) {
                    $yaml += "$($indent)$($property.Name): `n"
                    $yaml += ConvertTo-Yaml -InputObject $val -IndentLevel ($IndentLevel + 1)
                }
                elseif ( ($val.GetType().Name -eq "String") -or ($val.GetType().Name -eq "Boolean")) {
                    $yaml += "$($indent)$($property.Name): $val`n"
                }
            }
            else {
                $yaml += ConvertTo-Yaml -InputObject $val -IndentLevel ($IndentLevel + 1)
            }
        }
    }

    return $yaml
}

function Remove-EmptyBranches {
    param (
        [Parameter(Mandatory = $true)]
        [object]$jsonObject
    )

    # Create a hashtable to store non-empty properties
    $nonEmptyObject = @{}

    foreach ($property in $jsonObject.psobject.Properties) {
        $value = $property.Value

        if ($value -is [System.Collections.IDictionary]) {
            # If the value is an object (dictionary), recurse into it
            $nestedValue = Remove-EmptyBranches -jsonObject $value
            if ($nestedValue.Count -gt 0) {
                $nonEmptyObject[$property.Name] = $nestedValue
            }
        }
        elseif ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
            # If the value is an array, filter out empty items
            $filteredArray = $value | Where-Object { $_ -ne "" -and $_ -ne $null }
            if ($filteredArray.Count -gt 0) {
                $nonEmptyObject[$property.Name] = $filteredArray
            }
        }
        elseif ($value -is [PSCustomObject] -and -not ($value -is [string])) {
            $nestedValue = Remove-EmptyBranches -jsonObject $value
            if ($nestedValue.Count -gt 0) {
                $nonEmptyObject[$property.Name] = $nestedValue
            }            
        }
        elseif ($value -ne "" -and $value -ne $null) {
            # If it's a simple property with a non-empty value, keep it
            $nonEmptyObject[$property.Name] = $value
        }
    }

    return $nonEmptyObject
}

function Convert-JsonToYmlConfigFile {
    [CmdletBinding()]

    param (
        [Parameter(Mandatory = $true)] [string] $JsonSourceFileName,
        [Parameter(Mandatory = $true)] [string] $YmlDestFileName
    )

    if (Test-Path -Path $YmlDestFileName) {

        Add-To-Log -Message "'$YmlDestFileName' already exists. File unchanged." -LogMessageType Info -WriteToOutput $true

    }
    else {

        if (Test-Path -Path $JsonSourceFileName) {
            Add-To-Log -Message "Creating '$YmlDestFileName' (from '$JsonSourceFileName')" -LogMessageType Info -WriteToOutput $true
            
            # Read and parse the JSON file
            $jsonContent = Get-Content -Path $JsonSourceFileName -Raw | ConvertFrom-Json

            Add-To-Log -Message "JsonContent: $jsonContent" -LogMessageType Debug -WriteToOutput $false

            # Remove empty branches
            $cleanedJson = Remove-EmptyBranches -jsonObject $jsonContent
            # Convert the cleaned object back to JSON
            $cleanedJsonString = $cleanedJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json
            $yamlContent = ConvertTo-Yaml -InputObject $cleanedJsonString

            # No log for security reason
            # Add-To-Log -Message "Json ConvertTo-Yaml: $yamlContent" -LogMessageType Debug -WriteToOutput $false

            # Write the YAML content to a file
            Set-Content -Path $YmlDestFileName -Value $yamlContent
            Add-To-Log -Message "$YmlDestFileName created" -LogMessageType Info -WriteToOutput $true

        }
        else {
            Add-To-Log -Message "'$JsonSourceFileName' doesn't exist. Copy failed." -LogMessageType Info -WriteToOutput $true
        }     
    }
}

function CleanUp-AfterInstall {
    [CmdletBinding()]
    param()

    Add-To-Log -Message "Clean up installation"  -LogMessageType Debug -WriteToOutput $false

    [string]$jsonConfigFileTemplate = Join-Path -Path $InstallPath -ChildPath "xtrem-config.template.json" 
    if (Test-Path -Path $jsonConfigFileTemplate) {
        Add-To-Log -Message "Delete '$jsonConfigFileTemplate' "  -LogMessageType Debug -WriteToOutput $false
        Remove-Item $jsonConfigFileTemplate -Recurse -ErrorAction Ignore
    }

    [string]$jsonConfigFileSecurityTemplate = Join-Path -Path $InstallPath -ChildPath "xtrem-security.template.json" 
    if (Test-Path -Path $jsonConfigFileSecurityTemplate) {
        Add-To-Log -Message "Delete '$jsonConfigFileSecurityTemplate' "  -LogMessageType Debug -WriteToOutput $false
        Remove-Item $jsonConfigFileSecurityTemplate -Recurse -ErrorAction Ignore
    }

    Add-To-Log -Message "Clean up done"  -LogMessageType Debug -WriteToOutput $false
}

function Ping-Service {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)] [string] $x3ServicePingUrl,
        [Parameter(Mandatory = $true)] [string] $configFile,
        [Parameter(Mandatory = $true)] [string] $configSecurityFile
    )

    Add-To-Log -Message  "Requesting $x3ServicePingUrl .." -LogMessageType Info -WriteToOutput $false
    $response = Invoke-WebRequest -Uri $x3ServicePingUrl -UseBasicParsing -ErrorAction SilentlyContinue
    # Check if the status code is 200
    if ($response.StatusCode -eq 200) {
        Add-To-Log -Message  "Request to $x3ServicePingUrl successful. StatusCode: $($response.StatusCode)" -LogMessageType Info -WriteToOutput $true
        return $true
    }
    else {
        Add-To-Log -Message  "Request to $x3ServicePingUrl failed! StatusCode: $($response.StatusCode)"  -LogMessageType Info -WriteToOutput $true
        Add-To-Log -Message "Launch manually the file 'manual-start.cmd' to see logs, and check your configuration files $configFile and $configSecurityFile"
    }
    return $false
} 


function Install-Service {
    [CmdletBinding()]
    param()

    [string]$jsonConfigFileTemplate = Join-Path -Path $InstallPath -ChildPath "xtrem-config.template.json" 
    [string]$configFile = Join-Path -Path $InstallPath -ChildPath "xtrem-config.yml"

    Convert-JsonToYmlConfigFile -JsonSourceFileName $jsonConfigFileTemplate -YmlDestFileName $configFile

    [boolean]$updateMode = $false
    if (Test-Path -Path $configFile) {
        $updateMode = $true
    } 
    if ($updateMode) {
        Add-To-Log -Message "Update of '$ServiceDisplayName'/'$ServiceDescription' service ... "  -LogMessageType Info -WriteToOutput $true
    }
    else {
        Add-To-Log -Message "Creation of '$ServiceDisplayName'/'$ServiceDescription' service ... "  -LogMessageType Info -WriteToOutput $true
    }

    PrintHeader

    $configXtrem = Read-Config-File -ConfigFile $configFile  # "xtrem-config.yml"
    if ( $configXtrem.GetType().Name -eq "Object[]") {
        $configXtrem = $configXtrem[$configXtrem.Length - 1]
    }

    [string]$jsonConfigFileSecurityTemplate = Join-Path -Path $InstallPath -ChildPath "xtrem-security.template.json" 
    [string]$configSecurityFile = Join-Path -Path $InstallPath -ChildPath "xtrem-security.yml"

    Convert-JsonToYmlConfigFile -JsonSourceFileName $jsonConfigFileSecurityTemplate -YmlDestFileName $configSecurityFile

    $configSecurityProp = Read-Config-File -ConfigFile $configSecurityFile # "xtrem-security.yml"
    if ( $configSecurityProp.GetType().Name -eq "Object[]") {
        $configSecurityProp = $configSecurityProp[$configSecurityProp.Length - 1]
    }
    [string]$syracuseUrl = $($configSecurityProp["loginUrl"])    
    # [string]$clientId = $($configSecurityProp["syracuse.clientId"])    
    # [string]$secret = $($configSecurityProp["syracuse.secret"])
    [string]$x3servicePort = $configXtrem["server.port"]
    if ([string]::IsNullOrEmpty($x3servicePort)) {
        $x3servicePort = "8240"
    }


    # sagex3rd.local\QualityAdmin
    $domainName = ""
    $user = "$UserName"
    if ($UserName.IndexOf("\") -gt 0) {
        $domainName = $UserName.Substring(0, $UserName.IndexOf("\"))
        $user = $UserName.Substring($UserName.IndexOf("\") + 1)
    }

    $cmd = "$($InstallPath)\nodejs\win32-x64\node.exe $($InstallPath)\service.js --operation install --name '$ServiceDisplayName' --description '$ServiceDescription' --domain '$domainName' --user '$user' "
    # base64:
    if ($Password.StartsWith("base64:")) {
        [string]$passwordBase64 = $Password.Substring("base64:".Length)
        $cmd += "--base64passwd '$passwordBase64'"
    }
    else {
        $cmd += "--password '$Password'"
    }
        
    # Add-To-Log -Message "$cmd" -LogMessageType Info -WriteToOutput $true

    $proc = Invoke-CommandProcess -Command $cmd -HideOccurence "$Password"
    if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
        [string]$errorMessage = "$($proc.stderr)"
        Add-To-Log -Message "Error starting the service: $($ServiceDescription) - $($errorMessage)"  -LogMessageType Error -WriteToOutput $true
    }
    else {
        Start-Sleep -Seconds 4

        [boolean] $serviceCreated = Test-ServiceExists -ServiceDisplayName "$ServiceDisplayName"
        if ($serviceCreated) {
            Add-To-Log -Message "$ServiceDisplayName has been created" -LogMessageType Info -WriteToOutput $false
            [boolean] $serviceRunning = Test-ServiceRunning -ServiceDisplayName "$ServiceDisplayName"
            if ($serviceRunning) {
                $x3ServiceUrl = "http://$([System.Net.Dns]::GetHostName()):$($x3servicePort)"
                Add-To-Log -Message "$ServiceDisplayName is running" -LogMessageType Info -WriteToOutput $false
                Add-To-Log -Message "$($proc.stdout)" -LogMessageType Info -WriteToOutput $true 
                Add-To-Log -Message "Service '$ServiceDescription' started on $x3ServiceUrl" -LogMessageType Info -WriteToOutput $true

                $cmd = "sc failure ""$ServiceName"" reset=0 actions=restart/60000/restart/60000/none/0"
                $proc = Invoke-CommandProcess -Command $cmd
                if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
                    Add-To-Log -Message "Error while setting recovery rules on the service: $($ServiceDescription) - $($errorMessage)"  -LogMessageType Warning -WriteToOutput $true
                }
                else {
                    Add-To-Log -Message "Service '$ServiceDescription' recovery rules set" -LogMessageType Info -WriteToOutput $true
                }

                # Send the web request and store the response
                # Start-Sleep -Seconds 4
                # $x3ServicePingUrl = $x3ServiceUrl + "/ping"
                # $pingOK =Ping-Service -X3ServicePingUrl $x3ServicePingUrl -ConfigFile $configFile -ConfigSecurityFile $configSecurityFile
                if (-Not $updateMode) {
                    Add-To-Log -Message "Copy your 'ClientId' and 'Secret password' string from '$configSecurityFile' `n and copy them to X3 Syracuse to finalize the configuration between $ServiceDisplayName and Syracuse." -LogMessageType Info -WriteToOutput $true
                    Add-To-Log -Message "Open Syracuse on '$($syracuseUrl)', and check $ServiceDisplayName configuration in 'Global settings' and the linked Solution(s)." -LogMessageType Info -WriteToOutput $true
                }
            
                CleanUp-AfterInstall
            }
            else {
                Add-To-Log -Message "$ServiceDisplayName doesn't seem to run" -LogMessageType Warning -WriteToOutput $true                
            }
        }
        else {
            Add-To-Log -Message "$ServiceDisplayName doesn't seem to have been created" -LogMessageType Warning -WriteToOutput $true
        }

    }
}

function Stop-X3Services {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$ServiceName,
        [Parameter(Mandatory = $false)][int]$WaitAfterStart
    )

    [int]$waitSeconds = 5
    if ($WaitAfterStart -gt $waitSeconds) {
        $waitSeconds = $WaitAfterStart
    }

    if ($IsLinux) {

        [string]$cmd = "systemctl stop $ServiceName"
        $proc = Invoke-CommandProcess -Command $cmd 
        if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
            [string]$errorMessage = "$($proc.stderr)"
            Add-To-Log -Message "Error stopping the service: $($ServiceName) - $($errorMessage)"  -LogMessageType Error -WriteToOutput $true
        }
        else {
            Start-Sleep -Seconds $waitSeconds
            Add-To-Log -Message "Service '$ServiceName' stopped" -LogMessageType Info -WriteToOutput $true
        }
    }

    # MS Windows
    else {

        try {
            Stop-Service -Name $ServiceName -ErrorAction Stop
            Start-Sleep -Seconds $waitSeconds
            $service = Get-Service -Name $ServiceName
            if ($service.Status -eq 'Stopped') {
                Add-To-Log -Message  "The service '$ServiceName' has been stopped successfully (Status: $($service.Status))." -LogMessageType Info -WriteToOutput $true
            }
            else {
                Add-To-Log -Message  "Service '$ServiceName' status '$($service.Status)' not OK." -LogMessageType Warning -WriteToOutput $true
                Start-Sleep -Seconds $waitSeconds
                Add-To-Log -Message  "Service '$ServiceName' status '$($service.Status)'" -LogMessageType Warning -WriteToOutput $true
            }
        }
        catch {
            Add-To-Log -Message "An error occurred while stopping the service '$ServiceName': $_"  -LogMessageType Error -WriteToOutput $true
        }
    }
}

function Uninstall-Service {
    [CmdletBinding()]
    param()

    [string]$cmd = "$($InstallPath)\nodejs\win32-x64\node.exe $($InstallPath)\service.js uninstall"
    # For the next version: 
    $proc = Invoke-CommandProcess -Command $cmd 
    if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
        [string]$errorMessage = "$($proc.stderr)"
        Add-To-Log -Message "Error while uninstalling service 1/2: $($ServiceDisplayName) / $($ServiceDescription) - $($errorMessage)"  -LogMessageType Error -WriteToOutput $false
        Add-To-Log -Message "Retrying with new API .. "  -LogMessageType Error -WriteToOutput $false
        $cmd = "$($InstallPath)\nodejs\win32-x64\node.exe $($InstallPath)\service.js --operation uninstall --name '$($ServiceDisplayName)'"
        $proc = Invoke-CommandProcess -Command $cmd 
    }

    if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
        [string]$errorMessage = "$($proc.stderr)"
        Add-To-Log -Message "Error while uninstalling service 2/2: $($ServiceDisplayName) / $($ServiceDescription) - $($errorMessage)"  -LogMessageType Warn -WriteToOutput $true

        Stop-X3Services -ServiceName $($ServiceName) -WaitAfterStart 4
        $cmd = "sc.exe delete '$($ServiceName)'"
        $proc = Invoke-CommandProcess -Command $cmd 
        if (($null -ne $proc.ExitCode) -and ($proc.ExitCode -ne 0)) {  
            [string]$errorMessage = "$($proc.stderr)"
            Add-To-Log -Message "Error while deleting service: $($ServiceName)/$($ServiceDisplayName) - $($errorMessage)"  -LogMessageType Error -WriteToOutput $true
        }
        else {
            Add-To-Log -Message "Service deleted : $($ServiceDisplayName) / $($ServiceDescription) "  -LogMessageType Info -WriteToOutput $true
        }
    }
    
    [boolean] $serviceStillExists = Test-ServiceExists -ServiceDisplayName "$ServiceDisplayName"
    if (($proc.ExitCode -eq 0) -or ($serviceStillExists -eq $false) ) {
        Start-Sleep -Seconds 2
        Add-To-Log -Message "Service '$ServiceDescription' uninstalled from '$([System.Net.Dns]::GetHostName())'" -LogMessageType Info -WriteToOutput $true

        $foldersToDelete = @("nodejs", "node_modules")
        foreach ($folder in $foldersToDelete) {
            [string]$folder2Remove = Join-Path -Path  $InstallPath -ChildPath $folder
            if (Test-Path -Path  $folder2Remove) {
                Remove-Item $folder2Remove -Recurse -ErrorAction Ignore
                Add-To-Log -Message "Folder '$folder2Remove' deleted"  -LogMessageType Info -WriteToOutput $true
            }
        }
    }
}

function Check-Uninstall-Service-Necessary {
    Add-To-Log -Message "Check Service '$ServiceDisplayName' is existing" -LogMessageType Info -WriteToOutput $true
    $serviceExists = Test-ServiceExists -ServiceDisplayName $ServiceDisplayName
    if ($serviceExists) {
        Add-To-Log -Message "Service '$ServiceDisplayName' exists" -LogMessageType Info -WriteToOutput $true
        Uninstall-Service
    }
    else {
        Add-To-Log -Message "No Service named '$ServiceDisplayName' found" -LogMessageType Info -WriteToOutput $true
    }
}

function Test-ServiceExists {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string] $ServiceDisplayName
    )     
    if ($IsLinux) {
        $serviceList = Invoke-Expression "systemctl list-units --type=service"
        if ($serviceList -match $ServiceDisplayName) {
            return $true
        }
    }
    else {
        $service = Get-Service -Name "$ServiceDisplayName" -ErrorAction SilentlyContinue
        if ($service) {
            return $true
        }
    }
    return $false
}


function Test-ServiceRunning {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)][string]$ServiceDisplayName      
    )  
    $isServiceCreated = Test-ServiceExists -ServiceDisplayName $ServiceDisplayName 
    if ($isServiceCreated) {
        if ($IsLinux) {
            $serviceStatus = systemctl is-active $ServiceDisplayName
            if ($serviceStatus -eq 'Active') {
                return $true
            }        
        }
        else {
            $service = Get-Service -Name $ServiceDisplayName
            if ($service.Status -eq 'Running') {
                return $true
            }
        }
    }
    return $false
}




function Invoke-CommandProcess {
    <#
    .SYNOPSIS
    Launch a command with Linux Shell binary (/bin/sh)
        
    .PARAMETER command
    Command to execute. Ex: "cp file1.txt file2.txt"
    
    .PARAMETER sudoer
    Execute the command with root user.
    #>

    [CmdletBinding()]

    param(
        [Parameter(Mandatory = $true, HelpMessage = "Command to execute")][string]$command,
        [Parameter(Mandatory = $false)] [string]$hideOccurence,
        [Parameter(Mandatory = $false)] [bool]$sudoer
    )

    $proc = New-Object System.Diagnostics.Process
    $stdout = New-Object System.Text.StringBuilder
    $stderr = New-Object System.Text.StringBuilder
    $stdoutEvent = $null
    $stderrEvent = $null
    [string]$errorMessage = $null

    try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo        
        if ($IsLinux) {
            $pinfo.FileName = "/bin/sh"
            if ($sudoer -eq $true) {
                $pinfo.UserName = "root"
            }
            $command = $command.Replace("""", "'")
            $pinfo.Arguments = "-c ""$command"" "
        }
        else {
            $pinfo.FileName = "cmd.exe"
            $command = $command.Replace("'", """")
            $pinfo.Arguments = "/c $command ";
        }
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WorkingDirectory = $PSScriptRoot 
        $pinfo.CreateNoWindow = $true

        $proc.StartInfo = $pinfo
        # deadlocks due to the synchronous call to ReadToEnd(),
        #https://stackoverflow.com/questions/8761888/capturing-standard-out-and-error-with-start-process
        $stdoutEvent = Register-ObjectEvent $proc -EventName OutputDataReceived -MessageData $stdout -Action {
            $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
        }
          
        $stderrEvent = Register-ObjectEvent $proc -EventName ErrorDataReceived -MessageData $stderr -Action {
            $Event.MessageData.AppendLine($Event.SourceEventArgs.Data)
        }

        $proc.Start() | Out-Null  #  workaround to avoid $null value
        $proc.BeginOutputReadLine()
        $proc.BeginErrorReadLine()
        Wait-Process -Id $proc.Id -TimeoutSec 7200 # $proc.WaitForExit()

        if ($proc.HasExited) {
            $exitCode = $proc.ExitCode
        }
        else {
            Stop-Process -Force -Id $proc.Id
            $exitCode = -1
        }

        # Be sure to unregister.  You have been warned.
        Unregister-Event $stdoutEvent.Id
        Unregister-Event $stderrEvent.Id        
        $errorMessage = ($stderr.ToString()).Trim()
        $output = ($stdout.ToString()).Trim()
        # Check if the command succeeded
        if (($null -ne $exitCode) -and ($exitCode -eq 0)) {
            if (-Not [string]::IsNullOrEmpty($hideOccurence)) {
                $command = $command.Replace($hideOccurence, "<hidden>")
            }

            Add-To-Log -Message "Command succeeded: $command"  -LogMessageType Debug -WriteToOutput $false
            Add-To-Log -Message "Output: $output" -LogMessageType Debug -WriteToOutput $false
            # Sometimes, the output is located on the $errorMessage  
            Add-To-Log -Message "Message: $errorMessage" -LogMessageType Debug -WriteToOutput $false
        }
        else {
            # Write-Host "Command failed with exit code $($proc.ExitCode)  Command: $command"
            Add-To-Log -Message "Command failed: $command"  -LogMessageType Debug -WriteToOutput $false
            if (-not [string]::IsNullOrEmpty($output)) {
                $errorMessage = "$errorMessage `n $($output)"
            }

            if (-not ([string]::IsNullOrWhiteSpace( $errorMessage) ) ) {
                switch ($ErrorAction) {
                    Stop { 
                        throw $errorMessage
                    }
                    Default {}
                }
            } 
        }
    }
    catch {
        if (-Not [string]::IsNullOrEmpty($hideOccurence)) {
            $command = $command.Replace($hideOccurence, "<hidden>")
        }

        Add-To-Log -Message "Error while running cmd $command Error:$($_.Exception.Message)" -LogMessageType Error -WriteToOutput $true

        if ($null -ne $stdoutEvent) {
            Unregister-Event $stdoutEvent.Id
        }
        if ($null -ne $stderrEvent) {
            Unregister-Event $stderrEvent.Id        
        }
        $errorMessage = ($stderr.ToString()).Trim()
        $output = ($stdout.ToString()).Trim()

        if ((-not [string]::IsNullOrEmpty($errorMessage)) -or (-not [string]::IsNullOrEmpty($output))  ) {
            Add-To-Log -Message "$($output) `n $errorMessage" -LogMessageType Error -WriteToOutput $true
        }
        Add-To-Log -Message $_.ScriptStackTrace  -LogMessageType Error -WriteToOutput $false
        Add-To-Log -Message $_.ErrorDetails -LogMessageType Error -WriteToOutput $false
        
        switch ($ErrorAction) {
            Stop { 
                throw
            }
            Default {}
        }
    }

    return   [pscustomobject]@{
        ExitCode = $exitCode
        stdout   = $stdout.ToString()
        stderr   = $errorMessage
    }
}


enum LogMessageType{
    Info
    Warning
    Error
    Debug
}

[datetime]$global:StartTime = $(get-date)
if ([string]::isNullOrEmpty($InstallPath)) {
    $InstallPath = $PSScriptRoot
}
if ([string]::IsNullOrEmpty($logPath)) {
    $logPath = $InstallPath
}
[string]$filename = (Get-Item $PSCommandPath).BaseName
[string]$global:logFileName = "$($InstallPath){0}logs{0}$filename.log" -f [IO.Path]::DirectorySeparatorChar
if (-not [string]::IsNullOrEmpty( $logPath)) {
    if (-not (Test-Path -Path $logPath)) {
        New-Item -ItemType Directory -Force -Path $logPath
    }
    $logFileName = Join-Path -Path  $logPath -ChildPath "$filename.log"
}
if ([string]::IsNullOrEmpty( $ServiceName ) ) {
    $ServiceName = "sagex3services.exe"
}
if ([string]::IsNullOrEmpty( $ServiceDisplayName ) ) {
    $ServiceDisplayName = "Sage X3 Services"
}
if ([string]::IsNullOrEmpty( $ServiceDescription ) ) {
    $ServiceDescription = "Sage X3 Services"
}
Set-Location $InstallPath
[int]$exitCode = 0

#
# Main Program
# 
try {

    switch ( $Action ) {   
        Install {
            Install-Service
        } 
        Uninstall {
            PrintHeader
            Uninstall-Service
        }
        CheckHiddenUpdateCase {
            PrintHeader
            Check-Uninstall-Service-Necessary
        }

        default {
            Add-To-Log -Message "Action unknown: $Action" -LogMessageType Info -WriteToOutput $true
        }
    }
}
catch {
    $exitCode = 1
    Add-To-Log -Message $PSItem.Exception.Message  -LogMessageType Error -WriteToOutput $true
    Add-To-Log -Message "$($_.ErrorDetails) " -LogMessageType Error -WriteToOutput $false
    Add-To-Log -Message "$($_.ScriptStackTrace) "  -LogMessageType Error -WriteToOutput $false
} 
finally {
    $elapsedTime = new-timespan $StartTime $(get-date)
    Add-To-Log -Message "Elapsed time: $($elapsedTime.ToString('hh\:mm\:ss'))  ExitCode: $exitCode"  -LogMessageType Info -WriteToOutput $false
    Add-To-Log -Message "`n`n--------------------" -LogMessageType Info -WriteToOutput $false
}

exit $exitCode

