Stti.psm1

param(
    [switch] $SkipAdminCheck = $false
)

#Check if the user is in the administrator group. Warns and stops if the user is not.
if (-not ($SkipAdminCheck) -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    throw "You are not running this as local administrator. Run it again in an elevated prompt."
}

#Load or install SqlServer Module in Windows Powershell for compatibility
$sqlServerModule = Import-Module SqlServer -UseWindowsPowerShell -ErrorAction SilentlyContinue -PassThru
if ($null -eq $sqlServerModule){
    Start-Job -PSVersion 5.1 { Install-Module SqlServer -Force -AllowClobber } | Receive-Job -Wait
    $sqlServerModule = Import-Module SqlServer -UseWindowsPowerShell -ErrorAction SilentlyContinue -PassThru
    if ($null -eq $sqlServerModule){
        throw "Could not load SqlServer Module under Windows Powershell"
    }
}

enum SttiInstanceRoles {
    Worker
    Web
}

enum LogLevels {
    Debug = 1
    Verbose = 2
    Info = 3
    Warn = 4
    Error = 5
}

class SttiPackage{
    [System.Version] $Version
    [string] $Path
    [bool] $IsLocal

    SttiPackage([Version] $version, [string] $path, [bool] $isLocal){
        $this.Version = $version
        $this.Path = $path
        $this.IsLocal = $isLocal
    }
}

class SttiInstanceConfig{
    [string] $Name
    [string] $SqlServer
    [string] $Database
    [SttiInstanceRoles[]] $Roles
    [string] $Path
    [string] $ServiceUsername
    [bool] $StoreDbInInstanceDataPath
    [string] $WorkerSslCertificateSubject
    [uint] $WorkerPort
    [string] $WebSslCertificateSubject
    [uint] $WebPort
    [string] $WorkerHostname
    [string] $EncryptionCertificateThumbprint
    [string] $EncryptionMasterKeyName
}

class SttiInstanceInstallStatus{
    [bool] $Installed
    [SttiInstanceRoles[]] $Roles
    [Datetime] $Timestamp
    [string] $Version

    SttiInstanceInstallStatus(){
        $this.Installed = $false
    }
    [string] ToString(){
        if ($this.Installed -eq $true){
            return "Installed Version $($this.Version) with Roles [$($this.Roles)] at $($this.Timestamp)"
        }
        else {
            return "Not installed"
        }
    }
}

class SttiInstanceStatus{
    [bool] $DatabaseExists
    [bool] $InstanceExists
    [SttiInstanceInstallStatus] $InstallStatus
    [bool] $WorkerRoleRunning
    [bool] $WebRoleRunning
    [bool] $DatabaseEncryptionRunning

    SttiInstanceStatus(){
        $this.DatabaseExists = $false
        $this.InstanceExists = $false
        $this.WebRoleRunning = $false
        $this.WorkerRoleRunning = $false
        $this.InstallStatus = $null
        $this.DatabaseEncryptionRunning = $false
    }
}



#region Installation

function Install-SttiInstance {
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory)]
        [SttiPackage] $Package,
        
        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential,

        [Parameter()]
        [switch] $Force
    )

    Start-SttiInstallLogFile -Instance $Instance -Level Verbose
    Write-SttiInstallLogHeader
    
    try {
        Write-SttiLog "Start install instance $($Instance.Name)" -Level Info
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose

        Test-ADCredential $ServiceCredential -ErrorAction Stop > $null
        Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null

        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        if ($installStatus.Installed -eq $true){
            throw "The instance $($Instance.Name) is already installed"
        }
        
        $errorAction = $Force ? "Continue" : "Stop"
        Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction
        Build-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction
        Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction
        Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction
        New-SttiDatabase -Instance $instance -ErrorAction $errorAction
        Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Update-SttiDatabase -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction
        Start-SttiInstance -Instance $Instance -ErrorAction $errorAction

        Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null
        
        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        Write-SttiLog "Finished install instance $($Instance.Name)" -Extraline -Level Info
    }
    catch {
        Write-SttiLog "An error occurred during install of the instance" -Level Warn
        Write-SttiLog $_ -Level Error
        Write-SttiLog $_.Exception -Level Info
        Write-SttiLog $_.ScriptStackTrace -Level Info
    }
    finally{
        Write-SttiInstallLogFooter
        Stop-SttiInstallLogFile
    }
}
#Install-SttiInstance -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage "latest")

function Update-SttiInstance {
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory)]
        [SttiPackage] $Package,
        
        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential,

        [Parameter()]
        [switch] $Force        
    )

    Start-SttiInstallLogFile -Instance $Instance -Level Verbose
    Write-SttiInstallLogHeader
    
    try {
        Write-SttiLog "Start update instance $($Instance.Name)" -Level Info
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose

        Test-ADCredential $ServiceCredential -ErrorAction Stop > $null
        Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null

        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        if ($installStatus.Installed -eq $false -and $Force -eq $false){
            throw "The instance $($Instance.Name) is not installed"
        }
        
        $errorAction = $Force ? "Continue" : "Stop"
        Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction
        Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction
        Build-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction
        Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction
        Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction
        Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Update-SttiDatabase -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction
        Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction
        Start-SttiInstance -Instance $Instance -ErrorAction $errorAction

        Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null
        
        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        Write-SttiLog "Finished update instance $($Instance.Name)" -Extraline -Level Info
    }
    catch {
        Write-SttiLog "An error occurred during update of the instance" -Level Warn
        Write-SttiLog $_ -Level Error
        Write-SttiLog $_.Exception -Level Info
        Write-SttiLog $_.ScriptStackTrace -Level Info
    }
    finally{
        Write-SttiInstallLogFooter
        Stop-SttiInstallLogFile
    }
}
#Update-SttiInstance -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage "latest")

function Uninstall-SttiInstance {
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter()]
        [switch] $Force    
    )

    Start-SttiInstallLogFile -Instance $Instance -Level Verbose
    Write-SttiInstallLogHeader
    
    try {
        Write-SttiLog "Start uninstall instance $($Instance.Name)" -Level Info
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose

        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        if ($installStatus.Installed -eq $false -and $Force -eq $false){
            throw "The instance $($Instance.Name) is not installed"
        }
        
        $errorAction = $Force ? "Continue" : "Stop"
        Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction
        Remove-SttiInstanceServices -Instance $Instance -ErrorAction $errorAction
        Remove-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction
        Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction        

        Set-SttiInstanceInstallStatus -Instance $Instance -Installed $false -Roles $null -Version $null -ErrorAction Stop > $null
        
        $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
        $installStatus = $instanceStatus.InstallStatus
        Write-SttiLog "Current instance status" -Level Verbose
        $instanceStatus | Write-SttiLog -Level Verbose

        Write-SttiLog "Finished uninstall instance $($Instance.Name)" -Extraline -Level Info
    }
    catch {
        Write-SttiLog "An error occurred during uninstall of the instance" -Level Warn
        Write-SttiLog $_ -Level Error
        Write-SttiLog $_.Exception -Level Info
        Write-SttiLog $_.ScriptStackTrace -Level Info
    }
    finally{
        Write-SttiInstallLogFooter
        Stop-SttiInstallLogFile
    }
}
#Uninstall-SttiInstance -Instance (Get-SttiInstanceConfig dev)

function Get-SttiInstanceInstallStatus{
    [OutputType([SttiInstanceInstallStatus])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance")]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [X509Certificate] $Certificate
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $installStatus = [SttiInstanceInstallStatus]::new()
    if ($null -eq $Instance){
        $Instance = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop
        if ($null -eq $Instance){
            return $installStatus
        }
    }    
    $instanceInstallStatusFile = Get-SttiInstanceInstallStatusFileName -Name $Instance.Name
    if (-not (Test-Path $instanceInstallStatusFile)){
        Write-SttiLog "The instance installation status $instanceInstallStatusFile does not exists" -Level Verbose
        return $installStatus
    }

    return [SttiInstanceInstallStatus](Get-Content $instanceInstallStatusFile -Raw | ConvertFrom-Json)
}
#Get-SttiInstanceInstallStatus -Name dev
#Get-SttiInstanceInstallStatus -Instance (Get-SttiInstanceConfig -Name dev)

function Set-SttiInstanceInstallStatus{
    [OutputType([SttiInstanceInstallStatus])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance")]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [bool] $Installed,
        
        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $Version,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [SttiInstanceRoles[]] $Roles
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    if ($null -ne $Instance){
        $Name = $Instance.Name
    }
    
    $config = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop
    if ($null -eq $config){
        throw "The instance configuration does not exist"
    }

    $installStatus = Get-SttiInstanceInstallStatus -Name $Name -ErrorAction Stop
    
    if ($PSBoundParameters.ContainsKey("Installed")){
        $installStatus.Installed = $Installed
    }
    if ($PSBoundParameters.ContainsKey("Version")){
        $installStatus.Version = $Version
    }
    if ($PSBoundParameters.ContainsKey("Roles")){
        $installStatus.Roles = $Roles
    }
    $installStatus.Timestamp = Get-Date
    
    $instanceInstallStatusFile = Get-SttiInstanceInstallStatusFileName $Name

    $installStatus | ConvertTo-Json -ErrorAction Stop | Out-File $instanceInstallStatusFile -Encoding UTF8 -ErrorAction Stop

    $PSCmdlet.WriteObject($installStatus)
} 
#Set-SttiInstanceInstallStatus -Name dev -Installed $true -Roles Web, Worker -Version 1.0.0.0
#Set-SttiInstanceInstallStatus -Name dev -Installed $false -Roles @() -Version $null

function Clear-SttiInstanceDirectory{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance        
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        $instanceDirectory = Get-Item -Path $Instance.Path -ErrorAction Stop
        $instanceDirectory | Get-ChildItem -Exclude "data", "config.json", "install.json" -ErrorAction Stop | Remove-Item -Recurse -Force -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Clear-SttiInstanceDirectory -Instance (Get-SttiInstanceConfig dev)

function Build-SttiInstanceDirectory{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance        
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        $instanceDirectory = Get-Item -Path $Instance.Path

        New-Item -Path $instanceDirectory -Name "bin" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null

        $dataDirectory = Get-Item -Path ([System.IO.Path]::Combine($instanceDirectory, "data")) -ErrorAction SilentlyContinue || New-Item -Path $instanceDirectory -Name "data" -ItemType Directory -ErrorAction Stop
        New-Item -Path $dataDirectory -Name "logs" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
        New-Item -Path $dataDirectory -Name "clients" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
        if ($Instance.StoreDbInInstanceDataPath -eq $true){
            New-Item -Path $dataDirectory -Name "db" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
        }

        # Grant permissions for service user
        # Read permissions for instance directory
        Write-SttiLog "Set instance directory permissions" -Level Verbose
        $instanceDirectoryAcl = Get-Acl -Path $instanceDirectory -ErrorAction Stop
        $instanceDirectoryAcl.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($Instance.ServiceUsername, "Read",
            [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit,
            [System.Security.AccessControl.PropagationFlags]::None,
            "Allow"))
        Set-Acl -Path $instanceDirectory -AclObject $instanceDirectoryAcl -ErrorAction Stop

        # Full control for data directory
        Write-SttiLog "Set data directory permissions" -Level Verbose
        $dataDirectoryAcl = Get-Acl -Path $dataDirectory -ErrorAction Stop
        $dataDirectoryAcl.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($Instance.ServiceUsername, "FullControl", 
            [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit,
            [System.Security.AccessControl.PropagationFlags]::None,
            "Allow"))
        Set-Acl -Path $dataDirectory -AclObject $dataDirectoryAcl -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Build-SttiInstanceDirectory -Instance (Get-SttiInstanceConfig dev)

function Expand-SttiPackage{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,
        
        [Parameter(Mandatory)]
        [SttiPackage] $Package 
    
        )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        $tempPath = [System.IO.Path]::GetTempPath()
        $tempWorkingDir = New-Item -Name (Get-Random) -Path $tempPath -ItemType Directory -ErrorAction Stop
        Expand-Archive $Package.Path -DestinationPath $tempWorkingDir -ErrorAction Stop
        $tempContentPath = [System.IO.Path]::Combine($tempWorkingDir.FullName, "Content")
        $binPath = [System.IO.Path]::Combine($Instance.Path, "bin\")
        Get-ChildItem -Path $tempContentPath | Copy-Item -Destination $binPath -Recurse -Force -ErrorAction Stop
        
        $tempWorkingDir | Remove-Item  -Recurse -Force -ErrorAction Continue
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Expand-SttiPackage -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage 0.0.0.1)

function Edit-SttiInstanceConfigFiles{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance        
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $sqlConnectionString = Get-SttiDatabaseConnectionString -Instance $Instance
    
    try{
        # Set database connection
        Get-ChildItem $Instance.Path -Exclude "data" -Include "appsettings.json" -Recurse -PipelineVariable "currentFile" -ErrorAction Stop
            | Copy-Item -Destination {"$($_.FullName).bak"} -Force -PassThru -PipelineVariable "backupFile" -ErrorAction Stop # create backup file
            | ForEach-Object { Get-Content -Path $currentFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } # Read content as string in one block and deserialize json
            | ForEach-Object { 
                if ($currentFile.FullName -like "*\halvotec.stti.worker\*"){
                    $port = $Instance.WorkerPort
                    $sslCertificateSubject = $Instance.WorkerSslCertificateSubject
                }
                elseif ($currentFile.FullName -like "*\halvotec.stti.web\*"){
                    $port = $Instance.WebPort
                    $sslCertificateSubject = $Instance.WebSslCertificateSubject
                }

                if ($_.Stti -and $_.Stti.Connections -and $_.Stti.Connections.DBConnection) { 
                    $_.Stti.Connections.DBConnection = $sqlConnectionString 
                }
                if ($_.SttiWeb -and $_.SttiWeb.SttiWorkerService ) { 
                    $_.SttiWeb.SttiWorkerService = "https://$($Instance.WorkerHostname):$($Instance.WorkerPort)" 
                }
                if ($_.Kestrel){
                    if ($_.Kestrel.Endpoints -and $_.Kestrel.Endpoints.HttpsDefault -and $_.Kestrel.Endpoints.HttpsDefault.Url){
                        $_.Kestrel.Endpoints.HttpsDefault.Url = "https://*:$($port)"
                    }
                    if ($_.Kestrel.Certificates -and $_.Kestrel.Certificates.Default){
                        $_.Kestrel.Certificates.Default = [PSCustomObject]@{
                            Subject = $sslCertificateSubject
                            Store = "My"
                            Location = "CurrentUser"
                            AllowInvalid = $true
                        }
                    }
                }
                $_ 
              } # Replace connection string
            | ForEach-Object { ConvertTo-Json -InputObject $_ -Depth 50 -ErrorAction Stop | Out-File $currentFile -Encoding utf8 -ErrorAction Stop } # Serialize to Json and write to file
            | ForEach-Object { Remove-Item $backupFile -ErrorAction Continue } # Remove backup file
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Edit-SttiInstanceConfigFiles -Instance (Get-SttiInstanceConfig dev)

function Add-SttiInstanceServices{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,
        
        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential 
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        Write-SttiLog "Grant LogonAsService right to service user" -Level Verbose
        Grant-LogonAsServiceRight $ServiceCredential.UserName -ErrorAction Stop

        $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker)
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){
            if ($null -eq (Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue)){
                $workerBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe")
                Write-SttiLog "Create service for worker" -Level Verbose
                New-Service -Name $workerServiceName -BinaryPathName $workerBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Worker" -ErrorAction Stop > $null
            }
            else{
                Write-SttiLog "Service for worker already exists" -Level Verbose
            }
        }
        else {
            Write-SttiLog "Remove service for worker"
            Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop
        }

        $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web)
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Web)){
            if ($null -eq (Get-Service -Name $webServiceName -ErrorAction SilentlyContinue)){
                $webBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Web\Halvotec.Stti.Web.exe")
                Write-SttiLog "Create service for web" -Level Verbose
                New-Service -Name $webServiceName -BinaryPathName $webBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Web" -ErrorAction Stop > $null
            }
            else{
                Write-SttiLog "Service for web already exists" -Level Verbose
            }
        }
        else {
            Write-SttiLog "Remove service for web"
            Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Install-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev) -ServiceCredential (Get-Credential)

function Remove-SttiInstanceServices{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance        
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker)
        Write-SttiLog "Remove service for worker" -Level Verbose
        Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
    try{
        $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web)
        Write-SttiLog "Remove service for web" -Level Verbose
        Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Uninstall-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev)

function Get-SttiServiceName{
    [OutputType([string])]
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,
        [Parameter(Mandatory)]
        [SttiInstanceRoles] $Role        
    )

    return "STTI.$($Instance.Name).$Role"
}

#endregion Installation

#region Certificate management

function New-SttiUserCertificate{
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [string] $Subject,
        
        [string] $FriendlyName = $Subject,
        
        [DateTime] $NotAfter = (Get-Date).AddYears(10)
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    return New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation Cert:CurrentUser\My -KeyExportPolicy Exportable -Type Custom -KeyAlgorithm RSA -KeyLength 2048 -HashAlgorithm SHA256 -NotAfter $NotAfter -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2") -ErrorAction Stop
}

function New-SttiDatabaseEncryptionKey{
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [string] $Subject,
        [Parameter(Mandatory)]
        [string] $User,
        
        [string] $FriendlyName = $Subject,
        
        [DateTime] $NotAfter = (Get-Date).AddYears(10)
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $location = "Cert:CurrentUser\My"
    return New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation $location -KeyExportPolicy Exportable -Type DocumentEncryptionCert -KeyUsage KeyEncipherment -KeySpec KeyExchange -KeyLength 2048 -NotAfter $NotAfter -ErrorAction Stop
}

function Get-SttiDatabaseEncryptionKey {
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]    
        [string] $Thumbprint
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $key = Get-ChildItem -Path "Cert:CurrentUser\My" | Where-Object Thumbprint -eq $Thumbprint
    if ($null -eq $key){
        $key = Get-ChildItem -Path "Cert:LocalMachine\My" | Where-Object Thumbprint -eq $Thumbprint
    }
    if ($null -eq $key){ 
        Write-SttiLog "No database encryption key was found with thumbprint $Thumbprint" -Level Error
    }

    return $key
}

function Get-SttiDatabaseEncryptionKeyLocation {
    [OutputType([string])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]    
        [string] $Thumbprint
    )
    
    $key = Get-ChildItem -Path "Cert:CurrentUser\My" | Where-Object Thumbprint -eq $Thumbprint
    if ($null -ne $key){
        Write-SttiLog "Found database encryption key in Cert:CurrentUser\My" -Level Verbose
        return "CurrentUser"
    }
    else{
        $key = Get-ChildItem -Path "Cert:LocalMachine\My" | Where-Object Thumbprint -eq $Thumbprint
        if ($null -ne $key){
            Write-SttiLog "Found database encryption key in Cert:LocalMachine\My" -Level Verbose
            return "LocalMachine"
        }
        else { 
            Write-SttiLog "No database encryption key was found with thumbprint $Thumbprint" -Level Error
            return $null
        }
    }
}

#endregion Certificate management

#region Instance Management



function Get-SttiInstanceConfig{
    [OutputType([SttiInstanceConfig])]
    [CmdletBinding()]
    Param(
        [string] $Name
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $instanceNames = $PSBoundParameters.ContainsKey("Name") ? @($Name) : (Get-ChildItem -Path "$([System.IO.Path]::Combine((Get-SttiBasePath), "instances\*"))" | Select-Object -ExpandProperty Name)
    foreach ($name in $instanceNames) {
        $instanceConfigFile = Get-SttiInstanceConfigFileName -Name $Name
        if (-not (Test-Path $instanceConfigFile)){
            Write-SttiLog "The instance configuration $instanceConfigFile does not exists" -Level Verbose

            continue
        }

        $config = [SttiInstanceConfig](Get-Content $instanceConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
        $config.Path = Get-SttiInstancePath -Name $Name
    
        Write-Output $config
    }
}
#Get-SttiInstanceConfig -Name "dev"

function New-SttiInstanceConfig{
    [OutputType([SttiInstanceConfig])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [string] $Name,
        
        [Parameter(Mandatory=$true)]
        [string] $SqlServer,
        
        [Parameter()]
        [string] $Database = "STTI_$($Name.ToUpperInvariant())",
        
        [Parameter(Mandatory=$true)]
        [SttiInstanceRoles[]] $Roles,

        [Parameter(Mandatory=$true)]
        [string] $ServiceUsername,

        [Parameter()]
        [switch] $StoreDbInInstanceDataPath = $false,
        
        [Parameter(Mandatory)]
        [string] $WorkerSslCertificateSubject,

        [Parameter()]
        [uint] $WorkerPort = 5010,

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

        [Parameter()]
        [uint] $WebPort = 5011,

        [Parameter()]
        [string] $WorkerHostname = "localhost",

        [Parameter(Mandatory)]
        [string] $EncryptionCertificateThumbprint
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug
    
    $Name = $Name.ToUpperInvariant()

    $instancePath = Get-SttiInstancePath $Name
    if (-not (Test-Path $instancePath)){
        New-Item $instancePath -ItemType Directory -ErrorAction Stop | Out-Null
    }

    $instanceConfigFile = Get-SttiInstanceConfigFileName $Name
    if ((Test-Path $instanceConfigFile)){
        throw "The instance configuration $instanceConfigFile already exists"
    }
    $config = [SttiInstanceConfig]::new()
    $config.Name = $Name
    $config.SqlServer = $SqlServer
    $config.Database = $Database
    $config.Roles = $Roles
    $config.Path = $instancePath
    $config.ServiceUsername = $ServiceUsername
    $config.StoreDbInInstanceDataPath = $StoreDbInInstanceDataPath
    $config.WorkerSslCertificateSubject = $WorkerSslCertificateSubject
    $config.WorkerPort = $WorkerPort
    $config.WebSslCertificateSubject = $WebSslCertificateSubject
    $config.WebPort = $WebPort
    $config.WorkerHostname = $WorkerHostname
    $config.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint    
    $config.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint"
    
    $config | ConvertTo-Json -ErrorAction Stop | Out-File $instanceConfigFile -Encoding utf8 -ErrorAction Stop

    return $config
} 
#New-SttiInstanceConfig -Name "DEV" -SqlServer localdb -Database SttiDev -Roles Worker, Web

function Set-SttiInstanceConfig{
    [OutputType([SttiInstanceConfig])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $SqlServer,
        
        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $Database,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [SttiInstanceRoles[]] $Roles,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $ServiceUsername,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [switch] $StoreDbInInstanceDataPath = $false,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $WorkerSslCertificateSubject,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [uint] $WorkerPort,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $WebSslCertificateSubject,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [uint] $WebPort,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $WorkerHostname,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $EncryptionCertificateThumbprint
    )
    Process{
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

        if ($null -ne $Instance){
            $Name = $Instance.Name
        }
        
        $config = Get-SttiInstanceConfig ($Name) -ErrorAction Stop
        if ($null -eq $config){
            throw "The instance configuration does not exist"
        }
        
        if ($PSBoundParameters.ContainsKey("SqlServer")){
            $config.SqlServer = $SqlServer
        }
        if ($PSBoundParameters.ContainsKey("Database")){
            $config.Database = $Database
        }
        if ($PSBoundParameters.ContainsKey("Roles")){
            $config.Roles = $Roles
        }
        if ($PSBoundParameters.ContainsKey("ServiceUsername")){
            $config.ServiceUsername = $ServiceUsername
        }
        if ($PSBoundParameters.ContainsKey("StoreDbInInstanceDataPath")){
            $config.StoreDbInInstanceDataPath = $StoreDbInInstanceDataPath
        }
        if ($PSBoundParameters.ContainsKey("WorkerSslCertificateSubject")){
            $config.WorkerSslCertificateSubject = $WorkerSslCertificateSubject
        }
        if ($PSBoundParameters.ContainsKey("WorkerPort")){
            $config.WorkerPort = $WorkerPort
        }
        if ($PSBoundParameters.ContainsKey("WebSslCertificateSubject")){
            $config.WebSslCertificateSubject = $WebSslCertificateSubject
        }
        if ($PSBoundParameters.ContainsKey("WebPort")){
            $config.WebPort = $WebPort
        }
        if ($PSBoundParameters.ContainsKey("WorkerHostname")){
            $config.WorkerHostname = $WorkerHostname
        }
        if ($PSBoundParameters.ContainsKey("EncryptionCertificateThumbprint")){
            if ($null -ne $config.EncryptionCertificateThumbprint -and $EncryptionCertificateThumbprint -ne $config.EncryptionCertificateThumbprint){
                Write-SttiLog "The EncryptionCertificateThumbprint has already been set and cannot be changed. Use Rotate-SttiEncryptionMasterKey" -Level Error
            }
            $config.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint
            $config.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint"
        }
        
        $instanceConfigFile = Get-SttiInstanceConfigFileName $Name

        $config | ConvertTo-Json -ErrorAction Stop | Out-File $instanceConfigFile -Encoding utf8 -ErrorAction Stop

        $PSCmdlet.WriteObject($config)
    }
} 
#Set-SttiInstanceConfig -InstanceName dev -Database "Test" -Roles Web, Worker
#Get-SttiInstanceConfig dev | Set-SttiInstanceConfig -Database "Test" -Roles Web

function Test-SttiInstanceBinding{
    [OutputType([bool])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance
    )

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    foreach ($otherInstance in (Get-SttiInstanceConfig | Where-Object Name -ne $Instance.Name)) {
        if ($Instance.WorkerPort -eq $otherInstance.WorkerPort){
            Write-SttiLog "$($otherInstance.Name) is also configured for port $($Instance.WorkerPort)" -Level ((Get-SttiInstanceInstallStatus -Instance $otherInstance).Installed -eq $true ? "Error" : "Warn")
            return $false
        }
    }
    return $true
}

function Get-SttiInstancePath{
    Param(
        [Parameter(Mandatory, Position, ParameterSetName="InstanceName")]
        [string] $Name,

        [Parameter(Mandatory, ParameterSetName="Instance")]
        [SttiInstanceConfig] $Instance
    )
    if ($null -ne $Instance){
        $Name = $Instance.Name
    }

    return [System.IO.Path]::Combine((Get-SttiBasePath), "Instances\", $Name)
}

function Get-SttiInstanceConfigFileName{
    Param(
        [Parameter(Mandatory)]
        [string] $Name
    )

    return [System.IO.Path]::Combine((Get-SttiInstancePath $Name), "config.json")
}

function Get-SttiInstanceInstallStatusFilename{
    Param(
        [Parameter(Mandatory)]
        [string] $Name
    )

    return [System.IO.Path]::Combine((Get-SttiInstancePath $Name), "install.json")
}

function Get-SttiInstallLogFilename{
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [DateTime] $Timestamp
    )

    if ($Timestamp -and $null -ne $Timestamp){
        return [System.IO.Path]::Combine((Get-SttiBasePath), "install-$($Instance.Name)-$(("{0:s}" -f $Timestamp) -replace ":", "-").log")
    }
    else{
        return [System.IO.Path]::Combine((Get-SttiBasePath), "install-$($Instance.Name).log")
    }
}

function Get-SttiBasePath{
    [OutputType([string])]
    Param()

    $basePath = $env:SttiBasePath ?? "C:\STTI\"
    if (-not (Test-Path $basePath)){
        Write-SttiLog "STTI base path $basePath does not exist" -Level Warn
    }
    return $basePath
}

function Set-SttiBasePath{
    Param(
        [Parameter(Mandatory)]
        [string] $BasePath
    )

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    [System.Environment]::SetEnvironmentVariable("SttiBasePath", $BasePath, [System.EnvironmentVariableTarget]::Machine)
    $env:SttiBasePath = $BasePath

    if (-not (Test-Path $BasePath)){
        Write-SttiLog "STTI base path $BasePath does not exist. It will be created." -Level Warn

        New-Item $BasePath -ItemType Directory -ErrorAction Stop | Out-Null
    }

    $PackagePath = [System.IO.Path]::Combine($BasePath, "Packages")
    if (-not (Test-Path $PackagePath)){
        New-Item $PackagePath -ItemType Directory -ErrorAction Stop | Out-Null
    }
}
function Get-SttiInstanceStatus{
    [OutputType([SttiInstanceStatus])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [X509Certificate] $Certificate
    )    
    Process {
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

        $status = [SttiInstanceStatus]::new()
    
        if ($null -eq $Instance){
            $Instance = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop
            if ($null -eq $Instance){
                Write-SttiLog "No instance config found" -Level Verbose
                return $status
            }
        }   
        
        # Check if instance exists
        $status.InstanceExists = Test-Path (Get-SttiInstancePath -Name $Instance.Name)

        # Check install status
        $status.InstallStatus = Get-SttiInstanceInstallStatus -Instance $Instance

        # Check if database exists
        try {
            $sqlDb = Get-SqlDatabase -Name $Instance.Database -ServerInstance $Instance.SqlServer -ErrorAction Ignore
        }
        catch {
            Write-SttiLog "Could not access database $($Instance.Database) on $($Instance.SqlServer): $($_.Exception)." -Level Warn
        }    
        $status.DatabaseExists = ($null -ne $sqlDb)

        # Check if worker service is running
        $workerService = Get-Service -Name (Get-SttiServiceName -Instance $Instance -Role Worker) -ErrorAction SilentlyContinue
        $status.WorkerRoleRunning = ($null -ne $workerService -and $workerService.Status -eq "Running")

        # Check if webui service is running
        $webService = Get-Service -Name (Get-SttiServiceName -Instance $Instance -Role Web) -ErrorAction SilentlyContinue
        $status.WebRoleRunning = ($null -ne $webService -and $webService.Status -eq "Running")

        $PSCmdlet.WriteObject($status)
    }
}
#Get-SttiInstanceStatus -Name dev

function Start-SttiInstance {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [X509Certificate] $Certificate
    )    
    Process {
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

        try{
            $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker)
            Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Stopped | Start-Service -ErrorAction Stop 
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
        try{
            $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web)
            Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Stopped | Start-Service -ErrorAction Stop 
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Stop-SttiInstance {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $Name,
        [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [X509Certificate] $Certificate
    )    
    Process {
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

        try{
            $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker)
            $workerService = Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Running 
            if ($null -ne $workerService){
                $processId = Get-CimInstance Win32_Service -Verbose:$false | Where-Object { $_.Name -eq $workerService.Name } | Select-Object -ExpandProperty ProcessId
                Stop-Service $workerService
                Get-Process -Id $processId -ErrorAction SilentlyContinue | Wait-Process -TimeoutSec 120 -ErrorAction Stop
            }
            Start-Sleep -Seconds 0.5
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
        try{
            $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web)
            $webService = Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Running 
            if ($null -ne $webService){
                $processId = Get-CimInstance Win32_Service -Verbose:$false | Where-Object { $_.Name -eq $webService.Name } | Select-Object -ExpandProperty ProcessId
                Stop-Service $webService
                Get-Process -Id $processId -ErrorAction SilentlyContinue | Wait-Process -TimeoutSec 120 -ErrorAction Stop
            }
            Start-Sleep -Seconds 0.5
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}


#endregion Instance Management

#region Database Management

function New-SttiDatabase{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance
    ) 
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $dbPath = [System.IO.Path]::Combine((Get-SttiInstancePath -Name $Instance.Name), "data\db\")

    try{
        $command = "CREATE DATABASE [$($Instance.Database)];"
        $command = "
            DECLARE @DefaultDataPath varchar(max)
            SET @DefaultDataPath = (SELECT CONVERT(varchar(max), SERVERPROPERTY('INSTANCEDEFAULTDATAPATH')))
            DECLARE @DefaultLogPath varchar(max)
            SET @DefaultLogPath = (SELECT CONVERT(varchar(max), SERVERPROPERTY('INSTANCEDEFAULTLOGPATH')))
            DECLARE @DataPath varchar(max)
            SET @DataPath = $($Instance.StoreDbInInstanceDataPath ? "'$dbPath'" : "@DefaultDataPath")
            DECLARE @LogPath varchar(max)
            SET @LogPath = $($Instance.StoreDbInInstanceDataPath ? "'$dbPath'" : "@DefaultLogPath")
             
            EXECUTE('
            CREATE DATABASE [$($Instance.Database)]
            ON PRIMARY
            ( NAME = N''STTI'', FILENAME = N''' + @DataPath + 'STTI_DEV.mdf'' , SIZE = 262144KB , FILEGROWTH = 65536KB )
            LOG ON
            ( NAME = N''STTI_log'', FILENAME = N''' + @LogPath + 'STTI_DEV_log.ldf'' , SIZE = 65536KB , FILEGROWTH = 65536KB )
            COLLATE Latin1_General_100_CI_AS');
            GO
 
            ALTER DATABASE [$($Instance.Database)] SET COMPATIBILITY_LEVEL = 140
            ALTER DATABASE [$($Instance.Database)] SET ANSI_NULL_DEFAULT OFF
            ALTER DATABASE [$($Instance.Database)] SET ANSI_NULLS OFF
            ALTER DATABASE [$($Instance.Database)] SET ANSI_PADDING OFF
            ALTER DATABASE [$($Instance.Database)] SET ANSI_WARNINGS OFF
            ALTER DATABASE [$($Instance.Database)] SET ARITHABORT OFF
            ALTER DATABASE [$($Instance.Database)] SET AUTO_CLOSE OFF
            ALTER DATABASE [$($Instance.Database)] SET AUTO_SHRINK OFF
            ALTER DATABASE [$($Instance.Database)] SET AUTO_CREATE_STATISTICS ON(INCREMENTAL = OFF)
            ALTER DATABASE [$($Instance.Database)] SET AUTO_UPDATE_STATISTICS ON
            ALTER DATABASE [$($Instance.Database)] SET CURSOR_CLOSE_ON_COMMIT OFF
            ALTER DATABASE [$($Instance.Database)] SET CONCAT_NULL_YIELDS_NULL OFF
            ALTER DATABASE [$($Instance.Database)] SET NUMERIC_ROUNDABORT OFF
            ALTER DATABASE [$($Instance.Database)] SET QUOTED_IDENTIFIER OFF
            ALTER DATABASE [$($Instance.Database)] SET RECURSIVE_TRIGGERS OFF
            ALTER DATABASE [$($Instance.Database)] SET DISABLE_BROKER
            ALTER DATABASE [$($Instance.Database)] SET ALLOW_SNAPSHOT_ISOLATION ON
            ALTER DATABASE [$($Instance.Database)] SET READ_COMMITTED_SNAPSHOT ON
            ALTER DATABASE [$($Instance.Database)] SET RECOVERY FULL
            ALTER DATABASE [$($Instance.Database)] SET PAGE_VERIFY CHECKSUM
            GO
             
            IF NOT EXISTS
                (SELECT name FROM master.sys.server_principals WHERE name = '$($Instance.ServiceUsername)')
            BEGIN
                CREATE LOGIN [$($Instance.ServiceUsername)] FROM WINDOWS WITH DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english]
            END
            GO
             
            USE [$($Instance.Database)]
            GO
            IF NOT EXISTS
                (SELECT [login].name, [dbuser].name FROM sys.syslogins AS [login]
                    INNER JOIN sys.database_principals [dbuser] ON [login].sid = [dbuser].sid
                WHERE [login].name = '$($Instance.ServiceUsername)')
            BEGIN
                CREATE USER [$($Instance.ServiceUsername)] FOR LOGIN [$($Instance.ServiceUsername)]
                ALTER ROLE [db_owner] ADD MEMBER [$($Instance.ServiceUsername)]
            END
            GO"

        Invoke-Sqlcmd -ServerInstance $Instance.SqlServer -Database "master" -Query $command -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}   
#Get-SttiInstanceConfig dev | New-SttiDatabase -EncryptionMasterKey $null

function Add-SttiDatabaseEncryption{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter()]
        [PSCredential] $ServiceCredential = $null
    ) 
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug
    
    try{    
        
        $scriptBlock = {
            param(
                [string] $ScriptRoot,
                $Instance
            )  

            $key = Get-ChildItem -Path "Cert:CurrentUser\My" | Where-Object Thumbprint -eq $Instance.EncryptionCertificateThumbprint
            if ($null -ne $key){
                Write-Verbose "Found database encryption key in Cert:CurrentUser\My"
                $certificateStoreLocation = "CurrentUser"
            }
            else{
                $key = Get-ChildItem -Path "Cert:LocalMachine\My" | Where-Object Thumbprint -eq $Instance.EncryptionCertificateThumbprint
                if ($null -ne $key){
                    Write-Verbose "Found database encryption key in Cert:LocalMachine\My"
                    $certificateStoreLocation = "LocalMachine"
                }
                else { 
                    throw "No database encryption key was found with thumbprint $($Instance.EncryptionCertificateThumbprint)"
                }
            }

            $db = Get-SqlDatabase -Name $Instance.Database -ServerInstance $Instance.SqlServer -ErrorAction Stop
            if ($null -eq (Get-SqlColumnMasterKey -Name $Instance.EncryptionMasterKeyName -InputObject $db -ErrorAction SilentlyContinue)){
                $masterKeySettings = New-SqlCertificateStoreColumnMasterKeySettings -Thumbprint $Instance.EncryptionCertificateThumbprint -CertificateStoreLocation $certificateStoreLocation  -ErrorAction Stop
                New-SqlColumnMasterKey -Name $Instance.EncryptionMasterKeyName -InputObject $db -ColumnMasterKeySettings $masterKeySettings -ErrorAction Stop > $null
            }
            if ($null -eq (Get-SqlColumnEncryptionKey -Name "CEK" -InputObject $db -ErrorAction SilentlyContinue)){
                New-SqlColumnEncryptionKey -Name "CEK" -InputObject $db -ColumnMasterKeyName $Instance.EncryptionMasterKeyName -ErrorAction Stop > $null
            }
        }
        $argumentList = @($PSScriptRoot, $Instance)
        
        # Workaround, since start-job with credentials cannot be called in remoting session
        if ($null -eq $ServiceCredential -or ($env:useCurrentUser -and $env:useCurrentUser -eq $true)){
            Start-Job -ScriptBlock $scriptBlock -ArgumentList $argumentList -PSVersion 5.1 | Receive-Job -Wait -ErrorAction Stop
        }
        else{
            Start-Job -Credential $ServiceCredential -ScriptBlock $scriptBlock -ArgumentList $argumentList -PSVersion 5.1 | Receive-Job -Wait -ErrorAction Stop
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Update-SttiDatabase{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){
            try{
                $workerPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker")
                $workerBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe")
                $outPath = [System.IO.Path]::Combine($Instance.Path, "data\logs\migratedb-out.txt")
                $errorPath = [System.IO.Path]::Combine($Instance.Path, "data\logs\migratedb-err.txt")
                          
                #Workaround for running in remoting session where no different credentials can be used
                if ($env:useCurrentUser -and $env:useCurrentUser -eq $true){
                    $process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Wait -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -ErrorAction Stop
                }
                else{
                    $process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Credential $ServiceCredential -Wait -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -ErrorAction Stop
                }
                $exitCode = $process.ExitCode

                Get-Content $outPath -ErrorAction SilentlyContinue | Write-SttiLog -Level Verbose
                Get-Content $errorPath -ErrorAction SilentlyContinue | Write-SttiLog -Level Warn
                if ($exitCode -ne 0){
                    Write-SttiLog "Halvotec.Stti.Worker.exe exited with code $exitCode" -Level Error
                }                
            }
            finally{
                Remove-Item $outPath -ErrorAction SilentlyContinue
                Remove-Item $errorPath -ErrorAction SilentlyContinue
            }
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Test-SttiDatabaseEncryption{
    [OutputType([bool])]    
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance

        # [Parameter(Mandatory)]
        # [PSCredential] $ServiceCredential
    )
    
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{

        return $true
    }   
    catch{
        Write-SttiLog $_ -Level Error
        return $false
    }
}

function Get-SttiDatabaseConnectionString{
    [OutputType([string])]    
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance
    )

    return "Server=$($Instance.SqlServer);Database=$($Instance.Database);Integrated Security=true;Column Encryption Setting=enabled;ApplicationIntent=ReadWrite;Language=English"
}

#endregion Database Management

#region Package Management

$deploymentFilePattern = "^SttiApp.(?<version>\d+\.\d+\.\d+\.\d+)\.nupkg$"
function Get-SttiPackage {
    [OutputType([SttiPackage])]
    [CmdletBinding()]
    Param(
        [ValidatePattern("^\d+\.\d+\.\d+\.\d+$|^latest$")]
        [string] $VersionNumber = "latest"
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug
        
    $deploymentFiles = Get-ChildItem "$([System.IO.Path]::Combine((Get-SttiBasePath), "Packages", "*.nupkg"))"
    $packages = $deploymentFiles | Where-Object -Property Name -Match $deploymentFilePattern | ForEach-Object {([SttiPackage]::new([System.Version]::Parse($Matches.version), $_.FullName, $true))}

    if ($VersionNumber -eq "latest"){
        return $packages | Sort-Object -Property Version -Descending | Select-Object -First 1
    }
    else {
        $version = [System.Version]::Parse($VersionNumber)
        return $packages | Where-Object -Property Version -eq $version
    }
}
# Get-SttiPackage -VersionNumber "latest"

function Get-SttiUserHomePath{
    [OutputType([string])]
    Param()

    $sttiUserHomePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "Stti")
    if (!(Test-Path $sttiUserHomePath)){
        New-Item $sttiUserHomePath -ItemType Directory -Force > $null
    }
    return $sttiUserHomePath
}

function Set-SttiCustomerCredential{
    [CmdletBinding()]
    Param(
        [PSCredential] $CustomerCredential
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $customerCredentialFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "customercredentials.json")
    
    $customerCredentialData = @{
        UserName = $CustomerCredential.UserName
        Password = ($CustomerCredential.Password | ConvertFrom-SecureString)
    }
    $customerCredentialData | ConvertTo-Json | Out-File $customerCredentialFile -Encoding utf8
}
# $cred = Get-Credential
# Set-SttiCustomerCredential $cred

function Get-SttiCustomerCredential{
    [OutputType([PSCredential])]
    [CmdletBinding()]
    Param(
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $customerCredentialFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "customercredentials.json")
    if (!(Test-Path $customerCredentialFile)){
        Write-SttiLog "Customer credential file does not exist at $customerCredentialFile" -Level Error
    }

    $customerCredentialData = Get-Content $customerCredentialFile -Raw -Encoding utf8 | ConvertFrom-Json
    return [System.Management.Automation.PsCredential]::new($customerCredentialData.UserName, ($customerCredentialData.Password | ConvertTo-SecureString))
}
#Get-SttiCustomerCredential

function Find-SttiPackage{
    [OutputType([SttiPackage[]])]
    [CmdletBinding()]
    Param(
        [ValidatePattern("^\d+\.\d+\.\d+\.\d+$|^latest$")]
        [string] $VersionNumber,
        [string] $PackageSource = "https://appdemo8.halvotec.de/v3/index.json"
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $customerCredential = Get-SttiCustomerCredential -ErrorAction Stop
    Write-SttiLog "Getting feed resources" -Level Verbose
    $resources = (Invoke-RestMethod -Uri $PackageSource -Credential $customerCredential -ErrorAction Stop).resources
    $searchEndpoint = ($resources | Where-Object "@type" -like "SearchQueryService")."@id"
    if ($null -eq $searchEndpoint){
        $searchEndpoint = ($resources | Where-Object "@type" -like "SearchQueryService/*" | Select-Object -First 1)."@id"
    }
    if ($null -eq $searchEndpoint){
        Write-SttiLog "Could not find a v3 search endpoint in the feed" -Level Error
    }
    $packageContentEndpoint =  ($resources | Where-Object "@type" -like "PackageBaseAddress/3.0.0")."@id"
    if ($null -eq $packageContentEndpoint){
        Write-SttiLog "Could not find a v3 content endpoint in the feed" -Level Error
    }
    Write-SttiLog "Searching package" -Level Verbose
    $packageData = (Invoke-RestMethod -Uri "$($searchEndpoint)?q=SttiApp" -Credential $customerCredential -ErrorAction Stop).data
    $packages = $packageData.versions | ForEach-Object {([SttiPackage]::new([System.Version]::Parse($_.version), "$packageContentEndpoint/$($packageData.id.ToLower())/$($_.version.ToLower())/$($packageData.id.ToLower()).$($_.version.ToLower()).nupkg", $false ))}

    if (!$PSBoundParameters.ContainsKey("VersionNumber")){
        return $packages
    }
    elseif ($VersionNumber -like "latest") {
        return $packages | Where-Object Version -eq ([System.Version]::Parse($packageData.version))
    }
    else{
        return $packages | Where-Object Version -eq ([System.Version]::Parse($VersionNumber))
    }
}
#Find-SttiPackage latest

function Import-SttiPackage{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [SttiPackage] $Package
    )

    Process{
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

        if ($Package.IsLocal){
            Write-SttiLog "Package must not be local"
        }
        
        $customerCredential = Get-SttiCustomerCredential -ErrorAction Stop
        $fileName = [System.Uri]::new($Package.Path).Segments | Select-Object -Last 1
        $packagePath = [System.IO.Path]::Combine((Get-SttiBasePath), "Packages", $fileName)
        Write-SttiLog "Downloading package" -Level Verbose
        Invoke-RestMethod -Uri $Package.Path -Credential $customerCredential -OutFile $packagePath -ErrorAction Stop
    }
}
#Find-SttiPackage "latest" -Verbose | Import-SttiPackage -Verbose

#endregion Package Management

#region Windows Service Rights management

Add-Type @'
using System;
using System.Runtime.InteropServices;
 
public enum LSA_AccessPolicy : long
{
    // Other values omitted for clarity
    POLICY_ALL_ACCESS = 0x00001FFFL
}
 
[StructLayout(LayoutKind.Sequential)]
public struct LSA_UNICODE_STRING
{
    public UInt16 Length;
    public UInt16 MaximumLength;
    public IntPtr Buffer;
}
 
[StructLayout(LayoutKind.Sequential)]
public struct LSA_OBJECT_ATTRIBUTES
{
    public UInt32 Length;
    public IntPtr RootDirectory;
    public LSA_UNICODE_STRING ObjectName;
    public UInt32 Attributes;
    public IntPtr SecurityDescriptor;
    public IntPtr SecurityQualityOfService;
}
 
public static partial class AdvAPI32 {
    [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
    public static extern uint LsaOpenPolicy(
        ref LSA_UNICODE_STRING SystemName,
        ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
        uint DesiredAccess,
        out IntPtr PolicyHandle);
 
    [DllImport("advapi32.dll")]
    public static extern Int32 LsaClose(IntPtr ObjectHandle);
 
    [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
    public static extern uint LsaAddAccountRights(
        IntPtr PolicyHandle,
        byte[] AccountSid,
        LSA_UNICODE_STRING[] UserRights,
        uint CountOfRights);
}
'@


function Get-LsaPolicyHandle() {
    $system = New-Object LSA_UNICODE_STRING
    $attrib = New-Object LSA_OBJECT_ATTRIBUTES -Property @{
        Length = 0
        RootDirectory = [System.IntPtr]::Zero
        Attributes = 0
        SecurityDescriptor = [System.IntPtr]::Zero
        SecurityQualityOfService = [System.IntPtr]::Zero
    };

    $handle = [System.IntPtr]::Zero

    $hr = [AdvAPI32]::LsaOpenPolicy([ref] $system, [ref]$attrib, [LSA_AccessPolicy]::POLICY_ALL_ACCESS, [ref]$handle)

    if (($hr -ne 0) -or ($handle -eq [System.IntPtr]::Zero)) {
        Write-Error "Failed to open Local Security Authority policy. Error code: $hr"
    } else {
        $handle
    }
}

function New-Right([string]$rightName){
    $unicodeCharSize = 2
    New-Object LSA_UNICODE_STRING -Property @{
        Buffer = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($rightName)
        Length = $rightName.Length * $unicodeCharSize
        MaximumLength = ($rightName.Length + 1) * $unicodeCharSize
    }
}

function Grant-Rights([System.IntPtr]$policyHandle, [byte[]]$sid, [LSA_UNICODE_STRING[]]$rights) {
    $result = [AdvAPI32]::LsaAddAccountRights($policyHandle, $sid, $rights, 1)
    if ($result -ne 0) {
        Write-Error "Failed to grant right. Error code $result"
    } 
}

function Grant-LogonAsServiceRight { 
    [CmdletBinding()]
    Param(
        [string] $username
    )

    $sid = ((New-Object System.Security.Principal.NTAccount($username)).Translate([System.Security.Principal.SecurityIdentifier]))
    $sidBytes = [byte[]]::new($sid.BinaryLength)
    $sid.GetBinaryForm($sidBytes, 0)

    $logonAsServiceRightName = "SeServiceLogonRight"

    try {
        $policy = Get-LsaPolicyHandle
        $right = New-Right $logonAsServiceRightName
        Grant-Rights $policy $sidBytes @($right)
    }
    finally {
        if($null -ne $policy){
            [AdvAPI32]::LsaClose($policy) | Out-Null
        }
    }
}

#endregion Windows Service Rights management

#region Logging

function Start-SttiInstallLogFile{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [switch] $Append = $false,

        [LogLevels] $Level = [LogLevels]::Verbose
    )

    $Script:logFile = Get-SttiInstallLogFilename -Instance $Instance -Timestamp (Get-Date)
    $Script:archiveLogFile = Get-SttiInstallLogFilename -Instance $Instance
    $Script:logLevel = $Level
    
    Out-File $logFile -Append:$Append
    Write-SttiLog "Start installation logfile $($Script:logFile)" -Level Info

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug
}

function Stop-SttiInstallLogFile{
    [CmdletBinding()]
    Param(
       
    )

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    Write-SttiLog "Stop installation logfile $($Script:logFile)" -Level Info

    Get-Content -Path $Script:logFile -Raw | Out-File $Script:archiveLogFile -Append -ErrorAction Continue

    $Script:logFile = $null
    $Script:archiveLogFile = $null
    $Script:logLevel = $null
}

function Write-SttiInstallLogHeader{
    [CmdletBinding()]
    Param(
        
    )
    
    Write-SttiLog "ComputerInfo:" -Level Verbose
    Get-ComputerInfo | Write-SttiLog -Level Verbose
    Write-SttiLog "HostInfo:" -Level Verbose
    Get-Host | Write-SttiLog -Level Verbose
    Write-SttiLog "----------------------------------------------------------------" -Level Info -Extraline
}

function Write-SttiInstallLogFooter{
    [CmdletBinding()]
    Param(
        
    )
    
    Write-SttiLog "----------------------------------------------------------------" -Level Info
}

function Write-SttiLog{
    [CmdletBinding()]
    Param(
        [switch] $Extraline,

        [LogLevels] $Level = [LogLevels]::Info,
        
        [Parameter(ValueFromPipeline, Position=0)]
        [object[]] $InputObject
    )
    
    Process{
        if ($null -ne $_){
            $i = $_
        }
        elseif ($null -ne $InputObject) {
            $i = $InputObject
        }
        else {
            throw "InputObject has no value"
        }
        
        # Write to log if existing
        if ($Script:logFile -and $null -ne $Script:logFile `
            -and $Level -ge ($Script:logLevel ?? [LogLevels]::Verbose)) {
            $i | ForEach-Object{ if ($_ -is [string]) { "$(Get-Date) $($Level.ToString().ToUpperInvariant()) $_" } else { "$(Get-Date) $($Level.ToString().ToUpperInvariant()) $($_ | Format-List | Out-String)" }} | Out-File -FilePath $Script:logFile -Append
        }

        switch ($Level){
            "Error" {
                if ($i -is [System.Management.Automation.ErrorRecord]){
                    Write-Error -ErrorRecord $i    
                }
                elseif ($i -is [System.Exception]){
                    Write-Error -Exception $i    
                }
                else{
                    Write-Error ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }})
                }
            }
            "Warn" {
                Write-Warning ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }})
            }
            "Info" {
                Write-Output $i
            }
            "Verbose" {
                Write-Verbose ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }})
            }
            "Debug" {
                Write-Debug ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }})
            }
        }   
    }
    End{
        if ($Extraline -eq $true){
            if ($Script:logFile -and $null -ne $Script:logFile) {
                "" | Out-File -FilePath $Script:logFile -Append
            }
            switch ($Level){
                "Error" {
                    #Write-Error ""
                }
                "Warn" {
                    Write-Warning ""
                }
                "Info" {
                    Write-Output ""
                }
                "Verbose" {
                    Write-Verbose ""
                }
                "Debug" {
                    Write-Debug ""
                }
            }
        }
    }
}
#"Hallo" | Write-SttiLog -FilePath "Test.txt"

#endregion Logging

#region Helper

function Test-ADCredential {
    [OutputType([bool])]
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [pscredential] $credential
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $username = $credential.UserName
    $password = $credential.GetNetworkCredential().Password

    if (!($UserName) -or !($Password)) {
        Write-SttiLog "Please specify both user name and password" -Level Error
        return $false
    } else {
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        $isValid = $false
        try{
            $DS = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('domain')
            $isValid = $DS.ValidateCredentials($UserName, $Password)
            return $isValid
        }
        catch{
            Write-SttiLog -Exception $_ -Message "Could not validate the credentials. An error occurred." -Level Error
        }
        if (-not ($isValid)){
            Write-SttiLog "Could not validate the credentials. Username or password seems to be invalid." -Level Error
        }
    }
}

#endregion Helper

#region Deployment

function Deploy-Stti {
    [CmdletBinding()]
    [OutputType([SttiInstanceStatus])]
    Param(
        [string] $InstanceName,
        [PSCredential] $ServiceCredential,
        [string] $ServiceUserName,
        [string] $Version="latest",
        [string] $SqlServer,
        [string] $Database="STTI_$($InstanceName.ToUpperInvariant())",
        [string] $AdminKeyThumbprint,
        [string] $EncryptionCertificateThumbprint,
        [bool] $StoreDbInInstanceDataPath = $true,        
        [string] $WorkerSslCertificateSubject,
        [uint] $WorkerPort,
        [string] $WebSslCertificateSubject,
        [uint] $WebPort,
        [string] $WorkerHostname,
        [switch] $Uninstall=$false,
        [switch] $Force=$false
    )

    if ($ServiceCredential){
        $ServiceUserName = $ServiceCredential.UserName
    }

    # Create instance stub
    $instance = Get-SttiInstanceConfig -Name $InstanceName
    if ($null -eq $instance) {
        $instance = New-SttiInstanceConfig -Name $InstanceName -SqlServer $SqlServer -Database $Database -Roles Worker, Web -ServiceUserName $ServiceUserName -StoreDbInInstanceDataPath:$StoreDbInInstanceDataPath -WorkerSslCertificateSubject $WorkerSslCertificateSubject -WorkerPort $WorkerPort -WebPort $WebPort -WebSslCertificateSubject $WebSslCertificateSubject -WorkerHostname $WorkerHostname -EncryptionCertificateThumbprint $EncryptionCertificateThumbprint
    }
    else {
        $instance = Set-SttiInstanceConfig -Instance $instance -SqlServer $SqlServer -Database $Database -Roles Worker, Web -ServiceUserName $ServiceUserName -StoreDbInInstanceDataPath:$StoreDbInInstanceDataPath -WorkerSslCertificateSubject $WorkerSslCertificateSubject -WorkerPort $WorkerPort -WebPort $WebPort -WebSslCertificateSubject $WebSslCertificateSubject -WorkerHostname $WorkerHostname -EncryptionCertificateThumbprint $EncryptionCertificateThumbprint
    }

    $adminCert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object Thumbprint -eq $AdminKeyThumbprint

    $status = Get-SttiInstanceStatus -Instance $instance -Certificate $adminCert
    if ($Uninstall -eq $false){
        $package = Get-SttiPackage $Version
        if ($status.InstallStatus.Installed -eq $false){
            # Get credentials for service user
            $svcCred = $ServiceCredential ?? (Get-Credential -UserName $instance.ServiceUserName -Message "Please enter the credentials for the service user")

            Install-SttiInstance -Instance $instance -Package $package -ServiceCredential $svcCred -Force:$Force # -Certificate $adminCert
        }
        elseif ($status.InstallStatus.Version -ne $package.Version -or $Force){
            $svcCred = $ServiceCredential ?? (Get-Credential -UserName $instance.ServiceUserName -Message "Please enter the credentials for the service user")

            Update-SttiInstance -Instance $instance -Package $package -ServiceCredential $svcCred -Force:$Force # -Certificate $adminCert
        }
        else{
            #Nothing to do
            Write-Information "Version $($package.Version) was already installed"
        }
    }
    else {
        if ($status.InstallStatus.Installed -eq $true -or $Force){
            Uninstall-SttiInstance -Instance $instance -Force:$Force # -Certificate $adminCert
        }
        else {
            #Nothing to do
            Write-Information "The instance was already uninstalled"
        }
    }

    Get-SttiInstanceStatus -Instance $instance -Certificate $adminCert
}
 
#endregion Deployment

#region Firewall rules

function Add-SttiFirewallRules{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig]
        $Instance
    )

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){
            if ($null -eq (Get-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction SilentlyContinue)){
                Write-SttiLog "Add firewall rule for worker" -Level Verbose
                $workerBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe")
                New-NetFirewallRule â€“Name "Stti-Worker-$($Instance.Name)" -DisplayName "Stti Worker ($($Instance.Name))" -Program $workerBinaryPath -Direction Inbound -Action Allow -Protocol TCP -Profile Domain -Group "Stti" -ErrorAction Stop > $null
            }
            else {
                Write-SttiLog "Firewall rule for worker already exists" -Level Verbose
            }
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Remove-SttiFirewallRules{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig]
        $Instance
    )

    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    try{
        Write-SttiLog "Remove firewall rule for worker" -Level Verbose
        Remove-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction Stop
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

#endregion Firewall rules