Core/Logging.psm1

<#
    .SYNOPSIS
    CCF Logging Engine - Professional & Decoupled
     
    .DESCRIPTION
    Motor de logging para CCF. Permite registrar mensajes en consola y archivo.
    Implementa un sistema de proveedores para que la UI se conecte dinámicamente.
#>


$script:LogProvider = $null
$script:CurrentLogFile = $null
$script:LogBuffer = New-Object System.Collections.Generic.List[PSCustomObject]
$script:MaxLogSizeMB = 10
$script:LogLevels = @{ "DEBUG" = 0; "INFO" = 1; "WARN" = 2; "SUCCESS" = 2; "ERROR" = 3; "CRITICAL" = 4 }
$script:CurrentLogLevel = 1 # Por defecto INFO
$script:JsonOutput = $false

# --- Configuración de Nivel ---
function Set-CCFLogLevel {
    param([ValidateSet("DEBUG", "INFO", "WARN", "ERROR", "CRITICAL")]$Level)
    $script:CurrentLogLevel = $script:LogLevels[$Level]
}

# --- Registro de Proveedores ---
function Register-CCFLogProvider {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]$ProviderScript
    )
    $script:LogProvider = $ProviderScript
    Write-CCFLog "Log Provider registrado correctamente." -Level "SUCCESS"
}

# --- Motor de Sanitización (Análisis de Entropía) ---
function Test-CCFEntropyContent {
    param([string]$String)
    if ($null -eq $String -or $String.Length -lt 20 -or $String -match "\s") { return $false }
    if ($String -match "[\\/:]") { return $false }

    $hasUpper = $String -match "[A-Z]"
    $hasLower = $String -match "[a-z]"
    $hasNum = $String -match "[0-9]"
    $hasSpecial = $String -match "[-_+=]"
    
    $typeCount = 0
    if ($hasUpper) { $typeCount++ }
    if ($hasLower) { $typeCount++ }
    if ($hasNum) { $typeCount++ }
    if ($hasSpecial) { $typeCount++ }
    
    return ($typeCount -ge 3 -or ($typeCount -ge 2 -and $String.Length -gt 32))
}

function Get-CCFSecretsRegex {
    return "(?i)(password|passwd|pwd|token|api_key|secret|authorization|auth_token|apikey)"
}

function Invoke-CCFEntropyScan {
    param([string]$InputString)
    if ($null -eq $InputString) { return "" }
    $words = $InputString -split "\s+"
    $newWords = foreach ($word in $words) {
        $cleanWord = $word -replace "[,;.:]$", ""
        if (Test-CCFEntropyContent -String $cleanWord) {
            "[REDACTED-ENTROPY]"
        }
        else {
            $word
        }
    }
    return $newWords -join " "
}

function Protect-CCFSensitiveData {
    param(
        [Parameter(Mandatory = $true)][object]$InputData,
        [int]$Depth = 0
    )
    
    if ($Depth -gt 5 -or $null -eq $InputData) { return $InputData }
    
    $secretsRegex = Get-CCFSecretsRegex
    
    if ($InputData -is [string]) {
        if ($InputData -match "$secretsRegex\s*[:=]\s*(\S+)") {
            $InputData = $InputData -replace "$secretsRegex\s*[:=]\s*(\S+)", '$1=[REDACTED]'
        }
        
        if ($InputData.Length -lt 2000) {
            return Invoke-CCFEntropyScan -InputString $InputData
        }
        return $InputData
    }
    elseif ($InputData -is [hashtable] -or $InputData -is [PSCustomObject]) {
        if ($null -ne $InputData -and ($InputData.GetType().FullName -match "System\.Reflection|System\.Management|System\.Runtime")) {
            return $InputData.ToString()
        }

        $newData = if ($InputData -is [hashtable]) { @{} } else { [PSCustomObject]@{} }
        $props = if ($InputData -is [hashtable]) { $InputData.Keys } else { $InputData.PSObject.Properties.Name }
        
        foreach ($prop in $props) {
            if ($prop -eq "PSObject" -or $prop -eq "PSStandardMembers") { continue }
            $val = try { if ($InputData -is [hashtable]) { $InputData[$prop] } else { $InputData.$prop } } catch { $null }
            
            $isSecret = $prop -match $secretsRegex
            if ($val -is [string] -and (Test-CCFEntropyContent -String $val)) { $isSecret = $true }
            
            if ($isSecret) {
                if ($InputData -is [hashtable]) { $newData[$prop] = "[REDACTED]" } else { $newData | Add-Member -NotePropertyName $prop -NotePropertyValue "[REDACTED]" -Force }
            }
            elseif ($val -is [hashtable] -or $val -is [PSCustomObject]) {
                $redactedVal = Protect-CCFSensitiveData -InputData $val -Depth ($Depth + 1)
                if ($InputData -is [hashtable]) { $newData[$prop] = $redactedVal } else { $newData | Add-Member -NotePropertyName $prop -NotePropertyValue $redactedVal -Force }
            }
            else {
                if ($InputData -is [hashtable]) { $newData[$prop] = $val } else { $newData | Add-Member -NotePropertyName $prop -NotePropertyValue $val -Force }
            }
        }
        return $newData
    }
    return $InputData
}

# --- Salud e Integración ---
$script:LastProviderHeartbeat = $null

function Test-CCFHealth {
    [CmdletBinding()]
    param()
    
    $report = [ordered]@{
        Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
        Status    = "OK"
        Checks    = @()
    }

    $coreModules = @("Logging", "Configuration", "Execution", "Plugins")
    foreach ($mod in $coreModules) {
        $loaded = $null -ne (Get-Module -Name $mod)
        $report.Checks += @{ Target = "Module:$mod"; Pass = $loaded }
        if (-not $loaded) { $report.Status = "DEGRADED" }
    }

    return [PSCustomObject]$report
}

function Submit-CCFHeartbeat {
    [CmdletBinding()]
    param()
    $script:LastProviderHeartbeat = Get-Date
}

# --- Motor de Logging ---

function Initialize-CCFLogger {
    param(
        [string]$FileName = "CCF_Session.log",
        [int]$MaxMB = 10,
        [int]$MaxRolling = 5
    )
    
    $script:CCFSystemMeta = [ordered]@{
        Hostname = $env:COMPUTERNAME
        Version  = "1.3.0"
        Engine   = "ArgosCCF"
    }

    $logDir = Get-CCFPath -Target "Logs"
    if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
    
    $script:CurrentLogFile = Join-Path $logDir $FileName
    $script:MaxLogSizeMB = $MaxMB
    
    if (Test-Path $script:CurrentLogFile) {
        $size = (Get-Item $script:CurrentLogFile).Length / 1MB
        if ($size -gt $script:MaxLogSizeMB) {
            for ($i = $MaxRolling - 1; $i -ge 1; $i--) {
                $old = "$($script:CurrentLogFile).$i"
                $new = "$($script:CurrentLogFile).$($i + 1)"
                if (Test-Path $old) { Move-Item -Path $old -Destination $new -Force }
            }
            Move-Item -Path $script:CurrentLogFile -Destination "$($script:CurrentLogFile).1" -Force
        }
    }
}

function Write-CCFLog {
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Message,
        [ValidateSet("INFO", "WARN", "ERROR", "SUCCESS", "CRITICAL", "DEBUG")]
        [string]$Level = "INFO",
        [string]$Color = "White",
        [hashtable]$Data = @{},
        [string]$Category = "System",
        [switch]$NoBuffer
    )
    
    $msgLevel = $script:LogLevels[$Level]
    if ($msgLevel -lt $script:CurrentLogLevel) { return }

    $cleanMessage = Protect-CCFSensitiveData -InputData $Message
    $cleanData = Protect-CCFSensitiveData -InputData $Data
    $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ"
    $rsId = if ($null -ne [runspace]::DefaultRunspace) { [runspace]::DefaultRunspace.Id } else { 1 }
    
    $logEntry = [ordered]@{
        Timestamp = $timestamp
        Level     = $Level
        Category  = $Category
        Message   = $cleanMessage
        Data      = $cleanData
        Context   = @{ RSID = $rsId }
    }

    # Consola / Provider
    if ($null -ne $script:LogProvider) {
        try {
            Invoke-Command -ScriptBlock $script:LogProvider -ArgumentList @{ Entry = $logEntry; Color = $Color } -ErrorAction Stop
        }
        catch {
            Write-Host "[FALLBACK] [$Level] $cleanMessage" -ForegroundColor Yellow
        }
    }
    else {
        $fColor = switch ($Level) {
            "SUCCESS" { "Green" }
            "WARN" { "Yellow" }
            "ERROR" { "Red" }
            "CRITICAL" { "Magenta" }
            "DEBUG" { "Gray" }
            default { $Color }
        }
        Write-Host "[$timestamp] [$Level] [$Category] $cleanMessage" -ForegroundColor $fColor
    }
    
    # Archivo
    $targetFile = $script:CurrentLogFile
    if ($Category -ne "System") {
        $logDir = Get-CCFPath -Target "Logs"
        if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
        $targetFile = Join-Path $logDir "CCF_${Category}.log"
    }

    if ($null -ne $targetFile) {
        Write-Host "[DEBUG-LOG] Target: '$targetFile'" -ForegroundColor Cyan
        $outMsg = if ($script:JsonOutput) { $logEntry | ConvertTo-Json -Depth 5 -Compress } else { "[$timestamp] [$Level] [$Category] $cleanMessage" }
        try {
            $parent = Split-Path $targetFile -Parent
            if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
            $outMsg | Out-File -FilePath $targetFile -Append -Encoding UTF8 -ErrorAction Stop
        }
        catch {
            # Evitar bucle infinito de logs si falla el archivo
        }
    }
}

function Write-CCFErrorRecord {
    param([Parameter(Mandatory = $true)][System.Management.Automation.ErrorRecord]$ErrorRecord, [string]$Context = "General")
    $data = [ordered]@{ Exception = $ErrorRecord.Exception.GetType().Name; StackTrace = $ErrorRecord.ScriptStackTrace }
    Write-CCFLog -Message "[$Context] $($ErrorRecord.Exception.Message)" -Level "ERROR" -Category "Errors" -Data $data
}

function Write-CCFLogInfo ($m, $d = @{}) { Write-CCFLog $m -Level "INFO" -Data $d }
function Write-CCFLogSuccess ($m, $d = @{}) { Write-CCFLog $m -Level "SUCCESS" -Data $d }
function Write-CCFLogWarn ($m, $d = @{}) { Write-CCFLog $m -Level "WARN" -Data $d }
function Write-CCFLogError ($m, $d = @{}) { Write-CCFLog $m -Level "ERROR" -Data $d }
function Write-CCFLogCritical ($m, $d = @{}) { Write-CCFLog $m -Level "CRITICAL" -Data $d }
function Write-CCFLogDebug ($m, $d = @{}) { Write-CCFLog $m -Level "DEBUG" -Data $d }
function Write-CCFLogHeader ($m) { Write-CCFLog $m -Level "INFO" -Color "Cyan" }

# Alias para Bridge
New-Alias -Name Log-Info -Value Write-CCFLogInfo -Description "Alias para Write-CCFLogInfo"
New-Alias -Name Log-Success -Value Write-CCFLogSuccess
New-Alias -Name Log-Warn -Value Write-CCFLogWarn
New-Alias -Name Log-Error -Value Write-CCFLogError
New-Alias -Name Log-Critical -Value Write-CCFLogCritical
New-Alias -Name Log-Debug -Value Write-CCFLogDebug
New-Alias -Name Log-Header -Value Write-CCFLogHeader
New-Alias -Name Init-CCFLogger -Value Initialize-CCFLogger
New-Alias -Name Catch-CCFError -Value Write-CCFErrorRecord
New-Alias -Name Redact-CCFSensitiveData -Value Protect-CCFSensitiveData

Export-ModuleMember -Function Set-CCFLogLevel, Register-CCFLogProvider, Initialize-CCFLogger, Write-CCFLog, Write-CCFErrorRecord, `
    Write-CCFLogInfo, Write-CCFLogSuccess, Write-CCFLogWarn, Write-CCFLogError, Write-CCFLogCritical, Write-CCFLogDebug, Write-CCFLogHeader, `
    Test-CCFHealth, Submit-CCFHeartbeat, Protect-CCFSensitiveData -Alias *