PSFrameworkDsc.psm1

$script:ModuleRoot = $PSScriptRoot

enum Scope
{
    SystemDefault
    SystemEnforced
}

enum ConfigType {
    NotSpecified
    Bool
    Int
    UInt
    Int16
    Int32
    Int64
    UInt16
    UInt32
    UInt64
    Double
    String
    TimeSpan
    DateTime
    ConsoleColor
    BoolArray
    IntArray
    UIntArray
    Int16Array
    Int32Array
    Int64Array
    UInt16Array
    UInt32Array
    UInt64Array
    DoubleArray
    StringArray
    TimeSpanArray
    DateTimeArray
    ConsoleColorArray
    PsfConfig
}

class ConfigHelper {
    [Scope] $Scope

    [hashtable]$Config = @{ }

    ConfigHelper([Scope]$Scope) {
        $this.Scope = $Scope
    }

    [void]Load() {
        $providerProps = 'PSPath','PSParentPath','PSChildName','PSDrive','PSProvider'

        switch ($this.Scope) {
            SystemDefault {
                if (-not (Test-Path -Path $script:config.PathDefault)) {
                    $this.Config = @{}
                    return
                }

                $newConfig = @{}
                $regItem = Get-ItemProperty -Path $script:config.PathDefault
                foreach ($property in $regItem.PSObject.Properties) {
                    if ($property.Name -in $providerProps) { continue }
                    $newConfig[$property.Name] = $property.Value
                }
                $this.Config = $newConfig
            }
            SystemEnforced {
                if (-not (Test-Path -Path $script:config.PathEnforced)) {
                    $this.Config = @{}
                    return
                }

                $newConfig = @{}
                $regItem = Get-ItemProperty -Path $script:config.PathEnforced
                foreach ($property in $regItem.PSObject.Properties) {
                    if ($property.Name -in $providerProps) { continue }
                    $newConfig[$property.Name] = $property.Value
                }
                $this.Config = $newConfig
            }
            default { throw "Scope not implemented: $($this.Scope)" }
        }
    }
    [void]Write([string]$FullName, [object]$Value, [bool]$IsConfigString = $false) {
        $valueToWrite = $Value
        if (-not $IsConfigString) { $valueToWrite = [ConfigHelper]::Serialize($Value) }
        if ($valueToWrite -eq $this.Config[$FullName]) { return }

        switch ($this.Scope) {
            SystemDefault {
                if (-not (Test-Path -Path $script:config.PathDefault)) {
                    $null = New-Item -Path $script:config.PathDefault -Force
                }

                Set-ItemProperty -Path $script:config.PathDefault -Value $valueToWrite -Name $FullName -Force
            }
            SystemEnforced {
                if (-not (Test-Path -Path $script:config.PathEnforced)) {
                    $null = New-Item -Path $script:config.PathEnforced -Force
                }

                Set-ItemProperty -Path $script:config.PathEnforced -Value $valueToWrite -Name $FullName -Force
            }
            default { throw "Scope not implemented: $($this.Scope)" }
        }

        $this.Load()
    }
    [void]Remove([string]$FullName) {
        if ($this.Config.Keys -notcontains $FullName) { return }

        switch ($this.Scope) {
            SystemDefault {
                $fail = $null
                Remove-ItemProperty -Path $script:config.PathDefault -Name $FullName -Force -ErrorAction SilentlyContinue -ErrorVariable fail
                if ($fail -and $fail.CategoryInfo.Category -ne 'InvalidArgument') { throw $fail }
            }
            SystemEnforced {
                $fail = $null
                Remove-ItemProperty -Path $script:config.PathEnforced -Name $FullName -Force -ErrorAction SilentlyContinue -ErrorVariable fail
                if ($fail -and $fail.CategoryInfo.Category -ne 'InvalidArgument') { throw $fail }
            }
            default { throw "Scope not implemented: $($this.Scope)" }
        }
    }
    [object]GetConverted([string]$FullName) {
        if ($this.Config.Keys -notcontains $FullName) { return $null }

        return [ConfigHelper]::Deserialize($this.Config[$FullName])
    }

    [bool]Compare([object]$Value, [string]$FullName) {
        return $this.Compare($Value, $FullName, { $null -eq $args[0] })
    }
    [bool]Compare([object]$Value, [string]$FullName, [scriptblock]$NullCondition = { $null -eq $args[0] }) {
        $isNull = & $NullCondition $Value
        if ($isNull) {
            return $this.Config.Keys -notcontains $FullName
        }

        $resValue = [ConfigHelper]::Serialize($Value)

        return $resValue -eq $this.Config[$FullName]
    }

    [void]Apply([string]$FullName, [object]$Value) {
        $this.Apply($FullName, $Value, { $null -eq $args[0] })
    }
    [void]Apply([string]$FullName, [object]$Value, [scriptblock]$NullCondition) {
        $isNull = & $NullCondition $Value
        
        if ($isNull) {
            if ($this.Config.Keys -contains $FullName) {
                $this.Remove($FullName)
            }
            
            return
        }

        $this.Write($FullName, $Value, $false)
    }

    #region Statics
    static [object] Convert([string]$Value, [ConfigType]$ValueType) {
        switch ($ValueType) {
            NotSpecified { return $Value }
            Bool { return $Value -eq 'true' -or $Value -eq '1' }
            Int { return [int]$Value }
            UInt { return [uint32]$Value }
            Int16 { return [Int16]$Value }
            Int32 { return [Int32]$Value }
            Int64 { return [Int64]$Value }
            UInt16 { return [UInt16]$Value }
            UInt32 { return [UInt32]$Value }
            UInt64 { return [Uint64]$Value }
            Double { return [double]$Value }
            String { return $Value }
            TimeSpan { return [Timespan]$Value }
            DateTime { return [DateTime]$Value }
            ConsoleColor { return [ConsoleColor]$Value }
            BoolArray { return [ConfigHelper]::ConvertArray($Value, 'Bool') }
            IntArray { return [ConfigHelper]::ConvertArray($Value, 'Int') }
            UIntArray { return [ConfigHelper]::ConvertArray($Value, 'UInt') }
            Int16Array { return [ConfigHelper]::ConvertArray($Value, 'Int16') }
            Int32Array { return [ConfigHelper]::ConvertArray($Value, 'Int32') }
            Int64Array { return [ConfigHelper]::ConvertArray($Value, 'Int64') }
            UInt16Array { return [ConfigHelper]::ConvertArray($Value, 'UInt16') }
            UInt32Array { return [ConfigHelper]::ConvertArray($Value, 'UInt32') }
            UInt64Array { return [ConfigHelper]::ConvertArray($Value, 'UInt64') }
            DoubleArray { return [ConfigHelper]::ConvertArray($Value, 'Double') }
            StringArray { return [ConfigHelper]::ConvertArray($Value, 'String') }
            TimeSpanArray { return [ConfigHelper]::ConvertArray($Value, 'TimeSpan') }
            DateTimeArray { return [ConfigHelper]::ConvertArray($Value, 'DateTime') }
            ConsoleColorArray { return [ConfigHelper]::ConvertArray($Value, 'ConsoleColor') }
            PsfConfig { return $Value }
            default { return $Value }
        }
        # PowerShell Parser does not detect when there is no path in a switch statement that ends with a return.
        throw "Unhandled conversion error: Developer failed to do it right."
    }

    static hidden [object] ConvertArray([string]$Value, [ConfigType]$ValueType) {
        if ($Value -match 'þ') { $values = $Value -split 'þ' }
        else { $values = $Value -split '\|' }

        $results = foreach ($item in $Values) {
            [ConfigHelper]::Convert($item, $ValueType)
        }
        return @($results)
    }

    static [bool] IsLegalType($Object) {
        $typeName = $Object.GetType().FullName
        
        if ($typeName -eq "System.Object[]") {
            foreach ($item in $Object) {
                if (-not ([ConfigHelper]::IsLegalType($item))) { return $false }
            }
            return $true
        }
        
        $legalTypes = @(
            'System.Boolean',
            'System.Int16',
            'System.Int32',
            'System.Int64',
            'System.UInt16',
            'System.UInt32',
            'System.UInt64',
            'System.Double',
            'System.String',
            'System.TimeSpan',
            'System.DateTime',
            'System.ConsoleColor'
        )
        
        if ($legalTypes -contains $typeName) { return $true }
        return $false
    }
    
    static [string] Serialize($Object) {
        switch ($Object.GetType().FullName) {
            "System.Object[]" {
                $list = @()
                foreach ($item in $Object) {
                    $list += [ConfigHelper]::Serialize($item)
                }
                return "array:$($list -join "þþþ")"
            }
            "System.Boolean" {
                if ($Object) { return "bool:true" }
                else { return "bool:false" }
            }
            "System.Int16" { return "int:$Object" }
            "System.Int32" { return "int:$Object" }
            "System.Int64" { return "long:$Object" }
            "System.UInt16" { return "int:$Object" }
            "System.UInt32" { return "int:$Object" }
            "System.UInt64" { return "long:$Object" }
            "System.Double" { return "double:$Object" }
            "System.String" { return "string:$Object" }
            "System.TimeSpan" { return "timespan:$($Object.Ticks)" }
            "System.DateTime" { return "datetime:$($Object.Ticks)" }
            "System.ConsoleColor" { return "consolecolor:$Object" }
            default {
                if ($_ -notmatch '\[\]$') {
                    throw "$_ was not recognized as a legal type!"
                }

                $list = @()
                foreach ($item in $Object) {
                    $list += [ConfigHelper]::Serialize($item)
                }
                return "array:$($list -join "þþþ")"
            }
        }
        
        return "<illegal data>"
    }
    
    static [object] DeSerialize([string]$Item) {
        $index = $Item.IndexOf(":")
        if ($index -lt 1) { throw "No type identifier found!" }
        $type = $Item.Substring(0, $index).ToLower()
        $content = $Item.Substring($index + 1)
        
        switch ($type) {
            "bool" {
                if ($content -eq "true") { return $true }
                if ($content -eq "1") { return $true }
                if ($content -eq "false") { return $false }
                if ($content -eq "0") { return $false }
                throw "Failed to interpret as bool: $content"
            }
            "int" { return ([int]$content) }
            "double" { return [double]$content }
            "long" { return [long]$content }
            "string" { return $content }
            "timespan" { return (New-Object System.TimeSpan($content)) }
            "datetime" { return (New-Object System.DateTime($content)) }
            "consolecolor" { return ([System.ConsoleColor]$content) }
            "array" {
                $list = @()
                foreach ($item in ($content -split "þþþ")) {
                    $list += [ConfigHelper]::Deserialize($item)
                }
                return $list
            }
            
            default { throw "Unknown type identifier" }
        }
        
        return $null
    }
    #endregion Statics
}

class ExtendedConfigHelper {
    # Matching Property-Names to conditional logic to remove from config
    [hashtable]$Properties
    [string]$BasePath
    [object]$Item
    [Scope]$Scope

    [ConfigHelper]$Helper

    ExtendedConfigHelper([object]$Item, [Scope]$Scope, [string]$BasePath, [hashtable]$Properties) {
        $this.Item = $item
        $this.Scope = $Scope
        $this.BasePath = $BasePath
        $this.Properties = $Properties

        $this.Helper = [ConfigHelper]::new($Scope)
    }

    [bool]Test() {
        $this.Helper.Load()
        foreach ($property in $this.Properties.Keys) {
            if (-not $this.Helper.Compare($this.Item.$property, "$($this.BasePath).$property", $this.Properties.$property)) { return $false }
        }
        
        return $true
    }

    [hashtable]Get() {
        $this.Helper.Load()
        $result = @{}

        foreach ($property in $this.Properties.Keys) {
            if ($this.Helper.Config.Keys -contains "$($this.BasePath).$property") {
                $result[$property] = $this.Helper.Config."$($this.BasePath).$property"
            }
        }

        return $result
    }

    [void]Set() {
        $this.Helper.Load()

        foreach ($property in $this.Properties.Keys) {
            $this.Helper.Apply("$($this.BasePath).$property", $this.Item.$property, $this.Properties.$property)
        }
    }

    [void]Clear() {
        $this.Helper.Load()

        foreach ($property in $this.Properties.Keys) {
            $this.Helper.Remove("$($this.BasePath).$property")
        }
    }
}

enum Ensure {
    Absent
    Present
}

class Reason
{
    [DscProperty()]
    [string] $Code
        
    [DscProperty()]
    [string] $Phrase
}

class PsfLoggingHelper {
    [string] $ProviderName
    [string] $InstanceName

    [object] $LogConfig

    PsfLoggingHelper([object]$LogObject, [string]$ProviderName) {
        $this.LogConfig = $LogObject
        $this.ProviderName = $ProviderName
        $this.InstanceName = $LogObject.InstanceName
    }

    [hashtable]Get() {
        if (-not $this.InstanceName) { throw "Cannot read Instance configuration - no instance defined yet!" }

        $helper = [ConfigHelper]::new('SystemDefault')
        $helper.Load()

        $data = @{ }

        $baseName = "LoggingProvider.$($this.ProviderName).$($this.InstanceName)"
        $names = 'Enabled', 'ExcludeFunctions', 'ExcludeModules', 'ExcludeTags', 'IncludeFunctions', 'IncludeModules', 'IncludeTags', 'MaxLevel', 'MinLevel'
        foreach ($name in $names) {
            if ($helper.Config.Keys -notcontains "$baseName.$name") { continue }
            $data[$name] = $helper.Config["$baseName.$name"]
        }

        return $data
    }

    [bool]Test() {
        $helper = [ConfigHelper]::new('SystemDefault')
        $helper.Load()

        $baseName = "LoggingProvider.$($this.ProviderName).$($this.InstanceName)"

        if ($this.LogConfig.Enabled -ne $helper.GetConverted("$baseName.Enabled")) { return $false }
        if (-not $helper.Compare($this.LogConfig.MinLevel, "$baseName.MinLevel", { $args[0] -lt 1 })) { return $false }
        if (-not $helper.Compare($this.LogConfig.MaxLevel, "$baseName.MaxLevel", { $args[0] -lt 1 })) { return $false }

        $names = 'ExcludeFunctions', 'ExcludeModules', 'ExcludeTags', 'IncludeFunctions', 'IncludeModules', 'IncludeTags'
        foreach ($name in $names) {
            if (-not $helper.Compare($this.LogConfig.$name, "$baseName.$name")) { return $false }
        }

        return $true
    }

    [void]Set() {
        $helper = [ConfigHelper]::new('SystemDefault')
        $helper.Load()

        $baseName = "LoggingProvider.$($this.ProviderName).$($this.InstanceName)"

        if ($this.LogConfig.Ensure -eq 'Absent') {
            $names = 'Enabled', 'ExcludeFunctions', 'ExcludeModules', 'ExcludeTags', 'IncludeFunctions', 'IncludeModules', 'IncludeTags', 'MaxLevel', 'MinLevel'
            foreach ($name in $names) {
                $helper.Remove("$baseName.$name")
            }
            return
        }

        $helper.Write("$baseName.Enabled", $this.LogConfig.Enabled, $false)
        $helper.Apply("$baseName.MinLevel", $this.LogConfig.MinLevel, { $args[0] -lt 1 })
        $helper.Apply("$baseName.MaxLevel", $this.LogConfig.MaxLevel, { $args[0] -lt 1 })

        $names = 'ExcludeFunctions', 'ExcludeModules', 'ExcludeTags', 'IncludeFunctions', 'IncludeModules', 'IncludeTags'
        foreach ($name in $names) {
            $helper.Apply("$baseName.$name", $this.LogConfig.$name)
        }
    }

    [void]Clear() {
        $helper = [ConfigHelper]::new('SystemDefault')
        $helper.Load()

        $baseName = "LoggingProvider.$($this.ProviderName).$($this.InstanceName)"
        $names = 'Enabled', 'ExcludeFunctions', 'ExcludeModules', 'ExcludeTags', 'IncludeFunctions', 'IncludeModules', 'IncludeTags', 'MaxLevel', 'MinLevel'
        foreach ($name in $names) {
            $helper.Remove("$baseName.$name")
        }
    }
}

[DscResource()]
class PSFrameworkConfig {
    #region DSC Properties
    [DscProperty(Key)]
    [string]$FullName

    [DscProperty(Mandatory)]
    [Ensure]$Ensure

    <#
        The scope it is set to.
        Defaults to "SystemDefault"
        DSC can only access the System wide settings.
    #>

    [DscProperty()]
    [Scope]$ConfigScope = 'SystemDefault'
    
    <#
        The value to apply
        Is only mandatory when $Ensure is set to "Present"
    #>

    [DscProperty()]
    [string]$Value

    <#
        The type of the value to apply.
        Tries to parse out the from the string value provided.
        Use PsfConfig to provide the literal notation used by PSFramework for configuration.
        E.G. the result from this:
        [PSFramework.Configuration.ConfigurationHost]::ConvertToPersistedValue(42).TypeQualifiedPersistedValue
        Which would translate to:
        Int:42
    #>

    [DscProperty()]
    [ConfigType]$ValueType

    [DscProperty(NotConfigurable)]
    [Reason[]] $Reasons # Reserved for Azure Guest Configuration
    #endregion DSC Properties

    [void]Set() {
        $param = $this.GetConfigurableDscProperties()
        Set-PSFrameworkConfig @param
    }

    [PSFrameworkConfig]Get() {
        $param = $this.GetConfigurableDscProperties()
        return Get-PSFrameworkConfig @param
    }

    [bool]Test() {
        $param = $this.GetConfigurableDscProperties()
        $current = $this.Get()
        return Test-PSFrameworkConfig @param -Current $current
    }

    [Hashtable] GetConfigurableDscProperties() {
        # This method returns a hashtable of properties with two special workarounds
        # The hashtable will not include any properties marked as "NotConfigurable"
        # Any properties with a ValidateSet of "True","False" will beconverted to Boolean type
        # The intent is to simplify splatting to functions
        # Source: https://gist.github.com/mgreenegit/e3a9b4e136fc2d510cf87e20390daa44
        $dscProperties = @{}
        foreach ($property in [PSFrameworkConfig].GetProperties().Name) {
            # Checks if "NotConfigurable" attribute is set
            $notConfigurable = [PSFrameworkConfig].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.DscPropertyAttribute] }).NotConfigurable
            if (!$notConfigurable) {
                $paramValue = $this.$property
                # Gets the list of valid values from the ValidateSet attribute
                $validateSet = [PSFrameworkConfig].GetProperty($property).GetCustomAttributes($false).Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues
                if ($validateSet) {
                    # Workaround for boolean types
                    if ($null -eq (Compare-Object @('True', 'False') $validateSet)) {
                        $paramValue = [System.Convert]::ToBoolean($this.$property)
                    }
                }
                # Add property to new
                $dscProperties.add($property, $paramValue)
            }
        }
        return $dscProperties
    }

    static [object] Convert([string]$Value, [ConfigType]$ValueType) {
        switch ($ValueType) {
            NotSpecified { return $Value }
            Bool { return $Value -eq 'true' -or $Value -eq '1' }
            Int { return [int]$Value }
            UInt { return [uint32]$Value }
            Int16 { return [Int16]$Value }
            Int32 { return [Int32]$Value }
            Int64 { return [Int64]$Value }
            UInt16 { return [UInt16]$Value }
            UInt32 { return [UInt32]$Value }
            UInt64 { return [Uint64]$Value }
            Double { return [double]$Value }
            String { return $Value }
            TimeSpan { return [Timespan]$Value }
            DateTime { return [DateTime]$Value }
            ConsoleColor { return [ConsoleColor]$Value }
            BoolArray { return [PSFrameworkConfig]::ConvertArray($Value, 'Bool') }
            IntArray { return [PSFrameworkConfig]::ConvertArray($Value, 'Int') }
            UIntArray { return [PSFrameworkConfig]::ConvertArray($Value, 'UInt') }
            Int16Array { return [PSFrameworkConfig]::ConvertArray($Value, 'Int16') }
            Int32Array { return [PSFrameworkConfig]::ConvertArray($Value, 'Int32') }
            Int64Array { return [PSFrameworkConfig]::ConvertArray($Value, 'Int64') }
            UInt16Array { return [PSFrameworkConfig]::ConvertArray($Value, 'UInt16') }
            UInt32Array { return [PSFrameworkConfig]::ConvertArray($Value, 'UInt32') }
            UInt64Array { return [PSFrameworkConfig]::ConvertArray($Value, 'UInt64') }
            DoubleArray { return [PSFrameworkConfig]::ConvertArray($Value, 'Double') }
            StringArray { return [PSFrameworkConfig]::ConvertArray($Value, 'String') }
            TimeSpanArray { return [PSFrameworkConfig]::ConvertArray($Value, 'TimeSpan') }
            DateTimeArray { return [PSFrameworkConfig]::ConvertArray($Value, 'DateTime') }
            ConsoleColorArray { return [PSFrameworkConfig]::ConvertArray($Value, 'ConsoleColor') }
            PsfConfig { return $Value }
            default { return $Value }
        }
        # PowerShell Parser does not detect when there is no path in a switch statement that ends with a return.
        throw "Unhandled conversion error: Developer failed to do it right."
    }

    static hidden [object] ConvertArray([string]$Value, [ConfigType]$ValueType) {
        if ($Value -match 'þ') { $values = $Value -split 'þ' }
        else { $values = $Value -split '\|' }

        $results = foreach ($item in $Values) {
            [PSFrameworkConfig]::Convert($item, $ValueType)
        }
        return @($results)
    }

    static [bool] IsLegalType($Object) {
        $typeName = $Object.GetType().FullName
        
        if ($typeName -eq "System.Object[]") {
            foreach ($item in $Object) {
                if (-not ([PSFrameworkConfig]::IsLegalType($item))) { return $false }
            }
            return $true
        }
        
        $legalTypes = @(
            'System.Boolean',
            'System.Int16',
            'System.Int32',
            'System.Int64',
            'System.UInt16',
            'System.UInt32',
            'System.UInt64',
            'System.Double',
            'System.String',
            'System.TimeSpan',
            'System.DateTime',
            'System.ConsoleColor'
        )
        
        if ($legalTypes -contains $typeName) { return $true }
        return $false
    }
    
    static [string] Serialize($Object) {
        switch ($Object.GetType().FullName) {
            "System.Object[]" {
                $list = @()
                foreach ($item in $Object) {
                    $list += [PSFrameworkConfig]::Serialize($item)
                }
                return "array:$($list -join "þþþ")"
            }
            "System.Boolean" {
                if ($Object) { return "bool:true" }
                else { return "bool:false" }
            }
            "System.Int16" { return "int:$Object" }
            "System.Int32" { return "int:$Object" }
            "System.Int64" { return "long:$Object" }
            "System.UInt16" { return "int:$Object" }
            "System.UInt32" { return "int:$Object" }
            "System.UInt64" { return "long:$Object" }
            "System.Double" { return "double:$Object" }
            "System.String" { return "string:$Object" }
            "System.TimeSpan" { return "timespan:$($Object.Ticks)" }
            "System.DateTime" { return "datetime:$($Object.Ticks)" }
            "System.ConsoleColor" { return "consolecolor:$Object" }
            default { throw "$($Object.GetType().FullName) was not recognized as a legal type!" }
        }
        
        return "<illegal data>"
    }
    
    static [object] DeSerialize([string]$Item) {
        $index = $Item.IndexOf(":")
        if ($index -lt 1) { throw "No type identifier found!" }
        $type = $Item.Substring(0, $index).ToLower()
        $content = $Item.Substring($index + 1)
        
        switch ($type) {
            "bool" {
                if ($content -eq "true") { return $true }
                if ($content -eq "1") { return $true }
                if ($content -eq "false") { return $false }
                if ($content -eq "0") { return $false }
                throw "Failed to interpret as bool: $content"
            }
            "int" { return ([int]$content) }
            "double" { return [double]$content }
            "long" { return [long]$content }
            "string" { return $content }
            "timespan" { return (New-Object System.TimeSpan($content)) }
            "datetime" { return (New-Object System.DateTime($content)) }
            "consolecolor" { return ([System.ConsoleColor]$content) }
            "array" {
                $list = @()
                foreach ($item in ($content -split "þþþ")) {
                    $list += [PSFrameworkConfig]::Deserialize($item)
                }
                return $list
            }
            
            default { throw "Unknown type identifier" }
        }
        
        return $null
    }
}

[DscResource()]
class PSFrameworkLogEventLog {
    [DscProperty(Mandatory)]
    [Ensure]$Ensure

    #region Logging Provider Settings
    [DscProperty(Key)]
    [string]$InstanceName

    [DscProperty()]
    [string]$LogName

    [DscProperty()]
    [string]$Source

    [DscProperty()]
    [bool]$UseFallback = $true

    [DscProperty()]
    [int]$Category

    [DscProperty()]
    [int]$InfoID

    [DscProperty()]
    [int]$WarningID

    [DscProperty()]
    [int]$ErrorID

    [DscProperty()]
    [string]$ErrorTag

    [DscProperty()]
    [string]$TimeFormat

    [DscProperty()]
    [bool]$NumericTagAsID
    #endregion Logging Provider Settings

    #region Common Logging Settings
    [DscProperty()]
    [bool]$Enabled = $true

    [DscProperty()]
    [string[]]$IncludeModules
    
    [DscProperty()]
    [string[]]$ExcludeModules
    
    [DscProperty()]
    [string[]]$IncludeFunctions
    
    [DscProperty()]
    [string[]]$ExcludeFunctions
    
    [DscProperty()]
    [string[]]$IncludeTags
    
    [DscProperty()]
    [string[]]$ExcludeTags
    
    [DscProperty()]
    [int]$MinLevel
    
    [DscProperty()]
    [int]$MaxLevel
    #endregion Common Logging Settings

    #region DSC Properties
    [DscProperty(NotConfigurable)]
    [Reason[]] $Reasons # Reserved for Azure Guest Configuration
    #endregion DSC Properties

    hidden [hashtable] $PropertyMap = @{
        LogName        = { $false }
        Source         = { $false }
        UseFallback    = { $args[0] }
        Category       = { 1 -gt $args[0] }
        InfoID         = { 1 -gt $args[0] }
        WarningID      = { 1 -gt $args[0] }
        ErrorID        = { 1 -gt $args[0] }
        ErrorTag       = { -not $args[0] }
        TimeFormat     = { -not $args[0] }
        NumericTagAsID = { -not $args[0] }
    }

    [void]Set() {
        $this.AssertConfig()

        # Apply Desired State
        $logHelper = [PsfLoggingHelper]::new($this, 'Eventlog')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Eventlog.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            $extHelper.Clear()
            $logHelper.Clear()
            return
        }

        $extHelper.Set()
        $logHelper.Set()
    }

    [PSFrameworkLogEventLog]Get() {
        # Return current actual state

        $result = [PSFrameworkLogEventLog]::new()
        $result.InstanceName = $this.InstanceName
        $result.Ensure = 'Absent'

        $logHelper = [PsfLoggingHelper]::new($this, 'Eventlog')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Eventlog.$($this.InstanceName)", $this.PropertyMap)

        foreach ($pair in $logHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        foreach ($pair in $extHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        return $result
    }

    [bool]Test() {
        $this.AssertConfig()

        $logHelper = [PsfLoggingHelper]::new($this, 'Eventlog')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Eventlog.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            if ($logHelper.Get().Count -gt 0) { return $false }
            if ($extHelper.Get().Count -gt 0) { return $false }
            return $true
        }

        # Test Common Provider Settings
        
        if (-not $logHelper.Test()) { return $false }
        if (-not $extHelper.Test()) { return $false }
        return $true
    }

    [PsfLoggingHelper]GetLogHelper() {
        return [PsfLoggingHelper]::new($this, 'Eventlog')
    }
    [ExtendedConfigHelper]GetCfgHelper() {
        return [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Eventlog.$($this.InstanceName)", $this.PropertyMap)
    }

    [void]AssertConfig() {
        if ($this.Ensure -eq 'Absent') { return }

        if (-not $this.LogName) { throw "Missing Setting: LogName!" }
        if (-not $this.Source) { throw "Missing Setting: Source!" }
    }
}

[DscResource()]
class PSFrameworkLogFile {
    [DscProperty(Mandatory)]
    [Ensure]$Ensure

    #region Logging Provider Settings
    [DscProperty(Key)]
    [string]$InstanceName

    [DscProperty()]
    [string]$CsvDelimiter = ','

    [DscProperty()]
    [string]$FilePath

    [DscProperty()]
    [string]$FileType = 'csv'

    [DscProperty()]
    [string[]]$Headers = @('ComputerName', 'File', 'FunctionName', 'Level', 'Line', 'Message', 'ModuleName', 'Runspace', 'Tags', 'TargetObject', 'Timestamp', 'Type', 'Username')

    [DscProperty()]
    [bool]$IncludeHeader = $true

    [DscProperty()]
    [string]$Logname

    [DscProperty()]
    [string]$TimeFormat = "yyyy-MM-dd HH:mm:ss.fff"

    [DscProperty()]
    [string]$Encoding = 'UTF8'

    [DscProperty()]
    [bool]$UTC

    [DscProperty()]
    [string]$LogRotatePath

    [DscProperty()]
    [string]$LogRetentionTime

    [DscProperty()]
    [string]$LogRotateFilter

    [DscProperty()]
    [bool]$LogRotateRecurse

    [DscProperty()]
    [string]$MutexName

    [DscProperty()]
    [bool]$JsonCompress

    [DscProperty()]
    [bool]$JsonString

    [DscProperty()]
    [bool]$JsonNoComma

    [DscProperty()]
    [string]$MoveOnFinal

    [DscProperty()]
    [string]$CopyOnFinal
    #endregion Logging Provider Settings

    #region Common Logging Settings
    [DscProperty()]
    [bool]$Enabled = $true

    [DscProperty()]
    [string[]]$IncludeModules
    
    [DscProperty()]
    [string[]]$ExcludeModules
    
    [DscProperty()]
    [string[]]$IncludeFunctions
    
    [DscProperty()]
    [string[]]$ExcludeFunctions
    
    [DscProperty()]
    [string[]]$IncludeTags
    
    [DscProperty()]
    [string[]]$ExcludeTags
    
    [DscProperty()]
    [int]$MinLevel
    
    [DscProperty()]
    [int]$MaxLevel
    #endregion Common Logging Settings

    #region DSC Properties
    [DscProperty(NotConfigurable)]
    [Reason[]] $Reasons # Reserved for Azure Guest Configuration
    #endregion DSC Properties

    hidden [hashtable] $PropertyMap = @{
        CsvDelimiter     = { -not $args[0] }
        FilePath         = { $false }
        FileType         = { -not $args[0] }
        Headers          = { -not $args[0] }
        IncludeHeader    = { $false }
        Logname          = { -not $args[0] }
        TimeFormat       = { -not $args[0] }
        Encoding         = { -not $args[0] }
        UTC              = { -not $args[0] }
        LogRotatePath    = { -not $args[0] }
        LogRetentionTime = { -not $args[0] }
        LogRotateFilter  = { -not $args[0] }
        LogRotateRecurse = { -not $args[0] }
        MutexName        = { -not $args[0] }
        JsonCompress     = { -not $args[0] }
        JsonString       = { -not $args[0] }
        JsonNoComma      = { -not $args[0] }
        MoveOnFinal      = { -not $args[0] }
        CopyOnFinal      = { -not $args[0] }
    }

    [void]Set() {
        $this.AssertConfig()

        # Apply Desired State
        $logHelper = [PsfLoggingHelper]::new($this, 'Logfile')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Logfile.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            $extHelper.Clear()
            $logHelper.Clear()
            return
        }

        $extHelper.Set()
        $logHelper.Set()
    }

    [PSFrameworkLogfile]Get() {
        # Return current actual state

        $result = [PSFrameworkLogfile]::new()
        $result.InstanceName = $this.InstanceName
        $result.Ensure = 'Absent'

        $logHelper = [PsfLoggingHelper]::new($this, 'Logfile')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Logfile.$($this.InstanceName)", $this.PropertyMap)

        foreach ($pair in $logHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        foreach ($pair in $extHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        return $result
    }

    [bool]Test() {
        $this.AssertConfig()

        $logHelper = [PsfLoggingHelper]::new($this, 'Logfile')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Logfile.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            if ($logHelper.Get().Count -gt 0) { return $false }
            if ($extHelper.Get().Count -gt 0) { return $false }
            return $true
        }

        # Test Common Provider Settings
        
        if (-not $logHelper.Test()) { return $false }
        if (-not $extHelper.Test()) { return $false }
        return $true
    }

    [PsfLoggingHelper]GetLogHelper() {
        return [PsfLoggingHelper]::new($this, 'Logfile')
    }
    [ExtendedConfigHelper]GetCfgHelper() {
        return [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Logfile.$($this.InstanceName)", $this.PropertyMap)
    }

    [void]AssertConfig() {
        if ($this.Ensure -eq 'Absent') { return }

        if (-not $this.FilePath) { throw "Missing Setting: FilePath!" }
    }
}

[DscResource()]
class PSFrameworkLogSql {
    #region DSC Properties
    [DscProperty(Mandatory)]
    [Ensure]$Ensure

    [DscProperty(Key)]
    [string]$InstanceName

    [DscProperty()]
    [string]$SqlServer

    [DscProperty()]
    [string]$Database

    [DscProperty()]
    [string]$Schema

    [DscProperty()]
    [string]$Table

    [DscProperty()]
    [PSCredential]$Credential

    [DscProperty()]
    [string[]]$Headers

    #region Common Logging Settings
    [DscProperty()]
    [bool]$Enabled = $true

    [DscProperty()]
    [string[]]$IncludeModules
    
    [DscProperty()]
    [string[]]$ExcludeModules
    
    [DscProperty()]
    [string[]]$IncludeFunctions
    
    [DscProperty()]
    [string[]]$ExcludeFunctions
    
    [DscProperty()]
    [string[]]$IncludeTags
    
    [DscProperty()]
    [string[]]$ExcludeTags
    
    [DscProperty()]
    [int]$MinLevel
    
    [DscProperty()]
    [int]$MaxLevel
    #endregion Common Logging Settings

    [DscProperty(NotConfigurable)]
    [Reason[]] $Reasons # Reserved for Azure Guest Configuration
    #endregion DSC Properties

    hidden [hashtable] $PropertyMap = @{
        SqlServer = { $false }
        Database = { $false }
        Schema = { $false }
        Table = { $false }
        Credential = { $null -eq $args[0] }
        Headers = { $null -eq $args[0] }
    }

    [void]Set() {
        $this.AssertConfig()

        # Apply Desired State
        $logHelper = [PsfLoggingHelper]::new($this, 'Sql')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Sql.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            $extHelper.Clear()
            $logHelper.Clear()
            return
        }

        $extHelper.Set()
        $logHelper.Set()
    }

    [PSFrameworkLogSql]Get() {
        # Return current actual state

        $result = [PSFrameworkLogSql]::new()
        $result.InstanceName = $this.InstanceName
        $result.Ensure = 'Absent'

        $logHelper = [PsfLoggingHelper]::new($this, 'Sql')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Sql.$($this.InstanceName)", $this.PropertyMap)

        foreach ($pair in $logHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        foreach ($pair in $extHelper.Get().GetEnumerator()) {
            $result.Ensure = 'Present'
            $result.$($pair.Key) = $pair.Value
        }

        return $result
    }

    [bool]Test() {
        $this.AssertConfig()

        $logHelper = [PsfLoggingHelper]::new($this, 'Sql')
        $extHelper = [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Sql.$($this.InstanceName)", $this.PropertyMap)

        if ($this.Ensure -eq 'Absent') {
            if ($logHelper.Get().Count -gt 0) { return $false }
            if ($extHelper.Get().Count -gt 0) { return $false }
            return $true
        }

        # Test Common Provider Settings
        
        if (-not $logHelper.Test()) { return $false }
        if (-not $extHelper.Test()) { return $false }
        return $true
    }

    [PsfLoggingHelper]GetLogHelper() {
        return [PsfLoggingHelper]::new($this, 'Sql')
    }
    [ExtendedConfigHelper]GetCfgHelper() {
        return [ExtendedConfigHelper]::New($this, 'SystemDefault', "PSFramework.Logging.Sql.$($this.InstanceName)", $this.PropertyMap)
    }

    [void]AssertConfig() {
        if ($this.Ensure -eq 'Absent') { return }

        if (-not $this.SqlServer) { throw "Missing Setting: SqlServer!" }
        if (-not $this.Database) { throw "Missing Setting: Database!" }
        if (-not $this.Schema) { throw "Missing Setting: Schema!" }
        if (-not $this.Table) { throw "Missing Setting: Table!" }
    }
}

function Get-PSFrameworkConfig {
    <#
    .SYNOPSIS
        Returns the current state of the configuration setting.
     
    .DESCRIPTION
        Returns the current state of the configuration setting.
     
    .PARAMETER FullName
        The full name of the PSFramework configuration setting.
     
    .PARAMETER Ensure
        Whether it should be present or absent.
     
    .PARAMETER ConfigScope
        What scope it should be applied to.
     
    .PARAMETER Value
        The value it should have.
 
    .PARAMETER ValueType
        The type of the value that should be defined.
        Used because DSC does not allow Object types, thus requiring conversion afterwards.
     
    .EXAMPLE
        PS C:\> Get-PSFramework @param
 
        Returns the current state of the configuration setting.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $FullName,

        [Parameter(Mandatory = $true)]
        [Ensure]
        $Ensure,

        [Scope]
        $ConfigScope,

        [string]
        $Value,

        [ConfigType]
        $ValueType
    )

    process {
        $result = [PSFrameworkConfig]::new()
        $result.ConfigScope = $ConfigScope
        $result.FullName = $FullName

        if ($ConfigScope -eq 'SystemDefault') { $path = $script:config.PathDefault }
        else { $path = $script:config.PathEnforced }

        $result.Ensure = 'Absent'
        $item = Get-ItemProperty -Path $path -Name $FullName -ErrorAction Ignore
        
        if ($item -and $item.PSObject.Properties.Name -contains $FullName) {
            $result.Ensure = 'Present'
            $result.Value = $item.$FullName
        }

        $result
    }
}

function Set-PSFrameworkConfig {
    <#
    .SYNOPSIS
        Applies the desired value for the PSFramework configuration setting.
     
    .DESCRIPTION
        Applies the desired value for the PSFramework configuration setting.
     
    .PARAMETER FullName
        The full name of the PSFramework configuration setting.
     
    .PARAMETER Ensure
        Whether it should be present or absent.
     
    .PARAMETER ConfigScope
        What scope it should be applied to.
     
    .PARAMETER Value
        The value it should have.
 
    .PARAMETER ValueType
        The type of the value that should be defined.
        Used because DSC does not allow Object types, thus requiring conversion afterwards.
     
    .EXAMPLE
        PS C:\> Set-PSFrameworkConfig @param
 
        Applies the desired value for the PSFramework configuration setting.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $FullName,

        [Parameter(Mandatory = $true)]
        [Ensure]
        $Ensure,

        [Scope]
        $ConfigScope,

        [string]
        $Value,

        [ConfigType]
        $ValueType
    )
    process {
        if ($ConfigScope -eq 'SystemDefault') { $path = $script:config.PathDefault }
        else { $path = $script:config.PathEnforced }

        if ($Ensure -eq 'Absent') {
            if (-not (Test-Path -Path $path)) { return }

            $current = Get-ItemProperty -Path $path
            if ($current.PSObject.Properties.Name -notcontains $FullName) { return }
            
            # Ensure the casing is exact, just in case
            $name = $current.PSObject.Properties.Name | Where-Object { $_ -eq $FullName }
            Remove-ItemProperty -Path $path -Name $name -ErrorAction Stop

            return
        }

        try { $intendedValue = [PSFrameworkConfig]::Convert($Value, $ValueType) }
        catch { throw "Error setting $FullName : Failed to convert $Value to $ValueType! $_" }
        if ($ValueType -ne 'PsfConfig' -and -not [PSFrameworkConfig]::IsLegalType($intendedValue)) {
            if ($null -eq $intendedValue) { $convertedValueType = '<null>' }
            else { $convertedValueType = $intendedValue.GetType() }
            throw "Datatype not supported: $($convertedValueType)"
        }

        if (-not (Test-Path $path)) {
            $null = New-Item $Path -Force
        }

        $registryValue = $intendedValue
        if ($ValueType -ne 'PsfConfig') { $registryValue = [PSFrameworkConfig]::Serialize($intendedValue) }

        Set-ItemProperty -Path $path -Name $FullName -Value $registryValue -ErrorAction Stop
    }
}

function Test-PSFrameworkConfig {
    <#
    .SYNOPSIS
        Tests, whether the desired value/state for the PSFramework configuration setting applies.
     
    .DESCRIPTION
        Tests, whether the desired value/state for the PSFramework configuration setting applies.
     
    .PARAMETER FullName
        The full name of the PSFramework configuration setting.
     
    .PARAMETER Ensure
        Whether it should be present or absent.
     
    .PARAMETER ConfigScope
        What scope it should be applied to.
     
    .PARAMETER Value
        The value it should have.
 
    .PARAMETER ValueType
        The type of the value that should be defined.
        Used because DSC does not allow Object types, thus requiring conversion afterwards.
     
    .PARAMETER Current
        The object representing the current state.
     
    .EXAMPLE
        PS C:\> Test-PSFrameworkConfig @param
 
        Tests, whether the desired value/state for the PSFramework configuration setting applies.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [OutputType([bool])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $FullName,

        [Parameter(Mandatory = $true)]
        [Ensure]
        $Ensure,

        [Scope]
        $ConfigScope,

        [string]
        $Value,

        [ConfigType]
        $ValueType,

        [PSFrameworkConfig]
        $Current
    )
    process {
        if ($Current.Ensure -ne $Ensure) { return $false }
        if ($Ensure -eq 'Absent') { return $true }
        
        try { $intendedValue = [PSFrameworkConfig]::Convert($Value, $ValueType) }
        catch { throw "Error testing $FullName : Failed to convert $Value to $ValueType! $_" }
        $registryValue = $intendedValue
        if ($ValueType -ne 'PsfConfig') { $registryValue = [PSFrameworkConfig]::Serialize($intendedValue) }

        $registryValue -eq $Current.Value
    }
}

$script:config = @{
    PathDefault = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Default"
    PathEnforced = "HKLM:\SOFTWARE\Microsoft\WindowsPowerShell\PSFramework\Config\Enforced"
}