#Requires -Version 7.4

function Import-vSEALPluginMetadata {
    <#
    .SYNOPSIS
        Parses and validates plugin.toml metadata files for Plugin Architecture v2.

    .DESCRIPTION
        Reads a plugin.toml file and validates its structure and metadata. Plugin TOML
        files define plugin identity, requirements, and configuration through the
        [plugin] and [plugin.requires] sections.

        The function performs:
        1. TOML parsing via ConvertFrom-Toml (PSToml module)
        2. Required field validation (name, version, category, phases, priority, description)
        3. Optional field extraction (critical, author, license, tags, links)
        4. Requirements section parsing ([plugin.requires])
        5. Unknown field detection (generates warnings, not errors)

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

    .PARAMETER Path
        Path to the plugin.toml file. Must have .toml extension.

    .PARAMETER ExpectedFolderName
        When specified, validates that the TOML 'name' field matches this value exactly.
        Used to enforce folder-name consistency (folder must match plugin name).

    .PARAMETER ValidateOnly
        When specified, returns the validation result object instead of throwing
        on validation errors. Useful for validation tools and testing.

    .OUTPUTS
        [PSCustomObject] Validation result with properties:
        - IsValid:   Boolean indicating whether plugin.toml is valid
        - Errors:    Array of error objects with Path, Message, Expected, Actual
        - Warnings:  Array of warning strings
        - Metadata:  Hashtable with parsed plugin metadata

    .EXAMPLE
        Import-vSEALPluginMetadata -Path 'C:\Plugins\defender\plugin.toml'

        Parses and validates the plugin metadata. Throws if validation fails.

    .EXAMPLE
        $result = Import-vSEALPluginMetadata -Path './plugin.toml' -ValidateOnly
        if (-not $result.IsValid) {
            $result.Errors | ForEach-Object { Write-Warning $_.Message }
        }

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

    .EXAMPLE
        $result = Import-vSEALPluginMetadata -Path './plugin.toml' -ExpectedFolderName 'defender' -ValidateOnly
        $result.Metadata.Name         # Plugin name
        $result.Metadata.Phases       # Supported phases
        $result.Metadata.Requires     # Requirements section

        Access parsed metadata and requirements for inspection.

    .NOTES
        Part of the Plugin Architecture v2 refactor.
        Plugin TOML is the single source of truth for plugin metadata.

        Required fields: name, version, category, phases, priority, description
        Optional fields: critical, author, license, tags, homepage, docs, repository, issues

        Requirements section ([plugin.requires]) supports:
        - min_vseal_version, max_vseal_version (SemVer)
        - min_build, max_build (Windows build integers)
        - os_types, platforms, architectures (string arrays)

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

        [Parameter()]
        [string]$ExpectedFolderName,

        [Parameter()]
        [switch]$ValidateOnly
    )

    # Initialize result structure
    $errors = [System.Collections.ArrayList]::new()
    $warnings = [System.Collections.ArrayList]::new()
    $metadata = @{
        Name        = $null
        Version     = $null
        Category    = $null
        Phases      = @()
        Priority    = $null
        Description = $null
        Critical    = $false
        Author      = $null
        License     = $null
        Tags        = @()
        Homepage    = $null
        Docs        = $null
        Repository  = $null
        Issues      = $null
        Requires    = @{
            MinVsealVersion = $null
            MaxVsealVersion = $null
            MinBuild        = 0
            MaxBuild        = 99999
            OsTypes         = @()
            Platforms       = @()
            Architectures   = @()
        }
    }
    $isValid = $true

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

    # Validate file exists
    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        throw [System.IO.FileNotFoundException]::new("Plugin metadata 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("Plugin metadata file must have .toml extension: $resolvedPath")
    }

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

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

        # ConvertFrom-Toml may return null on parse error without throwing
        if ($null -eq $tomlData) {
            $isValid = $false
            [void]$warnings.Add("TOML parse error in '$resolvedPath': Failed to parse TOML content - skipping plugin")
            return [PSCustomObject]@{
                IsValid  = $false
                Errors   = $errors.ToArray()
                Warnings = $warnings.ToArray()
                Metadata = $metadata
            }
        }
    }
    catch [System.IO.FileNotFoundException] {
        throw [System.IO.FileNotFoundException]::new("Plugin metadata file not found: $resolvedPath")
    }
    catch [System.IO.IOException] {
        throw [System.IO.IOException]::new("I/O error reading plugin metadata file '$resolvedPath': $($_.Exception.Message)")
    }
    catch [System.UnauthorizedAccessException] {
        throw [System.UnauthorizedAccessException]::new("Access denied reading plugin metadata file '$resolvedPath': $($_.Exception.Message)")
    }
    catch {
        # PSToml parsing errors - return warning per CONTEXT.md (skip plugin, don't block)
        $isValid = $false
        [void]$warnings.Add("TOML parse error in '$resolvedPath': $($_.Exception.Message) - skipping plugin")
        return [PSCustomObject]@{
            IsValid  = $false
            Errors   = $errors.ToArray()
            Warnings = $warnings.ToArray()
            Metadata = $metadata
        }
    }

    # ===== HELPER FUNCTION =====

    # Helper to check key existence in OrderedDictionary or Hashtable
    $hasKey = {
        param($dict, $key)
        if ($null -eq $dict) { return $false }
        if ($dict -is [System.Collections.Specialized.OrderedDictionary]) {
            return $dict.Contains($key)
        }
        if ($dict -is [System.Collections.IDictionary]) {
            return $dict.ContainsKey($key)
        }
        return $false
    }

    # ===== [plugin] SECTION VALIDATION =====

    $hasPluginSection = & $hasKey $tomlData 'plugin'

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

        # ===== REQUIRED FIELDS =====

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

            # Validate name matches expected folder name if provided
            if (-not [string]::IsNullOrEmpty($ExpectedFolderName)) {
                if ($metadata.Name -cne $ExpectedFolderName) {
                    $isValid = $false
                    [void]$errors.Add(@{
                        Path     = 'plugin.name'
                        Message  = "Folder name '$ExpectedFolderName' doesn't match plugin name '$($metadata.Name)'"
                        Expected = $ExpectedFolderName
                        Actual   = $metadata.Name
                    })
                }
            }
        }

        # Required: version (SemVer strict X.Y.Z)
        if (-not (& $hasKey $plugin 'version') -or [string]::IsNullOrWhiteSpace($plugin['version'])) {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'plugin.version'
                Message  = 'Required field missing: version'
                Expected = 'string (SemVer X.Y.Z)'
                Actual   = ($plugin['version'] ?? $null)
            })
        }
        else {
            $versionStr = [string]$plugin['version']
            try {
                $semVer = [System.Management.Automation.SemanticVersion]::new($versionStr)
                $metadata.Version = $versionStr
            }
            catch {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'plugin.version'
                    Message  = "Invalid SemVer format: '$versionStr'"
                    Expected = 'string (SemVer X.Y.Z)'
                    Actual   = $versionStr
                })
            }
        }

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

        # Required: phases (non-empty array of lowercase strings)
        if (-not (& $hasKey $plugin 'phases')) {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'plugin.phases'
                Message  = 'Required field missing: phases'
                Expected = 'array of strings'
                Actual   = $null
            })
        }
        else {
            $phases = $plugin['phases']
            if ($phases -isnot [System.Collections.IEnumerable] -or $phases -is [string]) {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'plugin.phases'
                    Message  = 'Invalid type for phases'
                    Expected = 'array of strings'
                    Actual   = $phases.GetType().Name
                })
            }
            else {
                $phaseArray = @($phases | ForEach-Object { [string]$_ })
                if ($phaseArray.Count -eq 0) {
                    $isValid = $false
                    [void]$errors.Add(@{
                        Path     = 'plugin.phases'
                        Message  = 'Phases array cannot be empty'
                        Expected = 'non-empty array'
                        Actual   = '[]'
                    })
                }
                else {
                    # Validate each phase is known (seal, engage)
                    $knownPhases = @('seal', 'engage')
                    $validPhases = [System.Collections.ArrayList]::new()
                    foreach ($phase in $phaseArray) {
                        if ($phase -cnotin $knownPhases) {
                            $isValid = $false
                            [void]$errors.Add(@{
                                Path     = 'plugin.phases'
                                Message  = "Unknown phase '$phase'"
                                Expected = 'seal, engage'
                                Actual   = $phase
                            })
                        }
                        elseif ($phase -notin $validPhases) {
                            # Dedupe silently
                            [void]$validPhases.Add($phase)
                        }
                    }
                    $metadata.Phases = $validPhases.ToArray()
                }
            }
        }

        # Required: priority (integer)
        if (-not (& $hasKey $plugin 'priority')) {
            $isValid = $false
            [void]$errors.Add(@{
                Path     = 'plugin.priority'
                Message  = 'Required field missing: priority'
                Expected = 'integer'
                Actual   = $null
            })
        }
        else {
            $priority = $plugin['priority']
            if ($priority -is [long] -or $priority -is [int]) {
                $metadata.Priority = [int]$priority
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'plugin.priority'
                    Message  = 'Invalid type for priority'
                    Expected = 'integer'
                    Actual   = $priority.GetType().Name
                })
            }
        }

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

        # ===== OPTIONAL FIELDS =====

        # Optional: critical (boolean, default false)
        if (& $hasKey $plugin 'critical') {
            $critical = $plugin['critical']
            if ($critical -is [bool]) {
                $metadata.Critical = $critical
            }
            else {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'plugin.critical'
                    Message  = 'Invalid type for critical'
                    Expected = 'boolean'
                    Actual   = $critical.GetType().Name
                })
            }
        }

        # Optional: author (string)
        if (& $hasKey $plugin 'author') {
            $metadata.Author = [string]$plugin['author']
        }

        # Optional: license (string)
        if (& $hasKey $plugin 'license') {
            $metadata.License = [string]$plugin['license']
        }

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

        # Optional: homepage (string)
        if (& $hasKey $plugin 'homepage') {
            $metadata.Homepage = [string]$plugin['homepage']
        }

        # Optional: docs (string)
        if (& $hasKey $plugin 'docs') {
            $metadata.Docs = [string]$plugin['docs']
        }

        # Optional: repository (string)
        if (& $hasKey $plugin 'repository') {
            $metadata.Repository = [string]$plugin['repository']
        }

        # Optional: issues (string)
        if (& $hasKey $plugin 'issues') {
            $metadata.Issues = [string]$plugin['issues']
        }

        # ===== [plugin.requires] SECTION =====

        if (& $hasKey $plugin 'requires') {
            $requires = $plugin['requires']

            if ($requires -isnot [System.Collections.IDictionary]) {
                $isValid = $false
                [void]$errors.Add(@{
                    Path     = 'plugin.requires'
                    Message  = 'Invalid type for requires section'
                    Expected = 'table'
                    Actual   = $requires.GetType().Name
                })
            }
            else {
                # min_vseal_version (SemVer string)
                if (& $hasKey $requires 'min_vseal_version') {
                    $minVer = [string]$requires['min_vseal_version']
                    try {
                        $null = [System.Management.Automation.SemanticVersion]::new($minVer)
                        $metadata.Requires.MinVsealVersion = $minVer
                    }
                    catch {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = 'plugin.requires.min_vseal_version'
                            Message  = "Invalid SemVer format: '$minVer'"
                            Expected = 'string (SemVer X.Y.Z)'
                            Actual   = $minVer
                        })
                    }
                }

                # max_vseal_version (SemVer string)
                if (& $hasKey $requires 'max_vseal_version') {
                    $maxVer = [string]$requires['max_vseal_version']
                    try {
                        $null = [System.Management.Automation.SemanticVersion]::new($maxVer)
                        $metadata.Requires.MaxVsealVersion = $maxVer
                    }
                    catch {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = 'plugin.requires.max_vseal_version'
                            Message  = "Invalid SemVer format: '$maxVer'"
                            Expected = 'string (SemVer X.Y.Z)'
                            Actual   = $maxVer
                        })
                    }
                }

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

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

                # os_types (string array from known values)
                if (& $hasKey $requires 'os_types') {
                    $osTypes = $requires['os_types']
                    if ($osTypes -is [System.Collections.IEnumerable] -and $osTypes -isnot [string]) {
                        $knownOsTypes = @('workstation', 'server', 'multisession')
                        $validOsTypes = [System.Collections.ArrayList]::new()
                        foreach ($osType in $osTypes) {
                            $osTypeStr = [string]$osType
                            if ($osTypeStr -cnotin $knownOsTypes) {
                                [void]$warnings.Add("Unknown os_type '$osTypeStr' in plugin.requires.os_types - may be from newer vSEAL version")
                            }
                            if ($osTypeStr -notin $validOsTypes) {
                                [void]$validOsTypes.Add($osTypeStr)
                            }
                        }
                        $metadata.Requires.OsTypes = $validOsTypes.ToArray()
                    }
                    else {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = 'plugin.requires.os_types'
                            Message  = 'Invalid type for os_types'
                            Expected = 'array of strings'
                            Actual   = $osTypes.GetType().Name
                        })
                    }
                }

                # platforms (string array from known values)
                if (& $hasKey $requires 'platforms') {
                    $platforms = $requires['platforms']
                    if ($platforms -is [System.Collections.IEnumerable] -and $platforms -isnot [string]) {
                        $knownPlatforms = @('vmware', 'hyperv', 'nutanix', 'citrix', 'azure', 'physical')
                        $validPlatforms = [System.Collections.ArrayList]::new()
                        foreach ($platform in $platforms) {
                            $platformStr = [string]$platform
                            if ($platformStr -cnotin $knownPlatforms) {
                                [void]$warnings.Add("Unknown platform '$platformStr' in plugin.requires.platforms - may be from newer vSEAL version")
                            }
                            if ($platformStr -notin $validPlatforms) {
                                [void]$validPlatforms.Add($platformStr)
                            }
                        }
                        $metadata.Requires.Platforms = $validPlatforms.ToArray()
                    }
                    else {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = 'plugin.requires.platforms'
                            Message  = 'Invalid type for platforms'
                            Expected = 'array of strings'
                            Actual   = $platforms.GetType().Name
                        })
                    }
                }

                # architectures (string array from known values)
                if (& $hasKey $requires 'architectures') {
                    $architectures = $requires['architectures']
                    if ($architectures -is [System.Collections.IEnumerable] -and $architectures -isnot [string]) {
                        $knownArchitectures = @('x64', 'arm64')
                        $validArchitectures = [System.Collections.ArrayList]::new()
                        foreach ($arch in $architectures) {
                            $archStr = [string]$arch
                            if ($archStr -cnotin $knownArchitectures) {
                                [void]$warnings.Add("Unknown architecture '$archStr' in plugin.requires.architectures - may be from newer vSEAL version")
                            }
                            if ($archStr -notin $validArchitectures) {
                                [void]$validArchitectures.Add($archStr)
                            }
                        }
                        $metadata.Requires.Architectures = $validArchitectures.ToArray()
                    }
                    else {
                        $isValid = $false
                        [void]$errors.Add(@{
                            Path     = 'plugin.requires.architectures'
                            Message  = 'Invalid type for architectures'
                            Expected = 'array of strings'
                            Actual   = $architectures.GetType().Name
                        })
                    }
                }

                # Check for unknown requires fields
                $knownRequiresFields = @('min_vseal_version', 'max_vseal_version', 'min_build', 'max_build', 'os_types', 'platforms', 'architectures')
                foreach ($key in $requires.Keys) {
                    if ($key -notin $knownRequiresFields) {
                        [void]$warnings.Add("Unknown field 'plugin.requires.$key' - may be from newer vSEAL version")
                    }
                }
            }
        }

        # ===== UNKNOWN FIELD DETECTION =====

        # Check for unknown plugin fields
        $knownPluginFields = @('name', 'version', 'category', 'phases', 'priority', 'description', 'critical', 'author', 'license', 'tags', 'homepage', 'docs', 'repository', 'issues', 'requires')
        foreach ($key in $plugin.Keys) {
            if ($key -notin $knownPluginFields) {
                [void]$warnings.Add("Unknown field 'plugin.$key' - may be from newer vSEAL version")
            }
        }
    }

    # Check for unknown top-level sections
    $knownTopLevelSections = @('plugin')
    foreach ($key in $tomlData.Keys) {
        if ($key -notin $knownTopLevelSections) {
            [void]$warnings.Add("Unknown section '[$key]' - may be from newer vSEAL version")
        }
    }

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

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

    # Throw if invalid and not ValidateOnly
    if (-not $ValidateOnly -and -not $isValid) {
        $errorMessages = $errors | ForEach-Object {
            if ($_ -is [hashtable] -or $_ -is [System.Collections.IDictionary]) {
                "$($_['Path']): $($_['Message'])"
            }
            else {
                [string]$_
            }
        }
        $combinedMessage = "Plugin metadata validation failed for '$resolvedPath':`n" + ($errorMessages -join "`n")
        throw [System.InvalidOperationException]::new($combinedMessage)
    }

    return $result
}
