Core/Plugins.psm1

# --- Descubrimiento de Plugins ---
function Confirm-CCFPluginSignature {
    param([Parameter(Mandatory = $true)][string]$Path)
    
    # Asegurar que el archivo existe
    if (-not (Test-Path $Path)) { return @{ Valid = $false; Status = "FileNotFound" } }

    $sig = Get-AuthenticodeSignature -FilePath $Path
    # En desarrollo, "Valid" es lo ideal, pero "UnknownError" suele indicar falta de cadena de confianza (cert auto-firmado)
    # Sin embargo, para v1.3 exigiremos "Valid" para ser estrictos.
    if ($sig.Status -eq "Valid") {
        return @{ Valid = $true; Signer = $sig.SignerCertificate.Subject }
    }
    return @{ Valid = $false; Status = $sig.Status.ToString() }
}

function Get-CCFPlugins {
    param(
        [string]$Path = (Join-Path (Get-CCFPath -Target "Root") "Plugins")
    )
    
    if (-not (Test-Path $Path)) { return @() }
    
    $plugins = Get-ChildItem -Path $Path -Filter "*.ps1" | ForEach-Object {
        $file = $_
        $plugin = [PSCustomObject]@{
            Name        = $null
            Description = "Sin descripción"
            Version     = "1.0.0"
            Author      = "Unknown"
            Path        = $file.FullName
            Signed      = (Confirm-CCFPluginSignature -Path $file.FullName).Valid
            Manifest    = $null
        }

        # 1. Buscar Manifiesto (ccf_plugin.json)
        $manifestPath = Join-Path $file.DirectoryName "ccf_plugin.json"
        if (Test-Path $manifestPath) {
            try {
                $manifestContent = Get-Content $manifestPath -Raw
                if ($manifestContent) {
                    $manifest = $manifestContent | ConvertFrom-Json
                    $plugin.Name = $manifest.Name
                    $plugin.Version = $manifest.Version
                    $plugin.Author = $manifest.Author
                    $plugin.Manifest = $manifest
                    if ($manifest.Description) { $plugin.Description = $manifest.Description }
                }
            }
            catch {
                Log-Warn "Error al cargar manifiesto en $($file.Name): $($_.Exception.Message)"
            }
        }

        # 2. Fallback a metadatos incrustados (Legacy)
        $content = Get-Content $file.FullName -Raw
        if ($null -eq $plugin.Name) {
            if ($content -match "\.NAME\s+(.+)") { $plugin.Name = $matches[1].Trim() }
            elseif ($content -match "CCF_PLUGIN_NAME:\s*(.+)") { $plugin.Name = $matches[1].Trim() }
            else { $plugin.Name = $file.BaseName }
        }

        if ($plugin.Description -eq "Sin descripción") {
            if ($content -match "\.DESCRIPTION\s+(.+)") { $plugin.Description = $matches[1].Trim() }
            elseif ($content -match "CCF_PLUGIN_DESC:\s*(.+)") { $plugin.Description = $matches[1].Trim() }
        }

        if ($content -match "\.VERSION\s+(.+)") { $plugin.Version = $matches[1].Trim() }
        elseif ($content -match "CCF_PLUGIN_VERSION:\s*(.+)") { $plugin.Version = $matches[1].Trim() }

        if ($content -match "\.AUTHOR\s+(.+)") { $plugin.Author = $matches[1].Trim() }
        elseif ($content -match "CCF_PLUGIN_AUTHOR:\s*(.+)") { $plugin.Author = $matches[1].Trim() }
            
        $plugin
    }
    
    return $plugins
}

# --- Ejecución de Plugins con Ciclo de Vida y Sandboxing ---
function Invoke-CCFPlugin {
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Plugin,
        
        [hashtable]$Arguments = @{},
        
        [switch]$SkipHooks,
        
        [switch]$HardenedMode # Forzar ConstrainedLanguage
    )
    
    # 1. SEGURIDAD: Validación de Ubicación (Anti-Inyección)
    $allowedPluginsPath = Get-CCFPath -Target "Plugins"
    if ($Plugin.Path -notlike "$allowedPluginsPath*") {
        Log-Critical "BLOQUEO DE SEGURIDAD: Intento de cargar plugin desde ubicación no autorizada: $($Plugin.Path)"
        return $null
    }

    # 2. SEGURIDAD: Validación de Firma (v1.3)
    $requireSigned = $false
    if (Get-Command "Get-CCFConfig" -ErrorAction SilentlyContinue) {
        $coreConfig = Get-CCFConfig -ConfigName "ccf_core"
        $requireSigned = $coreConfig.Security.RequireSignedPlugins -eq $true
    }

    if ($requireSigned) {
        $sigResult = Confirm-CCFPluginSignature -Path $Plugin.Path
        if (-not $sigResult.Valid) {
            Log-Critical "BLOQUEO DE SEGURIDAD: El plugin $($Plugin.Name) no tiene una firma válida y la política RequireSignedPlugins está activa."
            return $null
        }
        Log-Info "Firma válida detectada: $($sigResult.Signer)"
    }

    # 3. SEGURIDAD: Configuración de Sandboxing
    $rs = $null
    if ($HardenedMode) {
        Log-Warn "Activando Sandboxing (ConstrainedLanguage) para $($Plugin.Name)"
        # Usar CreateRestricted para evitar CUALQUIER comando o proveedor por defecto
        # Solo permitimos Comandos básicos de lenguaje.
        $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateRestricted([System.Management.Automation.SessionCapabilities]::Language)
        $iss.LanguageMode = [System.Management.Automation.PSLanguageMode]::ConstrainedLanguage
        $rs = [runspacefactory]::CreateRunspace($iss)
    }

    Log-Info "Preparando ejecución de plugin: $($Plugin.Name)"
    
    # Crear un scope aislado para el plugin
    $scriptText = Get-Content $Plugin.Path -Raw
    
    try {
        # 3. Hook: Before-Execute
        if (-not $SkipHooks) {
            if ($scriptText -match "function\s+Before-Execute") {
                Log-Debug "Ejecutando hook Before-Execute..."
                # Nota: Los hooks se ejecutan en el Runspace LOCAL por ahora para interactuar con CCF.
                # Si el plugin es malicioso, el sandbox central lo atrapará en la fase principal.
                . $Plugin.Path
                if (Get-Command "Before-Execute" -ErrorAction SilentlyContinue) {
                    Before-Execute -Arguments $Arguments
                }
            }
        }

        # 4. Ejecución Principal
        $result = $null
        if ($HardenedMode) {
            $rs.Open()
            $ps = [powershell]::Create().AddScript($scriptText)
            $ps.AddParameter("Arguments", $Arguments)
            $ps.Runspace = $rs
            $result = $ps.Invoke()
            # Capturar errores del runspace
            if ($ps.HadErrors) {
                # Lanzamos una excepción para que Catch-CCFError la capture
                throw $ps.Streams.Error[0].Exception
            }
            $rs.Close()
            # Aplanar resultado para consistencia (Invoke retorna Colección)
            if ($null -ne $result -and $result.Count -eq 1) { $result = $result[0] }
            elseif ($null -ne $result -and $result.Count -eq 0) { $result = $null }
        }
        else {
            $scriptBlock = [scriptblock]::Create($scriptText)
            $result = & $scriptBlock -Arguments $Arguments
        }
        
        # 5. Hook: After-Execute
        if (-not $SkipHooks) {
            if ($scriptText -match "function\s+After-Execute") {
                Log-Debug "Ejecutando hook After-Execute..."
                if (-not (Get-Command "After-Execute" -ErrorAction SilentlyContinue)) { . $Plugin.Path }
                if (Get-Command "After-Execute" -ErrorAction SilentlyContinue) {
                    After-Execute -Result $result
                }
            }
        }

        Log-Success "Plugin $($Plugin.Name) finalizado con éxito."
        return $result
    }
    catch {
        Write-CCFErrorRecord -ErrorRecord $_ -Context "PluginExecution:$($Plugin.Name)"
        return $null
    }
}

# --- Alias de Retrocompatibilidad ---
New-Alias -Name Test-CCFPluginSignature -Value Confirm-CCFPluginSignature

Export-ModuleMember -Function Get-CCFPlugins, Invoke-CCFPlugin -Alias *