Stti.psm1

#Requires -Modules SqlServer

$initialUserCertificateThumbprint = "F0425D76A28A1180C5AB667B847EE9BF161B7B81"
$initialUserCertificatePassword = "0#dJB87I8UsL" | ConvertTo-SecureString -AsPlainText -Force
$initialUserId = "dbfd9d8b-a443-49d2-96b0-c4a15d5dba65"
$defaultPackageSource = "https://raqrepository.halvotec.de/packages/stti/v3/index.json"
$defaultBasePath = "C:\STTI\"

enum SttiInstanceRoles {
    Worker
    Web
}

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

enum SttiUserRoles{
    OperationsAdmin
    SecurityAdmin
    DataAdmin
    HealthWatcher
}

enum SttiAppLogLevel{
    Debug
    Information
    Warning
    Error
    Fatal
}

enum SttiClientCommunicationType{
    Api
    File
}

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
    [bool] $StoreDbInInstanceDataPath
    [string] $WorkerSslCertificateSubject
    [string] $WorkerSslCertificateStore
    [uint] $WorkerPort
    [string] $WebSslCertificateSubject
    [string] $WebSslCertificateStore
    [uint] $WebPort
    [string] $WorkerHostname
    [string] $EncryptionCertificateThumbprint
    [string] $EncryptionMasterKeyName
    [string] $HttpProxyAddress
    #obsolete
    [string] hidden $ServiceUsername
}

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

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

class SttiInstanceDefaults{
    [PSCredential] $ServiceCredential
    [string] $CertificateThumbprint
    [bool] $UseLocalServiceUser
}

class SttiDefaults{
    [PSCredential] $CustomerCredential
    [string] $PackageSource

    [hashtable] $InstanceDefaults = @{}

    [SttiInstanceDefaults] ForInstance($Instance) {
        if ($Instance -is [string]){
            $InstanceName = $Instance.ToUpperInvariant()
        }
        elseif ($Instance -is [SttiInstanceConfig]) {
            $InstanceName = $Instance.Name
        }
        else{
            throw "Parameter $Instance is of invalid type"
        }

        if (-not $this.InstanceDefaults.ContainsKey($InstanceName)){
            $this.InstanceDefaults[$InstanceName] = [SttiInstanceDefaults]::new()
        }
        return $this.InstanceDefaults[$InstanceName]
    }
}

class SttiShellSession{
    [SttiInstanceConfig] $Instance
    [int] $ApiCallTimeoutSec
}

class SttiSystemSettings{
    [string] $InstanceName
    [string] $DataDirectory   
    [bool] $UseProxy
    [string] $ProxyAddress
    [PSCustomObject] hidden $Dto

    SttiSystemSettings([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.InstanceName = $dto.instance
        $this.DataDirectory = $dto.dataDirectory
        $this.UseProxy = $dto.proxy.useProxy
        $this.ProxyAddress = $dto.proxy.address
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiModule{
    [string] $ModuleId
    [string] $Name   
    [PSCustomObject] hidden $Dto

    SttiModule([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.ModuleId = $dto.id
        $this.Name = $dto.name
    }

    static [SttiModule[]] FromArray([PSCustomObject[]] $dtoArray){
        if ($null -eq $dtoArray){
            return $null
        }
        return $dtoArray.ForEach({return [SttiModule]::new($_)})
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiModuleSettings{
    [string[]] $Environments
    [string] $ActiveEnvironment
    [string] $SourceEnvironment
    [PSCustomObject] $Settings
    [PSCustomObject] hidden $Configurations
    [PSCustomObject] hidden $Dto

    SttiModuleSettings([PSCustomObject] $dto, [string] $SourceEnvironment){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.ActiveEnvironment = $dto.currentEnvironment
        if (![string]::IsNullOrEmpty($SourceEnvironment)){
            $this.SourceEnvironment = $SourceEnvironment
        }
        else{
            $this.SourceEnvironment = $this.ActiveEnvironment
        }
        $this.Environments = $dto.configurations | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name
        $this.Configurations = $dto.configurations
        $this.Settings = $this.GetSettings($this.SourceEnvironment)
    }

    [PSCustomObject] GetSettings([string] $environment){
        if (!($this.Environments?.Contains($environment) ?? $false)){
            throw "Environment $environment does not exist"
        }
        return $this.Configurations.$environment.settings
    }

    static [SttiModuleSettings[]] FromArray([PSCustomObject[]] $dtoArray, [string] $SourceEnvironment){
        if ($null -eq $dtoArray){
            return $null
        }
        return $dtoArray.ForEach({return [SttiModuleSettings]::new($_, $SourceEnvironment)})
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiModuleSecrets{
    [string[]] $Environments
    [string] $ActiveEnvironment
    [string] $SourceEnvironment
    [PSCustomObject] $Secrets
    [PSCustomObject] hidden $Configurations
    [PSCustomObject] hidden $Dto

    SttiModuleSecrets([PSCustomObject] $dto, [string] $SourceEnvironment){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.ActiveEnvironment = $dto.currentEnvironment
        if (![string]::IsNullOrEmpty($SourceEnvironment)){
            $this.SourceEnvironment = $SourceEnvironment
        }
        else{
            $this.SourceEnvironment = $this.ActiveEnvironment
        }
        $this.Environments = $dto.configurations | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name
        $this.Configurations = $dto.configurations
        $this.Secrets = $this.GetSecrets($this.SourceEnvironment)
    }

    [PSCustomObject] GetSecrets([string] $environment){
        if (!($this.Environments?.Contains($environment) ?? $false)){
            throw "Environment $environment does not exist"
        }
        return $this.Configurations.$environment.secrets
    }

    static [SttiModuleSecrets[]] FromArray([PSCustomObject[]] $dtoArray, [string] $SourceEnvironment){
        if ($null -eq $dtoArray){
            return $null
        }
        return $dtoArray.ForEach({return [SttiModuleSecrets]::new($_, $SourceEnvironment)})
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiClient{
    [string] $ClientId
    [string] $DisplayName   
    [bool] $IsActive
    [PSCustomObject] hidden $Dto

    SttiClient([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.ClientId = $dto.id
        $this.DisplayName = $dto.displayName
        $this.IsActive = $dto.isActive
    }

    static [SttiClient[]] FromArray([PSCustomObject[]] $dtoArray){
        if ($null -eq $dtoArray){
            return $null
        }
        return $dtoArray.ForEach({return [SttiClient]::new($_)})
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiClientSettings{
    [string] $ClientId
    [string] $DisplayName   
    [bool] $IsActive
    [SttiClientCommunicationType] $Communication
    [PSCustomObject] hidden $Dto

    SttiClientSettings([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.ClientId = $dto.id
        $this.DisplayName = $dto.displayName
        $this.Communication = $dto.communication
        $this.IsActive = $dto.isActive
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }
}

class SttiCredential{
    [string] $CredentialId
    [string] $Thumbprint
    [string] $Description
    [PSCustomObject] hidden $Dto

    SttiCredential([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.CredentialId = $dto.id
        $this.Thumbprint = $dto.thumbprint
        $this.Description = $dto.description
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }

    static [SttiCredential[]] FromArray([PSCustomObject[]] $dtos){
        if ($null -eq $dtos){
            return $null
        }

        return $dtos.ForEach({[SttiCredential]::new($_)})
    }
}

class SttiEncryptionKey{
    [string] $KeyId
    [string] $Key   
    [string] $Algorithm   
    [bool] $Disabled
    [PSCustomObject] hidden $Dto

    SttiEncryptionKey([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.KeyId = $dto.keyId
        $this.Key = $dto.key
        $this.Algorithm = $dto.algorithm
        $this.Disabled = $dto.disabled
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }

    static [SttiEncryptionKey[]] FromArray([PSCustomObject[]] $dtos){
        if ($null -eq $dtos){
            return $null
        }

        return $dtos.ForEach({[SttiEncryptionKey]::new($_)})
    }
}

class SttiUser{
    [string] $UserId
    [string] $DisplayName
    [bool] $IsActive
    [PSCustomObject] hidden $Dto

    SttiUser([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.UserId = $dto.id
        $this.DisplayName = $dto.displayName
        $this.IsActive = $dto.isActive
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }

    static [SttiUser[]] FromArray([PSCustomObject[]] $dtos){
        if ($null -eq $dtos){
            return $null
        }

        return $dtos.ForEach({[SttiUser]::new($_)})
    }
}

class SttiUserSettings{
    [string] $UserId
    [string] $DisplayName
    [bool] $IsActive
    [SttiUserRoles[]] $Roles
    [PSCustomObject] hidden $Dto

    SttiUserSettings([PSCustomObject] $dto){
        if ($null -eq $dto){
            throw 'Parameter dto must not be null'
        }
        
        $this.Dto = $dto
        $this.UserId = $dto.id
        $this.DisplayName = $dto.displayName
        $this.IsActive = $dto.isActive
        $this.Roles = Convert-ObjectToRoles($dto.roles)
    }

    [string] AsJson (){
        return $this.Dto | ConvertTo-Json -Depth 10
    }

    static [SttiUserSettings[]] FromArray([PSCustomObject[]] $dtos){
        if ($null -eq $dtos){
            return $null
        }

        return $dtos.ForEach({[SttiUserSettings]::new($_)})
    }
}

#region Installation

function Test-UserHasAdminRole {
    #Check if the user is in the administrator group. Warns and stops if the user is not.
    if (-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."
    }
}

function Test-SttiModuleConsistency {
    # Check if the current loaded module has a different version than the module in the shared module directory
        
    #$userModulePath = "$HOME\Documents\PowerShell\Modules"
    $loadedModule = (Get-Module stti) ?? (Get-Module stti -ListAvailable -All | Select-Object -Last 1)
    $sharedModulePath = "$($Env:ProgramFiles)\PowerShell\Modules"
    $latestSharedModule = Get-Module stti -ListAvailable | Where-Object {$_.Path -ilike "$sharedModulePath\*" -and $_.ModuleType -eq "Manifest"} |Sort-Object Version | Select-Object -Last 1
    if ($null -eq $latestSharedModule){
        Write-Warning "The stti module was not installed in the allusers scope. Use the command Install-Module stti -Scope AllUsers."
    }
    else{
        if ($loadedModule.Path -inotlike "$sharedModulePath\*"){
            Write-Warning "The stti module was loaded from the current users module directory."
        }
        if ($loadedModule.Version -ne $latestSharedModule.Version){
            Write-Warning "The loaded stti module has a different version than the module in the AllUsers scope."
            $loadedModule | Format-List | Out-String -Stream | Write-Warning
            $latestSharedModule | Format-List | Out-String -Stream | Write-Warning
        }
    }
}

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [SttiPackage] $Package = (Get-SttiPackage -Version latest),
        
        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential,

        [switch] $SkipCredentialTest,

        [switch] $SkipSslCertificateTest,

        [switch] $UseExistingDatabase,

        [switch] $UseLocalServiceUser,

        [switch] $SkipInstallInitialUserCertificate = ($UseExistingDatabase -and ![string]::IsNullOrEmpty((Get-SttiDefaultValues)?.ForInstance($Instance)?.CertificateThumbprint)),

        [string] $InstallCertificateThumbprint = ($SkipInstallInitialUserCertificate ? (Get-SttiDefaultValues)?.ForInstance($Instance)?.CertificateThumbprint : $initialUserCertificateThumbprint),

        [switch] $Force
    )

    Start-SttiInstallLogFile -Instance $Instance -Level Verbose
    Write-SttiInstallLogHeader
    
    try {
        Test-UserHasAdminRole -ErrorAction Stop
        
        Write-SttiLog "Start install instance $($Instance.Name) with version $($Package.Version) from $($Package.Path)" -Level Info
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose

        $errorAction = $Force ? "Continue" : "Stop"
        $serviceUsername = $ServiceCredential.UserName
        
        $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus
        if ($installStatus.Installed -eq $true){
            Write-SttiLog "The instance $($Instance.Name) is already installed" -Level Error -ErrorAction $errorAction
        }

        $steps = @(
            @{Name="Test-Credential"; Expression={Test-Credential $ServiceCredential -ErrorAction Stop > $null}; CheckExpression={!$SkipCredentialTest}},
            @{Name="Test-SttiInstanceBinding"; Expression={Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null}},
            @{Name="Test-SttiDatabaseConnection"; Expression={Test-SttiDatabaseConnection $Instance -ServiceCredential:$ServiceCredential -SkipPermissionCheck:$UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
            @{Name="Test-SttiDatabase"; Expression={Test-SttiDatabase $Instance -ServiceCredential:$ServiceCredential -UseExistingDatabase:$UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
            @{Name="Test-SttiSslCertificateForWorkerRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Worker -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipSslCertificateTest}},
            @{Name="Test-SttiSslCertificateForWebRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Web -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Web) -and !$SkipSslCertificateTest}},
            @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}},
            @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Build-SttiInstanceDirectory"; Expression={Build-SttiInstanceDirectory -Instance $Instance -ServiceUsername $serviceUsername -ErrorAction $errorAction}},
            @{Name="Expand-SttiPackage"; Expression={Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction}},
            @{Name="Install-SttiDatabaseEncryptionCertificate"; Expression={Install-SttiDatabaseEncryptionCertificate -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$Instance.EncryptionCertificateThumbprint}},
            @{Name="Edit-SttiInstanceConfigFiles"; Expression={Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction}},
            @{Name="New-SttiDatabase"; Expression={New-SttiDatabase -Instance $instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseExistingDatabase}},
            @{Name="Update-SttiDatabaseUser"; Expression={Update-SttiDatabaseUser -Instance $instance -ServiceUsername $serviceUsername -SkipServerChanges:$UseExistingDatabase -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseLocalServiceUser}},
            @{Name="Add-SttiDatabaseEncryption"; Expression={Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
            @{Name="Invoke-SttiDatabaseMigration"; Expression={Invoke-SttiDatabaseMigration -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
            @{Name="Add-SttiInstanceServices"; Expression={Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}},
            @{Name="Add-SttiFirewallRules"; Expression={Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Start-SttiInstance"; Expression={Start-SttiInstance -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Install-SttiInitialUserCertificate"; Expression={Install-SttiInitialUserCertificate -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipInstallInitialUserCertificate}}
            @{Name="Update-SttiInstanceSettings"; Expression={Update-SttiInstanceSettings -Instance $Instance -CertificateThumbprint $InstallCertificateThumbprint -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}
            @{Name="Restart-SttiInstance"; Expression={Restart-SttiInstance -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}
            @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null}},
            @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}
        )
        Invoke-SttiDeploymentSteps -Activity "Install STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction

        Write-SttiLog "Finished install instance $($Instance.Name)" -Extraline -Level Info
    }
    catch {
        Write-SttiLog "An error occurred during installation 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,

        [ValidateNotNullOrEmpty()]
        [SttiPackage] $Package = (Get-SttiPackage -VersionNumber latest),
        
        [Parameter(Mandatory)]
        [PSCredential] $ServiceCredential,
      
        [switch] $SkipCredentialTest,

        [switch] $SkipSslCertificateTest,

        [switch] $UseLocalServiceUser,

        [switch] $Force
    )
    DynamicParam
    {
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){
            $attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $attributes.Add([System.Management.Automation.ParameterAttribute]@{
                Mandatory = $true
            })

            $parameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
            $parameters.Add('CertificateThumbprint', [System.Management.Automation.RuntimeDefinedParameter]::new(
                'CertificateThumbprint', [string], $attributes
            ))
            return $parameters
        }
    }

    Begin{
        $CertificateThumbprint = $PSBoundParameters['CertificateThumbprint']
    }
    Process{
        Start-SttiInstallLogFile -Instance $Instance -Level Verbose
        Write-SttiInstallLogHeader
        
        try {
            Test-UserHasAdminRole -ErrorAction Stop
            
            Write-SttiLog "Start update instance $($Instance.Name) with version $($Package.Version) from $($Package.Path)" -Level Info
            Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose

            $errorAction = $Force ? "Continue" : "Stop"
            $serviceUsername = $ServiceCredential.UserName
            $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus

            if ($installStatus.Installed -eq $false -and $Force -eq $false){
                Write-SttiLog "The instance $($Instance.Name) is not installed" -Level Error -ErrorAction $errorAction
            }

            $steps = @(
                @{Name="Test-Credential"; Expression={Test-Credential $ServiceCredential -ErrorAction Stop > $null}; CheckExpression={!$SkipCredentialTest}},
                @{Name="Test-SttiInstanceBinding"; Expression={Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null}},
                @{Name="Test-SttiDatabaseConnection"; Expression={Test-SttiDatabaseConnection $Instance -ServiceCredential:$ServiceCredential -SkipPermissionCheck -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
                @{Name="Test-SttiDatabase"; Expression={Test-SttiDatabase $Instance -ServiceCredential:$ServiceCredential -UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
                @{Name="Test-SttiSslCertificateForWorkerRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Worker -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipSslCertificateTest}},
                @{Name="Test-SttiSslCertificateForWebRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Web -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Web) -and !$SkipSslCertificateTest}},
                @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}},
                @{Name="Stop-SttiInstance"; Expression={Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction}},
                @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}},
                @{Name="Build-SttiInstanceDirectory"; Expression={Build-SttiInstanceDirectory -Instance $Instance -ServiceUsername $serviceUsername -ErrorAction $errorAction}},
                @{Name="Expand-SttiPackage"; Expression={Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction}},
                @{Name="Edit-SttiInstanceConfigFiles"; Expression={Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction}},
                @{Name="Update-SttiDatabaseUser"; Expression={Update-SttiDatabaseUser -Instance $instance -ServiceUsername $serviceUsername -SkipServerChanges -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseLocalServiceUser}},
                @{Name="Add-SttiDatabaseEncryption"; Expression={Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
                @{Name="Invoke-SttiDatabaseMigration"; Expression={Invoke-SttiDatabaseMigration -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
                @{Name="Add-SttiInstanceServices"; Expression={Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}},
                @{Name="Add-SttiFirewallRules"; Expression={Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}},
                @{Name="Start-SttiInstance"; Expression={Start-SttiInstance -Instance $Instance -ErrorAction $errorAction}},
                @{Name="Update-SttiInstanceSettings"; Expression={Update-SttiInstanceSettings -Instance $Instance -CertificateThumbprint $CertificateThumbprint -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}},
                @{Name="Restart-SttiInstance"; Expression={Restart-SttiInstance -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}
                @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null}},
                @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}
            )           
            Invoke-SttiDeploymentSteps -Activity "Update STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction
            
            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(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [switch] $Force    
    )

    Start-SttiInstallLogFile -Instance $Instance -Level Verbose
    Write-SttiInstallLogHeader
    
    try {
        Test-UserHasAdminRole -ErrorAction Stop

        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

        $errorAction = $Force ? "Continue" : "Stop"
        $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus

        if ($installStatus.Installed -eq $false -and $Force -eq $false){
            Write-SttiLog "The instance $($Instance.Name) is not installed" -Level Error -ErrorAction $errorAction
        }

        $steps = @(
            @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}},
            @{Name="Stop-SttiInstance"; Expression={Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Remove-SttiInstanceServices"; Expression={Remove-SttiInstanceServices -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Remove-SttiFirewallRules"; Expression={Remove-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}},
            @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $false -Roles $null -Version $null -ErrorAction Stop > $null}},
            @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}
        )   
        Invoke-SttiDeploymentSteps -Activity "Uninstall STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction

        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 Invoke-SttiDeploymentSteps{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Activity,
        
        [Parameter(Mandatory)]
        [pscustomobject] $Steps
    )

    try{
        # check if the prerequisites for the step are met
        $checkPrereqs = {
            param(
                [PSCustomObject] $Step
            )
            if ($Step.CheckExpression){
                if (!(& $Step.CheckExpression)){
                    Write-SttiLog "Prerequisite expression {$($Step.CheckExpression)} for step $($Step.Name) evaluated to false" -Level Verbose
                    return $false
                }
                else{
                    return $true
                }
            }
            return $true
        }
        $filteredSteps = $steps.Where({& $checkPrereqs -Step $_ })

        for ($i = 0; $i -lt $filteredSteps.Count; $i++){
            $step = $filteredSteps[$i]
            
            Write-Progress -Id 1 -Activity $activity -CurrentOperation $step.Name -PercentComplete (($i + 1) / $filteredSteps.Count * 100)
            & $step.Expression
        }
    }
    finally{
        Write-Progress -Id 1 -Activity $activity -Completed
    }
}

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,
        
        [Parameter(Mandatory)]
        [string] $ServiceUsername        
    )
    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($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($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
                    $sslCertificateStore = $Instance.WorkerSslCertificateStore
                }
                elseif ($currentFile.FullName -like "*\halvotec.stti.web\*"){
                    $port = $Instance.WebPort
                    $sslCertificateSubject = $Instance.WebSslCertificateSubject
                    $sslCertificateStore = $Instance.WebSslCertificateStore
                }

                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 = $sslCertificateStore
                            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)){
            $workerDependencies = @()
            
            # check if sql server is a local instance
            $sqlServerParsed = [Regex]::Match($Instance.SqlServer,'([^\\]+)(?:\\([^\\]+))?')
            if ($sqlServerParsed.Success){
                ($null, $sqlComputername, $sqlInstance) = $sqlServerParsed.Groups.Value
                if ([string]::IsNullOrWhiteSpace($sqlInstance)){
                    $sqlInstance = 'MSSQLSERVER'
                }
                $isDatabaseLocal = ($sqlComputername -like "localhost*" -or $sqlComputername -like "(local)*" -or $sqlComputername -like "$($env:COMPUTERNAME)*")            
            }
            else{
                Write-SttiLog "SqlServer name $($Instance.SqlServer) could not be parsed" -Level Warn
            }
            
            if ($isDatabaseLocal){
                Write-SttiLog "SqlServer $($Instance.SqlServer) is local" -Level Verbose
                $sqlLocalInstances = Get-ItemProperty 'HKLM:\Software\Microsoft\Microsoft SQL Server\Instance Names\SQL' -ErrorAction SilentlyContinue | Get-Member -Type Properties | Where-Object Name -NotLike 'PS*' | Select-Object -ExpandProperty Name
                $foundSqlInstance = $sqlLocalInstances -icontains $sqlInstance
                if ($foundSqlInstance ){
                    $sqlServiceName = ($sqlInstance -like "MSSQLSERVER" ? "MSSQLSERVER" : "MSSQL`$$sqlInstance")
                    if ($null -ne (Get-Service -Name $sqlServiceName -ErrorAction SilentlyContinue)){
                        $workerDependencies += $sqlServiceName
                    }
                    else{
                        Write-SttiLog "Could not find local sql server service $sqlServiceName for instance $sqlInstance" -Level Warn
                    }
                }
                else{
                    Write-SttiLog "Sql server instance $sqlInstance could not be found in local instances $sqlLocalInstances" -Level Warn
                }
            }

            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 role" -Level Verbose
                New-Service -Name $workerServiceName -BinaryPathName $workerBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Worker" -ErrorAction Stop > $null
            }
            else{
                Write-SttiLog "Update service for worker role" -Level Verbose
                Set-Service -Name $workerServiceName -Credential $ServiceCredential -ErrorAction Stop > $null
            }
            try{
                Write-SttiLog "Set worker service dependencies $workerDependencies" -Level Verbose
                
                # Array with empty string is necessary to reset dependencies
                if ($workerDependencies.count -eq 0){
                    $workerDependencies = @('')
                }
                $workerServiceCimInstance = Get-CimInstance win32_Service -Filter "Name='$workerServiceName'"
                $workerServiceCimInstance | Invoke-CimMethod -MethodName Change -Arguments @{ServiceDependencies=$workerDependencies} > null
            }
            catch{
                Write-SttiLog "Could not set worker service dependencies: $_" -Level Warn
            }
        }
        elseif ($null -ne (Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue)){
            Write-SttiLog "Remove service for worker role" -Level Verbose
            Remove-Service -Name $workerServiceName -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 role" -Level Verbose
                New-Service -Name $webServiceName -BinaryPathName $webBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Web" -ErrorAction Stop > $null
            }
            else{
                Write-SttiLog "Update service for web role" -Level Verbose
                Set-Service -Name $webServiceName -Credential $ServiceCredential -ErrorAction Stop > $null
            }
        }
        elseif ($null -ne (Get-Service -Name $webServiceName -ErrorAction SilentlyContinue)){
            Write-SttiLog "Remove service for web role" -Level Verbose
            Remove-Service -Name $webServiceName -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"
}

function Update-SttiInstanceSettings{
    [CmdletBinding()]
    Param(
        # Parameter help description
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,
        
        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )
    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{
        Retry-Command -ScriptBlock {
            Set-SttiSystemSettings -Instance $Instance -CertificateThumbprint $CertificateThumbprint `
                -InstanceName $Instance.Name -DataDirectory "$($Instance.Path)\data" `
                -UseProxy (![string]::IsNullOrEmpty($Instance.HttpProxyAddress)) -ProxyAddress $Instance.HttpProxyAddress `
                -ErrorAction Stop > $null
        } -RetryCount 5 -TimeoutInSecs 10 -ErrorAction Stop -FailureMessage 'Set-SttiSystemSettings failed' -SuccessMessage 'Set-SttiSystemSettings succeeded'
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}
#Uninstall-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev)

#endregion Installation

#region Certificate management

function New-SttiUserCertificate{
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [string] $Subject,
        
        [string] $FriendlyName = $Subject,
        
        [DateTime] $NotAfter = (Get-Date).AddYears(20)
    )
    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"
    $cert = New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation $location -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
    Write-SttiLog "Created new cert with thumbprint $($cert.Thumbprint) for $Subject in $location" -Level Info
    return $cert
}

function New-SttiDatabaseEncryptionCertificate{
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [string] $Subject,
        
        [string] $FriendlyName = $Subject,
        
        [DateTime] $NotAfter = (Get-Date).AddYears(50)
    )
    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"
    $cert = New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation $location -KeyExportPolicy Exportable -Type DocumentEncryptionCert -KeyUsage KeyEncipherment -KeySpec KeyExchange -KeyLength 2048 -NotAfter $NotAfter -ErrorAction Stop
    Write-SttiLog "Created new cert with thumbprint $($cert.Thumbprint) for $Subject in $location" -Level Info
    return $cert
}

function Import-SttiPfxCertificate{
    [OutputType([X509Certificate])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [string] $FilePath,
        
        [string] $Location = "Cert:\CurrentUser\My",

        [SecureString] $Password,
        
        [PSCredential] $UserCredential
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $importScript = {
        $cert = Import-PfxCertificate -FilePath $Using:FilePath -CertStoreLocation $Using:Location -Password:$Using:Password -Exportable
        return $cert
    }

    $cert = Start-Job -ScriptBlock $importScript -Credential:$UserCredential | Receive-Job -Wait -ErrorAction Stop
    
    return $cert
}

function Export-SttiPfxCertificate{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [string] $FilePath,
        
        [Parameter(Mandatory, ValueFromPipeline)]
        [X509Certificate] $Certificate,

        [SecureString] $Password,
        
        [PSCredential] $UserCredential,

        [switch] $RemoveFromStore,

        [ValidateSet("TripleDES_SHA1", "AES256_SHA256")]
        [string] $CryptoAlgorithmOption = "TripleDES_SHA1",

        [string] $CertificatePurpose
    )

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

    Process{
        if (!$Password){
            $Password = Read-Password -Prompt "Please enter or paste a password for the exported $CertificatePurpose certificate" -ErrorAction Stop
        }

        $exportScript = {
            $cryptoAlgorithmParameterAvailable = (Get-Command Export-PfxCertificate).Parameters.ContainsKey("CryptoAlgorithmOption")
            # It is necessary to test if the parameter CryptoAlgorithmOption is available since it was added in Windows 1809
            if ($cryptoAlgorithmParameterAvailable){
                $Using:Certificate | Export-PfxCertificate -FilePath $Using:FilePath -CryptoAlgorithmOption:$Using:CryptoAlgorithmOption -Password:$Using:Password -ErrorAction Stop > $null
            }
            else{
                $Using:Certificate | Export-PfxCertificate -FilePath $Using:FilePath  -Password:$Using:Password -ErrorAction Stop > $null
            }
            if ($Using:RemoveFromStore){
                $Using:Certificate | Remove-Item
            }
        }

        Start-Job -ScriptBlock $exportScript -Credential:$UserCredential | Receive-Job -Wait -ErrorAction Stop
        
        return Get-Item $FilePath
    }
}

function Get-SttiDatabaseEncryptionCertificateLocation {
    [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
        }
    }
}

function Install-SttiDatabaseEncryptionCertificate{
    [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{
        $targetLocation = "cert:\CurrentUser\My"
        $subject = "Stti $($Instance.Name) Database Encryption"
        $notAfter = (Get-Date).AddYears(50)

        $newCertThumbprint = Start-Job -ScriptBlock { 
            Param([string] $location, [string] $subject, [DateTime] $notAfter) 

            $cert = New-SelfSignedCertificate -Subject $subject -FriendlyName $subject -CertStoreLocation $location -KeyExportPolicy Exportable -Type DocumentEncryptionCert -KeyUsage KeyEncipherment -KeySpec KeyExchange -KeyLength 2048 -NotAfter $notAfter -ErrorAction Stop
            return $cert.Thumbprint

        } -Credential $ServiceCredential -ArgumentList $targetLocation, $subject, $notAfter | Receive-Job -Wait -ErrorAction Stop

        Set-SttiInstanceConfig -Instance $Instance -EncryptionCertificateThumbprint $newCertThumbprint -ErrorAction Stop > $null
        Write-SttiLog "A new database encryption certificate with thumbprint $newCertThumbprint has been created. Do not forget to backup the certificate and store it in a safe location. Therefore Export-SttiDatabaseEncryptionCertificate should be used." -Level Warn
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Export-SttiDatabaseEncryptionCertificate{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

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

        [ValidateSet("TripleDES_SHA1", "AES256_SHA256")]
        [string] $CryptoAlgorithmOption = "TripleDES_SHA1",

        [securestring] $DatabaseEncryptionKeyPassword = (Read-Password -Prompt "Please enter or paste a password for the exported database encryption key 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

    try{
        $location = "cert:\CurrentUser\My"
        $certPath = "$($location)\$($Instance.EncryptionCertificateThumbprint)"
        $exportPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\", "data\", "Stti_$($Instance.Name)_DatabaseEncryption.pfx")

        Start-Job -ScriptBlock { 
            if (!(Test-Path -Path $Using:certPath)){
                throw "$certPath does not exist"
            }
            $cryptoAlgorithmParameterAvailable = (Get-Command Export-PfxCertificate).Parameters.ContainsKey("CryptoAlgorithmOption")
            # It is necessary to test if the parameter CryptoAlgorithmOption is available since it was added in Windows 1809
            if ($cryptoAlgorithmParameterAvailable){
                Get-Item -Path $Using:certPath | Export-PfxCertificate -FilePath $Using:exportPath -CryptoAlgorithmOption:$Using:CryptoAlgorithmOption -Password $Using:DatabaseEncryptionKeyPassword -ErrorAction Stop > $null
            }
            else{
                Get-Item -Path $Using:certPath | Export-PfxCertificate -FilePath $Using:exportPath -Password $Using:DatabaseEncryptionKeyPassword -ErrorAction Stop > $null
            }           

        } -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop

        Write-SttiLog "Database encryption certificate with thumbprint $($Instance.EncryptionCertificateThumbprint) was exported to $exportPath. Backup file to a secure location and remember password. Then delete the file." -Level Info
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Install-SttiInitialUserCertificate{
    [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{
        $targetLocation = "cert:\CurrentUser\My"
        $alreadyExists = Test-Path -Path "$($targetLocation)\$($initialUserCertificateThumbprint)"
        if (!$alreadyExists){
            Write-SttiLog "Initial user certificate with thumbprint $initialUserCertificateThumbprint does not exist in $targetLocation." -Level Verbose
            $initialUserCertPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\bin\", "InitialUser.pfx")
            Import-PfxCertificate -FilePath $initialUserCertPath -CertStoreLocation $targetLocation -Password $initialUserCertificatePassword -ErrorAction Stop > $null
            Write-SttiLog "Imported initial user certificate with thumbprint $initialUserCertificateThumbprint to $targetLocation." -Level Verbose
        }
        else{
            Write-SttiLog "Initial user certificate with thumbprint $initialUserCertificateThumbprint already exists in $targetLocation." -Level Verbose
        }

        Write-SttiLog "A new STTI instance always has a standard initial user which compromises security. Do not forget to switch to new user account. Therefore Switch-SttiInitialUser should be used." -Level Warn
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Switch-SttiInitialUser{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [bool] $NewRootCertificate = $true,

        [bool] $NewUserCertificate = $true,

        [string] $CertificateThumbprint = $initialUserCertificateThumbprint,

        [securestring] $RootCertificatePassword,

        [ValidateSet("TripleDES_SHA1", "AES256_SHA256")]
        [string] $CryptoAlgorithmOption = "TripleDES_SHA1"
    )
    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 ($NewRootCertificate){
        $subject = "Stti $($Instance.Name) Root User"
        $rootCert = New-SttiUserCertificate -Subject $subject -FriendlyName $subject -NotAfter ([datetime]::Now.AddYears(50)) -ErrorAction Stop
        try{
            $exportPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\", "data\", "Stti_$($Instance.Name)_RootUser.pfx")
            $rootCert | Export-SttiPfxCertificate -FilePath $exportPath -Password:$RootCertificatePassword -RemoveFromStore -CryptoAlgorithmOption:$CryptoAlgorithmOption -CertificatePurpose 'Root-user private key' -ErrorAction Stop > $null
            New-SttiUser -Instance:$Instance -DisplayName "$subject $($rootCert.Thumbprint)" -UserCertificateThumbprint $rootCert.Thumbprint -Roles SecurityAdmin -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null
            Write-SttiLog "Root user certificate with thumbprint $($rootCert.Thumbprint) was exported to $exportpath. Backup file to a secure location and remember password. Then delete the file." -Level Info
            Write-SttiLog "Root user was added to stti with securityadmin role" -Level Info
        }
        catch{
            $rootCert | Remove-Item -ErrorAction Continue
            Write-SttiLog "Error in path NewRootCertificate. The following exception occured: $_" -Level Error -ErrorAction Stop
        }
    }
    if ($NewUserCertificate){
        $subject = "Stti $($Instance.Name) User $env:UserName"
        $userCert = New-SttiUserCertificate -Subject $subject -FriendlyName $subject -ErrorAction Stop
        try{        
            New-SttiUser -Instance:$Instance -DisplayName "$subject $($userCert.Thumbprint)" -UserCertificateThumbprint $userCert.Thumbprint -Roles SecurityAdmin, OperationsAdmin, DataAdmin -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null
            Write-SttiLog "Current user was added to stti with securityadmin, operationsadmin roles" -Level Info
        }
        catch{
            $userCert | Remove-Item -ErrorAction Continue
            Write-SttiLog "Error in path NewUserCertificate. The following exception occured: $_" -Level Error -ErrorAction Stop
        }
    }

    try{
        $initialUser = Get-SttiUser -Instance:$Instance -UserId $initialUserId -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop
        if ($initialUser.IsActive){
            $initialUser | Disable-SttiUser -Instance:$Instance -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null
            Write-SttiLog "Initial installation user was disabled. Please use the new certificate thumbprint." -Level Info
        }
    }
    catch{
        Write-SttiLog "Could not disable initial user in STTI. The following exception occured: $_" -Level Error -ErrorAction Stop
    }

    $instanceDefaults = (Get-SttiDefaultValues).ForInstance($Instance)
    $defaultCertificateThumbprint = $null -ne $instanceDefaults ? $instanceDefaults.CertificateThumbprint : $null
    if ([string]::IsNullOrWhiteSpace($defaultCertificateThumbprint) -and $userCert){
        try{
            Set-SttiDefaultValues -InstanceName $Instance.Name -CertificateThumbprint $userCert.Thumbprint -ErrorAction Stop > $null
            Write-SttiLog "The new user certificate thumbprint was set as default." -Level Info
        }
        catch{
            Write-SttiLog "Could not set the new user certificate thumbprint as default. The following exception occured: $_" -Level Error -ErrorAction Continue
        }
    }
}

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

        [pscredential] $ServiceCredential,

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

    # check presence of certificate
    if ($InstanceRole -contains [SttiInstanceRoles]::Worker) {$store = $Instance.WorkerSslCertificateStore; $subject = $Instance.WorkerSslCertificateSubject}
    elseif ($InstanceRole -contains [SttiInstanceRoles]::Web) {$store = $Instance.WebSslCertificateStore; $subject = $Instance.WebSslCertificateSubject}
    else {throw "Invalid instance role $InstanceRole"}
    
    switch (($null -ne $store ? $store.ToLowerInvariant() : $null)){
        "CurrentUser" { $location = "Cert:\CurrentUser\My" }
        "LocalMachine" { $location = "Cert:\LocalMachine\My" }
        default {throw "Invalid certificate store $store"}
    }
    
    $checkCertificateScript = {
        Param(
            [string] $subject,        
            [string] $location,
            [string] $store,
            [string] $serviceUsername
        )

        # find certificates with subject
        try{
            $certsWithSubject = Get-ChildItem -Path $location -SSLServerAuthentication -DnsName $subject
            if ($null -eq $certsWithSubject){
                throw "Could not find ssl certificate with subject ""$subject"" in $location"
            }
            else{
                $certsWithSubject | Foreach-Object { Write-Verbose "Found certificate with thumbprint $($_.Thumbprint)" }
            }
        }
        catch{
            Write-Error "Could not find a matching ssl certificate: $($_.Exception)" -ErrorAction Stop
            return $false
        }

        $foundValidCert = $false
        foreach ($cert in $certsWithSubject){
            # check usage and date
            try{
                $cert | Test-Certificate -Policy SSL -EKU "1.3.6.1.5.5.7.3.1" -AllowUntrustedRoot -ErrorAction Stop -WarningAction SilentlyContinue -WarningVariable testWarnings > $null
                if ($testWarnings) {$testWarnings | Write-Warning }
            }
            catch{
                Write-Warning "SSL certificate with thumbprint $($cert.Thumbprint) is not valid: $($_.Exception)"
                continue
            }

            # check presence of private key
            try{
                $certKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
                if ($null -eq $certKey){
                    throw "Could not load private key for the certificate"
                }
            }
            catch{
                Write-Warning "Could not load private key for the certificate with thumbprint $($cert.Thumbprint): $($_.Exception)"
                continue
            }
            # grant read access for service user if certificate ist in local machine store
            if ($store -like "LocalMachine"){
                try{
                    $keyFileName = $certKey.key.UniqueName
                    $keyPath = "$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\$keyFileName"
                    $keyPermissions = Get-Acl -Path $keyPath

                    if ($null -eq ($keyPermissions.Access | Where-Object {$_.IdentityReference -like $serviceUsername -and $_.AccessControlType -eq "Allow" -and $_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::Read })){
                        Write-Verbose "Add read access to private key for $serviceUsername to certificate with thumbprint $($cert.Thumbprint)"
                        $keyPermissions.AddAccessRule(([System.Security.Accesscontrol.FileSystemAccessRule]::new($serviceUsername,"Read","Allow")))
                        Set-Acl -Path $keyPath -AclObject $keyPermissions
                    }
                }
                catch{
                    Write-Warning "Could not ensure read access for $($ServiceCredential.UserName) to the certificate with thumbprint $($cert.Thumbprint): $($_.Exception)"
                    continue
                }
            }

            Write-Verbose "Found valid certificate with thumbprint $($cert.Thumbprint)"
            $foundValidCert = $true
        }
        return $foundValidCert
    }
    try{
        if ($store -eq "LocalMachine"){
            ($foundValidCert = Invoke-Command -ScriptBlock $checkCertificateScript -ArgumentList $subject,$location,$store,$ServiceCredential.UserName -ErrorAction Stop 6>$null) >$null 2>&1 3>&1 4>&1 5>&1 | Write-SttiLog -Level Warn
        }
        elseif ($store -eq "CurrentUser"){
            ($foundValidCert = Start-Job -ScriptBlock $checkCertificateScript -ArgumentList $subject,$location,$store,$ServiceCredential.UserName -Credential $ServiceCredential -ErrorAction Stop | Receive-Job -Wait -ErrorAction Stop 6>$null) >$null 2>&1 3>&1 4>&1 5>&1 | Write-SttiLog -Level Warn
        }
    }
    catch{
        Write-SttiLog "Could not execute certificate checking: $($_.Exception)" -Level Error -ErrorAction Stop
    }

    if (!$foundValidCert){
        Write-SttiLog "Could not find a valid SSL certificate" -Level Error -ErrorAction Stop
    }
    return $foundValidCert
}

#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 | Where-Object { (Test-Path (Get-SttiInstanceConfigFileName -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 Error

            continue
        }

        $config = [SttiInstanceConfig](Get-Content $instanceConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
        $config.Path = Get-SttiInstancePath -Name $Name
        # Migration - Add default values for new settings
        if (!$config.WorkerSslCertificateStore){
            $config.WorkerSslCertificateStore = "CurrentUser"
        }
        if (!$config.WebSslCertificateStore){
            $config.WebSslCertificateStore = "CurrentUser"
        }
    
        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()]
        [SttiInstanceRoles[]] $Roles = @([SttiInstanceRoles]::Worker, [SttiInstanceRoles]::Web),

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

        [ValidateSet("LocalMachine", "CurrentUser")]
        [string] $WorkerSslCertificateStore = "CurrentUser",

        [Parameter(Mandatory)]
        [uint] $WorkerPort,

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

        [ValidateSet("LocalMachine", "CurrentUser")]
        [string] $WebSslCertificateStore = "CurrentUser",

        [Parameter(Mandatory)]
        [uint] $WebPort,

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

        [string] $EncryptionCertificateThumbprint,

        [Parameter()]
        [AllowNull()]
        [ValidateScript({
            if ([string]::IsNullOrWhiteSpace($_) -or $_ -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$") {
                $true
            }
            else{
                throw "Value must be an url like http://127.0.0.1:3128"
            }})]
        [string] $HttpProxyAddress
    )
    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.StoreDbInInstanceDataPath = $StoreDbInInstanceDataPath
    $config.WorkerSslCertificateSubject = $WorkerSslCertificateSubject
    $config.WorkerSslCertificateStore = $WorkerSslCertificateStore
    $config.WorkerPort = $WorkerPort
    $config.WebSslCertificateSubject = $WebSslCertificateSubject
    $config.WebSslCertificateStore = $WebSslCertificateStore
    $config.WebPort = $WebPort
    $config.WorkerHostname = $WorkerHostname
    $config.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint    
    $config.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint"
    $config.HttpProxyAddress = $HttpProxyAddress
    
    $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")]
        [switch] $StoreDbInInstanceDataPath = $false,

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

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [ValidateSet("LocalMachine", "CurrentUser")]
        [string] $WorkerSslCertificateStore,

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

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

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [ValidateSet("LocalMachine", "CurrentUser")]
        [string] $WebSslCertificateStore,

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

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

        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [string] $EncryptionCertificateThumbprint,
        
        [Parameter(ParameterSetName="InstanceName")]
        [Parameter(ParameterSetName="Instance")]
        [AllowNull()]
        [ValidateScript({
            if ([string]::IsNullOrWhiteSpace($_) -or $_ -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$") {
                $true
            }
            else{
                throw "Value must be an url like http://127.0.0.1:3128"
            }})]
        [string] $HttpProxyAddress
    )
    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
        }
        else{
            $Instance = Get-SttiInstanceConfig ($Name) -ErrorAction Stop
        }
        $actualConfig = Get-SttiInstanceConfig ($Name) -ErrorAction Stop
        
        $Instance.SqlServer = ($PSBoundParameters.ContainsKey("SqlServer") ? $SqlServer : $actualConfig.SqlServer)
        $Instance.Database = ($PSBoundParameters.ContainsKey("Database") ? $Database : $actualConfig.Database)
        $Instance.Roles = ($PSBoundParameters.ContainsKey("Roles") ? $Roles : $actualConfig.Roles)
        $Instance.StoreDbInInstanceDataPath = ($PSBoundParameters.ContainsKey("StoreDbInInstanceDataPath") ? $StoreDbInInstanceDataPath : $actualConfig.StoreDbInInstanceDataPath)
        $Instance.WorkerSslCertificateSubject = ($PSBoundParameters.ContainsKey("WorkerSslCertificateSubject") ? $WorkerSslCertificateSubject : $actualConfig.WorkerSslCertificateSubject)
        $Instance.WorkerSslCertificateStore = ($PSBoundParameters.ContainsKey("WorkerSslCertificateStore") ? $WorkerSslCertificateStore : $actualConfig.WorkerSslCertificateStore)
        $Instance.WorkerPort = ($PSBoundParameters.ContainsKey("WorkerPort") ? $WorkerPort : $actualConfig.WorkerPort)
        $Instance.WebSslCertificateSubject = ($PSBoundParameters.ContainsKey("WebSslCertificateSubject") ? $WebSslCertificateSubject : $actualConfig.WebSslCertificateSubject)
        $Instance.WebSslCertificateStore = ($PSBoundParameters.ContainsKey("WebSslCertificateStore") ? $WebSslCertificateStore : $actualConfig.WebSslCertificateStore)
        $Instance.WebPort = ($PSBoundParameters.ContainsKey("WebPort") ? $WebPort : $actualConfig.WebPort)
        $Instance.WorkerHostname = ($PSBoundParameters.ContainsKey("WorkerHostname") ? $WorkerHostname : $actualConfig.WorkerHostname)
        
        if ($PSBoundParameters.ContainsKey("EncryptionCertificateThumbprint")){
            if (![string]::IsNullOrEmpty($actualConfig.EncryptionCertificateThumbprint) -and $EncryptionCertificateThumbprint -ne $actualConfig.EncryptionCertificateThumbprint -and (Get-SttiInstanceInstallStatus -Instance $Instance).Installed){
                Write-SttiLog "The EncryptionCertificateThumbprint has already been set and cannot be changed. Use Rotate-SttiEncryptionMasterKey" -Level Error -ErrorAction Stop
            }
            $Instance.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint
            $Instance.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint"
        }
        else{
            $Instance.EncryptionCertificateThumbprint = $actualConfig.EncryptionCertificateThumbprint
            $Instance.EncryptionMasterKeyName = $actualConfig.EncryptionMasterKeyName
        }
        $Instance.HttpProxyAddress = ($PSBoundParameters.ContainsKey("HttpProxyAddress") ? $HttpProxyAddress : $actualConfig.HttpProxyAddress)
        
        $instanceConfigFile = Get-SttiInstanceConfigFileName $Name

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

        $PSCmdlet.WriteObject($Instance)

        # Renew session and default Values
        if ($script:shellSession.Instance -and $script:shellSession.Instance.Name -ieq $Instance.Name){
            Set-SttiSessionValues -Instance $Instance
        }
    }
} 
#Set-SttiInstanceConfig -InstanceName dev -Database "Test" -Roles Web, Worker
#Get-SttiInstanceConfig dev | Set-SttiInstanceConfig -Database "Test" -Roles Web

function Set-SttiSessionValues {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ParameterSetName="InstanceName")]
        [string] $InstanceName,
        
        [Parameter(Position=1, ParameterSetName="Instance")]
        [SttiInstanceConfig] $Instance,

        [Parameter(ParameterSetName="Instance")]
        [Parameter(ParameterSetName="InstanceName")]
        [int] $ApiCallTimeoutSec
    )
    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 ($PSBoundParameters.ContainsKey("InstanceName")){
        $Instance = Get-SttiInstanceConfig -Name $InstanceName -ErrorAction Stop
        $script:shellSession.Instance = $Instance
    }
    elseif ($PSBoundParameters.ContainsKey("Instance")){
        $script:shellSession.Instance = $Instance
    } 
    
    if ($PSBoundParameters.ContainsKey("ApiCallTimeoutSec")){
        $script:shellSession.ApiCallTimeoutSec = $ApiCallTimeoutSec
    }

    Restore-SttiDefaultParameterValues
}

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 ?? $script:defaultBasePath
    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

    Write-SttiLog "STTI base path was set to $BasePath." -Level Warn

    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, ValueFromPipeline)]
        [SttiInstanceConfig] $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, ValueFromPipeline)]
        [SttiInstanceConfig] $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
        }
    }
}

function Restart-SttiInstance {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [SttiInstanceConfig] $Instance,

        [X509Certificate] $Certificate
    )  

    Process{
        Stop-SttiInstance -Instance:$Instance -Certificate:$Certificate
        Start-SttiInstance -Instance:$Instance -Certificate:$Certificate
    }
}

#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 = "
            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 + '$($Instance.Database).mdf'' , SIZE = 262144KB , FILEGROWTH = 65536KB )
            LOG ON
            ( NAME = N''STTI_log'', FILENAME = N''' + @LogPath + '$($Instance.Database).ldf'' , SIZE = 65536KB , FILEGROWTH = 65536KB )
            COLLATE Latin1_General_100_CI_AS');
            GO
 
            ALTER DATABASE [$($Instance.Database)] SET COMPATIBILITY_LEVEL = 130
            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"

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

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

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

        [switch] $SkipServerChanges
    ) 
    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{
        $command = 
            (!$SkipServerChanges ? "
            IF NOT EXISTS
                (SELECT name FROM master.sys.server_principals WHERE name = '$($ServiceUsername)')
            BEGIN
                CREATE LOGIN [$($ServiceUsername)] FROM WINDOWS WITH DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english]
            END
            GO
            "
: "") `
            + "USE [$($Instance.Database)]
            GO
 
            DECLARE @uid INT
            SELECT @uid = principal_id from sys.database_principals WHERE sid = SUSER_SID('$($ServiceUsername)')
 
            IF @uid IS NULL
            BEGIN
                CREATE USER [$($ServiceUsername)] FOR LOGIN [$($ServiceUsername)]
                ALTER ROLE [db_owner] ADD MEMBER [$($ServiceUsername)]
            END
            ELSE IF NOT EXISTS
                (SELECT * FROM sys.database_role_members WHERE role_principal_id = DATABASE_PRINCIPAL_ID('db_owner') AND member_principal_id = @uid)
            BEGIN
                ALTER ROLE [db_owner] ADD MEMBER [$($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(
                $Instance,
                $EncryptColumnEncryptionKeyFunc
            )  

            Import-Module SqlServer

            # search for the key
            $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)"
                }
            }

            # construct script for creating a new column master key
            $masterKeyCertificatePath = "$certificateStoreLocation/My/$($Instance.EncryptionCertificateThumbprint)"
            $masterKeyCertificate = $key
            $newMasterKeyScript = "CREATE COLUMN MASTER KEY [$($Instance.EncryptionMasterKeyName)] WITH (
                KEY_STORE_PROVIDER_NAME = 'MSSQL_CERTIFICATE_STORE',
                KEY_PATH = '$masterKeyCertificatePath'
                );
                GO
            "


            # create a new random encryption key
            $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
            try{
                $encryptionKey = [byte[]]::new(32)
                $rng.GetBytes($encryptionKey)
            }
            finally{
                $rng.Dispose()
            }
            $encryptionKeyName = "CEK"
            # encrypt the key
            if ($EncryptColumnEncryptionKeyFunc -is [string]){
                $EncryptColumnEncryptionKeyFunc = [scriptblock]::Create($EncryptColumnEncryptionKeyFunc)
            }
            $encryptedKey = (& $EncryptColumnEncryptionKeyFunc -Certificate $masterKeyCertificate -MasterKeyPath $masterKeyCertificatePath -ColumnEncryptionKey $encryptionKey)
            # seralize to format that sql server wants
            $encryptedKeySerialized = "0x" + [BitConverter]::ToString($encryptedKey).Replace("-", "")
            # construct script for creating a new column encryption key
            $newEncryptionKeyScript = "CREATE COLUMN ENCRYPTION KEY [$encryptionKeyName] WITH VALUES (
                COLUMN_MASTER_KEY = [$($Instance.EncryptionMasterKeyName)],
                ALGORITHM = 'RSA_OAEP',
                ENCRYPTED_VALUE = $encryptedKeySerialized
                );
                GO
            "


            $db = Get-SqlDatabase -Name $Instance.Database -ServerInstance $Instance.SqlServer -ErrorAction Stop
            $masterKeyExists = ($null -ne (Get-SqlColumnMasterKey -Name $Instance.EncryptionMasterKeyName -InputObject $db -ErrorAction SilentlyContinue))
            $encryptionKeyExists = ($null -ne (Get-SqlColumnEncryptionKey -Name "CEK" -InputObject $db -ErrorAction SilentlyContinue))
            
            $script = "
                BEGIN TRANSACTION
                    $(!$masterKeyExists ? $newMasterKeyScript : '')
                    $(!$encryptionKeyExists ? $newEncryptionKeyScript : '')
                COMMIT TRANSACTION
            "

            Invoke-SqlCmd -ServerInstance $Instance.SqlServer -Database $Instance.Database -Query $script -ErrorAction Stop
        }
        $argumentList = @($Instance, $encryptColumnEncryptionKeyFunc)
        
        # 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 | Receive-Job -Wait -ErrorAction Stop
        }
        else{
            Start-Job -Credential $ServiceCredential -ScriptBlock $scriptBlock -ArgumentList $argumentList | Receive-Job -Wait -ErrorAction Stop
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

$encryptColumnEncryptionKeyFunc = {
    Param(
        [X509Certificate] $Certificate,
        [string] $MasterKeyPath,
        [byte[]] $ColumnEncryptionKey
    )
    # Construct the encryptedColumnEncryptionKey
    # Format is
    # version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
    # We currently only support one version
    $version = [byte[]] @( 0x01 )

    # Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
    $masterKeyPathBytes = [System.Text.UnicodeEncoding]::Unicode.GetBytes($masterKeyPath.ToLowerInvariant())
    $keyPathLength = [System.BitConverter]::GetBytes([Int16]$masterKeyPathBytes.Length)

    # Encrypt the plain text
    $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($Certificate)
    $cipherText = $rsa.Encrypt($columnEncryptionKey, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)
    $cipherTextLength = [System.BitConverter]::GetBytes([Int16]$cipherText.Length)

    # Compute hash
    # SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
    $sha256 =  [System.Security.Cryptography.SHA256CryptoServiceProvider]::new()
    try
    {
        $sha256.TransformBlock($version, 0, $version.Length, $version, 0) > $null
        $sha256.TransformBlock($keyPathLength, 0, $keyPathLength.Length, $keyPathLength, 0) > $null
        $sha256.TransformBlock($cipherTextLength, 0, $cipherTextLength.Length, $cipherTextLength, 0) > $null
        $sha256.TransformBlock($masterKeyPathBytes, 0, $masterKeyPathBytes.Length, $masterKeyPathBytes, 0) > $null
        $sha256.TransformFinalBlock($cipherText, 0, $cipherText.Length) > $null
        $hash = $sha256.Hash
    }
    finally{
        $sha256.Dispose()
    }

    # Sign the hash
    $rsaPrivate = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
    $rsaFormatter = [System.Security.Cryptography.RSAPKCS1SignatureFormatter]::new($rsaPrivate)
    $rsaFormatter.SetHashAlgorithm('SHA256')
    $signedHash = $rsaFormatter.CreateSignature($hash)

    # Construct the encrypted column encryption key
    # EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
    $encryptedColumnEncryptionKeyLength = $version.Length + $cipherTextLength.Length + $keyPathLength.Length + $cipherText.Length + $masterKeyPathBytes.Length + $signedHash.Length
    $encryptedColumnEncryptionKey = [byte[]]::new($encryptedColumnEncryptionKeyLength)

    # Copy version byte
    $currentIndex = 0;
    [System.Buffer]::BlockCopy($version, 0, $encryptedColumnEncryptionKey, $currentIndex, $version.Length) > $null
    $currentIndex += $version.Length

    # Copy key path length
    [System.Buffer]::BlockCopy($keyPathLength, 0, $encryptedColumnEncryptionKey, $currentIndex, $keyPathLength.Length) > $null
    $currentIndex += $keyPathLength.Length

    # Copy ciphertext length
    [System.Buffer]::BlockCopy($cipherTextLength, 0, $encryptedColumnEncryptionKey, $currentIndex, $cipherTextLength.Length) > $null
    $currentIndex += $cipherTextLength.Length

    # Copy key path
    [System.Buffer]::BlockCopy($masterKeyPathBytes, 0, $encryptedColumnEncryptionKey, $currentIndex, $masterKeyPathBytes.Length) > $null
    $currentIndex += $masterKeyPathBytes.Length

    # Copy ciphertext
    [System.Buffer]::BlockCopy($cipherText, 0, $encryptedColumnEncryptionKey, $currentIndex, $cipherText.Length) > $null
    $currentIndex += $cipherText.Length

    # copy the signature
    [System.Buffer]::BlockCopy($signedHash, 0, $encryptedColumnEncryptionKey, $currentIndex, $signedHash.Length) > $null

    return $encryptedColumnEncryptionKey
}

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

        [pscredential] $ServiceCredential,

        [switch] $SkipPermissionCheck        
    )
   
    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 = {
            Invoke-Sqlcmd -ServerInstance $Using:Instance.SqlServer -Database master -Query "SELECT 1" -ErrorAction Stop > $null
        }
        Start-Job -ScriptBlock $scriptBlock -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop
    }
    catch{
        Write-SttiLog "Could not connect to Sql Server instance $($Instance.SqlServer): $($_.Exception)" -Level Error -ErrorAction Stop
        return $false
    }
    #TODO: checks if version and settings of server are correct

    if (!$SkipPermissionCheck){
        try{
            $query = "SELECT permission_name FROM fn_my_permissions (NULL, 'SERVER') WHERE permission_Name IN ('CREATE ANY DATABASE', 'ALTER ANY LOGIN')"
            $result = Invoke-Sqlcmd -ServerInstance $Instance.SqlServer -Database master -Query $query -ErrorAction Stop
        }
        catch{
            Write-SttiLog "Could not query server permissions: $($_.Exception)" -Level Error -ErrorAction Stop
            return $false
        }
        $createDatabasePermission = $null -ne ($result | Where-Object permission_name -Like "CREATE ANY DATABASE")
        $createLoginPermission = $null -ne ($result | Where-Object permission_name -Like "ALTER ANY LOGIN")
        if (!$createDatabasePermission || !$createLoginPermission){
            Write-SttiLog "Not enough permissions in Sql Server for installation. CREATE ANY DATABASE=$createDatabasePermission. ALTER ANY LOGIN=$createLoginPermission" -Level Error -ErrorAction Stop
            return $false
        }
    }

    return $true
}

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

        [switch] $UseExistingDatabase,

        [pscredential] $ServiceCredential,

        [switch] $SkipPermissionCheck
    )
   
    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 = {
            $query = "SELECT name FROM sys.databases WHERE name LIKE '$($Using:Instance.Database)'"
            return Invoke-Sqlcmd -ServerInstance $Using:Instance.SqlServer -Database master -Query $query -ErrorAction Stop
        }
        $databases = Start-Job -ScriptBlock $scriptBlock -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop
    }
    catch{
        Write-SttiLog "Could not enumerate Sql databases: $($_.Exception)" -Level Error -ErrorAction Stop
        return $false
    }
    $databaseExists = $null -ne ($databases | Where-Object name -Like $Instance.Database)

    if (!$UseExistingDatabase)
    {
        if ($databaseExists){
            Write-SttiLog "Database $($Instance.Database) already exists. Specify parameter -UseExistingDatabase if you want to use the existing database." -Level Error -ErrorAction Stop
            return $false
        }
    }
    else{
        if (!$databaseExists){
            Write-SttiLog "Database $($Instance.Database) does not exist." -Level Error -ErrorAction Stop
            return $false
        }
        #TODO: checks if settings of database are correct
        if (!$SkipPermissionCheck){
            try{
                $query = "SELECT permission_name FROM fn_my_permissions (NULL, 'DATABASE') WHERE permission_Name IN ('ALTER ANY ROLE', 'ALTER ANY USER')"
                $result = Invoke-Sqlcmd -ServerInstance $Instance.SqlServer -Database $Instance.Database -Query $query -ErrorAction Stop
            }
            catch{
                Write-SttiLog "Could not query database permissions: $($_.Exception)" -Level Error -ErrorAction Stop
                return $false
            }
            $alterRolePermission = $null -ne ($result | Where-Object permission_name -Like "ALTER ANY ROLE")
            if (!$alterRolePermission){
                Write-SttiLog "Not enough permissions in Sql Server database $($Instance.Database) for installation. ALTER ANY ROLE=$alterRolePermission." -Level Error -ErrorAction Stop
                return $false
            }
        }
    }

    return $true
}

function Invoke-SttiDatabaseMigration{
    [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 -WindowStyle Hidden -ErrorAction Stop
                }
                else{
                    $process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Credential $ServiceCredential -Wait -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -WindowStyle Hidden -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;Encrypt=false"
}

#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"){
        $package = $packages | Sort-Object -Property Version -Descending | Select-Object -First 1
    }
    else {
        $version = [System.Version]::Parse($VersionNumber)
        $package = $packages | Where-Object -Property Version -eq $version
    }
    if ($null -eq $package){
        throw "no package found"
    }

    return $package
}
# Get-SttiPackage -VersionNumber "latest"


function Set-SttiDefaultValues{
    [CmdletBinding(DefaultParameterSetName="InstanceName")]
    Param(
        [Parameter(ParameterSetName="Instance")]
        [SttiInstanceConfig] $Instance,

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

        [Parameter(ParameterSetName="Instance")]
        [Parameter(ParameterSetName="InstanceName")]
        [PSCredential] $CustomerCredential,

        [Parameter(ParameterSetName="Instance")]
        [Parameter(ParameterSetName="InstanceName")]
        [PSCredential] $ServiceCredential,

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

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

        [Parameter(ParameterSetName="Instance")]
        [Parameter(ParameterSetName="InstanceName")]
        [string] $UseLocalServiceUser
    )
    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 ($PSBoundParameters.ContainsKey("Instance")){
        $InstanceName = $Instance.Name.ToUpperInvariant()
    }    
    else{
        if ($InstanceName){
            $InstanceName = $InstanceName.ToUpperInvariant()
        }
    }

    # Restore
    $defaults = Get-SttiDefaultValues
    
    # Set new Values
    if ($PSBoundParameters.ContainsKey("CustomerCredential")){
        $defaults.CustomerCredential = $CustomerCredential
    }
    if ($PSBoundParameters.ContainsKey("PackageSource")){
        $defaults.PackageSource = $PackageSource
    }
    if ($PSBoundParameters.ContainsKey("ServiceCredential")){
        if (!$InstanceName) {
            throw "For parameter ServiceCredential also parameter Instance or Instancename must be specified"
        }
        $defaults.ForInstance($InstanceName).ServiceCredential = $ServiceCredential
    }
    if ($PSBoundParameters.ContainsKey("CertificateThumbprint")){
        if (!$InstanceName) {
            throw "For parameter CertificateThumbprint also parameter Instance or Instancename must be specified"
        }
        $defaults.ForInstance($InstanceName).CertificateThumbprint = $CertificateThumbprint
    }
    if ($PSBoundParameters.ContainsKey("UseLocalServiceUser")){
        if (!$InstanceName) {
            throw "For parameter UseLocalServiceUser also parameter Instance or Instancename must be specified"
        }
        $defaults.ForInstance($InstanceName).UseLocalServiceUser = $UseLocalServiceUser
    }

    #Serialize
    $defaultsFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "defaults.json")
    $defaultsData = @{
        InstanceDefaults = @{}
    }
    foreach ($key in ($defaults | Get-Member -Type Properties | Select-Object -ExpandProperty Name)){
        if ($key -eq "InstanceDefaults"){
            $instanceDefaultsTable = $defaults.$Key
            foreach ($in in $instanceDefaultsTable.Keys){
                $instanceDefaults = $defaults.ForInstance($in)
                $instanceDefaultsData = $defaultsData.InstanceDefaults[$in] = @{}

                foreach ($ikey in ($instanceDefaults | Get-Member -Type Properties | Select-Object -ExpandProperty Name)){
                    if ($ikey -like "*Credential"){
                        $instanceDefaultsData[$ikey] = Convert-CredentialToHashtable -Credential $instanceDefaults.$ikey
                    }
                    else{
                        $instanceDefaultsData[$ikey] = $instanceDefaults.$ikey
                    }
                }
            }
        }
        elseif ($key -like "*Credential"){
            $defaultsData[$key] = Convert-CredentialToHashtable -Credential $defaults.$key
        }
        else{
            $defaultsData[$key] = $defaults.$key
        }
    }
    
    $defaultsData | ConvertTo-Json -Depth 10 -ErrorAction Stop | Out-File $defaultsFile -Encoding utf8 -ErrorAction Stop

    Restore-SttiDefaultParameterValues
}
#Set-SttiDefaults

function Get-SttiDefaultValues{
    [OutputType([SttiDefaults])]
    [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

    $defaults = [SttiDefaults]::new()
    $defaults.InstanceDefaults = @{}

    $defaultsFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "defaults.json")
    if (!(Test-Path $defaultsFile)){
        return $defaults    
    }

    $defaultsData = Get-Content $defaultsFile -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable
    foreach ($key in $defaultsData.Keys){
        if ($key -eq "InstanceDefaults"){
            $instanceDefaultsTable = $defaultsData[$key]
            foreach ($in in $instanceDefaultsTable.Keys){
                $instanceDefaultsData = $instanceDefaultsTable[$in]
                $instanceDefaults = $defaults.ForInstance($in)
                foreach ($ikey in $instanceDefaultsData.Keys){
                    if ($ikey -like "*Credential"){
                        $instanceDefaults.$ikey = Convert-HashtableToCredential -Data $instanceDefaultsData[$ikey]
                    }
                    else{
                        $instanceDefaults.$ikey = $instanceDefaultsData[$ikey]
                    }
                }
            }
        }
        elseif ($key -like "*Credential"){
            $defaults.$key = Convert-HashtableToCredential -Data $defaultsData[$key]
        }
        else{
            $defaults.$key = $defaultsData[$key]
        }
    } 

    return $defaults
}
#Get-SttiDefaults

function Restore-SttiDefaultParameterValues{
    [CmdletBinding()]
    Param(
    )

    $defaults = Get-SttiDefaultValues
    $params = @{}

    foreach ($prop in ($defaults | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){
        if ($prop -eq "InstanceDefaults"){
            $instanceDefaults = [SttiInstanceDefaults]::new()
            if ($script:shellSession.Instance){
                $instanceDefaults = $defaults.ForInstance($script:shellSession.Instance)
            }
            
            foreach ($iprop in ($instanceDefaults | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){
                $value = $instanceDefaults.$iprop
                $params.Add($iprop, $value)
            }
        }
        else{
            $value = $defaults.$prop
            $params.Add($prop, $value)
        }
    }

    foreach ($prop in ($script:shellSession | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){
        $value = $script:shellSession.$prop
        $params.Add($prop, $value)
    }

    $params.Add("NoProxy", $true)

    foreach ($param in $params.Keys){
        $paramNames = switch ($param) {
            "ApiCallTimeoutSec" { "Invoke-RestMethod:TimeoutSec", "Invoke-WebRequest:TimeoutSec" }
            "NoProxy" { "Invoke-RestMethod:NoProxy", "Invoke-WebRequest:NoProxy" }
            default { [string[]]"*-Stti*:$param" }
        }
        
        $paramValue = $params[$param]
        foreach ($paramName in $paramNames){
            if ($paramValue){
                $Global:PSDefaultParameterValues[$paramName] = $paramValue
            }
            else{
                $Global:PSDefaultParameterValues.Remove($paramName)
            }
        }
    }
}

function Get-SttiUserHomePath{
    [OutputType([string])]
    Param()
    
    $sttiUserHomePath = [System.IO.Path]::Combine([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData), "Stti")
    if (!(Test-Path $sttiUserHomePath)){
        New-Item $sttiUserHomePath -ItemType Directory -Force > $null
    }
    return $sttiUserHomePath
}

function Convert-CredentialToHashtable{
    [OutputType([hashtable])]
    param(
        [pscredential]$Credential
    )

    if ($Credential){
        return @{
            UserName = $Credential.UserName
            Password = ($Credential.Password | ConvertFrom-SecureString)
        }
    }
    else {
        return $null
    }
}

function Convert-HashtableToCredential{
    [OutputType([pscredential])]
    param(
        [hashtable]$Data
    )

    if ($Data){
        return [System.Management.Automation.PsCredential]::new($Data.UserName, ($Data.Password | ConvertTo-SecureString))
    }
    else {
        return $null
    }
}

function Find-SttiPackage{
    [OutputType([SttiPackage[]])]
    [CmdletBinding()]
    Param(
        [ValidatePattern("^\d+\.\d+\.\d+(?:\.\d+)?$|^latest$")]
        [string] $VersionNumber,
        [string] $PackageSource = $script:defaultPackageSource,
        [Parameter(Mandatory)]
        [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

    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{
    [OutputType([SttiPackage])]
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [SttiPackage] $Package,
        
        [Parameter(Mandatory)]
        [pscredential] $CustomerCredential
    )

    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

        Test-UserHasAdminRole -ErrorAction Stop
        
        if ($Package.IsLocal){
            Write-SttiLog "Package must not be local"
        }
        
        $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

        Get-SttiPackage -Version $Package.Version
    }
}
#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 "ModuleInfo:" -Level Verbose
    Get-Module | Where-Object Name -in @("Stti", "SqlServer") | Select-Object -Property Name, Version, PreRelease, ModuleType | Format-Table | Out-String | Write-SttiLog -Level Verbose
    Write-SttiLog "HostInfo:" -Level Verbose
    Get-Host | Write-SttiLog -Level Verbose
    Write-SttiLog "ComputerInfo:" -Level Verbose
    Get-ComputerInfo | 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-Host $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"

function Write-SttiInstanceStatus{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance
    )
    $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop
    Write-SttiLog "Current instance status" -Level Verbose
    $instanceStatus | Write-SttiLog -Level Verbose
}

#endregion Logging

#region Helper

function Read-Password {
    [OutputType([SecureString])]
    Param(
        [string] $Prompt
    )

    $pw = $null
    $validValue = $false
    while (!$validValue){
        $pw1 = [SecureString](Read-Host -Prompt $Prompt -AsSecureString -ErrorAction Stop)
        $pw2 = [SecureString](Read-Host -Prompt "Please repeat the password" -AsSecureString -ErrorAction Stop)
        $pw1Text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw1))
        $pw2Text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw2))
        $validValue = $pw1Text.CompareTo($pw2Text) -eq 0
        if (!$validValue){
            Write-Host "The password do not match"
        }
        else {
            $pw = $pw1
        }
    }    
    return $pw
}

function Test-Credential {
    [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

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

    # Check that all components are present
    if (!($user) -or !($password)) {
        Write-SttiLog "Please specify both username and password" -Level Error
        return $false
    }
    # Check format
    if ($user -match "^[^\\]+\\[^\\]+$"){
        $components = $user -split "\\"
        $domain = $components[0]
        #$username = $components[1]

        if ($domain -eq "."){
            Write-SttiLog "Don't use '.' as domain or computername." -Level Error
            return $false    
        }
    }
    else{
        Write-SttiLog "Username must be specified in format DOMAIN\USERNAME. For local accounts use COMPUTERNAME\USERNAME." -Level Error
        return $false
    }

    $isValid = $false
    try{
        # Workaround, since start-job with credentials cannot be called in remoting session
        if ($null -eq $ServiceCredential -or ($env:useCurrentUser -and $env:useCurrentUser -eq $true)){
            $isValid = $true
        }
        else{
            $isValid = Start-Job -ScriptBlock { return $true } -Credential $Credential -ErrorAction Stop| Receive-Job -Wait -ErrorAction Stop
        }
        return $isValid
    }
    catch{
        Write-SttiLog "Could not validate the credentials. An error occurred: $($_.Exception)" -Level Error -ErrorAction Stop
    }
    if (-not ($isValid)){
        Write-SttiLog "Could not validate the credentials. Username or password seems to be invalid." -Level Error -ErrorAction Stop
        return $isValid
    }
}

function Test-Parameter{
    Param(
        [Parameter(Position=1)]
        $Value,
        [Parameter(Mandatory, Position=2)]
        [string]$Name,
        [ValidateNotNullOrEmpty]
        [string]$CheckExpression = '$null -ne $Value',
        [ValidateNotNullOrEmpty]
        [string]$Message = "Parameter $Name is null"
    )    
    if (!(Invoke-Expression $CheckExpression)){        
        throw $Message
    }
}

function Retry-Command {
    [CmdletBinding()]
    param (
        [parameter(Mandatory, ValueFromPipeline)] 
        [ValidateNotNullOrEmpty()]
        [scriptblock] $ScriptBlock,
        [int] $RetryCount = 3,
        [int] $TimeoutInSecs = 30,
        [string] $SuccessMessage = "Command executed successfuly!",
        [string] $FailureMessage = "Failed to execute the command"
        )
        
    process {
        $Attempt = 1
        $Flag = $true
        
        do {
            try {
                $PreviousPreference = $ErrorActionPreference
                $ErrorActionPreference = 'Stop'
                Invoke-Command -ScriptBlock $ScriptBlock -OutVariable Result              
                $ErrorActionPreference = $PreviousPreference

                # flow control will execute the next line only if the command in the scriptblock executed without any errors
                # if an error is thrown, flow control will go to the 'catch' block
                Write-SttiLog "$SuccessMessage" -Level Verbose
                $Flag = $false
                return $Result
            }
            catch {
                if ($Attempt -gt $RetryCount) {
                    Write-SttiLog "$FailureMessage! Total retry attempts: $RetryCount" -Level Error
                    $Flag = $false
                    throw                    
                }
                else {
                    Write-SttiLog "[$Attempt/$RetryCount] $FailureMessage. Retrying in $TimeoutInSecs seconds..." -Level Warn
                    Start-Sleep -Seconds $TimeoutInSecs
                    $Attempt = $Attempt + 1
                }
            }
        }
        While ($Flag)        
    }
}

#endregion Helper

#region Deployment

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

    Test-UserHasAdminRole -ErrorAction Stop

    # Create instance stub
    $instance = Get-SttiInstanceConfig -Name $InstanceName -ErrorAction SilentlyContinue
    if ($null -eq $instance) {
        $instance = New-SttiInstanceConfig -Name $InstanceName -SqlServer $SqlServer -Database $Database -Roles $Roles -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 $Roles -StoreDbInInstanceDataPath:$StoreDbInInstanceDataPath -WorkerSslCertificateSubject $WorkerSslCertificateSubject -WorkerPort $WorkerPort -WebPort $WebPort -WebSslCertificateSubject $WebSslCertificateSubject -WorkerHostname $WorkerHostname -EncryptionCertificateThumbprint $EncryptionCertificateThumbprint
    }

    $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 -Message "Please enter the credentials for the service user")

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

            Update-SttiInstance -Instance $instance -Package $package -ServiceCredential $svcCred -Force:$Force -CertificateThumbprint $AdminCertificateThumbprint
        }
        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 role" -Level Verbose
                $binaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe")
                New-NetFirewallRule â€“Name "Stti-Worker-$($Instance.Name)" -DisplayName "Stti Worker Role($($Instance.Name))" -Program $binaryPath -Direction Inbound -Action Allow -Protocol TCP -Profile Domain -Group "Stti" -ErrorAction Stop > $null
            }
            else {
                Write-SttiLog "Firewall rule for worker role already exists" -Level Verbose
            }
        }
        if ($Instance.Roles.Contains([SttiInstanceRoles]::Web)){
            if ($null -eq (Get-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction SilentlyContinue)){
                Write-SttiLog "Add firewall rule for web role" -Level Verbose
                $binaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Web\Halvotec.Stti.Web.exe")
                New-NetFirewallRule â€“Name "Stti-Web-$($Instance.Name)" -DisplayName "Stti Web Role($($Instance.Name))" -Program $binaryPath -Direction Inbound -Action Allow -Protocol TCP -Profile Domain -Group "Stti" -ErrorAction Stop > $null
            }
            else {
                Write-SttiLog "Firewall rule for web role 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{
        if ($null -ne (Get-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction SilentlyContinue)){
            Write-SttiLog "Remove firewall rule for web role" -Level Verbose
            Remove-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction Stop
        }
        else {
            Write-SttiLog "Firewall rule for web role was not existing" -Level Verbose
        }
        if ($null -ne (Get-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction SilentlyContinue)){
            Write-SttiLog "Remove firewall rule for worker role" -Level Verbose
            Remove-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction Stop
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

#endregion Firewall rules

#region Config

function Get-SttiApiUrl{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance
    )

    return "https://$($Instance.WorkerHostname):$($Instance.WorkerPort)/api"
}

function Get-SttiSystemSettings{
    [OutputType([SttiSystemSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    try{
        try
        {
            $config = Invoke-RestMethod "$apiUrl/config/system" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop    
            return [SttiSystemSettings]::new($config.data)
        }
        catch [Microsoft.PowerShell.Commands.HttpResponseException]{
            if ($_.Exception.Response.StatusCode -ne 'BadRequest'){ 
                throw
            }
            # Use old API, if the new API is not available
            $config = Invoke-RestMethod "$apiUrl/config" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiSystemSettings]::new($config.data.system)
        }
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Set-SttiSystemSettings{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName="SettingUpdates")]
        [Parameter(Mandatory, ParameterSetName="AsParameters")]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ParameterSetName="SettingUpdates")]
        $SettingUpdates,

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

        [Parameter(ParameterSetName="AsParameters")]
        [string] $DataDirectory,

        [Parameter(ParameterSetName="AsParameters")]
        [bool] $UseProxy,

        [Parameter(ParameterSetName="AsParameters")]
        [string] $ProxyAddress,

        [Parameter(Mandatory, ParameterSetName="SettingUpdates")]
        [Parameter(Mandatory, ParameterSetName="AsParameters")]
        [string] $CertificateThumbprint
    )
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
    Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    if (!$PSBoundParameters.ContainsKey("SettingUpdates")){
        $SettingUpdates = @{}

        if ($PSBoundParameters.ContainsKey("InstanceName")){
            $SettingUpdates.instance = $InstanceName
        }
        if ($PSBoundParameters.ContainsKey("DataDirectory")){
            $SettingUpdates.dataDirectory = $DataDirectory
        }
        if ($PSBoundParameters.ContainsKey("UseProxy")){
            $SettingUpdates.useProxy = $UseProxy
        }
        if ($PSBoundParameters.ContainsKey("ProxyAddress")){
            $SettingUpdates.proxyAddress = $ProxyAddress
        }
    }

    try{
        $result = Invoke-RestMethod "$apiUrl/config/system" -Method Put -Body (ConvertTo-Json $SettingUpdates) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        return [SttiSystemSettings]::new($result.data.system)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Get-SttiModule{
    [OutputType([SttiModule])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [string] $ModuleId,

        [string] $Name,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    try{
        $result = Invoke-RestMethod "$apiUrl/config/modules" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        $modules = $result.data
        $filteredModules = $modules | Where-Object { (!$PSBoundParameters.ContainsKey("ModuleId") -or $_.id -like $ModuleId) -and (!$PSBoundParameters.ContainsKey("Name") -or $_.name -like $Name) }        

        return [SttiModule]::FromArray($filteredModules)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Set-SttiModuleEnvironment{
    [OutputType([SttiModuleSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

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

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

        [string] $ClientId
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId/env/$Environment" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSettings]::new($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiModuleSettings{
    [OutputType([SttiModuleSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

        [string] $Environment,

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

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSettings]::new($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Set-SttiModuleSettings{
    [OutputType([SttiModuleSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

        [Parameter(Mandatory)]
        [PSCustomObject] $SettingUpdates,

        [string] $Environment,

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

        [string] $ClientId
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/settings" -Method Put -Body (ConvertTo-Json $SettingUpdates) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSettings]::new($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiModuleSecrets{
    [OutputType([SttiModuleSecrets])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

        [string] $Environment,

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

        [string] $ClientId
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSecrets]::new($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function New-SttiModuleTokenSecret{
    [OutputType([SttiModuleSecrets])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

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

        [datetime] $ValidFrom,

        [datetime] $ValidThru,

        [string] $Environment,

        [ValidateSet('AccessToken', 'RefreshToken')]
        [string] $Usage = 'AccessToken',

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

        [string] $ClientId
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $Secret = @{
            type = "TokenSecretInfo"
            token = $Token
            usage = $Usage
        }
        if ($PSBoundParameters.ContainsKey("ValidFrom")){
            $Secret.validFrom = $ValidFrom
        }
        if ($PSBoundParameters.ContainsKey("ValidThru")){
            $Secret.validThrough = $ValidThru
        }
        $Data = @{
            moduleId = ""
            secret = $Secret     
        }

        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/secrets" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSecrets]::new($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Remove-SttiModuleSecret{
    [CmdletBinding()]
    [OutputType([SttiModuleSecrets])]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ModuleId,

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

        [string] $Environment,

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

        [string] $ClientId
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/secrets/$SecretId" -Method Delete -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiModuleSecrets]::FromArray($result.data, $Environment)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

#endregion Config

#region Clients

function Get-SttiClient{
    [OutputType([SttiClient[]])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [string] $ClientId,

        [string] $DisplayName,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    try{
        $response = Invoke-RestMethod "$apiUrl/security/clients" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        $clients = $response.data

        $filteredClients = $clients | Where-Object { (!$PSBoundParameters.ContainsKey("ClientId") -or $_.id -like $ClientId) -and (!$PSBoundParameters.ContainsKey("DisplayName") -or $_.displayName -like $DisplayName) }        
        return [SttiClient]::FromArray($filteredClients)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Get-SttiClientSettings{
    [OutputType([SttiClientSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiClientSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }   
}

function New-SttiClient{
    [OutputType([SttiClient])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

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

        [string] $ClientCertificateThumbprint,

        [SttiClientCommunicationType] $Communication = [SttiClientCommunicationType]::Api,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    $Data = @{
        displayName = $DisplayName
        encryptionKey = @{
            algorithm = "Aes256"
        }
    }
    if ($PSBoundParameters.ContainsKey("ClientCertificateThumbprint")){
        $Data.certificateCredential = @{
            thumbprint = $ClientCertificateThumbprint
            description = "Set on $(Get-Date -Format "s")"
        }
    }

    $Data.communication = "$($Communication)"

    try{
        $response = Invoke-RestMethod "$apiUrl/security/clients" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        return [SttiClient]::new($response.data)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Set-SttiClientSettings{
    [OutputType([SttiClientSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [string] $DisplayName,

        [SttiClientCommunicationType] $Communication,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $Data = @{
        }
        $hasModifications = $false

        if ($PSBoundParameters.ContainsKey("DisplayName")){
            $Data.displayName = $DisplayName
            $hasModifications = $true
        }
        if ($PSBoundParameters.ContainsKey("Communication")){
            $Data.communication = "$($Communication)"
            $hasModifications = $true
        }

        if (!$hasModifications){
            throw "Parameters do not result in any modification"
        }

        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -Method Put -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiClientSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiClientCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::FromArray($response.data.credentials)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }   
}

function Add-SttiClientCertificateCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $Data = @{
        }

        $Data.certificateCredential = @{
            thumbprint = $ClientCertificateThumbprint
            description = "Set on $(Get-Date -Format "s")"
        }

        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/credentials" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::new(($response.data.credentials | Select-Object -Last 1))
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Remove-SttiClientCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/credentials/$CredentialId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::FromArray($response.data.credentials)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Enable-SttiClient{
    [OutputType([SttiClientSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiClientSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Disable-SttiClient{
    [OutputType([SttiClientSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiClientSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiClientEncryptionKey{
    [OutputType([SttiEncryptionKey])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiEncryptionKey]::FromArray($response.data.encryption.keys)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function New-SttiClientEncryptionKey{
    [OutputType([SttiEncryptionKey])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $data = @{
            key = @{
                algorithm = "Aes256"
            }
        }

        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys" -Method Post -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiEncryptionKey]::new(($response.data.encryption.keys | Select-Object -Last 1))
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Enable-SttiClientEncryptionKey{
    [OutputType([SttiEncryptionKey])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiEncryptionKey]::new(($response.data.encryption.keys | Where-Object { $_.keyId -like $KeyId }))
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Disable-SttiClientEncryptionKey{
    [OutputType([SttiEncryptionKey])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiEncryptionKey]::new(($response.data.encryption.keys | Where-Object { $_.keyId -like $KeyId }))
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Remove-SttiClientEncryptionKey{
    [OutputType([SttiEncryptionKey])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $ClientId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiEncryptionKey]::FromArray($response.data.encryption.keys)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

#endregion Clients

#region Users

function Get-SttiUser{
    [OutputType([SttiUser])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [string] $UserId,

        [string] $DisplayName,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    try{
        $response = Invoke-RestMethod "$apiUrl/security/users" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        $users = $response.data

        $filteredUsers = $users | Where-Object { (!$PSBoundParameters.ContainsKey("UserId") -or $_.id -like $UserId) -and (!$PSBoundParameters.ContainsKey("DisplayName") -or $_.displayName -like $DisplayName) }
        return [SttiUser]::FromArray($filteredUsers)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Convert-ObjectToRoles{
    [OutputType([SttiUserRoles[]])]
    Param(
        [PSCustomObject] $rolesObj
    )

    $roles = $rolesObj | ConvertTo-Json -Depth 10 | ConvertFrom-Json -Depth 10 -AsHashtable
    $result = [System.Collections.ArrayList]::new()
    if ($roles.operationsAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::OperationsAdmin) }
    if ($roles.dataAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::DataAdmin) }
    if ($roles.securityAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::SecurityAdmin) }
    if ($roles.healthWatcherRole -eq $true) { [void]$result.Add([SttiUserRoles]::HealthWatcher) }

    return $result
}

function Convert-RolesToObject{
    [OutputType([pscustomobject])]
    Param(
        [SttiUserRoles[]] $roles,
        $objectToUse
    )

    if ($objectToUse){
        $result = $objectToUse
    }
    else{
        $result = @{}
    }
    $result.operationsAdminRole = $Roles.Contains([SttiUserRoles]::OperationsAdmin)
    $result.securityAdminRole = $Roles.Contains([SttiUserRoles]::SecurityAdmin)
    $result.dataAdminRole = $Roles.Contains([SttiUserRoles]::DataAdmin)
    $result.healthWatcherRole = $Roles.Contains([SttiUserRoles]::HealthWatcher)

    if (!$objectToUse){
        return $result
    }
}

function New-SttiUser{
    [OutputType([SttiUser])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

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

        [string] $UserCertificateThumbprint,

        [Parameter(Mandatory)]
        [SttiUserRoles[]] $Roles,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    $data = @{
        displayName = $DisplayName
    }
    if ($PSBoundParameters.ContainsKey("UserCertificateThumbprint")){
        $Data.certificateCredential = @{
            thumbprint = $UserCertificateThumbprint
            description = "Set on $(Get-Date -Format "s")"
        }
    }

    if ($PSBoundParameters.ContainsKey("Roles")){
        Convert-RolesToObject -Roles $Roles -ObjectToUse $data
    }

    try{
        $response = Invoke-RestMethod "$apiUrl/security/users" -Method Post -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        return [SttiUser]::new($response.data)
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Get-SttiUserSettings{
    [OutputType([SttiUserSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiUserSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Set-SttiUserSettings{
    [OutputType([SttiUserSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

        [string] $DisplayName,

        [SttiUserRoles[]] $Roles,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $Data = @{
        }
        $hasModifications = $false

        if ($PSBoundParameters.ContainsKey("DisplayName")){
            $Data.displayName = $DisplayName
            $hasModifications = $true
        }
        if ($PSBoundParameters.ContainsKey("Roles")){
            Convert-RolesToObject -Roles $Roles -ObjectToUse $Data
            $hasModifications = $true
        }

        if (!$hasModifications){
            throw "Parameters do not result in any modification"
        }

        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -Method Put -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiUserSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiUserCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::FromArray($response.data.credentials)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }   
}

function Add-SttiUserCertificateCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        $Data = @{
        }

        $Data.certificateCredential = @{
            thumbprint = $UserCertificateThumbprint
            description = "Set on $(Get-Date -Format "s")"
        }

        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/credentials" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::new(($response.data.credentials | Select-Object -Last 1))
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Remove-SttiUserCredential{
    [OutputType([SttiCredential])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

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

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/credentials/$CredentialId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiCredential]::FromArray($response.data.credentials)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Enable-SttiUser{
    [OutputType([SttiUserSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiUserSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Disable-SttiUser{
    [OutputType([SttiUserSettings])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string] $UserId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return [SttiUserSettings]::new($response.data)
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

#endregion Users

#region Diagnostics

function Get-SttiLogEntry{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [datetime] $From,
        [datetime] $To,
        [SttiAppLogLevel] $MinimumLevel,
        [string] $ClientId,
        [string] $FlowId,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    $filter = [System.Collections.Generic.List[string]]::new()
    if ($PSBoundParameters.ContainsKey("MinimumLevel")){
        $filter.Add("MinimumLogLevel=$MinimumLevel")
    }
    if ($PSBoundParameters.ContainsKey("ClientId")){
        $filter.Add("ClientId=$ClientId")
    }
    if ($PSBoundParameters.ContainsKey("FlowId")){
        $filter.Add("FlowId=$FlowId")
    }
    if ($PSBoundParameters.ContainsKey("From")){
        $filter.Add("From=" + [System.Uri]::EscapeDataString("{0:o}" -f $From))
    }
    if ($PSBoundParameters.ContainsKey("To")){
        $filter.Add("To=" + [System.Uri]::EscapeDataString("{0:o}" -f $To))
    }
    if ($filter.Count -gt 0){
        $params = [string]::Join('&', $filter)
    }

    try{
        $response = Invoke-RestMethod "$apiUrl/diag/logs$($params ? '?' + $params : '')" -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        return $response.entries
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

#endregion Diagnostics

#region FlowData

function Get-SttiFlow{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

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

    $apiUrl = Get-SttiApiUrl -Instance $Instance

    try{
        $response = Invoke-RestMethod "$apiUrl/stti/flows" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
        return $response.Data.items
    }
    catch{
        Write-SttiLog $_ -Level Error
    }
}

function Get-SttiFlowRequestMessage{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $FlowId,

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/request/message" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return $response.SttiApiResponseOfFlowRequestMessageInfo.Data.Message.OuterXml
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

function Get-SttiFlowTrace{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $FlowId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )
    Begin{
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose
        Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug
    
        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }
    
    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/trace" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return $response.data.entries
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }    
}

function Get-SttiFlowResponses{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $FlowId,

        [switch] $IncludeContent,

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }
    
    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            $flowResponses = $response.data
    
            if ($IncludeContent){
                $flowResponses | ForEach-Object { 
                    $responseId = $_.id
                    $contentResponse = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses/$responseId" -Headers @{ "Accept"="application/json" } -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
                    return $contentResponse
                }
            }
            else{
                return $flowResponses
            }
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }   
}

function Get-SttiFlowCurrentResponse{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $FlowId,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }
    
    Process{
        try{
            $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses/current/message" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return $response.output.InnerXml
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }    
}

#endregion FlowData

#region Testing

function Invoke-SttiMockFlowStep{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SttiInstanceConfig] $Instance,

        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string] $FlowId,

        [Parameter(Mandatory)]
        [bool] $Success,

        [Parameter()]
        [string] $InfoMessage,

        [Parameter(Mandatory)]
        [string] $CertificateThumbprint
    )

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

        $apiUrl = Get-SttiApiUrl -Instance $Instance
    }

    Process{

        $paramValues = [System.Collections.Generic.List[string]]::new()
        if ($PSBoundParameters.ContainsKey("Success")){
            $paramValues.Add("success=$Success")
        }
        if ($PSBoundParameters.ContainsKey("InfoMessage")){
            $paramValues.Add("infoMessage=$InfoMessage")
        }
        if ($paramValues.Count -gt 0){
            $params = [string]::Join('&', $paramValues)
        }

        try{
            
            $result = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/mockStep$($params ? '?' + $params : '')" -Method Put  -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop
            return $result
        }
        catch{
            Write-SttiLog $_ -Level Error
        }
    }
}

#endregion Testing

# Test consistency of powershell modules
Test-SttiModuleConsistency

# Ensure Stti BasePath
if (!$env:SttiBasePath){
    Set-SttiBasePath $defaultBasePath
}

# Ensure Shell Session
$shellSession = [SttiShellSession]::new()
$shellSession.ApiCallTimeoutSec = 60

# Restore default parameter Values from user defaults
Restore-SttiDefaultParameterValues