# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
#
# Logging.ps1 — Session logging, log rotation, and cleanup functions for the
# DevCage PowerShell launcher. Dot-sourced by devcage.ps1.
# Depends on: $env:WORKSPACE, $ContainerEngine, $ComposeFiles, $EmbedDocker,
#             $ResolvedMemory, $env:SANDBOX_MEMORY_SWAP, $LogKeep, $SandboxTmpDir,
#             $OrigTemp, $OrigTmp.

# ── Per-sandbox temp directory ────────────────────────────────────
# Creates an isolated temp dir for this launcher session with restricted ACLs.
# Sets: $script:SandboxTmpDir, $script:OrigTemp, $script:OrigTmp
function New-SandboxTmpDir {
    $script:OrigTemp = $env:TEMP
    $script:OrigTmp = $env:TMP
    $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "devcage-$([System.IO.Path]::GetRandomFileName().Replace('.',''))"
    New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
    try {
        $dirAcl = New-Object System.Security.AccessControl.DirectorySecurity
        $dirAcl.SetAccessRuleProtection($true, $false)
        $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        $dirAcl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($currentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")))
        Set-Acl -Path $tmpDir -AclObject $dirAcl
    } catch {
        Write-Host "Warning: could not restrict temp directory ACLs: $_" -ForegroundColor Yellow
    }
    $script:SandboxTmpDir = $tmpDir
    $env:TEMP = $tmpDir
    $env:TMP = $tmpDir
}

# ── Cleanup: shred sensitive files and remove temp dir ────────────
function Remove-SandboxTmpDir {
    if ($null -ne $OrigTemp) { $env:TEMP = $OrigTemp }
    if ($null -ne $OrigTmp) { $env:TMP = $OrigTmp }
    if ($SandboxTmpDir -and (Test-Path $SandboxTmpDir)) {
        foreach ($sf in @(".credentials.json", ".git-env")) {
            $sfp = Join-Path $SandboxTmpDir $sf
            if (Test-Path $sfp) {
                try {
                    $fl = (Get-Item $sfp).Length
                    if ($fl -gt 0) { [System.IO.File]::WriteAllBytes($sfp, (New-Object byte[] $fl)) }
                } catch {}
                Remove-Item -Force $sfp -ErrorAction SilentlyContinue
            }
        }
        Remove-Item -Recurse -Force $SandboxTmpDir -ErrorAction SilentlyContinue
    }
}

# ── Session log helpers ───────────────────────────────────────────
function Write-Log {
    param([string]$Message)
    if ($SessionLog) {
        try { Add-Content -Path $SessionLog -Value $Message -ErrorAction SilentlyContinue } catch {}
    }
}

function Write-LogSection {
    param([string]$Title)
    Write-Log ""
    Write-Log "=== $Title ==="
}

# ── Session log setup ─────────────────────────────────────────────
# Creates log directory, session log file, latest.txt pointer, and writes header.
# Sets: $script:LogDir, $script:SessionTs, $script:SessionLog
function Initialize-SessionLog {
    $script:LogDir = Join-Path (Join-Path $env:WORKSPACE ".devcage") "logs"
    if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null }
    $logGitignore = Join-Path $LogDir ".gitignore"
    if (-not (Test-Path $logGitignore)) {
        try { Set-Content -Path $logGitignore -Value '*' -NoNewline -ErrorAction SilentlyContinue } catch {}
    }
    $script:SessionTs = "$(Get-Date -Format 'yyyyMMdd-HHmmss')-$PID"
    $script:SessionLog = Join-Path $LogDir "session-${script:SessionTs}.log"
    try { Set-Content -Path (Join-Path $LogDir "latest.txt") -Value "session-${script:SessionTs}.log" -NoNewline -ErrorAction SilentlyContinue } catch {}

    # Write session header
    Write-LogSection "DevCage Session"
    Write-Log "Timestamp : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')"
    Write-Log "Version   : $DevcageVersion"
    Write-Log "Engine    : $ContainerEngine"
    Write-Log "Workspace : $($env:WORKSPACE)"
    Write-Log "Command   : $($ContainerArgs -join ' ')"
    Write-Log "Memory    : $ResolvedMemory"
    Write-Log "CPUs      : $ResolvedCpus"
    $dindHdr = if ($EmbedDocker) { "on" } else { "off" }
    Write-Log "DinD      : $dindHdr"
    Write-Log "Image     : $(if ($env:DEVCAGE_IMAGE) { $env:DEVCAGE_IMAGE } else { 'devcage-workspace' })"
    Write-Log "Host PID  : $PID"
    Write-Log "==========================="
}

# ── Session footer + log rotation ────────────────────────────────
# Writes session end info and recent docker events to the log.
function Write-SessionFooter {
    param([int]$ExitCode)
    if (-not $SessionLog) { return }

    Write-LogSection "Session End"
    Write-Log "Exit code : $ExitCode"
    Write-Log "Ended at  : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss K')"
    # Check actual OOM status for exit 137 — docker exec returns 137 when the
    # container stops (idle timeout, docker stop) as well as actual OOM.
    $script:OomKilledStatus = "unknown"
    if ($ExitCode -eq 137 -and $script:ReuseContainerId) {
        $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
        $script:OomKilledStatus = & $ContainerEngine inspect --format '{{.State.OOMKilled}}' $script:ReuseContainerId 2>$null
        $ErrorActionPreference = $prevEAP
        if (-not $script:OomKilledStatus) { $script:OomKilledStatus = "unknown" }
    }
    switch ($ExitCode) {
        0   { Write-Log "Status    : clean exit" }
        130 { Write-Log "Status    : interrupted (SIGINT)" }
        137 {
            if ($script:OomKilledStatus -eq "true") {
                Write-Log "Status    : killed (OOM -- out of memory)"
            } elseif ($script:OomKilledStatus -eq "false") {
                Write-Log "Status    : killed (container stopped -- idle timeout or manual stop)"
            } else {
                Write-Log "Status    : killed (SIGKILL -- likely OOM or forced termination)"
            }
        }
        143 { Write-Log "Status    : terminated (SIGTERM)" }
        default { Write-Log "Status    : exited with code $ExitCode" }
    }

    # Capture recent docker events for this workspace (OOM, die, kill)
    if ($ContainerEngine -eq "docker") {
        $prevEAP3 = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
        $events = & $ContainerEngine events --since 5m --until 0s `
            --filter "label=dev-sandbox.workspace=$($env:WORKSPACE)" `
            --format '{{.Time}} {{.Type}} {{.Action}} {{.Actor.Attributes}}' 2>$null
        $ErrorActionPreference = $prevEAP3
        if ($events) {
            Write-Log ""
            Write-Log "--- Docker Events (last 5 min) ---"
            foreach ($evt in $events) { Write-Log $evt }
        }
    }

    # Check if entrypoint init completed
    if (Test-Path $SessionLog) {
        $logContent = Get-Content $SessionLog -Raw -ErrorAction SilentlyContinue
        if ($logContent -and $logContent -notmatch "Initialization complete") {
            Write-Log ""
            Write-Log "WARNING: Entrypoint initialization did not complete. Check entrypoint errors above."
        }
    }

    # OOM hint — only when actually OOM-killed or when OOM status is unknown
    if ($ExitCode -eq 137 -and $script:OomKilledStatus -ne "false") {
        Write-Log ""
        Write-Log "NOTE: Container memory limit was $ResolvedMemory (+swap to $($env:SANDBOX_MEMORY_SWAP)). Try --memory with a higher value."
    }

    # Log rotation
    Invoke-LogRotation
}

function Invoke-LogRotation {
    if (-not $LogDir) { return }
    $logFiles = @(Get-ChildItem -Path $LogDir -Filter "session-*.log" -ErrorAction SilentlyContinue | Sort-Object Name)
    if ($logFiles.Count -gt $LogKeep) {
        $toRemove = $logFiles.Count - $LogKeep
        $logFiles | Select-Object -First $toRemove | ForEach-Object {
            Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
        }
    }
}

# ── Console diagnostics on non-zero exit ─────────────────────────
function Print-ExitDiagnostics {
    param([int]$ExitCode)
    if ($ExitCode -ne 0) {
        [Console]::Error.WriteLine("")
        [Console]::Error.WriteLine("DevCage exited with code $ExitCode.")
        switch ($ExitCode) {
            130 { [Console]::Error.WriteLine("Container was interrupted (SIGINT).") }
            137 {
                # In reuse mode, exit 137 from docker exec means the container stopped
                # while the session was attached (idle timeout, docker stop, etc.) -- not
                # necessarily OOM. Check actual OOM status via docker inspect.
                $oomKilled = "unknown"
                if ($script:ReuseContainerId) {
                    $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
                    $oomKilled = & $ContainerEngine inspect --format '{{.State.OOMKilled}}' $script:ReuseContainerId 2>$null
                    $ErrorActionPreference = $prevEAP
                    if (-not $oomKilled) { $oomKilled = "unknown" }
                }
                if ($oomKilled -eq "true") {
                    [Console]::Error.WriteLine("Container was killed (OOM -- out of memory). Memory limit was $ResolvedMemory (+swap to $($env:SANDBOX_MEMORY_SWAP)).")
                } elseif ($oomKilled -eq "false") {
                    [Console]::Error.WriteLine("Session was terminated (container stopped). Likely cause: idle timeout or manual stop.")
                    [Console]::Error.WriteLine("  Tip: use --idle-timeout with a higher value, or check the session log.")
                } else {
                    [Console]::Error.WriteLine("Container was killed (likely OOM -- out of memory). Memory limit was $ResolvedMemory (+swap to $($env:SANDBOX_MEMORY_SWAP)).")
                }
            }
            143 { [Console]::Error.WriteLine("Container was terminated (SIGTERM).") }
        }
        if ($SessionLog -and (Test-Path $SessionLog)) {
            [Console]::Error.WriteLine("Session log: $SessionLog")
        }
    }
}

# ── Cleanup helper (finally block) ────────────────────────────────
# Releases build lock, tears down DinD sidecar, and removes temp directory.
function Invoke-CleanupOnExit {
    # Release build lock if held (safety net for abnormal exit)
    Exit-BuildLock
    # Stop the dind sidecar if it was started
    if ($EmbedDocker) {
        $prevEAP = $ErrorActionPreference; $ErrorActionPreference = "SilentlyContinue"
        & $ContainerEngine compose @ComposeFiles exec -T dind sh -c 'docker ps -aq 2>/dev/null | xargs docker rm -f 2>/dev/null' *>$null
        & $ContainerEngine compose @ComposeFiles down --remove-orphans *>$null
        $ErrorActionPreference = $prevEAP
    }
    # Restore original TEMP/TMP and shred sensitive files before removing temp dir
    if ($null -ne $OrigTemp) { $env:TEMP = $OrigTemp }
    if ($null -ne $OrigTmp) { $env:TMP = $OrigTmp }
    if ($SandboxTmpDir -and (Test-Path $SandboxTmpDir)) {
        foreach ($sensitiveFile in @(".credentials.json", ".git-env")) {
            $sensitiveFilePath = Join-Path $SandboxTmpDir $sensitiveFile
            if (Test-Path $sensitiveFilePath) {
                try {
                    $fileLen = (Get-Item $sensitiveFilePath).Length
                    if ($fileLen -gt 0) {
                        [System.IO.File]::WriteAllBytes($sensitiveFilePath, (New-Object byte[] $fileLen))
                    }
                } catch {}
                Remove-Item -Force $sensitiveFilePath -ErrorAction SilentlyContinue
            }
        }
        Remove-Item -Recurse -Force $SandboxTmpDir -ErrorAction SilentlyContinue
    }
}
