#Requires -Version 7.4

function Import-vSEALTemplate {
    <#
    .SYNOPSIS
        Parses and validates a TOML template file for declarative plugin definitions.

    .DESCRIPTION
        Reads a TOML template file and validates its structure and metadata. Templates
        define plugin behavior through declarative action groups, conditions, and metadata.

        The function performs:
        1. TOML parsing via ConvertFrom-Toml (PSToml module)
        2. Metadata validation (required: name, version; optional: description, min_build, etc.)
        3. Groups extraction and structural validation
        4. Unknown field detection (generates warnings, not errors)

        Use -ValidateOnly to get a validation result without throwing on errors.

    .PARAMETER Path
        Path to the TOML template file. Must have .toml extension.

    .PARAMETER ValidateOnly
        When specified, returns the validation result object instead of throwing
        on validation errors. TOML syntax errors always throw regardless.

    .OUTPUTS
        [PSCustomObject] Validation result with properties:
        - IsValid:   Boolean indicating whether template is valid
        - Errors:    Array of error objects with Path, Message, Expected, Actual
        - Warnings:  Array of warning strings
        - Metadata:  Extracted metadata hashtable
        - Groups:    Array of group hashtables ready for Invoke-vSEALActionGroup

    .EXAMPLE
        Import-vSEALTemplate -Path 'C:\Templates\defender.toml'

        Parses and validates the template. Throws if validation fails.

    .EXAMPLE
        $result = Import-vSEALTemplate -Path 'C:\Templates\defender.toml' -ValidateOnly
        if (-not $result.IsValid) {
            $result.Errors | ForEach-Object { Write-Warning $_.Message }
        }

        Gets validation result without throwing. Useful for template authoring tools.

    .EXAMPLE
        $result = Import-vSEALTemplate -Path './plugin.toml' -ValidateOnly
        $result.Metadata.Name        # Template name
        $result.Groups.Count         # Number of action groups

        Access parsed metadata and groups for inspection.

    .NOTES
        Part of the vARC (vSEAL Action Rollback Core) template system.
        Templates follow TOML array-of-tables format: [[groups]], [[groups.actions]], etc.

        Required metadata fields: name, version
        Optional metadata fields: description, min_build, max_build, tags, author (nested)

        Unknown fields generate warnings (not errors) for forward compatibility.
    #>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter()]
        [switch]$ValidateOnly
    )

    # Initialize result structure
    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()
    $metadata = @{
        Name        = $null
        Version     = $null
        Description = $null
        MinBuild    = $null
        MaxBuild    = $null
        Tags        = @()
        Author      = @{ Name = $null; Email = $null; Url = $null }
    }
    $groups = @()
    $isValid = $true

    # ===== FILE VALIDATION =====

    # Validate file exists
    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        throw [System.IO.FileNotFoundException]::new("Template file not found: $Path")
    }

    # Resolve to absolute path
    $resolvedPath = (Resolve-Path -Path $Path -ErrorAction Stop).Path

    # Validate .toml extension
    if (-not $resolvedPath.EndsWith('.toml', [System.StringComparison]::OrdinalIgnoreCase)) {
        throw [System.ArgumentException]::new("Template file must have .toml extension: $resolvedPath")
    }

    # ===== TOML PARSING =====

    try {
        $content = Get-Content -Path $resolvedPath -Raw -ErrorAction Stop
        $template = ConvertFrom-Toml -InputObject $content -ErrorAction Stop

        # ConvertFrom-Toml may return null on parse error without throwing
        if ($null -eq $template) {
            throw [System.FormatException]::new("TOML syntax error in '$resolvedPath': Failed to parse TOML content")
        }
    }
    catch [System.IO.FileNotFoundException] {
        throw [System.IO.FileNotFoundException]::new("Template file not found: $resolvedPath")
    }
    catch [System.IO.IOException] {
        throw [System.IO.IOException]::new("I/O error reading template file '$resolvedPath': $($_.Exception.Message)")
    }
    catch [System.UnauthorizedAccessException] {
        throw [System.UnauthorizedAccessException]::new("Access denied reading template file '$resolvedPath': $($_.Exception.Message)")
    }
    catch [System.FormatException] {
        # Re-throw FormatException as-is
        throw
    }
    catch {
        # PSToml parsing errors - always throw (can't continue without valid TOML)
        throw [System.FormatException]::new("TOML syntax error in '$resolvedPath': $($_.Exception.Message)")
    }

    # ===== METADATA EXTRACTION AND VALIDATION =====

    # Helper to check key existence (OrderedDictionary uses Contains, not ContainsKey)
    $hasKey = {
        param($dict, $key)
        if ($dict -is [System.Collections.Specialized.OrderedDictionary]) {
            return $dict.Contains($key)
        }
        return $dict.ContainsKey($key)
    }

    if (-not (& $hasKey $template 'template')) {
        $isValid = $false
        [void]$errors.Add(@{
            Path     = 'template'
            Message  = 'Missing required [template] section'
            Expected = 'table'
            Actual   = $null
        })
    }
    else {
        $meta = $template['template']

        # Required: name
        if (-not (& $hasKey $meta 'name') -or [string]::IsNullOrWhiteSpace($meta['name'])) {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'template.name'
                Message  = 'Required field missing: name'
                Expected = 'string'
                Actual   = ($meta['name'] ?? $null)
            })
        }
        else {
            $metadata.Name = [string]$meta['name']
        }

        # Required: version
        if (-not (& $hasKey $meta 'version') -or [string]::IsNullOrWhiteSpace($meta['version'])) {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'template.version'
                Message  = 'Required field missing: version'
                Expected = 'string'
                Actual   = ($meta['version'] ?? $null)
            })
        }
        else {
            $metadata.Version = [string]$meta['version']
        }

        # Optional: description
        if (& $hasKey $meta 'description') {
            $metadata.Description = [string]$meta['description']
        }

        # Optional: min_build (int64)
        if (& $hasKey $meta 'min_build') {
            $minBuild = $meta['min_build']
            if ($minBuild -is [long] -or $minBuild -is [int]) {
                $metadata.MinBuild = [long]$minBuild
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'template.min_build'
                    Message  = 'Invalid type for min_build'
                    Expected = 'integer'
                    Actual   = $minBuild.GetType().Name
                })
            }
        }

        # Optional: max_build (int64)
        if (& $hasKey $meta 'max_build') {
            $maxBuild = $meta['max_build']
            if ($maxBuild -is [long] -or $maxBuild -is [int]) {
                $metadata.MaxBuild = [long]$maxBuild
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'template.max_build'
                    Message  = 'Invalid type for max_build'
                    Expected = 'integer'
                    Actual   = $maxBuild.GetType().Name
                })
            }
        }

        # Optional: tags (string array)
        if (& $hasKey $meta 'tags') {
            $tags = $meta['tags']
            if ($tags -is [System.Collections.IEnumerable] -and $tags -isnot [string]) {
                $metadata.Tags = @($tags | ForEach-Object { [string]$_ })
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'template.tags'
                    Message  = 'Invalid type for tags'
                    Expected = 'array'
                    Actual   = $tags.GetType().Name
                })
            }
        }

        # Optional: author (nested table)
        if (& $hasKey $meta 'author') {
            $author = $meta['author']
            if ($author -is [System.Collections.IDictionary]) {
                if (& $hasKey $author 'name') {
                    $metadata.Author.Name = [string]$author['name']
                }
                if (& $hasKey $author 'email') {
                    $metadata.Author.Email = [string]$author['email']
                }
                if (& $hasKey $author 'url') {
                    $metadata.Author.Url = [string]$author['url']
                }

                # Check for unknown author fields
                $knownAuthorFields = @('name', 'email', 'url')
                foreach ($key in $author.Keys) {
                    if ($key -notin $knownAuthorFields) {
                        [void]$warnings.Add("Unknown field 'template.author.$key' - will be ignored")
                    }
                }
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'template.author'
                    Message  = 'Invalid type for author'
                    Expected = 'table'
                    Actual   = $author.GetType().Name
                })
            }
        }

        # Check for unknown template fields
        $knownTemplateFields = @('name', 'version', 'description', 'min_build', 'max_build', 'tags', 'author')
        foreach ($key in $meta.Keys) {
            if ($key -notin $knownTemplateFields) {
                [void]$warnings.Add("Unknown field 'template.$key' - will be ignored")
            }
        }
    }

    # ===== GROUPS EXTRACTION =====

    if (& $hasKey $template 'groups') {
        $groupsRaw = $template['groups']

        # Validate groups is an array
        if ($groupsRaw -is [System.Collections.IEnumerable] -and $groupsRaw -isnot [string] -and $groupsRaw -isnot [System.Collections.IDictionary]) {
            $groupList = [System.Collections.ArrayList]::new()
            $groupIndex = 0

            foreach ($group in $groupsRaw) {
                $groupPath = "groups[$groupIndex]"

                # Convert OrderedDictionary to hashtable recursively
                $normalizedGroup = ConvertTo-TemplateHashtable -InputObject $group

                # Validate required: name
                if (-not $normalizedGroup.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($normalizedGroup['name'])) {
                    $isValid = $false
                    [void]$errors.Add(@{
                        Path     = "$groupPath.name"
                        Message  = 'Required field missing: group name'
                        Expected = 'string'
                        Actual   = ($normalizedGroup['name'] ?? $null)
                    })
                }

                # Validate required: actions array OR packages array must exist
                # Groups can have actions (declarative), packages (pattern-based), or both
                $hasActions = $normalizedGroup.ContainsKey('actions')
                $hasPackages = $normalizedGroup.ContainsKey('packages')

                if (-not $hasActions -and -not $hasPackages) {
                    $isValid = $false
                    [void]$errors.Add(@{
                        Path     = "$groupPath"
                        Message  = 'Required field missing: groups must have actions and/or packages array'
                        Expected = 'actions or packages array'
                        Actual   = $null
                    })
                }

                # Validate actions array if present
                if ($hasActions) {
                    if ($normalizedGroup['actions'] -isnot [System.Collections.IEnumerable] -or $normalizedGroup['actions'] -is [string] -or $normalizedGroup['actions'] -is [System.Collections.IDictionary]) {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = "$groupPath.actions"
                            Message  = 'Invalid type for actions'
                            Expected = 'array'
                            Actual   = $normalizedGroup['actions'].GetType().Name
                        })
                    }
                }

                # Validate packages array if present
                if ($hasPackages) {
                    if ($normalizedGroup['packages'] -isnot [System.Collections.IEnumerable] -or $normalizedGroup['packages'] -is [string] -or $normalizedGroup['packages'] -is [System.Collections.IDictionary]) {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = "$groupPath.packages"
                            Message  = 'Invalid type for packages'
                            Expected = 'array'
                            Actual   = $normalizedGroup['packages'].GetType().Name
                        })
                    }
                    else {
                        # Validate each package entry has required fields
                        $packageIndex = 0
                        foreach ($pkg in $normalizedGroup['packages']) {
                            $packagePath = "$groupPath.packages[$packageIndex]"
                            if (-not $pkg.ContainsKey('pattern') -or [string]::IsNullOrWhiteSpace($pkg['pattern'])) {
                                $isValid = $false
                                [void]$errors.Add(@{
                                    Path     = "$packagePath.pattern"
                                    Message  = 'Required field missing: package pattern'
                                    Expected = 'string (wildcard pattern)'
                                    Actual   = $null
                                })
                            }
                            $packageIndex++
                        }
                    }
                }

                # Set defaults
                if (-not $normalizedGroup.ContainsKey('enabled')) {
                    $normalizedGroup['enabled'] = $true
                }
                if (-not $normalizedGroup.ContainsKey('on_error')) {
                    $normalizedGroup['on_error'] = 'continue'
                }

                # Warnings for empty or disabled groups
                $actionsCount = if ($normalizedGroup.ContainsKey('actions') -and $normalizedGroup['actions'] -is [System.Collections.IEnumerable]) { @($normalizedGroup['actions']).Count } else { 0 }
                $packagesCount = if ($normalizedGroup.ContainsKey('packages') -and $normalizedGroup['packages'] -is [System.Collections.IEnumerable]) { @($normalizedGroup['packages']).Count } else { 0 }
                if ($actionsCount -eq 0 -and $packagesCount -eq 0) {
                    [void]$warnings.Add("Group '$($normalizedGroup['name'] ?? $groupIndex)' has 0 actions and 0 packages")
                }
                if ($normalizedGroup.ContainsKey('enabled') -and $normalizedGroup['enabled'] -eq $false) {
                    [void]$warnings.Add("Group '$($normalizedGroup['name'] ?? $groupIndex)' is disabled")
                }

                # Check for unknown group fields
                $knownGroupFields = @('name', 'description', 'enabled', 'on_error', 'conditions', 'actions', 'packages')
                foreach ($key in $normalizedGroup.Keys) {
                    if ($key -notin $knownGroupFields) {
                        [void]$warnings.Add("Unknown field '$groupPath.$key' - will be ignored")
                    }
                }

                # Helper: merge validation result errors/warnings into parent lists
                $mergeValidationResult = {
                    param($validationResult)
                    foreach ($err in $validationResult.Errors) {
                        $isValid = $false
                        [void]$errors.Add($err)
                    }
                    foreach ($warn in $validationResult.Warnings) {
                        [void]$warnings.Add($warn)
                    }
                }

                # ===== GROUP-LEVEL CONDITION VALIDATION =====
                if ($normalizedGroup.ContainsKey('conditions') -and $normalizedGroup['conditions'] -is [System.Collections.IEnumerable] -and $normalizedGroup['conditions'] -isnot [string]) {
                    $conditionIndex = 0
                    foreach ($condition in $normalizedGroup['conditions']) {
                        $conditionPath = "$groupPath.conditions[$conditionIndex]"
                        . $mergeValidationResult (Test-TemplateCondition -Condition $condition -ConditionPath $conditionPath)
                        $conditionIndex++
                    }
                }

                # ===== ACTION VALIDATION =====
                if ($normalizedGroup.ContainsKey('actions') -and $normalizedGroup['actions'] -is [System.Collections.IEnumerable] -and $normalizedGroup['actions'] -isnot [string]) {
                    $actionIndex = 0
                    foreach ($action in $normalizedGroup['actions']) {
                        $actionPath = "$groupPath.actions[$actionIndex]"
                        . $mergeValidationResult (Test-TemplateAction -Action $action -ActionPath $actionPath)

                        # ===== PER-ACTION CONDITION VALIDATION =====
                        if ($action.ContainsKey('conditions') -and $action['conditions'] -is [System.Collections.IEnumerable] -and $action['conditions'] -isnot [string]) {
                            $actionConditionIndex = 0
                            foreach ($actionCondition in $action['conditions']) {
                                $actionConditionPath = "$actionPath.conditions[$actionConditionIndex]"
                                . $mergeValidationResult (Test-TemplateCondition -Condition $actionCondition -ConditionPath $actionConditionPath)
                                $actionConditionIndex++
                            }
                        }

                        $actionIndex++
                    }
                }

                [void]$groupList.Add($normalizedGroup)
                $groupIndex++
            }

            $groups = @($groupList)
        }
        else {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'groups'
                Message  = 'Groups must be an array (use [[groups]] syntax)'
                Expected = 'array'
                Actual   = $groupsRaw.GetType().Name
            })
        }
    }
    else {
        # No groups section - valid but empty
        [void]$warnings.Add("Template has no groups defined")
    }

    # Check for unknown top-level fields
    $knownTopLevelFields = @('template', 'groups')
    foreach ($key in $template.Keys) {
        if ($key -notin $knownTopLevelFields) {
            [void]$warnings.Add("Unknown top-level field '$key' - will be ignored")
        }
    }

    # ===== BUILD RESULT =====

    $result = [PSCustomObject]@{
        IsValid  = $isValid
        Errors   = @($errors)
        Warnings = @($warnings)
        Metadata = $metadata
        Groups   = $groups
    }

    # ===== RETURN RESULT =====

    if (-not $ValidateOnly -and -not $isValid) {
        $errorMessages = $errors | ForEach-Object { "[$($_.Path)] $($_.Message)" }
        throw [System.ArgumentException]::new("Template validation failed:`n$($errorMessages -join "`n")")
    }

    return $result
}


function ConvertTo-TemplateHashtable {
    <#
    .SYNOPSIS
        Recursively converts OrderedDictionary to hashtable for template processing.

    .DESCRIPTION
        PSToml returns OrderedDictionary objects which don't have the same method
        signatures as Hashtable. This function converts the entire structure to
        regular hashtables for consistent processing.

        Arrays are preserved as arrays even when they contain a single element.

    .PARAMETER InputObject
        The object to convert (can be any type).

    .OUTPUTS
        The converted object - hashtable for dictionaries, array for collections, pass-through for scalars.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        $InputObject
    )

    if ($InputObject -is [System.Collections.IDictionary]) {
        $result = @{}
        foreach ($key in $InputObject.Keys) {
            $converted = ConvertTo-TemplateHashtable -InputObject $InputObject[$key]
            # Preserve arrays even with single element
            if ($InputObject[$key] -is [array] -or ($InputObject[$key] -is [System.Collections.IEnumerable] -and $InputObject[$key] -isnot [string] -and $InputObject[$key] -isnot [System.Collections.IDictionary])) {
                $result[$key] = [array]$converted
            }
            else {
                $result[$key] = $converted
            }
        }
        return $result
    }
    elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
        $result = [System.Collections.ArrayList]::new()
        foreach ($item in $InputObject) {
            [void]$result.Add((ConvertTo-TemplateHashtable -InputObject $item))
        }
        # Use comma operator to prevent single-element array unwrapping
        return , [array]$result.ToArray()
    }
    else {
        return $InputObject
    }
}


function Test-TemplateAction {
    <#
    .SYNOPSIS
        Validates a single action from a template.

    .DESCRIPTION
        Validates action structure and required fields based on action type.
        Supported types: registry, service, task, command.
        Unknown types produce errors. Unknown fields produce warnings.

    .PARAMETER Action
        The action hashtable to validate.

    .PARAMETER ActionPath
        The path string for error reporting (e.g., "groups[0].actions[2]").

    .OUTPUTS
        Hashtable with Errors and Warnings arrays.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Action,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid action types (case-insensitive per CONTEXT.md)
    $validActionTypes = @('registry', 'service', 'task', 'command')

    # Check for required type field
    if (-not $Action.ContainsKey('type')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.type"
            Message  = 'Required field missing: type'
            Expected = 'string (registry|service|task|command)'
            Actual   = $null
        })
        return @{ Errors = @($errors); Warnings = @($warnings) }
    }

    $actionType = [string]$Action['type']

    # Validate action type (case-insensitive)
    if ($actionType.ToLower() -notin $validActionTypes) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.type"
            Message  = "Unknown action type: $actionType"
            Expected = 'registry|service|task|command'
            Actual   = $actionType
        })
        return @{ Errors = @($errors); Warnings = @($warnings) }
    }

    # Dispatch to type-specific validation
    $typeValidators = @{
        'registry' = 'Test-TemplateRegistryAction'
        'service'  = 'Test-TemplateServiceAction'
        'task'     = 'Test-TemplateTaskAction'
        'command'  = 'Test-TemplateCommandAction'
    }

    $result = & $typeValidators[$actionType.ToLower()] -Action $Action -ActionPath $ActionPath
    foreach ($e in $result.Errors) { [void]$errors.Add($e) }
    foreach ($w in $result.Warnings) { [void]$warnings.Add($w) }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateRegistryAction {
    <#
    .SYNOPSIS
        Validates a registry action.

    .DESCRIPTION
        Validates registry action required fields:
        - action (set|delete|deletekey)
        - key (HKLM or HKCR only, HKCU rejected)
        - name (required for set/delete)
        - data (required for set)
        - datatype (optional, validates aliases)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Action,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid registry actions
    $validActions = @('set', 'delete', 'deletekey')

    # Valid datatype aliases (all map to PowerShell types)
    $validDataTypes = @{
        'string'       = 'String'
        'dword'        = 'DWord'
        'reg_dword'    = 'DWord'
        'qword'        = 'QWord'
        'reg_qword'    = 'QWord'
        'binary'       = 'Binary'
        'reg_binary'   = 'Binary'
        'expandstring' = 'ExpandString'
        'multistring'  = 'MultiString'
    }

    # Known fields for registry actions
    $knownFields = @('type', 'action', 'key', 'name', 'data', 'datatype', 'conditions', 'enabled', 'description')

    # Required: action
    if (-not $Action.ContainsKey('action')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.action"
            Message  = 'Required field missing: action'
            Expected = 'string (set|delete|deletekey)'
            Actual   = $null
        })
    }
    else {
        $regAction = [string]$Action['action']
        if ($regAction.ToLower() -notin $validActions) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.action"
                Message  = "Invalid registry action: $regAction"
                Expected = 'set|delete|deletekey'
                Actual   = $regAction
            })
        }
    }

    # Required: key
    if (-not $Action.ContainsKey('key')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.key"
            Message  = 'Required field missing: key'
            Expected = 'string (HKLM:\... or HKCR:\...)'
            Actual   = $null
        })
    }
    else {
        $key = [string]$Action['key']

        # HKCU rejection (per CONTEXT.md: rejected at parse time)
        if ($key -match '^HKCU:?\\' -or $key -match '^HKEY_CURRENT_USER\\') {
            [void]$errors.Add(@{
                Path     = "$ActionPath.key"
                Message  = 'HKCU registry paths are not allowed. Use GPO/Intune for user settings.'
                Expected = 'HKLM:\... or HKCR:\...'
                Actual   = $key
            })
        }
        # Validate HKLM or HKCR
        elseif (-not ($key -match '^HKLM:?\\' -or $key -match '^HKEY_LOCAL_MACHINE\\' -or $key -match '^HKCR:?\\' -or $key -match '^HKEY_CLASSES_ROOT\\')) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.key"
                Message  = 'Registry key must start with HKLM:\ or HKCR:\'
                Expected = 'HKLM:\... or HKCR:\...'
                Actual   = $key
            })
        }
    }

    # Conditional requirements based on action type
    $regAction = if ($Action.ContainsKey('action')) { [string]$Action['action'] } else { $null }

    if ($regAction -and $regAction.ToLower() -in @('set', 'delete')) {
        # Required: name for set/delete
        if (-not $Action.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($Action['name'])) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.name"
                Message  = "Required field missing: name (required for '$regAction' action)"
                Expected = 'string'
                Actual   = ($Action['name'] ?? $null)
            })
        }
    }

    if ($regAction -and $regAction.ToLower() -eq 'set') {
        # Required: data for set
        if (-not $Action.ContainsKey('data')) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.data"
                Message  = "Required field missing: data (required for 'set' action)"
                Expected = 'value'
                Actual   = $null
            })
        }
    }

    # Optional: datatype validation
    if ($Action.ContainsKey('datatype')) {
        $datatype = [string]$Action['datatype']
        if ($datatype.ToLower() -notin $validDataTypes.Keys) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.datatype"
                Message  = "Invalid registry datatype: $datatype"
                Expected = ($validDataTypes.Keys -join '|')
                Actual   = $datatype
            })
        }
    }

    # Check for unknown fields (generates warnings, not errors)
    foreach ($key in $Action.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ActionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateServiceAction {
    <#
    .SYNOPSIS
        Validates a service action.

    .DESCRIPTION
        Validates service action required fields:
        - action (disable)
        - name (service name)
        - start_mode (optional: Automatic|Manual|Disabled or 2|3|4)
        - stop_if_running (optional bool)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Action,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid service actions (currently only disable per Phase 17)
    $validActions = @('disable')

    # Valid start modes (friendly names and Windows constants)
    $validStartModes = @('automatic', 'manual', 'disabled', '2', '3', '4')

    # Known fields for service actions
    $knownFields = @('type', 'action', 'name', 'start_mode', 'stop_if_running', 'conditions', 'enabled', 'description')

    # Required: action
    if (-not $Action.ContainsKey('action')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.action"
            Message  = 'Required field missing: action'
            Expected = 'string (disable)'
            Actual   = $null
        })
    }
    else {
        $svcAction = [string]$Action['action']
        if ($svcAction.ToLower() -notin $validActions) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.action"
                Message  = "Invalid service action: $svcAction"
                Expected = 'disable'
                Actual   = $svcAction
            })
        }
    }

    # Required: name
    if (-not $Action.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($Action['name'])) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.name"
            Message  = 'Required field missing: name'
            Expected = 'string (service name)'
            Actual   = ($Action['name'] ?? $null)
        })
    }

    # Optional: start_mode validation
    if ($Action.ContainsKey('start_mode')) {
        $startMode = [string]$Action['start_mode']
        if ($startMode.ToLower() -notin $validStartModes) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.start_mode"
                Message  = "Invalid start_mode: $startMode"
                Expected = 'Automatic|Manual|Disabled (or 2|3|4)'
                Actual   = $startMode
            })
        }
    }

    # Optional: stop_if_running validation (must be bool)
    if ($Action.ContainsKey('stop_if_running')) {
        $stopIfRunning = $Action['stop_if_running']
        if ($stopIfRunning -isnot [bool]) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.stop_if_running"
                Message  = "Invalid type for stop_if_running"
                Expected = 'boolean'
                Actual   = $stopIfRunning.GetType().Name
            })
        }
    }

    # Check for unknown fields
    foreach ($key in $Action.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ActionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateTaskAction {
    <#
    .SYNOPSIS
        Validates a task action.

    .DESCRIPTION
        Validates task action required fields:
        - action (enable|disable)
        - name (full task path, backslash format)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Action,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid task actions
    $validActions = @('enable', 'disable')

    # Known fields for task actions
    $knownFields = @('type', 'action', 'name', 'conditions', 'enabled', 'description')

    # Required: action
    if (-not $Action.ContainsKey('action')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.action"
            Message  = 'Required field missing: action'
            Expected = 'string (enable|disable)'
            Actual   = $null
        })
    }
    else {
        $taskAction = [string]$Action['action']
        if ($taskAction.ToLower() -notin $validActions) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.action"
                Message  = "Invalid task action: $taskAction"
                Expected = 'enable|disable'
                Actual   = $taskAction
            })
        }
    }

    # Required: name
    if (-not $Action.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($Action['name'])) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.name"
            Message  = 'Required field missing: name'
            Expected = 'string (full task path)'
            Actual   = ($Action['name'] ?? $null)
        })
    }

    # Check for unknown fields
    foreach ($key in $Action.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ActionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateCommandAction {
    <#
    .SYNOPSIS
        Validates a command action.

    .DESCRIPTION
        Validates command action required fields:
        - action (run)
        - command (command line)
        - description (optional)
        - timeout (optional, 1-600)
        - success_exit_codes (optional int array)
        - rollback_command (optional)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Action,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid command actions
    $validActions = @('run')

    # Known fields for command actions
    $knownFields = @('type', 'action', 'command', 'description', 'timeout', 'success_exit_codes', 'rollback_command', 'conditions', 'enabled')

    # Required: action
    if (-not $Action.ContainsKey('action')) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.action"
            Message  = 'Required field missing: action'
            Expected = 'string (run)'
            Actual   = $null
        })
    }
    else {
        $cmdAction = [string]$Action['action']
        if ($cmdAction.ToLower() -notin $validActions) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.action"
                Message  = "Invalid command action: $cmdAction"
                Expected = 'run'
                Actual   = $cmdAction
            })
        }
    }

    # Required: command
    if (-not $Action.ContainsKey('command') -or [string]::IsNullOrWhiteSpace($Action['command'])) {
        [void]$errors.Add(@{
            Path     = "$ActionPath.command"
            Message  = 'Required field missing: command'
            Expected = 'string (command line)'
            Actual   = ($Action['command'] ?? $null)
        })
    }

    # Optional: timeout validation (1-600)
    if ($Action.ContainsKey('timeout')) {
        $timeout = $Action['timeout']
        if ($timeout -is [long] -or $timeout -is [int]) {
            if ($timeout -lt 1 -or $timeout -gt 600) {
                [void]$errors.Add(@{
                    Path     = "$ActionPath.timeout"
                    Message  = "Timeout must be between 1 and 600 seconds"
                    Expected = '1-600'
                    Actual   = $timeout
                })
            }
        }
        else {
            [void]$errors.Add(@{
                Path     = "$ActionPath.timeout"
                Message  = "Invalid type for timeout"
                Expected = 'integer'
                Actual   = $timeout.GetType().Name
            })
        }
    }

    # Optional: success_exit_codes validation (must be array of integers)
    if ($Action.ContainsKey('success_exit_codes')) {
        $exitCodes = $Action['success_exit_codes']
        if ($exitCodes -is [System.Collections.IEnumerable] -and $exitCodes -isnot [string]) {
            foreach ($code in $exitCodes) {
                if ($code -isnot [long] -and $code -isnot [int]) {
                    [void]$errors.Add(@{
                        Path     = "$ActionPath.success_exit_codes"
                        Message  = "success_exit_codes must contain only integers"
                        Expected = 'int[]'
                        Actual   = $code.GetType().Name
                    })
                    break
                }
            }
        }
        else {
            [void]$errors.Add(@{
                Path     = "$ActionPath.success_exit_codes"
                Message  = "success_exit_codes must be an array"
                Expected = 'int[]'
                Actual   = $exitCodes.GetType().Name
            })
        }
    }

    # Optional: rollback_command validation (must be string if present)
    if ($Action.ContainsKey('rollback_command')) {
        $rollback = $Action['rollback_command']
        if ($rollback -isnot [string]) {
            [void]$errors.Add(@{
                Path     = "$ActionPath.rollback_command"
                Message  = "rollback_command must be a string"
                Expected = 'string'
                Actual   = $rollback.GetType().Name
            })
        }
    }

    # Check for unknown fields
    foreach ($key in $Action.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ActionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateCondition {
    <#
    .SYNOPSIS
        Validates a single condition from a template.

    .DESCRIPTION
        Validates condition structure and required fields based on condition type.
        Supported types: globals, registry, service, task.
        Unknown types produce errors.

    .PARAMETER Condition
        The condition hashtable to validate.

    .PARAMETER ConditionPath
        The path string for error reporting (e.g., "groups[0].conditions[1]").

    .OUTPUTS
        Hashtable with Errors and Warnings arrays.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Condition,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid condition types (case-insensitive per CONTEXT.md)
    $validConditionTypes = @('globals', 'registry', 'service', 'task')

    # Check for required type field
    if (-not $Condition.ContainsKey('type')) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.type"
            Message  = 'Required field missing: type'
            Expected = 'string (globals|registry|service|task)'
            Actual   = $null
        })
        return @{ Errors = @($errors); Warnings = @($warnings) }
    }

    $conditionType = [string]$Condition['type']

    # Validate condition type (case-insensitive)
    if ($conditionType.ToLower() -notin $validConditionTypes) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.type"
            Message  = "Unknown condition type: $conditionType"
            Expected = 'globals|registry|service|task'
            Actual   = $conditionType
        })
        return @{ Errors = @($errors); Warnings = @($warnings) }
    }

    # Dispatch to type-specific validation
    $typeValidators = @{
        'globals'  = 'Test-TemplateGlobalsCondition'
        'registry' = 'Test-TemplateRegistryCondition'
        'service'  = 'Test-TemplateServiceCondition'
        'task'     = 'Test-TemplateTaskCondition'
    }

    $result = & $typeValidators[$conditionType.ToLower()] -Condition $Condition -ConditionPath $ConditionPath
    foreach ($e in $result.Errors) { [void]$errors.Add($e) }
    foreach ($w in $result.Warnings) { [void]$warnings.Add($w) }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateGlobalsCondition {
    <#
    .SYNOPSIS
        Validates a globals condition.

    .DESCRIPTION
        Validates globals condition required fields:
        - path (OS.* or Platform.* paths)
        - operator (comparison operators)
        - value
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Condition,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Known globals paths (warn on unknown, not error - for forward compatibility)
    $knownOSPaths = @(
        'os.name', 'os.edition', 'os.version', 'os.build',
        'os.releaseid', 'os.isserver', 'os.isworkstation', 'os.ismultisession'
    )
    $knownPlatformPaths = @(
        'platform.isazure', 'platform.isavd', 'platform.isavdregistered',
        'platform.isvmware', 'platform.ishyperv', 'platform.isxenserver',
        'platform.isnutanixahv', 'platform.hypervisor'
    )

    # Valid operators for globals
    $validOperators = @('=', '!=', '>', '>=', '<', '<=', 'in', 'not_in', 'contains')

    # Known fields
    $knownFields = @('type', 'path', 'operator', 'value')

    # Required: path
    if (-not $Condition.ContainsKey('path') -or [string]::IsNullOrWhiteSpace($Condition['path'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.path"
            Message  = 'Required field missing: path'
            Expected = 'string (OS.* or Platform.*)'
            Actual   = ($Condition['path'] ?? $null)
        })
    }
    else {
        $condPath = [string]$Condition['path']
        # Check if path is known (warn if not, for forward compatibility)
        if ($condPath.ToLower() -notin ($knownOSPaths + $knownPlatformPaths)) {
            [void]$warnings.Add("Unknown globals path '$condPath' at $ConditionPath.path - may not exist in current vSEAL version")
        }
    }

    # Required: operator
    if (-not $Condition.ContainsKey('operator') -or [string]::IsNullOrWhiteSpace($Condition['operator'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.operator"
            Message  = 'Required field missing: operator'
            Expected = ($validOperators -join '|')
            Actual   = ($Condition['operator'] ?? $null)
        })
    }
    else {
        $operator = [string]$Condition['operator']
        if ($operator -notin $validOperators) {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.operator"
                Message  = "Invalid operator: $operator"
                Expected = ($validOperators -join '|')
                Actual   = $operator
            })
        }

        # For 'in' and 'not_in', value must be array
        if ($operator -in @('in', 'not_in') -and $Condition.ContainsKey('value')) {
            $value = $Condition['value']
            if ($value -isnot [System.Collections.IEnumerable] -or $value -is [string]) {
                [void]$errors.Add(@{
                    Path     = "$ConditionPath.value"
                    Message  = "Value must be an array for '$operator' operator"
                    Expected = 'array'
                    Actual   = $value.GetType().Name
                })
            }
        }
    }

    # Required: value
    if (-not $Condition.ContainsKey('value')) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.value"
            Message  = 'Required field missing: value'
            Expected = 'value'
            Actual   = $null
        })
    }

    # Check for unknown fields
    foreach ($key in $Condition.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ConditionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateRegistryCondition {
    <#
    .SYNOPSIS
        Validates a registry condition.

    .DESCRIPTION
        Validates registry condition required fields:
        - key (HKLM or HKCR only, HKCU rejected)
        - operator (exists|not_exists)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Condition,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid operators for registry conditions
    $validOperators = @('exists', 'not_exists')

    # Known fields
    $knownFields = @('type', 'key', 'operator', 'name')

    # Required: key
    if (-not $Condition.ContainsKey('key') -or [string]::IsNullOrWhiteSpace($Condition['key'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.key"
            Message  = 'Required field missing: key'
            Expected = 'string (HKLM:\... or HKCR:\...)'
            Actual   = ($Condition['key'] ?? $null)
        })
    }
    else {
        $key = [string]$Condition['key']

        # HKCU rejection (per CONTEXT.md: rejected at parse time)
        if ($key -match '^HKCU:?\\' -or $key -match '^HKEY_CURRENT_USER\\') {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.key"
                Message  = 'HKCU registry paths are not allowed. Use GPO/Intune for user settings.'
                Expected = 'HKLM:\... or HKCR:\...'
                Actual   = $key
            })
        }
        # Validate HKLM or HKCR
        elseif (-not ($key -match '^HKLM:?\\' -or $key -match '^HKEY_LOCAL_MACHINE\\' -or $key -match '^HKCR:?\\' -or $key -match '^HKEY_CLASSES_ROOT\\')) {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.key"
                Message  = 'Registry key must start with HKLM:\ or HKCR:\'
                Expected = 'HKLM:\... or HKCR:\...'
                Actual   = $key
            })
        }
    }

    # Required: operator
    if (-not $Condition.ContainsKey('operator') -or [string]::IsNullOrWhiteSpace($Condition['operator'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.operator"
            Message  = 'Required field missing: operator'
            Expected = ($validOperators -join '|')
            Actual   = ($Condition['operator'] ?? $null)
        })
    }
    else {
        $operator = [string]$Condition['operator']
        if ($operator.ToLower() -notin $validOperators) {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.operator"
                Message  = "Invalid operator for registry condition: $operator"
                Expected = ($validOperators -join '|')
                Actual   = $operator
            })
        }
    }

    # Check for unknown fields
    foreach ($key in $Condition.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ConditionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateServiceCondition {
    <#
    .SYNOPSIS
        Validates a service condition.

    .DESCRIPTION
        Validates service condition required fields:
        - name (service name)
        - operator (exists|not_exists)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Condition,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid operators for service conditions
    $validOperators = @('exists', 'not_exists')

    # Known fields
    $knownFields = @('type', 'name', 'operator')

    # Required: name
    if (-not $Condition.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($Condition['name'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.name"
            Message  = 'Required field missing: name'
            Expected = 'string (service name)'
            Actual   = ($Condition['name'] ?? $null)
        })
    }

    # Required: operator
    if (-not $Condition.ContainsKey('operator') -or [string]::IsNullOrWhiteSpace($Condition['operator'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.operator"
            Message  = 'Required field missing: operator'
            Expected = ($validOperators -join '|')
            Actual   = ($Condition['operator'] ?? $null)
        })
    }
    else {
        $operator = [string]$Condition['operator']
        if ($operator.ToLower() -notin $validOperators) {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.operator"
                Message  = "Invalid operator for service condition: $operator"
                Expected = ($validOperators -join '|')
                Actual   = $operator
            })
        }
    }

    # Check for unknown fields
    foreach ($key in $Condition.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ConditionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}


function Test-TemplateTaskCondition {
    <#
    .SYNOPSIS
        Validates a task condition.

    .DESCRIPTION
        Validates task condition required fields:
        - name (task name, wildcards allowed)
        - operator (exists|not_exists)
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Condition,

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

    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()

    # Valid operators for task conditions
    $validOperators = @('exists', 'not_exists')

    # Known fields
    $knownFields = @('type', 'name', 'operator')

    # Required: name (wildcards * and ? allowed per Phase 18)
    if (-not $Condition.ContainsKey('name') -or [string]::IsNullOrWhiteSpace($Condition['name'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.name"
            Message  = 'Required field missing: name'
            Expected = 'string (task name, wildcards allowed)'
            Actual   = ($Condition['name'] ?? $null)
        })
    }

    # Required: operator
    if (-not $Condition.ContainsKey('operator') -or [string]::IsNullOrWhiteSpace($Condition['operator'])) {
        [void]$errors.Add(@{
            Path     = "$ConditionPath.operator"
            Message  = 'Required field missing: operator'
            Expected = ($validOperators -join '|')
            Actual   = ($Condition['operator'] ?? $null)
        })
    }
    else {
        $operator = [string]$Condition['operator']
        if ($operator.ToLower() -notin $validOperators) {
            [void]$errors.Add(@{
                Path     = "$ConditionPath.operator"
                Message  = "Invalid operator for task condition: $operator"
                Expected = ($validOperators -join '|')
                Actual   = $operator
            })
        }
    }

    # Check for unknown fields
    foreach ($key in $Condition.Keys) {
        if ($key -notin $knownFields) {
            [void]$warnings.Add("Unknown field '$ConditionPath.$key' - will be ignored")
        }
    }

    return @{ Errors = @($errors); Warnings = @($warnings) }
}
