$script:RBClientVersion = '0.7.2-beta'
$script:RBTermsVersion = '2026-09-02'

function Get-RBLanguage {
    param($State = $null)
    if ($State -and $State.Config -and $State.Config.PSObject.Properties.Name -contains 'language' -and $State.Config.language -eq 'en') { return 'en' }
    if ($State -and $State.Config) { return 'it' }
    if ([Globalization.CultureInfo]::CurrentUICulture.TwoLetterISOLanguageName -eq 'it') { return 'it' }
    return 'en'
}

function Get-RBText {
    param([Parameter(Mandatory=$true)][string]$Italian, [string]$Language = '')
    if (-not $Language) { $Language = Get-RBLanguage }
    if ($Language -ne 'en') { return $Italian }
    $translations = @{
        'Devi scegliere almeno una cartella.'='You must choose at least one folder.'
        'Devi conservare almeno una cartella.'='You must keep at least one folder.'
        'RansomBackup FREE non è ancora configurato.'='RansomBackup FREE is not configured yet.'
        'Formato credenziali non riconosciuto.'='Unrecognized credential format.'
        'Il server non ha restituito il collegamento personale.'='The server did not return your personal link.'
        "Impossibile aprire l'area personale."='Unable to open your account.'
        'Scegli una cartella da proteggere'='Choose a folder to protect'
        'Inserisci il codice monouso ricevuto da RansomBackup.'='Enter the one-time code received from RansomBackup.'
        'Inserisci un indirizzo email valido.'='Enter a valid email address.'
        'Aggiungi almeno una cartella da proteggere.'='Add at least one folder to protect.'
        'Impossibile inizializzare il backup:'='Unable to initialize the backup:'
        'Configurazione completata.'='Setup complete.'
        'Il primo backup è partito. Puoi aggiungere o rimuovere cartelle dal menu Start.'='The first backup has started. You can add or remove folders from the Start menu.'
        'Configurazione non completata.'='Setup was not completed.'
        'Dettagli salvati nel log di installazione.'='Details were saved in the installation log.'
        'Devi accettare le Condizioni d''uso.'='You must accept the Terms of Use.'
        'Hai raggiunto i 2 GiB FREE. I nuovi backup sono sospesi.'='You have reached the 2 GiB FREE quota. New backups are suspended.'
    }
    if ($translations.ContainsKey($Italian)) { return $translations[$Italian] }
    return $Italian
}

function Get-RBPaths {
    [pscustomobject]@{
        Install = $PSScriptRoot
        Data = Join-Path $env:APPDATA 'RansomBackupFREE'
        Cache = Join-Path $env:LOCALAPPDATA 'RansomBackupFREE\cache'
        Logs = Join-Path $env:LOCALAPPDATA 'RansomBackupFREE\logs'
        Config = Join-Path $env:APPDATA 'RansomBackupFREE\config.json'
        Secrets = Join-Path $env:APPDATA 'RansomBackupFREE\secrets.json'
        StorageKey = Join-Path $env:APPDATA 'RansomBackupFREE\storage.key'
        Status = Join-Path $env:LOCALAPPDATA 'RansomBackupFREE\status.json'
        QuotaNotice = Join-Path $env:LOCALAPPDATA 'RansomBackupFREE\quota-notice.json'
        ArchivedAccounts = Join-Path $env:APPDATA 'RansomBackupFREE\ArchivedAccounts'
        Restic = Join-Path $PSScriptRoot 'restic.exe'
    }
}

function New-RBAppIcon {
    $paths = Get-RBPaths
    New-Item -ItemType Directory -Force -Path $paths.Data | Out-Null
    $iconPath = Join-Path $paths.Data 'RansomBackup.ico'
    if (Test-Path -LiteralPath $iconPath) { return $iconPath }
    Add-Type -AssemblyName System.Drawing
    $bitmap = New-Object Drawing.Bitmap 64,64
    $graphics = [Drawing.Graphics]::FromImage($bitmap)
    try {
        $graphics.SmoothingMode = 'AntiAlias'
        $graphics.Clear([Drawing.Color]::FromArgb(9,45,32))
        $green = New-Object Drawing.SolidBrush ([Drawing.Color]::FromArgb(167,239,112))
        $dark = New-Object Drawing.SolidBrush ([Drawing.Color]::FromArgb(9,45,32))
        try {
            $graphics.FillEllipse($green, 7, 7, 50, 50)
            $shield = @(
                (New-Object Drawing.Point 32,17), (New-Object Drawing.Point 46,23),
                (New-Object Drawing.Point 44,39), (New-Object Drawing.Point 32,49),
                (New-Object Drawing.Point 20,39), (New-Object Drawing.Point 18,23)
            )
            $graphics.FillPolygon($dark, [Drawing.Point[]]$shield)
            $pen = New-Object Drawing.Pen $green,4
            try { $graphics.DrawLines($pen, [Drawing.Point[]]@((New-Object Drawing.Point 25,33),(New-Object Drawing.Point 30,38),(New-Object Drawing.Point 40,27))) } finally { $pen.Dispose() }
        } finally { $green.Dispose(); $dark.Dispose() }
        $icon = [Drawing.Icon]::FromHandle($bitmap.GetHicon())
        $stream = [IO.File]::Open($iconPath, [IO.FileMode]::Create)
        try { $icon.Save($stream) } finally { $stream.Dispose(); $icon.Dispose() }
    } finally { $graphics.Dispose(); $bitmap.Dispose() }
    return $iconPath
}

function New-RBRecoveryKey {
    $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
    $bytes = New-Object byte[] 32
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
    $characters = foreach ($byte in $bytes) { $alphabet[$byte % $alphabet.Length] }
    $raw = -join $characters
    return (($raw -split '(.{4})' | Where-Object { $_ }) -join '-')
}

function Protect-RBLegacyString {
    param([Parameter(Mandatory=$true)][string]$Value, [Parameter(Mandatory=$true)][byte[]]$Key)
    $aes = [Security.Cryptography.Aes]::Create()
    try {
        $aes.Key = $Key
        $aes.GenerateIV()
        $plain = [Text.Encoding]::UTF8.GetBytes($Value)
        $encrypted = $aes.CreateEncryptor().TransformFinalBlock($plain, 0, $plain.Length)
        [Convert]::ToBase64String($aes.IV + $encrypted)
    } finally { $aes.Dispose() }
}

function Unprotect-RBLegacyString {
    param([Parameter(Mandatory=$true)][string]$Value, [Parameter(Mandatory=$true)][byte[]]$Key)
    $payload = [Convert]::FromBase64String($Value)
    $aes = [Security.Cryptography.Aes]::Create()
    try {
        $aes.Key = $Key
        $iv = New-Object byte[] ($aes.BlockSize / 8)
        [Array]::Copy($payload, 0, $iv, 0, $iv.Length)
        $aes.IV = $iv
        $plain = $aes.CreateDecryptor().TransformFinalBlock($payload, $iv.Length, $payload.Length - $iv.Length)
        [Text.Encoding]::UTF8.GetString($plain)
    } finally { $aes.Dispose() }
}

function Protect-RBString {
    param([Parameter(Mandatory=$true)][string]$Value)
    Add-Type -AssemblyName System.Security
    $plain = [Text.Encoding]::UTF8.GetBytes($Value)
    $protected = [Security.Cryptography.ProtectedData]::Protect($plain, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
    [Convert]::ToBase64String($protected)
}

function Unprotect-RBString {
    param([Parameter(Mandatory=$true)][string]$Value)
    Add-Type -AssemblyName System.Security
    $protected = [Convert]::FromBase64String($Value)
    $plain = [Security.Cryptography.ProtectedData]::Unprotect($protected, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
    [Text.Encoding]::UTF8.GetString($plain)
}

function Write-RBProtectedSecrets {
    param([Parameter(Mandatory=$true)]$Paths, [Parameter(Mandatory=$true)][string]$BackupToken, [Parameter(Mandatory=$true)][string]$RepositoryPassword)
    [pscustomobject]@{
        mode = 'windows-dpapi-current-user'
        backup_token = Protect-RBString -Value $BackupToken
        repository_password = Protect-RBString -Value $RepositoryPassword
    } | ConvertTo-Json | Set-Content -LiteralPath $Paths.Secrets -Encoding UTF8
    & icacls.exe $Paths.Secrets /inheritance:r /grant:r "$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name):(F)" | Out-Null
}

function Save-RBConfiguration {
    param(
        [Parameter(Mandatory=$true)]$Enrollment,
        [Parameter(Mandatory=$true)][string]$Email,
        [Parameter(Mandatory=$true)][string[]]$Sources,
        [Parameter(Mandatory=$true)][string]$Gateway,
        [Parameter(Mandatory=$true)][string]$RecoveryKey,
        [ValidateSet('it','en')][string]$Language = 'it'
    )
    $paths = Get-RBPaths
    $resolvedSources = @(Resolve-RBSources -Sources $Sources)
    if (-not $resolvedSources.Count) { throw 'Devi scegliere almeno una cartella.' }
    New-Item -ItemType Directory -Force -Path $paths.Data, $paths.Cache, $paths.Logs | Out-Null
    [pscustomobject]@{
        version = $script:RBClientVersion
        email = $Email
        source = $resolvedSources[0]
        sources = $resolvedSources
        gateway = $Gateway.TrimEnd('/')
        username = $Enrollment.username
        repository = $Enrollment.repository
        plan = $Enrollment.plan
        quota_bytes = [int64]$Enrollment.quota_bytes
        quota_tolerance_bytes = if ($Enrollment.quota_tolerance_bytes) { [int64]$Enrollment.quota_tolerance_bytes } else { 0 }
        terms_version = $script:RBTermsVersion
        language = $Language
        configured_at = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    } | ConvertTo-Json | Set-Content -LiteralPath $paths.Config -Encoding UTF8
    Write-RBProtectedSecrets -Paths $paths -BackupToken $Enrollment.backup_token -RepositoryPassword $RecoveryKey
}

function Get-RBSources {
    param([Parameter(Mandatory=$true)]$State)
    if ($State.Config.PSObject.Properties.Name -contains 'sources' -and $State.Config.sources) {
        return @($State.Config.sources | ForEach-Object { [string]$_ } | Select-Object -Unique)
    }
    if ($State.Config.source) { return @([string]$State.Config.source) }
    return @()
}

function Set-RBSources {
    param([Parameter(Mandatory=$true)]$State, [Parameter(Mandatory=$true)][string[]]$Sources)
    $resolvedSources = @(Resolve-RBSources -Sources $Sources)
    if (-not $resolvedSources.Count) { throw 'Devi conservare almeno una cartella.' }
    $config = $State.Config
    $config | Add-Member -NotePropertyName source -NotePropertyValue $resolvedSources[0] -Force
    $config | Add-Member -NotePropertyName sources -NotePropertyValue $resolvedSources -Force
    $config | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $State.Paths.Config -Encoding UTF8
}

function Resolve-RBSources {
    param([Parameter(Mandatory=$true)][string[]]$Sources)
    $resolved = New-Object Collections.Generic.List[string]
    foreach ($source in $Sources) {
        $path = (Resolve-Path -LiteralPath $source).Path
        if (-not ($resolved | Where-Object { $_.Equals($path, [StringComparison]::OrdinalIgnoreCase) })) { $resolved.Add($path) }
    }
    for ($leftIndex = 0; $leftIndex -lt $resolved.Count; $leftIndex++) {
        for ($rightIndex = $leftIndex + 1; $rightIndex -lt $resolved.Count; $rightIndex++) {
            $left = $resolved[$leftIndex].TrimEnd('\','/')
            $right = $resolved[$rightIndex].TrimEnd('\','/')
            if ($left.StartsWith($right + '\', [StringComparison]::OrdinalIgnoreCase) -or
                $right.StartsWith($left + '\', [StringComparison]::OrdinalIgnoreCase)) {
                throw "Le cartelle non possono essere contenute una dentro l'altra: $($resolved[$leftIndex]) e $($resolved[$rightIndex])"
            }
        }
    }
    return $resolved.ToArray()
}

function Get-RBConfiguration {
    $paths = Get-RBPaths
    if (-not (Test-Path -LiteralPath $paths.Config) -or -not (Test-Path -LiteralPath $paths.Secrets)) {
        throw 'RansomBackup FREE non è ancora configurato.'
    }
    $stored = Get-Content -LiteralPath $paths.Secrets -Raw | ConvertFrom-Json
    if ($stored.mode -eq 'windows-dpapi-current-user') {
        $token = Unprotect-RBString -Value $stored.backup_token
        $password = Unprotect-RBString -Value $stored.repository_password
    } elseif ($stored.mode -eq 'aes-256-local-key') {
        $key = [IO.File]::ReadAllBytes($paths.StorageKey)
        $token = Unprotect-RBLegacyString -Value $stored.backup_token -Key $key
        $password = Unprotect-RBLegacyString -Value $stored.repository_password -Key $key
        Write-RBProtectedSecrets -Paths $paths -BackupToken $token -RepositoryPassword $password
    } else { throw 'Formato credenziali non riconosciuto.' }
    [pscustomobject]@{
        Config = Get-Content -LiteralPath $paths.Config -Raw | ConvertFrom-Json
        Secrets = [pscustomobject]@{ backup_token=$token; repository_password=$password }
        Paths = $paths
    }
}

function Set-RBResticEnvironment {
    param([Parameter(Mandatory=$true)]$State, [string]$RestoreToken = '')
    $env:RESTIC_REPOSITORY = "rest:$($State.Config.gateway)/$($State.Config.repository)/"
    $env:RESTIC_REST_USERNAME = $State.Config.username
    $env:RESTIC_REST_PASSWORD = if ($RestoreToken) { $RestoreToken } else { $State.Secrets.backup_token }
    $env:RESTIC_PASSWORD = $State.Secrets.repository_password
    $env:RESTIC_PROGRESS_FPS = '0.1'
}

function Clear-RBResticEnvironment {
    Remove-Item Env:RESTIC_REPOSITORY, Env:RESTIC_REST_USERNAME, Env:RESTIC_REST_PASSWORD, Env:RESTIC_PASSWORD -ErrorAction SilentlyContinue
}

function Get-RBBasicHeader {
    param([Parameter(Mandatory=$true)]$State)
    $token = $State.Secrets.backup_token
    $encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$($State.Config.username):$token"))
    @{ Authorization = "Basic $encoded" }
}

function Send-RBClientReport {
    param([Parameter(Mandatory=$true)]$State, [Parameter(Mandatory=$true)][string]$Status, [Parameter(Mandatory=$true)][int]$ExitCode)
    try {
        $body = @{ status=$Status; exit_code=$ExitCode; client_version=$script:RBClientVersion } | ConvertTo-Json
        return Invoke-RestMethod -Method Post -Uri "$($State.Config.gateway)/api/v1/client-report" -Headers (Get-RBBasicHeader $State) -ContentType 'application/json' -Body $body -TimeoutSec 15
    } catch { return $null }
}

function Get-RBAccountState {
    param([Parameter(Mandatory=$true)]$State)
    Invoke-RestMethod -Method Get -Uri "$($State.Config.gateway)/api/v1/account" -Headers (Get-RBBasicHeader $State) -TimeoutSec 15
}

function Show-RBQuotaNotice {
    param([Parameter(Mandatory=$true)]$State, [Parameter(Mandatory=$true)]$Account)
    if ($Account.quota_state -notin @('warning','grace','blocked')) { return }
    $paths = $State.Paths
    $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    $last = $null
    if (Test-Path -LiteralPath $paths.QuotaNotice) {
        try { $last = Get-Content -LiteralPath $paths.QuotaNotice -Raw | ConvertFrom-Json } catch { $last = $null }
    }
    $repeatAfter = if ($Account.quota_state -in @('grace','blocked')) { 86400 } else { 7 * 86400 }
    if ($last -and $last.state -eq $Account.quota_state -and ($now - [int64]$last.shown_at) -lt $repeatAfter) { return }
    $language = Get-RBLanguage $State
    $used = ([double]$Account.used_bytes / 1GB).ToString('0.00')
    if ($language -eq 'en') {
        $title = 'RansomBackup FREE storage'
        $message = if ($Account.quota_state -in @('grace','blocked')) {
            "You have reached the FREE quota ($used GiB). New backups are suspended. Upgrade to PLUS or start again with a new account and a new email."
        } else { "Your FREE archive is filling up ($used of $(([double]$Account.quota_bytes / 1GB).ToString('0.##')) GiB). Open your account to review the available options." }
    } else {
        $title = 'Spazio RansomBackup FREE'
        $message = if ($Account.quota_state -in @('grace','blocked')) {
            "Hai raggiunto la quota FREE ($used GiB). I nuovi backup sono sospesi. Passa a PLUS oppure ricomincia con un nuovo account e una nuova email."
        } else { "Il tuo archivio FREE si sta riempiendo ($used di $(([double]$Account.quota_bytes / 1GB).ToString('0.##')) GiB). Apri l'area personale per controllare le opzioni." }
    }
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
    $icon = New-Object Windows.Forms.NotifyIcon
    try {
        $icon.Icon = [Drawing.SystemIcons]::Warning
        $icon.Visible = $true
        $icon.BalloonTipTitle = $title
        $icon.BalloonTipText = $message
        $icon.BalloonTipIcon = 'Warning'
        $icon.ShowBalloonTip(10000)
        Start-Sleep -Seconds 11
    } finally { $icon.Visible = $false; $icon.Dispose() }
    [pscustomobject]@{ state=[string]$Account.quota_state; shown_at=$now } | ConvertTo-Json | Set-Content -LiteralPath $paths.QuotaNotice -Encoding UTF8
}

function Send-RBSnapshotCatalog {
    param([Parameter(Mandatory=$true)]$State, [Parameter(Mandatory=$true)]$Snapshots)
    try {
        # ConvertFrom-Json in Windows PowerShell 5 can preserve a JSON array as
        # one nested Object[] when there is more than one snapshot. Flatten one
        # level before reading the fields so both single and multiple results
        # behave identically.
        $snapshotItems = @($Snapshots | ForEach-Object {
            if ($_ -is [Array]) { $_ | ForEach-Object { $_ } } else { $_ }
        })
        $entries = @($snapshotItems | ForEach-Object {
            if (-not $_.id -or -not $_.time) { throw 'Metadati snapshot non validi.' }
            # Windows PowerShell 5 accepts Restic's round-trip timestamp reliably
            # with the single-argument overload; its culture overload can reject
            # otherwise valid timestamps on some localized installations.
            $snapshotDate = if ($_.time -is [DateTime]) { [DateTimeOffset]$_.time } else { [DateTimeOffset]::Parse([string]$_.time) }
            [pscustomobject]@{
                snapshot_id = [string]$_.id
                snapshot_time = $snapshotDate.ToUnixTimeSeconds()
                folder_count = @($_.paths).Count
            }
        })
        $body = [pscustomobject]@{ snapshots=[object[]]$entries } | ConvertTo-Json -Depth 5
        Invoke-RestMethod -Method Post -Uri "$($State.Config.gateway)/api/v1/snapshot-catalog" -Headers (Get-RBBasicHeader $State) -ContentType 'application/json' -Body $body -TimeoutSec 30 | Out-Null
    } catch {
        Add-Content -LiteralPath (Join-Path $State.Paths.Logs 'backup.log') -Encoding UTF8 -Value "Catalogo snapshot non aggiornato: $($_.Exception.Message)"
    }
}

function Register-RBScheduledTask {
    try {
        $paths = Get-RBPaths
        $action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument "`"$(Join-Path $paths.Install 'LaunchHidden.vbs')`" Backup.ps1"
        $triggers = @(
            (New-ScheduledTaskTrigger -AtLogOn)
            (New-ScheduledTaskTrigger -Daily -At '18:00')
        )
        $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 6)
        $principal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
        $task = New-ScheduledTask -Action $action -Trigger $triggers -Settings $settings -Principal $principal -Description 'Backup automatico gratuito RansomBackup FREE'
        Register-ScheduledTask -TaskName 'RansomBackup FREE' -InputObject $task -Force -ErrorAction Stop | Out-Null
        Remove-RBStartupScheduler
        return 'task-scheduler'
    } catch {
        Register-RBStartupScheduler
        return 'startup-scheduler'
    }
}

function Register-RBStartupScheduler {
    $paths = Get-RBPaths
    $startup = [Environment]::GetFolderPath('Startup')
    New-Item -ItemType Directory -Force -Path $startup | Out-Null
    $shortcutPath = Join-Path $startup 'RansomBackup FREE.lnk'
    $shell = New-Object -ComObject WScript.Shell
    $shortcut = $shell.CreateShortcut($shortcutPath)
    $shortcut.TargetPath = 'wscript.exe'
    $shortcut.Arguments = "`"$(Join-Path $paths.Install 'LaunchHidden.vbs')`" Scheduler.ps1"
    $shortcut.WorkingDirectory = $paths.Install
    $shortcut.Save()
    $schedulerPathPattern = [regex]::Escape((Join-Path $paths.Install 'Scheduler.ps1'))
    Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
        Where-Object {
            $_.ProcessId -ne $PID -and
            $_.Name -match '^(powershell|pwsh)(\.exe)?$' -and
            $_.CommandLine -match "(?i)-File\s+`"?$schedulerPathPattern`"?(\s|$)"
        } |
        ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
    Start-Process wscript.exe -ArgumentList @("`"$(Join-Path $paths.Install 'LaunchHidden.vbs')`"",'Scheduler.ps1')
}

function Remove-RBStartupScheduler {
    $shortcutPath = Join-Path ([Environment]::GetFolderPath('Startup')) 'RansomBackup FREE.lnk'
    if (Test-Path -LiteralPath $shortcutPath) { Remove-Item -LiteralPath $shortcutPath -Force }
}
