#Requires -Version 7.4

# Script-scope state (initialized by Initialize-vSEALLogger)
$script:LoggerInitialized = $false
$script:LogConfig = @{
    ConsoleLevel   = 'INFO'
    FileLevel      = 'DEBUG'
    ConsoleContext = 'auto'
    LogPath        = ''
    LogFilePath    = ''
}

# Streaming mode state (initialized by Initialize-vSEALLogger)
# Enables NDJSON event emission for Go CLI communication
$script:StreamConfig = @{
    Mode    = $null    # null, 'stdout', or 'stderr'
    Enabled = $false   # true if Mode is 'stdout' or 'stderr'
    OpId    = ''       # 8-char hex operation identifier
    Phase   = ''       # lowercase phase name (seal, engage, analyze, rollback)
}

# Level hierarchy for filtering
$script:LevelOrder = @{
    'DEBUG'   = 0
    'VERBOSE' = 1
    'INFO'    = 2
    'SUCCESS' = 3
    'WARNING' = 4
    'ERROR'   = 5
    'FATAL'   = 6
}

# Sensitive data patterns for redaction
$script:SensitivePatterns = @(
    # API keys (various formats)
    @{ Pattern = '(?i)(api[_-]?key|apikey)[=:"\s]+([a-zA-Z0-9_\-]{16,})'; Replace = '$1=***REDACTED***' }
    @{ Pattern = '(?i)(x-api-key|authorization)[=:"\s]+([a-zA-Z0-9_\-\.]+)'; Replace = '$1=***REDACTED***' }

    # Tokens (Bearer, JWT, OAuth)
    @{ Pattern = '(?i)(bearer|token|access[_-]?token|refresh[_-]?token)[=:"\s]+([a-zA-Z0-9_\-\.]+)'; Replace = '$1=***REDACTED***' }
    @{ Pattern = '(?i)eyJ[a-zA-Z0-9_\-]+\.eyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+'; Replace = '***JWT_REDACTED***' }

    # Passwords
    @{ Pattern = '(?i)(password|passwd|pwd|secret)[=:"\s]+([^\s"'',;]+)'; Replace = '$1=***REDACTED***' }

    # Connection strings
    @{ Pattern = '(?i)(connectionstring|conn[_-]?str)[=:"\s]+([^"'';\n]+)'; Replace = '$1=***REDACTED***' }
    @{ Pattern = '(?i)(Server|Data Source)=[^;]+;.*?(Password|Pwd)=[^;]+'; Replace = '***CONNECTION_STRING_REDACTED***' }

    # Azure/AWS keys
    @{ Pattern = '(?i)(azure[_-]?key|aws[_-]?secret|aws[_-]?access[_-]?key)[=:"\s]+([a-zA-Z0-9/+]{20,})'; Replace = '$1=***REDACTED***' }

    # Client secrets
    @{ Pattern = '(?i)(client[_-]?secret|app[_-]?secret)[=:"\s]+([a-zA-Z0-9_\-\.]{16,})'; Replace = '$1=***REDACTED***' }

    # Certificates and keys (base64 encoded)
    @{ Pattern = '-----BEGIN [A-Z ]+-----[\s\S]+?-----END [A-Z ]+-----'; Replace = '***CERTIFICATE_REDACTED***' }
)

function Redact-SensitiveData {
    <#
    .SYNOPSIS
        Redacts sensitive data from a string using pattern matching.
    .DESCRIPTION
        Scans the input string for patterns matching API keys, tokens, passwords,
        connection strings, and other sensitive data. Replaces matches with
        redaction markers.
    #>
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Text
    )

    if ([string]::IsNullOrEmpty($Text)) {
        return $Text
    }

    $result = $Text
    foreach ($item in $script:SensitivePatterns) {
        $result = $result -replace $item.Pattern, $item.Replace
    }

    return $result
}

function Redact-Hashtable {
    <#
    .SYNOPSIS
        Recursively redacts sensitive data from a hashtable.
    .DESCRIPTION
        Traverses all string values in a hashtable (including nested hashtables
        and arrays) and applies sensitive data redaction.
    #>
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Data
    )

    $redacted = @{}

    foreach ($key in $Data.Keys) {
        $value = $Data[$key]

        if ($null -eq $value) {
            $redacted[$key] = $null
        }
        elseif ($value -is [string]) {
            $redacted[$key] = Redact-SensitiveData -Text $value
        }
        elseif ($value -is [hashtable]) {
            $redacted[$key] = Redact-Hashtable -Data $value
        }
        elseif ($value -is [array]) {
            $redacted[$key] = @($value | ForEach-Object {
                if ($_ -is [string]) {
                    Redact-SensitiveData -Text $_
                }
                elseif ($_ -is [hashtable]) {
                    Redact-Hashtable -Data $_
                }
                else {
                    $_
                }
            })
        }
        else {
            $redacted[$key] = $value
        }
    }

    return $redacted
}

function Write-JsonLogEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [Parameter(Mandatory)]
        [string]$Level,

        [string]$CalledFrom
    )

    $entry = [ordered]@{
        timestamp  = (Get-Date).ToString('o')
        level      = $Level
        message    = $Message
        calledFrom = $CalledFrom
    }

    $json = $entry | ConvertTo-Json -Compress
    Add-Content -Path $script:LogConfig.LogFilePath -Value $json -Encoding utf8
}

function Get-LogSource {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $callStack = Get-PSCallStack

    # Skip frames: [0]=Get-LogSource, [1]=Write-vSEALLog, [2]=actual caller
    $callerIndex = 2
    if ($callStack.Count -lt 3) {
        return 'unknown'
    }

    $caller = $callStack[$callerIndex]

    # Case 1: Caller has a ScriptName (normal .ps1 file)
    if ($caller.ScriptName) {
        $fileName = [System.IO.Path]::GetFileName($caller.ScriptName)
        return "${fileName}:$($caller.ScriptLineNumber)"
    }

    # Case 2: Ephemeral module - check plugin context
    $ctx = Get-CurrentPluginContext
    if ($ctx -and $caller.ScriptLineNumber -gt 0) {
        $path = $ctx.Path
        if (-not $path.EndsWith('.ps1')) {
            $path = "$path.ps1"
        }
        return "${path}:$($caller.ScriptLineNumber)"
    }

    # Case 3: Interactive or unknown
    return 'interactive'
}

function Get-ErrorEventData {
    <#
    .SYNOPSIS
        Extracts structured error details from an ErrorRecord for event emission.
    .DESCRIPTION
        Converts a PowerShell ErrorRecord into a hashtable suitable for NDJSON
        event data. Captures exception type, message, category, script position,
        and stack trace.
    #>
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    $data = [ordered]@{
        exceptionType = $ErrorRecord.Exception.GetType().FullName
        message       = $ErrorRecord.Exception.Message
        category      = $ErrorRecord.CategoryInfo.Category.ToString()
    }

    # Add script position if available
    if ($ErrorRecord.InvocationInfo) {
        $inv = $ErrorRecord.InvocationInfo
        if ($inv.ScriptName) {
            $data.script = [System.IO.Path]::GetFileName($inv.ScriptName)
            $data.line = $inv.ScriptLineNumber
        }
        if ($inv.Line) {
            $data.statement = $inv.Line.Trim()
        }
    }

    # Add stack trace if available
    if ($ErrorRecord.ScriptStackTrace) {
        $data.stackTrace = $ErrorRecord.ScriptStackTrace -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
    }

    # Add target object info if available
    if ($ErrorRecord.TargetObject) {
        $data.targetObject = $ErrorRecord.TargetObject.ToString()
    }

    return $data
}

function Write-StreamEvent {
    <#
    .SYNOPSIS
        Emits an NDJSON event for Go CLI consumption.
    .DESCRIPTION
        Internal helper that writes structured events to stdout or stderr
        based on VSEAL_STREAM_MODE setting. Used by Write-vSEALLog when
        streaming mode is enabled.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [Parameter(Mandatory)]
        [string]$Level,

        [string]$Source,

        [string]$Type,

        [hashtable]$Data,

        [int]$StepCurrent,

        [int]$StepTotal,

        [string]$StepName,

        [long]$DurationMs
    )

    # Apply redaction to message and data before building event
    $redactedMessage = Redact-SensitiveData -Text $Message
    $redactedData = if ($Data) { Redact-Hashtable -Data $Data } else { $null }

    # Build event with required fields
    $event = [ordered]@{
        ts     = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
        level  = $Level.ToLower()
        msg    = $redactedMessage
        source = $Source
        opId   = $script:StreamConfig.OpId
        phase  = $script:StreamConfig.Phase
    }

    # Add optional fields if provided
    if ($Type) { $event.type = $Type }
    if ($redactedData) { $event.data = $redactedData }

    # Add step tracking if provided
    if ($StepCurrent -gt 0) {
        $event.step = [ordered]@{
            current = $StepCurrent
        }
        if ($StepTotal -gt 0) { $event.step.total = $StepTotal }
        if ($StepName) { $event.step.name = $StepName }
    }

    # Add duration if provided
    if ($DurationMs -gt 0) { $event.durationMs = $DurationMs }

    # Convert to single-line JSON (NDJSON format)
    $json = $event | ConvertTo-Json -Compress -Depth 10

    # Emit to appropriate stream based on mode
    # stdout mode: emit to stdout (console suppressed by Write-ConsoleLog)
    # stderr mode: emit to stderr (console output continues normally)
    if ($script:StreamConfig.Mode -eq 'stderr') {
        [Console]::Error.WriteLine($json)
    } else {
        [Console]::Out.WriteLine($json)
    }
}

function Write-vSEALLog {
    <#
    .SYNOPSIS
        Writes a log message to console, file, and optional NDJSON stream.

    .DESCRIPTION
        Central logging function for the vSEAL module. Writes messages to the console
        with level-based formatting, to a JSON log file for audit trails, and optionally
        emits NDJSON events for Go CLI consumption when streaming mode is enabled.

        Supports seven severity levels (DEBUG through FATAL) with independent console
        and file thresholds. Automatically detects the calling function for source
        attribution. Sensitive data (API keys, tokens, passwords) is redacted before
        emission.

    .PARAMETER Message
        The log message text. Must not be null or empty.

    .PARAMETER Level
        The severity level of the message. Valid values: DEBUG, VERBOSE, INFO,
        SUCCESS, WARNING, ERROR, FATAL. Defaults to INFO.

    .PARAMETER Console
        When specified, skips file logging and only writes to the console.

    .PARAMETER Type
        Optional NDJSON event type for structured streaming (e.g., 'operation.start',
        'plugin.success'). Only used when streaming mode is enabled.

    .PARAMETER Data
        Optional hashtable of structured data to include in the NDJSON event payload.
        Sensitive values are automatically redacted.

    .PARAMETER StepCurrent
        Current step number for progress tracking in streamed events.

    .PARAMETER StepTotal
        Total number of steps for progress tracking in streamed events.

    .PARAMETER StepName
        Descriptive name of the current step for streamed events.

    .PARAMETER DurationMs
        Duration in milliseconds to include in the streamed event.

    .PARAMETER ErrorRecord
        Optional ErrorRecord to attach structured error details to the event.
        Captures exception type, message, category, script position, and stack trace.

    .EXAMPLE
        Write-vSEALLog "Starting plugin execution"

        Writes an INFO-level message to console and log file.

    .EXAMPLE
        Write-vSEALLog "Registry key not found" -Level WARNING

        Writes a WARNING-level message.

    .EXAMPLE
        Write-vSEALLog "Plugin completed" -Level SUCCESS -Type 'plugin.success' -DurationMs 1234

        Writes a success message and emits an NDJSON event with duration metadata.

    .OUTPUTS
        [void]
    #>
    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Position = 1)]
        [ValidateSet('DEBUG', 'VERBOSE', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'FATAL')]
        [string]$Level = 'INFO',

        [Parameter()]
        [switch]$Console,

        # Extended event parameters for NDJSON streaming
        [Parameter()]
        [string]$Type,

        [Parameter()]
        [hashtable]$Data,

        [Parameter()]
        [int]$StepCurrent,

        [Parameter()]
        [int]$StepTotal,

        [Parameter()]
        [string]$StepName,

        [Parameter()]
        [long]$DurationMs,

        [Parameter()]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    # Get caller info using context-aware function
    $callerInfo = Get-LogSource

    # Extract function name only for streaming source (not file:line)
    $source = if ($callerInfo -match '^([^:]+)\.ps1') {
        $Matches[1]
    } else {
        $callerInfo
    }

    # Streaming mode: emit NDJSON events
    if ($script:StreamConfig.Enabled) {
        # Build event data - start with provided Data or empty hashtable
        $eventData = if ($Data) { $Data.Clone() } else { $null }

        # If ErrorRecord provided, merge error details into data
        if ($ErrorRecord) {
            $errorData = Get-ErrorEventData -ErrorRecord $ErrorRecord
            if ($eventData) {
                # Merge error data into existing data
                $eventData.error = $errorData
            } else {
                $eventData = @{ error = $errorData }
            }
        }

        # Emit the event with all optional fields
        $streamParams = @{
            Message = $Message
            Level   = $Level
            Source  = $source
        }
        if ($Type) { $streamParams.Type = $Type }
        if ($eventData) { $streamParams.Data = $eventData }
        if ($StepCurrent -gt 0) { $streamParams.StepCurrent = $StepCurrent }
        if ($StepTotal -gt 0) { $streamParams.StepTotal = $StepTotal }
        if ($StepName) { $streamParams.StepName = $StepName }
        if ($DurationMs -gt 0) { $streamParams.DurationMs = $DurationMs }

        Write-StreamEvent @streamParams

        # stdout mode: skip console output entirely (events go to stdout)
        # stderr mode: console output continues normally (events go to stderr)
        if ($script:StreamConfig.Mode -eq 'stderr') {
            $msgLevel = $script:LevelOrder[$Level]
            $consoleThreshold = $script:LevelOrder[$script:LogConfig.ConsoleLevel]
            if ($msgLevel -ge $consoleThreshold) {
                Write-ConsoleLog -Message $Message -Level $Level -Context $script:LogConfig.ConsoleContext -CallerInfo $callerInfo
            }
        }

        # File logging continues normally for audit trail
        if ($script:LoggerInitialized -and -not $Console) {
            $msgLevel = $script:LevelOrder[$Level]
            $fileThreshold = $script:LevelOrder[$script:LogConfig.FileLevel]
            if ($msgLevel -ge $fileThreshold) {
                Write-JsonLogEntry -Message $Message -Level $Level -CalledFrom $callerInfo
            }
        }
        return
    }

    # Legacy mode: standard console + file output
    $msgLevel = $script:LevelOrder[$Level]
    $consoleThreshold = $script:LevelOrder[$script:LogConfig.ConsoleLevel]

    if ($msgLevel -ge $consoleThreshold) {
        Write-ConsoleLog -Message $Message -Level $Level -Context $script:LogConfig.ConsoleContext -CallerInfo $callerInfo
    }

    # File output - only if logger is initialized and -Console not specified
    if ($script:LoggerInitialized -and -not $Console) {
        $fileThreshold = $script:LevelOrder[$script:LogConfig.FileLevel]
        if ($msgLevel -ge $fileThreshold) {
            Write-JsonLogEntry -Message $Message -Level $Level -CalledFrom $callerInfo
        }
    }
}
