modules/CheatLab.Commands.ps1

function Show-CheatLabHelp {
    Write-Host "`n=== CheatLab PowerShell Client === " -ForegroundColor Cyan
    Write-Host "Usage: cheat [method] [endpoint] [options]`n" -ForegroundColor White

    Write-Host "METHODS:" -ForegroundColor Yellow
    Write-Host " get - Retrieve content by ID/key/endpoint"
    Write-Host " post - Create new text entry (stored in database)"
    Write-Host " delete - Delete cheat entry"
    Write-Host " ai - Interactive AI mode or single prompt"
    Write-Host " config - Configure credentials"
    Write-Host " upload - Upload file(s) - supports single/multi-file, any type"
    Write-Host " download - Download file(s) with automatic filename preservation"
    Write-Host ""

    Write-Host "COMMON OPTIONS:" -ForegroundColor Yellow
    Write-Host " -text, -t Text content"
    Write-Host " -key, -k Custom key identifier"
    Write-Host " -who, -w Author name"
    Write-Host " -password, -p Password protection"
    Write-Host " -admin_pass, -ap Admin password"
    Write-Host " -protect_view, -pvi Protect from viewing"
    Write-Host " -protect_delete, -pd Protect from deletion"
    Write-Host " -confidential, -c Auto-enable both protections"
    Write-Host " -days <1-30> Set upload expiry days (default 3, max 30)"
    Write-Host " -details, -d Show detailed info"
    Write-Host ""
    Write-Host "UPLOAD OPTIONS:" -ForegroundColor Yellow
    Write-Host " -clipboard, -cb Upload files from clipboard (Windows Explorer only)"
    Write-Host " * Shows file list with sizes for confirmation"
    Write-Host " * Rejects text clipboard (use 'cheat post' instead)"
    Write-Host " * Supports multi-file (auto-creates ZIP)"
    Write-Host ""
    Write-Host "DOWNLOAD OPTIONS:" -ForegroundColor Yellow
    Write-Host " -o, -outfile <name> Specify custom output filename"
    Write-Host " * Default: uses original filename from upload"
    Write-Host " * Multi-file: prompts to extract ZIP"
    Write-Host ""
    Write-Host "HELPER FUNCTIONS:" -ForegroundColor Yellow
    Write-Host " file <path> Read file contents as a raw string"
    Write-Host " (available when cheat.ps1 is dot-sourced)"
    Write-Host " update-cheat Download and install the latest CheatLab update"
    Write-Host ""

    Write-Host "EXAMPLES:" -ForegroundColor Yellow
    Write-Host " # Configuration" -ForegroundColor Gray
    Write-Host " cheat config -username 'user' -auth_key 'key'"
    Write-Host ""
    Write-Host " # Text operations (stored in database)" -ForegroundColor Gray
    Write-Host " cheat post -t 'Hello World' -k 'greeting'"
    Write-Host " cheat get latest"
    Write-Host " cheat get greeting -p 'secret'"
    Write-Host ""
    Write-Host " # File uploads (any type, stored in Supabase)" -ForegroundColor Gray
    Write-Host " cheat upload file.zip -k 'archive' -c"
    Write-Host " cheat upload doc.pdf image.png data.json -k 'multi' (auto-zip)"
    Write-Host " cheat upload -cb -k 'doc' (clipboard, shows confirmation)"
    Write-Host ""
    Write-Host " # File downloads" -ForegroundColor Gray
    Write-Host " cheat download 5 (uses original filename)"
    Write-Host " cheat download 5 -o myfile.txt (custom name)"
    Write-Host ""
    Write-Host " # Other features" -ForegroundColor Gray
    Write-Host " cheat ai 'how to use PowerShell'"
    Write-Host " cheat delete 123 -p 'password'"
    Write-Host " file USER_MANUAL.txt (helper function)"
    Write-Host " update-cheat (update CheatLab to latest)"
    Write-Host ""
    Write-Host "NOTES:" -ForegroundColor Yellow
    Write-Host " * Multi-file uploads automatically create ZIP archives"
    Write-Host " * Clipboard upload only accepts files (copy in Explorer)"
    Write-Host " * For text content, use 'cheat post' to save database storage"
    Write-Host " * Original filenames are preserved in downloads"
    Write-Host " * All file types are supported (no MIME restrictions)"
    Write-Host ""
}

function Invoke-CheatConfig {
    param(
        [string]$Username,
        [string]$AuthKey
    )

    Set-CheatCredentials -Username $Username -AuthKey $AuthKey
}

function Invoke-CheatAi {
    param(
        [string]$BaseUrl,
        [string]$Endpoint
    )

    function Send-AiPrompt {
        param([string]$prompt)

        $body = @{
            prompt  = $prompt
            history = $global:CheatAiHistory
        }
        $url = "$BaseUrl/ai?username=$global:CheatUsername&auth_key=$global:CheatAuthKey"
        $reply = Invoke-CheatApi -url $url -method POST -body $body -timeoutSec 60
        if ($reply) {
            $replyText = $reply -join "`n"

            $width = $Host.UI.RawUI.WindowSize.Width - 1
            $wrapped = foreach ($line in ($replyText -split "`n")) {
                if ($line.Length -le $width) {
                    $line
                }
                else {
                    $words = $line -split ' '
                    $current = ""
                    foreach ($word in $words) {
                        if ($current.Length -eq 0) {
                            $current = $word
                        }
                        elseif (($current.Length + 1 + $word.Length) -le $width) {
                            $current += " $word"
                        }
                        else {
                            $current
                            $current = $word
                        }
                    }
                    if ($current.Length -gt 0) { $current }
                }
            }

            Write-Host ""
            Write-Host ($wrapped -join "`n") -ForegroundColor Magenta
            Write-Host ""

            $global:CheatAiHistory += @{ role = "user"; content = $prompt }
            $global:CheatAiHistory += @{ role = "assistant"; content = $replyText }
        }
    }

    if ($Endpoint -eq "clear") {
        $global:CheatAiHistory = @()
        Write-Host "Chat history cleared." -ForegroundColor Green
        return
    }

    if ($Endpoint) {
        Send-AiPrompt $Endpoint
        return
    }

    while ($true) {
        $p = Read-Host "Prompt"
        if ($p -eq "exit") { break }
        if ($p -eq "clear") {
            $global:CheatAiHistory = @()
            Write-Host "Chat history cleared." -ForegroundColor Green
            continue
        }
        Send-AiPrompt $p
    }
}

function Invoke-CheatGet {
    param(
        [string]$BaseUrl,
        [string]$Endpoint,
        [string]$Password,
        [string]$AdminPass,
        [switch]$Details
    )

    $path = Resolve-Endpoint $Endpoint
    $url = Build-Url "$BaseUrl$path" @{
        password   = $Password
        admin_pass = $AdminPass
        details    = if ($Details) { "true" }
    }

    Invoke-CheatApi -url $url
}

function Invoke-CheatPost {
    param(
        [string]$BaseUrl,
        [string]$Endpoint,
        [string]$Text,
        [string]$Who,
        [string]$Password,
        [string]$Key,
        [switch]$ProtectView,
        [switch]$ProtectDelete,
        [switch]$Confidential
    )

    if (-not $Text -and $Endpoint) { $Text = $Endpoint }

    $body = @{ text = $Text }
    if ($Who) { $body.who = $Who }
    if ($Password) { $body.password = $Password }
    if ($Key) { $body.key = $Key }
    if ($ProtectView) { $body.protect_view = $true }
    if ($ProtectDelete) { $body.protect_delete = $true }
    if ($Confidential) { $body.confidential = $true }

    Invoke-CheatApi -url "$BaseUrl/new?username=$global:CheatUsername&auth_key=$global:CheatAuthKey" -method POST -body $body
}

function Invoke-CheatDelete {
    param(
        [string]$BaseUrl,
        [string]$Endpoint,
        [string]$Password,
        [string]$AdminPass
    )

    if (-not $Endpoint) {
        Write-Host "Error: endpoint required" -ForegroundColor Red
        return
    }

    $queryParams = @{}
    if ($Password) { $queryParams.password = $Password }
    if ($AdminPass) { $queryParams.admin_pass = $AdminPass }

    if ($Endpoint -eq "latest") {
        $url = Build-Url "$BaseUrl/delete/latest" $queryParams
    }
    else {
        $path = Resolve-Endpoint $Endpoint
        $url = Build-Url "$BaseUrl$path" $queryParams
    }

    try {
        $response = Invoke-RestMethod -Uri $url -Method DELETE -TimeoutSec 30
        Write-Host $response -ForegroundColor White
    }
    catch {
        if ($_.Exception.Message -match "timeout|timed out") {
            Write-Error "Delete request timed out (database may be slow)"
        }
        else {
            Write-Host ('Error: ' + $_.Exception.Message) -ForegroundColor Red
        }
    }
}

function Invoke-CheatUpload {
    param(
        [string]$BaseUrl,
        [string]$Endpoint,
        [string[]]$AdditionalFiles,
        [string]$Key,
        [string]$Password,
        [string]$Who,
        [int]$Days,
        [switch]$ProtectView,
        [switch]$ProtectDelete,
        [switch]$Confidential,
        [switch]$Clipboard
    )

    $filePaths = @()

    if ($Clipboard) {
        $clipboardFiles = $null
        try {
            $clipboardFiles = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue
        }
        catch {
        }

        if ($clipboardFiles -and $clipboardFiles.Count -gt 0) {
            Write-Host "`nDetected $($clipboardFiles.Count) file(s) in clipboard:" -ForegroundColor Cyan

            foreach ($file in $clipboardFiles) {
                $fileInfo = Get-Item $file -ErrorAction SilentlyContinue
                if ($fileInfo) {
                    $sizeKB = [math]::Round($fileInfo.Length / 1KB, 2)
                    $sizeMB = [math]::Round($fileInfo.Length / 1MB, 2)
                    $sizeStr = if ($sizeMB -ge 1) { "$sizeMB MB" } else { "$sizeKB KB" }
                    Write-Host " - $($fileInfo.Name) ($sizeStr)" -ForegroundColor Gray
                }
            }

            Write-Host ""
            $confirmation = Read-Host "Upload these file(s)? (Y/N)"

            if ($confirmation -notmatch '^[Yy]') {
                Write-Host "Upload cancelled" -ForegroundColor Yellow
                return
            }

            $filePaths = @($clipboardFiles)
        }
        else {
            Write-Host "Error: Clipboard must contain files (copied from Explorer)" -ForegroundColor Red
            Write-Host "For text content, use: cheat post -t 'your text' -k 'key'" -ForegroundColor Yellow
            return
        }
    }
    else {
        if ($Endpoint) { $filePaths += $Endpoint }
        if ($AdditionalFiles -and $AdditionalFiles.Count -gt 0) { $filePaths += $AdditionalFiles }

        if ($filePaths.Count -eq 0) {
            Write-Host "Error: No files specified" -ForegroundColor Red
            return
        }
    }

    if (-not $Clipboard) {
        foreach ($fp in $filePaths) {
            if (-not (Test-Path $fp)) {
                Write-Host "File not found: $fp" -ForegroundColor Red
                return
            }
        }
    }

    $isMultiFile = $filePaths.Count -gt 1
    $tempZipCreated = $false
    $multiFileCount = 0

    if ($isMultiFile) {
        $multiFileCount = $filePaths.Count
        Write-Host "Multiple files detected ($multiFileCount files) - creating zip archive..." -ForegroundColor Cyan

        $tempZipName = "cheatlab_multifile_$(Get-Date -Format 'yyyyMMddHHmmss').zip"
        $tempZipPath = Join-Path $PWD $tempZipName

        try {
            Compress-Archive -Path $filePaths -DestinationPath $tempZipPath -Force
            Write-Host "Archive created: $tempZipName" -ForegroundColor Green
            $file = Get-Item $tempZipPath
            $tempZipCreated = $true
        }
        catch {
            Write-Host "Failed to create zip: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }
    else {
        $file = Get-Item $filePaths[0]
    }

    $mimeMap = @{
        ".png"  = "image/png"
        ".jpg"  = "image/jpeg"
        ".jpeg" = "image/jpeg"
        ".pdf"  = "application/pdf"
        ".pptx" = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
        ".zip"  = "application/zip"
        ".txt"  = "text/plain"
        ".js"   = "text/javascript"
        ".ts"   = "text/typescript"
        ".py"   = "text/x-python"
        ".cs"   = "text/x-csharp"
        ".json" = "application/json"
        ".xml"  = "application/xml"
        ".html" = "text/html"
        ".css"  = "text/css"
        ".md"   = "text/markdown"
        ".ps1"  = "text/plain"
        ".sh"   = "text/x-shellscript"
    }

    $ext = $file.Extension.ToLower()
    if ($mimeMap.ContainsKey($ext)) {
        $mime = $mimeMap[$ext]
    }
    else {
        if ($Clipboard) {
            $mime = "text/plain"
        }
        else {
            Write-Host "Unknown extension; sending as application/octet-stream" -ForegroundColor Yellow
            $mime = "application/octet-stream"
        }
    }

    try {
        Write-Host "Initializing upload..." -ForegroundColor Gray
        $init = Invoke-RestMethod `
            -Uri "$BaseUrl/upload?username=$global:CheatUsername&auth_key=$global:CheatAuthKey" `
            -Method POST `
            -Body (@{ filename = $file.Name; mime = $mime } | ConvertTo-Json) `
            -ContentType "application/json" `
            -TimeoutSec 30
    }
    catch {
        if ($_.Exception.Message -match "timeout|timed out") {
            Write-Host "Upload initialization timed out (database may be slow)" -ForegroundColor Red
        }
        else {
            Write-Host "Upload init failed: $($_.Exception.Message)" -ForegroundColor Red
        }
        return
    }

    Write-Host "Uploading file..." -ForegroundColor Gray
    Invoke-WebRequest -UseBasicParsing -Uri $init.upload_url -Method PUT -InFile $file.FullName -TimeoutSec 90

    Write-Host "Processing upload..." -ForegroundColor Gray

    $expiryDays = if ($Days) { $Days } else { 3 }
    $expirySeconds = $expiryDays * 24 * 60 * 60

    $dlBody = @{ path = $init.path; expires_days = $expiryDays; ttl = $expirySeconds }

    $dl = Invoke-RestMethod `
        -Uri "$BaseUrl/download?username=$global:CheatUsername&auth_key=$global:CheatAuthKey" `
        -Method POST `
        -Body ($dlBody | ConvertTo-Json) `
        -ContentType "application/json" `
        -TimeoutSec 30

    $body = @{ text = $dl.download_url }
    if ($Key) { $body.key = $Key }
    if ($Password) { $body.password = $Password }
    if ($Who) { $body.who = $Who }

    if ($isMultiFile) {
        $body.metadata = @{ type = "multi-file"; file_count = $multiFileCount }
    }

    if ($ProtectView) { $body.protect_view = $true }
    if ($ProtectDelete) { $body.protect_delete = $true }
    if ($Confidential) { $body.confidential = $true }

    $postUrl = "$BaseUrl/new?username=$global:CheatUsername&auth_key=$global:CheatAuthKey"
    Write-Host "Finalizing upload (POST /new)..." -ForegroundColor Gray
    try {
        Invoke-RestMethod -Uri $postUrl -Method POST -Body ($body | ConvertTo-Json) -ContentType "application/json" -TimeoutSec 30
    }
    catch {
        Write-Host "Finalization failed: $($_.Exception.Message)" -ForegroundColor Red
        if ($_.Exception.Response) {
            try {
                $stream = $_.Exception.Response.GetResponseStream()
                $reader = New-Object System.IO.StreamReader($stream)
                $responseBody = $reader.ReadToEnd()
                $reader.Close()
                $stream.Close()
                if ($responseBody) { Write-Host "Server response: $responseBody" -ForegroundColor Yellow }
            }
            catch { }
        }
    }

    if ($tempZipCreated -and (Test-Path $file.FullName)) {
        Remove-Item $file.FullName -Force
        Write-Host "Temporary archive cleaned up" -ForegroundColor Gray
    }

    if ($Clipboard -and (Test-Path $file.FullName)) {
        Remove-Item $file.FullName -Force -ErrorAction SilentlyContinue
    }

    if ($Clipboard) {
        Write-Host "Clipboard uploaded as: $($file.Name)" -ForegroundColor Green
    }
    else {
        Write-Host "Upload complete" -ForegroundColor Green
    }
}

function Invoke-CheatDownload {
    param(
        [string]$BaseUrl,
        [string]$Endpoint,
        [string]$Password,
        [string]$AdminPass,
        [string]$Outfile,
        [string[]]$AdditionalFiles
    )

    $path = Resolve-Endpoint $Endpoint

    $detailsUrl = Build-Url "$BaseUrl$path" @{
        password   = $Password
        admin_pass = $AdminPass
        details    = "true"
    }

    Write-Host "Getting file info..." -ForegroundColor Gray
    try {
        $fileInfo = Invoke-RestMethod -Uri $detailsUrl -TimeoutSec 30
    }
    catch {
        Write-Host "Failed to get file info" -ForegroundColor Red
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Yellow
        if ($_.ErrorDetails.Message) {
            Write-Host "Details: $($_.ErrorDetails.Message)" -ForegroundColor Yellow
        }
        return
    }

    $isMultiFile = $false
    $fileCount = 0

    if ($fileInfo.metadata) {
        try {
            $meta = if ($fileInfo.metadata -is [string]) { $fileInfo.metadata | ConvertFrom-Json } else { $fileInfo.metadata }
            if ($meta.type -eq "multi-file") {
                $isMultiFile = $true
                $fileCount = [int]$meta.file_count
            }
        }
        catch {
            $isMultiFile = $false
        }
    }

    $url = Build-Url "$BaseUrl$path" @{
        password   = $Password
        admin_pass = $AdminPass
    }

    try {
        $dl = Invoke-RestMethod -Uri $url -TimeoutSec 30
    }
    catch {
        Write-Host "Failed to get download URL: $($_.Exception.Message)" -ForegroundColor Red
        return
    }

    if ($dl -notmatch '^https?://') {
        Write-Host "Error: Response is not a valid download URL" -ForegroundColor Red
        Write-Host "Received: $dl" -ForegroundColor Yellow
        return
    }

    $extractedFilename = $null
    if (-not $isMultiFile) {
        try {
            $urlPath = ([uri]$dl).AbsolutePath
            $filenameWithTimestamp = Split-Path $urlPath -Leaf
            if ($filenameWithTimestamp -match '^\d{13}_(.+)$') {
                $extractedFilename = $matches[1]
            }
        }
        catch {
            $extractedFilename = $null
        }
    }

    $out = if ($Outfile) { $Outfile } elseif ($AdditionalFiles -and $AdditionalFiles.Count -gt 0) { $AdditionalFiles[0] } elseif ($extractedFilename) { $extractedFilename } else { "cheatlab_$Endpoint.zip" }

    $shouldExtract = $false
    if ($isMultiFile) {
        Write-Host ""
        Write-Host "This is a zip archive containing $fileCount files." -ForegroundColor Cyan
        $response = Read-Host "Extract files to current folder? (Y/n)"
        $shouldExtract = $response -eq "" -or $response -eq "Y" -or $response -eq "y"

        if ($shouldExtract) {
            $out = "cheatlab_temp_$Endpoint.zip"
        }
    }

    Write-Host "Downloading file..." -ForegroundColor Gray

    try {
        Invoke-WebRequest -UseBasicParsing -Uri $dl -OutFile $out -TimeoutSec 90

        if ($shouldExtract) {
            Write-Host "Extracting files..." -ForegroundColor Gray
            try {
                Expand-Archive -Path $out -DestinationPath "." -Force
                Remove-Item $out -Force
                Write-Host "Extracted $fileCount files to current folder" -ForegroundColor Green
            }
            catch {
                Write-Host "Extraction failed: $($_.Exception.Message)" -ForegroundColor Red
                Write-Host "Zip file saved as: $out" -ForegroundColor Yellow
            }
        }
        else {
            Write-Host "Saved as $out" -ForegroundColor Green
        }
    }
    catch {
        Write-Host "Download failed: $($_.Exception.Message)" -ForegroundColor Red
    }
}

function htype {
    param(
        [string]$Text,
        [string]$FilePath
    )

    Add-Type -AssemblyName System.Windows.Forms

    if ($FilePath) {
        if (Test-Path $FilePath) {
            $Text = Get-Content -Path $FilePath -Raw
            Write-Host "Loaded file: $FilePath ($($Text.Length) characters)"
        }
        else {
            Write-Error "File not found: $FilePath"
            return
        }
    }

    if (-not $Text) {
        Write-Error "Please provide either -Text or -FilePath parameter"
        return
    }

    Write-Host "Focus your editor now. Typing starts in 5 seconds..."
    Start-Sleep -Seconds 5

    $tokens = @()
    $allChars = $Text.ToCharArray()
    $i = 0

    while ($i -lt $allChars.Length) {
        $tokenSize = Get-Random -Minimum 5 -Maximum 10
        $tokenSize = [Math]::Min($tokenSize, $allChars.Length - $i)
        $token = -join $allChars[$i..($i + $tokenSize - 1)]
        $tokens += $token
        $i += $tokenSize
    }

    foreach ($token in $tokens) {
        [System.Windows.Forms.Clipboard]::SetText($token)
        [System.Windows.Forms.SendKeys]::SendWait('^v')
        [System.Windows.Forms.SendKeys]::SendWait('{LEFT}')
        [System.Windows.Forms.SendKeys]::SendWait('{RIGHT}')
    }
}

function update-cheat {
    param()

    Write-Host "Downloading CheatLab installer..." -ForegroundColor Cyan
    try {
        $url = "https://cheatlab.onrender.com/install-cheat"
        $script = Invoke-RestMethod -Uri $url

        if ($null -eq $script -or $script -eq "") {
            Write-Error "Failed to download installer or empty response received."
            return
        }

        Write-Host "Running installer script..." -ForegroundColor Cyan
        Invoke-Expression $script
    }
    catch {
        Write-Error "update-cheat failed: $($_.Exception.Message)"
    }
}