$code = @"
using System;
using System.Runtime.InteropServices;

public class WinSQLite {
    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_open", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Open(string filename, out IntPtr db);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_close", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Close(IntPtr db);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_prepare_v2", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Prepare(IntPtr db, string sql, int numBytes, out IntPtr statement, out IntPtr tail);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_step", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Step(IntPtr statement);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_text", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr ColumnText(IntPtr statement, int column);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_finalize", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Finalize(IntPtr statement);

    [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_count", CallingConvention = CallingConvention.Cdecl)]
    public static extern int ColumnCount(IntPtr statement);
}
"@

try {
    Add-Type -TypeDefinition $code
} catch {
    # Ignore if already defined
}

function Get-UTF8String($ptr) {
    if ($ptr -eq [IntPtr]::Zero) { return $null }
    $len = 0
    while ([System.Runtime.InteropServices.Marshal]::ReadByte($ptr, $len) -ne 0) {
        $len++
    }
    if ($len -eq 0) { return "" }
    $bytes = New-Object byte[] $len
    [System.Runtime.InteropServices.Marshal]::Copy($ptr, $bytes, 0, $len)
    return [System.Text.Encoding]::UTF8.GetString($bytes)
}

# ============================================================
# SETTINGS — persisted in kaspi_settings.json next to script
# ============================================================
$scriptDir      = Split-Path -Parent $MyInvocation.MyCommand.Path
$settingsPath   = Join-Path $scriptDir "kaspi_settings.json"

function Load-Settings {
    if (Test-Path $settingsPath) {
        try {
            $raw = Get-Content $settingsPath -Raw -Encoding UTF8
            return $raw | ConvertFrom-Json
        } catch {
            Write-Warning "⚠️  Баптаулар файлын оқу мүмкін болмады. Жаңа файл жасалады."
        }
    }
    return $null
}

function Save-Settings($cfg) {
    $cfg | ConvertTo-Json -Depth 5 | Set-Content $settingsPath -Encoding UTF8
}

# ============================================================
# PERSISTENT RETRY QUEUE — pending_donations.json
# ============================================================
$pendingPath = Join-Path $scriptDir "pending_donations.json"

function Load-PendingQueue {
    if (Test-Path $pendingPath) {
        try {
            $raw = Get-Content $pendingPath -Raw -Encoding UTF8
            if (![string]::IsNullOrWhiteSpace($raw)) {
                return @($raw | ConvertFrom-Json)
            }
        } catch { }
    }
    return @()
}

function Save-PendingQueue($queue) {
    try {
        $queue | ConvertTo-Json -Depth 5 | Set-Content $pendingPath -Encoding UTF8
    } catch { }
}

function Enqueue-PendingDonation($name, $amount, $message) {
    $queue = @(Load-PendingQueue)
    $item = [PSCustomObject]@{
        id         = [System.Guid]::NewGuid().ToString("N")
        name       = $name
        amount     = $amount
        message    = $message
        createdAt  = (Get-Date).ToString("o")
        retryCount = 0
    }
    $queue += $item
    Save-PendingQueue $queue
    Write-Host "📥 Донат қайта жіберу кезегіне сақталды: $name, $amount ₸" -ForegroundColor Cyan
    Process-PendingQueue
}

function Process-PendingQueue {
    $queue = @(Load-PendingQueue)
    if ($queue.Count -eq 0) { return }

    if ([string]::IsNullOrEmpty($deviceToken)) {
        Write-Warning "⚠️ Құрылғы әлі байланбаған. Кезекте $($queue.Count) донат күтуде."
        return
    }

    $remaining = @()
    $stopped = $false

    foreach ($item in $queue) {
        if ($stopped) {
            $remaining += $item
            continue
        }

        try {
            $body = @{
                name        = $item.name
                amount      = $item.amount
                message     = $item.message
                username    = $username
                deviceUuid  = $deviceUuid
                deviceToken = $deviceToken
            } | ConvertTo-Json

            $response = Invoke-RestMethod -Uri "$serverUrl/donate" `
                -Method Post -Body $body -ContentType "application/json; charset=utf-8" -TimeoutSec 10 -ErrorAction Stop

            Write-Host "✅ Донат серверге сәтті жіберілді! ($($item.name), $($item.amount) ₸)" -ForegroundColor Green
        } catch {
            $stopped = $true
            $item.retryCount++
            $remaining += $item
            Write-Warning "⏳ Донат жіберу қатесі: $_. Донат кезекте сақталды ($($item.name), $($item.amount) ₸, әрекет #$($item.retryCount))"
        }
    }

    Save-PendingQueue $remaining
}

function New-Guid-String {
    return [System.Guid]::NewGuid().ToString()
}

# Load or initialise settings
$cfg = Load-Settings
if ($null -eq $cfg) {
    # First run — ask for required info
    Write-Host ""
    Write-Host "======================================================" -ForegroundColor Cyan
    Write-Host "  KAZ DONAT — Kaspi Listener алғаш рет іске қосылды" -ForegroundColor Cyan
    Write-Host "======================================================" -ForegroundColor Cyan
    Write-Host ""
    $inputUsername  = Read-Host "📛 Стример пайдаланушы атын енгізіңіз (username)"
    $inputServerUrl = Read-Host "🌐 Сервер URL (Enter = http://localhost:3000)"
    if ([string]::IsNullOrWhiteSpace($inputServerUrl)) {
        $inputServerUrl = "http://localhost:3000"
    }
    $cfg = [PSCustomObject]@{
        username    = $inputUsername.Trim().ToLower()
        serverUrl   = $inputServerUrl.TrimEnd("/")
        deviceUuid  = New-Guid-String
        deviceToken = ""
    }
    Save-Settings $cfg
    Write-Host ""
    Write-Host "✅ Баптаулар сақталды: $settingsPath" -ForegroundColor Green
}

$username   = $cfg.username
$serverUrl  = $cfg.serverUrl.TrimEnd("/")
$deviceUuid = $cfg.deviceUuid
$deviceToken = $cfg.deviceToken

# ============================================================
# DEVICE LINKING — must be approved before we can send donates
# ============================================================
function Request-DeviceLink {
    Write-Host ""
    Write-Host "🔗 Құрылғыны сайтқа қосу сұранысы жіберілуде..." -ForegroundColor Yellow
    $deviceName = "Kaspi Listener PS ($env:COMPUTERNAME)"
    $body = @{ username = $username; deviceUuid = $deviceUuid; deviceName = $deviceName } | ConvertTo-Json
    try {
        $resp = Invoke-RestMethod -Uri "$serverUrl/api/app/request-link" `
            -Method Post -Body $body -ContentType "application/json; charset=utf-8" -ErrorAction Stop
        Write-Host "✅ Сұраныс жіберілді!" -ForegroundColor Green
        return $true
    } catch {
        Write-Warning "❌ Сұраныс жіберу сәтсіз: $_"
        return $false
    }
}

function Poll-DeviceApproval {
    Write-Host ""
    Write-Host "⏳ Растауды күтудеміз..." -ForegroundColor Yellow
    Write-Host "   👉 KAZ DONAT сайтына кіріп, «Kaspi Listener Құрылғылары»" -ForegroundColor Cyan
    Write-Host "      бөліміне өтіп, осы сұранысты ҚАБЫЛДАҢЫЗ (Қабылдау)." -ForegroundColor Cyan
    Write-Host "   Сайт: $serverUrl" -ForegroundColor Cyan
    Write-Host ""

    while ($true) {
        Start-Sleep -Seconds 5
        try {
            $body = @{ username = $username; deviceUuid = $deviceUuid } | ConvertTo-Json
            $resp = Invoke-RestMethod -Uri "$serverUrl/api/app/check-link" `
                -Method Post -Body $body -ContentType "application/json; charset=utf-8" -ErrorAction Stop
            if ($resp.approved -eq $true -and -not [string]::IsNullOrEmpty($resp.token)) {
                Write-Host "🎉 Құрылғы сәтті расталды!" -ForegroundColor Green
                return $resp.token
            } else {
                Write-Host "   ...күтудеміз (сайттан растаңыз)" -ForegroundColor DarkGray
            }
        } catch {
            Write-Warning "   Серверге қосыла алмады: $_"
        }
    }
}

# If no device token, run the linking flow now
if ([string]::IsNullOrEmpty($deviceToken)) {
    Write-Host ""
    Write-Host "══════════════════════════════════════════════" -ForegroundColor Magenta
    Write-Host "  ҚҰРЫЛҒЫ БАЙЛАНЫСЫ ЖОҚ — алдымен қосу қажет" -ForegroundColor Magenta
    Write-Host "══════════════════════════════════════════════" -ForegroundColor Magenta

    $sent = Request-DeviceLink
    if (-not $sent) {
        Write-Error "Серверге қосыла алмады. Сервер іске қосылғанын тексеріңіз."
        exit 1
    }

    $newToken = Poll-DeviceApproval
    $deviceToken = $newToken

    # Persist the token
    $cfg.deviceToken = $deviceToken
    Save-Settings $cfg
    Write-Host "💾 Токен сақталды." -ForegroundColor Green
}

# ============================================================
# Kaspi notification DB paths
# ============================================================
$dbPath     = "$env:LOCALAPPDATA\Microsoft\Windows\Notifications\wpndatabase.db"
$tempDbPath = "$env:TEMP\wpn_kaspi.db"

function Get-LatestNotificationTime {
    if (!(Test-Path $dbPath)) { return 0 }
    try {
        Copy-Item -Path $dbPath -Destination $tempDbPath -Force
        $walPath = "$dbPath-wal"
        if (Test-Path $walPath) { Copy-Item -Path $walPath -Destination "$tempDbPath-wal" -Force }
        $shmPath = "$dbPath-shm"
        if (Test-Path $shmPath) { Copy-Item -Path $shmPath -Destination "$tempDbPath-shm" -Force }
        $db = [IntPtr]::Zero
        [WinSQLite]::Open($tempDbPath, [ref]$db) | Out-Null

        $sql = "SELECT ArrivalTime FROM Notification ORDER BY ArrivalTime DESC LIMIT 1"
        $stmt = [IntPtr]::Zero
        $tail = [IntPtr]::Zero
        [WinSQLite]::Prepare($db, $sql, -1, [ref]$stmt, [ref]$tail) | Out-Null

        $maxTime = 0
        if ([WinSQLite]::Step($stmt) -eq 100) {
            $ptr = [WinSQLite]::ColumnText($stmt, 0)
            $str = Get-UTF8String($ptr)
            if ($str) { $maxTime = [int64]::Parse($str) }
        }
        [WinSQLite]::Finalize($stmt) | Out-Null
        [WinSQLite]::Close($db) | Out-Null
        return $maxTime
    } catch {
        return 0
    }
}

$lastTime = Get-LatestNotificationTime
if ($lastTime -eq 0) {
    $lastTime = [DateTime]::UtcNow.ToFileTime()
}

$processedPayloads = [System.Collections.Generic.List[string]]::new()
$recentDonations = [System.Collections.Generic.List[PSCustomObject]]::new()

function Is-RecentDuplicate {
    param (
        [string]$name,
        [double]$amount,
        [string]$message
    )
    $now = [DateTime]::UtcNow
    
    # Clean up entries older than 30 seconds
    for ($i = $recentDonations.Count - 1; $i -ge 0; $i--) {
        if (($now - $recentDonations[$i].Timestamp).TotalSeconds -gt 30) {
            $recentDonations.RemoveAt($i)
        }
    }
    
    foreach ($d in $recentDonations) {
        $sameAmount = [Math]::Abs($d.Amount - $amount) -lt 0.01
        $sameMessage = $d.Message -eq $message
        
        # If names are equal (and not Anonymous), amounts are equal, and messages are equal
        if ($name -ne "Аноним" -and $d.Name -eq $name -and $sameAmount -and $sameMessage) {
            return $true
        }
        
        # If both are Anonymous, match if amounts/messages are equal and within 15 seconds
        if ($name -eq "Аноним" -and $d.Name -eq "Аноним" -and $sameAmount -and $sameMessage -and ($now - $d.Timestamp).TotalSeconds -le 15) {
            return $true
        }
    }
    
    # Add to list
    $recentDonations.Add([PSCustomObject]@{
        Name      = $name
        Amount    = $amount
        Message   = $message
        Timestamp = $now
    })
    return $false
}

Write-Host ""
if (!(Test-Path $dbPath)) {
    Write-Warning "⚠️ Windows хабарламалар базасы табылмады ($dbPath). Хабарламалар оқылмайды!"
    Write-Warning "   Связь с телефоном (Phone Link) немесе Intel Unison бағдарламасы қосылып тұрғанын тексеріңіз."
} else {
    Write-Host "✅ Хабарламалар базасына қолжетімділік: ЖҰМЫС ІСТЕП ТҰР" -ForegroundColor Green
}

Write-Host "🟢 Kaspi Listener іске қосылды." -ForegroundColor Green
Write-Host "   Пайдаланушы : $username" -ForegroundColor Gray
Write-Host "   Сервер      : $serverUrl" -ForegroundColor Gray
Write-Host "   UUID        : $deviceUuid" -ForegroundColor Gray
Write-Host "   Kaspi хабарламаларын күтудеміз... (Ctrl+C — тоқтату)" -ForegroundColor Gray
Write-Host "--------------------------------------------------------"

# ============================================================
# MAIN POLLING LOOP
# ============================================================
while ($true) {
    Start-Sleep -Seconds 1
    Process-PendingQueue

    if (!(Test-Path $dbPath)) {
        Write-Warning "⚠️ Хабарламалар базасы табылмады: $dbPath. Рұқсаттарды тексеріңіз!"
        Start-Sleep -Seconds 5
        continue
    }

    try {
        Copy-Item -Path $dbPath -Destination $tempDbPath -Force
        $walPath = "$dbPath-wal"
        if (Test-Path $walPath) {
            Copy-Item -Path $walPath -Destination "$tempDbPath-wal" -Force
        } else {
            Remove-Item -Path "$tempDbPath-wal" -Force -ErrorAction SilentlyContinue
        }
        $shmPath = "$dbPath-shm"
        if (Test-Path $shmPath) {
            Copy-Item -Path $shmPath -Destination "$tempDbPath-shm" -Force
        } else {
            Remove-Item -Path "$tempDbPath-shm" -Force -ErrorAction SilentlyContinue
        }

        $db = [IntPtr]::Zero
        if ([WinSQLite]::Open($tempDbPath, [ref]$db) -ne 0) { continue }

        $sql  = "SELECT N.ArrivalTime, N.Payload, H.PrimaryId FROM Notification N JOIN NotificationHandler H ON N.HandlerId = H.RecordId WHERE N.ArrivalTime > $lastTime ORDER BY N.ArrivalTime ASC"
        $stmt = [IntPtr]::Zero
        $tail = [IntPtr]::Zero

        if ([WinSQLite]::Prepare($db, $sql, -1, [ref]$stmt, [ref]$tail) -eq 0) {
            while ([WinSQLite]::Step($stmt) -eq 100) {
                $timePtr    = [WinSQLite]::ColumnText($stmt, 0)
                $payloadPtr = [WinSQLite]::ColumnText($stmt, 1)
                $handlerPtr = [WinSQLite]::ColumnText($stmt, 2)

                $timeStr    = Get-UTF8String($timePtr)
                $payload    = Get-UTF8String($payloadPtr)
                $handlerId  = Get-UTF8String($handlerPtr)

                $notifTime = [int64]::Parse($timeStr)
                if ($notifTime -gt $lastTime) { $lastTime = $notifTime }

                # Check for Kaspi keywords first to decide if we should warn
                $hasKeywords = $payload -match "Kaspi" -or $payload -match "перевод" -or $payload -match "аударылды" -or $payload -match "₸" -or $payload -match "Оплата" -or $payload -match "Төлем"

                if ($hasKeywords) {
                    if ([string]::IsNullOrEmpty($handlerId) -or 
                        (!$handlerId.Contains("YourPhone") -and 
                         !$handlerId.Contains("CrossDevice") -and
                         !$handlerId.Contains("Phone") -and
                         !$handlerId.Contains("Unison") -and
                         !$handlerId.Contains("Samsung") -and
                         !$handlerId.Contains("Flow") -and
                         !$handlerId.Contains("KDE") -and
                         !$handlerId.Contains("Connect"))) {
                        Write-Warning "⚠️ Тексерілді: '$handlerId' пакетінен Kaspi хабарламасы келді, бірақ тізімде болмағандықтан өткізілмеді."
                        continue
                    }
                } else {
                    continue
                }

                # Kaspi keyword filter
                if ($payload -match "Kaspi" -or $payload -match "перевод" -or $payload -match "аударылды" -or
                    $payload -match "₸"     -or $payload -match "Оплата"  -or $payload -match "Төлем") {

                    # Avoid our own toast loop
                    if ($payload -match "Жаңа Төлем") { continue }

                    # Dedup
                    if ($processedPayloads.Contains($payload)) { continue }
                    $processedPayloads.Add($payload)
                    if ($processedPayloads.Count -gt 100) { $processedPayloads.RemoveAt(0) }

                    # ---- Parse XML texts ----
                    $texts    = @()
                    $rawTexts = @()
                    try {
                        [xml]$xml = $payload
                        $rawTexts = $xml.SelectNodes("//text") | ForEach-Object { $_.InnerText }
                    } catch {
                        $matches2 = [regex]::Matches($payload, '<text.*?>(.*?)</text>')
                        foreach ($m in $matches2) { $rawTexts += $m.Groups[1].Value }
                    }

                    foreach ($t in $rawTexts) {
                        if ($t) {
                            $lines = $t -split "`r`n" -split "`r" -split "`n"
                            foreach ($line in $lines) {
                                $tl = $line.Trim()
                                if ($tl -ne "") { $texts += $tl }
                            }
                        }
                    }

                    # Enforce that the notification must originate from a Kaspi app (Kaspi.kz, Kaspi Pay, Kaspi Gold, Kaspi)
                    $isFromKaspi = $false
                    foreach ($t in $texts) {
                        if ($t -eq "Kaspi.kz" -or $t -eq "Kaspi Pay" -or $t -eq "Kaspi Gold" -or $t -eq "Kaspi") {
                            $isFromKaspi = $true
                            break
                        }
                    }
                    if (!$isFromKaspi) { continue }

                    # ---- Extract amount, name, message ----
                    $amount    = 0
                    $name      = ""
                    $message   = ""
                    $isPayment = $false

                    foreach ($text in $texts) {
                        $trimmed = $text.Trim()
                        if ($trimmed -eq "") { continue }

                        if ($trimmed -eq "Kaspi Gold" -or $trimmed -eq "Kaspi.kz" -or
                            $trimmed -eq "Kaspi Pay"  -or $trimmed -eq "Kaspi") { continue }

                        if ($trimmed.StartsWith("Подтвердить") -or $trimmed.StartsWith("Отклонить") -or
                            $trimmed.StartsWith("Показать")    -or $trimmed.StartsWith("Скрыть")) { continue }

                        if ($trimmed -match '([\d\s\.,]+)\s*₸') {
                            $amountStr = $Matches[1] -replace '[\s\.,]', ''
                            if ([double]::TryParse($amountStr, [ref]$amount)) { $isPayment = $true }

                            if      ($trimmed -match 'от\s+([\p{L}\s\-]+?\s+\p{L}\.?)')         { $name = $Matches[1].Trim() }
                            elseif  ($trimmed -match '([\p{L}\s\-]+?\s+\p{L}\.?)\-(дан|ден|нан|нен|тан|тен)') { $name = $Matches[1].Trim() }
                            elseif  ($trimmed -match 'from\s+([\p{L}\s\-]+?\s+\p{L}\.?)')       { $name = $Matches[1].Trim() }
                            continue
                        }

                        $colonIdx = $trimmed.IndexOf(':')
                        if ($colonIdx -gt 0) {
                            $left  = $trimmed.Substring(0, $colonIdx).Trim()
                            $right = $trimmed.Substring($colonIdx + 1).Trim()
                            if ($left -ne "Пополнение" -and $left -ne "Перевод" -and $left -ne "Списание" -and
                                $left -ne "Оплата"     -and $left -ne "Төлем"   -and $left -ne "Аударылды") {
                                $name    = $left
                                $message = $right
                            }
                        } else {
                            if      ($trimmed -match 'от\s+([\p{L}\s\-]+?\s+\p{L}\.?)')         { $name = $Matches[1].Trim() }
                            elseif  ($trimmed -match '([\p{L}\s\-]+?\s+\p{L}\.?)\-(дан|ден|нан|нен|тан|тен)') { $name = $Matches[1].Trim() }
                            elseif  ($trimmed -match 'from\s+([\p{L}\s\-]+?\s+\p{L}\.?)')       { $name = $Matches[1].Trim() }
                            elseif  ($trimmed -match '^([\p{L}\s\-]+?\s+\p{L}\.?)$')            { $name = $Matches[1].Trim() }
                            elseif  ($name -eq "")                                               { $name = $trimmed }
                        }
                    }

                    if ($isPayment -and $amount -gt 0) {
                        if ($name -eq "") {
                            $name = "Аноним"
                            if ($texts.Count -gt 0 -and $texts[0] -ne "") {
                                $message = "Жүйелік төлем: " + $texts[0]
                            }
                        } elseif ($message -eq "") {
                            $message = "Kaspi арқылы аударым"
                        }

                        # Deduplication check
                        if (Is-RecentDuplicate $name $amount $message) {
                            Write-Warning "⚠️ Дубликат донат өткізілмеді: Жіберуші: $name, Сома: $amount ₸"
                            continue
                        }

                        Write-Host "💰 Жаңа Kaspi Төлем! Жіберуші: $name, Сома: $amount ₸" -ForegroundColor Yellow

                        # Save to persistent retry queue and attempt delivery
                        Enqueue-PendingDonation $name $amount $message
                    }
                }
            }
            [WinSQLite]::Finalize($stmt) | Out-Null
        }
        [WinSQLite]::Close($db) | Out-Null
    } catch {
        Write-Warning "Хабарламалар базасын оқу кезінде қате: $_"
    }
}
