#requires -Version 5.1
<#
Inventario de equipos - agente PowerShell v2.2.3.

Envia el inventario a la intranet por HTTPS/HTTP en JSON UTF-8.
Configurar por parametros o variables de entorno:
  INTRANET_INVENTARIO_URL
  INTRANET_INVENTARIO_TOKEN
  INTRANET_INVENTARIO_IGNORE_TLS_ERRORS
  INTRANET_INVENTARIO_RUN_DEPLOYMENTS
  INTRANET_INVENTARIO_AGENT_URL

Para ejecutar despliegues de la consola de software, este agente debe lanzarse con:
  powershell.exe -ExecutionPolicy Bypass -File .\inventtory.ps1 -RunDeployments

Tambien se puede activar con:
  INTRANET_INVENTARIO_RUN_DEPLOYMENTS=1

Sin -RunDeployments el script solo envia inventario y no ejecuta trabajos remotos.
#>

[CmdletBinding()]
param(
    [string]$Endpoint = $env:INTRANET_INVENTARIO_URL,
    [string]$Token = $env:INTRANET_INVENTARIO_TOKEN,
    [string]$AgentEndpoint = $env:INTRANET_INVENTARIO_AGENT_URL,
    [switch]$SkipSoftware,
    [switch]$RunDeployments,
    [switch]$IgnoreTlsErrors,
    [int]$TimeoutSec = 60,
    [int]$DeploymentLimit = 3
)

$ErrorActionPreference = 'Stop'

$DefaultEndpoint = 'https://10.100.21.46/intranet/apps/inventario/api.php'
$DefaultToken = 'oOivy8mNU1md35MFYGRrTy+AUQKlVk1dR85bGXDoDZo='
$DefaultIgnoreTlsErrors = $true

if ([string]::IsNullOrWhiteSpace($Endpoint)) {
    $Endpoint = $DefaultEndpoint
}

if ([string]::IsNullOrWhiteSpace($Token)) {
    $Token = $DefaultToken
}

$IgnoreTlsEnv = [string]$env:INTRANET_INVENTARIO_IGNORE_TLS_ERRORS
$IgnoreTlsValidation = $DefaultIgnoreTlsErrors -or $IgnoreTlsErrors.IsPresent -or ($IgnoreTlsEnv -match '^(1|true|yes|si|on)$')
$RunDeploymentsEnv = [string]$env:INTRANET_INVENTARIO_RUN_DEPLOYMENTS
$RunDeploymentsEnabled = $RunDeployments.IsPresent -or ($RunDeploymentsEnv -match '^(1|true|yes|si|on)$')

function Get-SafeCim {
    param(
        [Parameter(Mandatory = $true)][string]$ClassName,
        [string]$Filter = ''
    )

    try {
        if ([string]::IsNullOrWhiteSpace($Filter)) {
            return Get-CimInstance -ClassName $ClassName -ErrorAction Stop
        }
        return Get-CimInstance -ClassName $ClassName -Filter $Filter -ErrorAction Stop
    } catch {
        return $null
    }
}

function Convert-ToIsoUtc {
    param($Value)
    if ($null -eq $Value) { return $null }
    try {
        return ([DateTime]$Value).ToUniversalTime().ToString('o')
    } catch {
        return [string]$Value
    }
}

function Convert-ToGb {
    param($Bytes)
    if ($null -eq $Bytes) {
        return $null
    }
    try {
        return [Math]::Round(([double]$Bytes / 1GB), 2)
    } catch {
        return $null
    }
}

function Get-InstalledSoftware {
    $regKeys = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )

    Get-ItemProperty $regKeys -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -and -not $_.SystemComponent -and -not $_.ParentKeyName } |
        ForEach-Object {
            [pscustomobject][ordered]@{
                name = [string]$_.DisplayName
                version = if ($_.DisplayVersion) { [string]$_.DisplayVersion } else { $null }
                publisher = if ($_.Publisher) { [string]$_.Publisher } else { $null }
                install_date = if ($_.InstallDate) { [string]$_.InstallDate } else { $null }
                uninstall_key = if ($_.PSChildName) { [string]$_.PSChildName } else { $null }
            }
        } |
        Sort-Object name, version -Unique
}

function Get-DomainUserProfiles {
    param(
        [string]$ComputerName,
        [string]$ComputerDomain
    )

    $blockedDomains = @('NT AUTHORITY', 'BUILTIN', 'WINDOW MANAGER', 'FONT DRIVER HOST', 'UMFD')
    $blockedUsers = @('Administrator', 'DefaultAccount', 'Guest', 'WDAGUtilityAccount')
    $profiles = @(Get-SafeCim -ClassName 'Win32_UserProfile' | Where-Object { $_ -and -not $_.Special -and $_.SID -match '^S-1-5-21-' })
    $seen = @{}

    foreach ($profile in $profiles) {
        $sid = [string]$profile.SID
        if ([string]::IsNullOrWhiteSpace($sid) -or $seen.ContainsKey($sid)) {
            continue
        }

        $account = $null
        try {
            $account = (New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount]).Value
        } catch {
            continue
        }

        if ([string]::IsNullOrWhiteSpace($account) -or $account -notmatch '\\') {
            continue
        }

        $parts = $account.Split('\', 2)
        $domain = [string]$parts[0]
        $username = [string]$parts[1]
        if ([string]::IsNullOrWhiteSpace($domain) -or [string]::IsNullOrWhiteSpace($username)) {
            continue
        }
        if ($blockedDomains -contains $domain.ToUpperInvariant()) {
            continue
        }
        if ($domain.Equals($ComputerName, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }
        if ($blockedUsers -contains $username -or $username.EndsWith('$')) {
            continue
        }

        $localPath = if ($profile.LocalPath) { [string]$profile.LocalPath } else { $null }
        if ($localPath -and $localPath -match '\\(default|public|all users|localservice|networkservice|systemprofile)$') {
            continue
        }

        $seen[$sid] = $true
        [ordered]@{
            account_name = [string]$account
            domain = $domain
            username = $username
            sid = $sid
            profile_path = $localPath
            last_use_time = Convert-ToIsoUtc $profile.LastUseTime
        }
    }
}

function Get-NetworkInventory {
    $adapters = @()
    try {
        $adapters = @(Get-NetAdapter -Physical -ErrorAction Stop)
    } catch {
        return @()
    }

    foreach ($adapter in $adapters) {
        $ipv4 = @()
        $ipv6 = @()
        try {
            $ipConfig = Get-NetIPConfiguration -InterfaceIndex $adapter.ifIndex -ErrorAction Stop
            $ipv4 = @($ipConfig.IPv4Address | Where-Object { $_.IPAddress } | Select-Object -ExpandProperty IPAddress)
            $ipv6 = @($ipConfig.IPv6Address | Where-Object { $_.IPAddress } | Select-Object -ExpandProperty IPAddress)
        } catch {
            $ipv4 = @()
            $ipv6 = @()
        }

        $wakeOnMagicPacket = 'No soportado'
        try {
            $power = Get-NetAdapterPowerManagement -Name $adapter.Name -ErrorAction Stop
            if ($null -ne $power.WakeOnMagicPacket) {
                $wakeOnMagicPacket = [string]$power.WakeOnMagicPacket
            }
        } catch {
            $wakeOnMagicPacket = 'No soportado'
        }

        [ordered]@{
            name = [string]$adapter.Name
            interface_description = [string]$adapter.InterfaceDescription
            status = [string]$adapter.Status
            mac_address = [string]$adapter.MacAddress
            link_speed = [string]$adapter.LinkSpeed
            ipv4 = $ipv4
            ipv6 = $ipv6
            wake_on_magic_packet = $wakeOnMagicPacket
        }
    }
}

function Resolve-AgentEndpoint {
    param(
        [string]$InventoryEndpoint,
        [string]$ConfiguredAgentEndpoint
    )

    if (-not [string]::IsNullOrWhiteSpace($ConfiguredAgentEndpoint)) {
        return $ConfiguredAgentEndpoint
    }
    if ($InventoryEndpoint -match 'api\.php(\?.*)?$') {
        return ($InventoryEndpoint -replace 'api\.php(\?.*)?$', 'agent.php')
    }
    return $InventoryEndpoint.TrimEnd('/') + '/agent.php'
}

function ConvertTo-Utf8JsonBytes {
    param($Value)
    $jsonText = $Value | ConvertTo-Json -Depth 8 -Compress
    return [System.Text.Encoding]::UTF8.GetBytes($jsonText)
}

function Limit-DeployText {
    param($Value)
    $text = [string]$Value
    if ($text.Length -gt 20000) {
        return $text.Substring($text.Length - 20000)
    }
    return $text
}

function Test-DeploymentExitCodeSuccess {
    param([int]$ExitCode)
    return @(0, 1641, 3010) -contains $ExitCode
}

function Invoke-DeploymentCommand {
    param($Job)

    $command = [string]$Job.command
    $timeoutSec = 1800
    if ($Job.timeout_sec) {
        $timeoutSec = [Math]::Max(30, [Math]::Min(14400, [int]$Job.timeout_sec))
    }

    $runner = Start-Job -ScriptBlock {
        param([string]$CommandText)
        $ErrorActionPreference = 'Continue'
        try {
            $script = [scriptblock]::Create($CommandText)
            $global:LASTEXITCODE = $null
            $rawOutput = @(& $script *>&1)
            $exitCode = if ($null -ne $global:LASTEXITCODE) { [int]$global:LASTEXITCODE } else { 0 }
            $lines = @($rawOutput | ForEach-Object { [string]$_ })
            [pscustomobject]@{
                exit_code = $exitCode
                stdout = ($lines -join "`n")
                stderr = ''
                error_message = $null
            }
        } catch {
            [pscustomobject]@{
                exit_code = 1
                stdout = ''
                stderr = ''
                error_message = [string]$_.Exception.Message
            }
        }
    } -ArgumentList $command

    if (-not (Wait-Job -Job $runner -Timeout $timeoutSec)) {
        Stop-Job -Job $runner -Force | Out-Null
        Remove-Job -Job $runner -Force | Out-Null
        return [pscustomobject]@{
            exit_code = 124
            stdout = ''
            stderr = ''
            error_message = "Timeout tras $timeoutSec segundos"
        }
    }

    $result = Receive-Job -Job $runner
    Remove-Job -Job $runner -Force | Out-Null
    if ($null -eq $result) {
        return [pscustomobject]@{
            exit_code = 1
            stdout = ''
            stderr = ''
            error_message = 'Sin resultado del proceso'
        }
    }
    return $result | Select-Object -First 1
}

function Invoke-IntranetDeployments {
    param(
        [string]$InventoryEndpoint,
        [string]$ConfiguredAgentEndpoint,
        [string]$InventoryToken,
        [string]$EquipmentName,
        [int]$Limit,
        [int]$HttpTimeoutSec
    )

    $agentUrl = Resolve-AgentEndpoint -InventoryEndpoint $InventoryEndpoint -ConfiguredAgentEndpoint $ConfiguredAgentEndpoint
    $headers = @{
        'X-Inventory-Token' = $InventoryToken
        'X-Inventory-Agent' = 'intranet-inventory-ps/2.2.3'
    }

    $pollPayload = [ordered]@{
        action = 'poll'
        equipment_name = $EquipmentName
        limit = $Limit
    }
    $poll = Invoke-RestMethod -Uri $agentUrl -Method Post -Headers $headers -Body (ConvertTo-Utf8JsonBytes $pollPayload) -ContentType 'application/json; charset=utf-8' -TimeoutSec $HttpTimeoutSec
    if ($poll.ok -ne $true) {
        throw "La intranet rechazo la consulta de trabajos: $($poll.error)"
    }
    if (-not $poll.jobs -or @($poll.jobs).Count -eq 0) {
        Write-Host 'Sin trabajos remotos pendientes.'
        return
    }

    foreach ($job in @($poll.jobs)) {
        Write-Host "Ejecutando trabajo remoto #$($job.id): $($job.package_name)"
        $result = Invoke-DeploymentCommand -Job $job
        $exitCode = [int]$result.exit_code
        $reportPayload = [ordered]@{
            action = 'result'
            equipment_name = $EquipmentName
            job_id = [int]$job.id
            status = if (Test-DeploymentExitCodeSuccess -ExitCode $exitCode) { 'succeeded' } else { 'failed' }
            exit_code = $exitCode
            stdout = Limit-DeployText $result.stdout
            stderr = Limit-DeployText $result.stderr
            error_message = Limit-DeployText $result.error_message
        }
        Invoke-RestMethod -Uri $agentUrl -Method Post -Headers $headers -Body (ConvertTo-Utf8JsonBytes $reportPayload) -ContentType 'application/json; charset=utf-8' -TimeoutSec $HttpTimeoutSec | Out-Null
        Write-Host "Trabajo remoto #$($job.id) finalizado con salida $exitCode."
    }
}

if ([string]::IsNullOrWhiteSpace($Endpoint)) {
    throw 'Configura -Endpoint o la variable INTRANET_INVENTARIO_URL.'
}
if ([string]::IsNullOrWhiteSpace($Token)) {
    throw 'Configura -Token o la variable INTRANET_INVENTARIO_TOKEN.'
}

if ($IgnoreTlsValidation) {
    [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
        param($Sender, $Certificate, $Chain, $SslPolicyErrors)
        return $true
    }
}
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12

$cs = Get-SafeCim -ClassName 'Win32_ComputerSystem'
$bios = Get-SafeCim -ClassName 'Win32_BIOS'
$os = Get-SafeCim -ClassName 'Win32_OperatingSystem'
$cpu = Get-SafeCim -ClassName 'Win32_Processor' | Select-Object -First 1
$memory = Get-SafeCim -ClassName 'Win32_PhysicalMemory'
$ramBytes = ($memory | Measure-Object -Property Capacity -Sum).Sum
$disks = @(Get-SafeCim -ClassName 'Win32_LogicalDisk' -Filter 'DriveType=3' | Where-Object { $null -ne $_ })

$diskPayload = foreach ($disk in $disks) {
    [ordered]@{
        device_id = [string]$disk.DeviceID
        volume_name = if ($disk.VolumeName) { [string]$disk.VolumeName } else { $null }
        total_gb = Convert-ToGb $disk.Size
        free_gb = Convert-ToGb $disk.FreeSpace
    }
}

$softwarePayload = @()
if (-not $SkipSoftware) {
    $softwarePayload = @(Get-InstalledSoftware)
}

$payload = [ordered]@{
    inventory_version = '2.2.3'
    collected_at = (Get-Date).ToUniversalTime().ToString('o')
    computer = [ordered]@{
        name = [string]$env:COMPUTERNAME
        domain = if ($cs.Domain) { [string]$cs.Domain } else { $null }
        manufacturer = if ($cs.Manufacturer) { [string]$cs.Manufacturer } else { $null }
        model = if ($cs.Model) { [string]$cs.Model } else { $null }
        serial_number = if ($bios.SerialNumber) { [string]$bios.SerialNumber } else { $null }
        bios_version = if ($bios.SMBIOSBIOSVersion) { [string]$bios.SMBIOSBIOSVersion } else { $null }
    }
    os = [ordered]@{
        caption = if ($os.Caption) { [string]$os.Caption } else { $null }
        version = if ($os.Version) { [string]$os.Version } else { $null }
        build = if ($os.BuildNumber) { [string]$os.BuildNumber } else { $null }
        architecture = if ($os.OSArchitecture) { [string]$os.OSArchitecture } else { $null }
        install_date = Convert-ToIsoUtc $os.InstallDate
        last_boot_up_time = Convert-ToIsoUtc $os.LastBootUpTime
    }
    hardware = [ordered]@{
        processor = if ($cpu.Name) { [string]$cpu.Name } else { $null }
        ram_gb = if ($ramBytes) { [Math]::Round($ramBytes / 1GB, 2) } else { $null }
        disks = @($diskPayload)
    }
    users = [ordered]@{
        current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        last_logged_on = if ($cs.UserName) { [string]$cs.UserName } else { $null }
        previous_domain_users = @(Get-DomainUserProfiles -ComputerName $env:COMPUTERNAME -ComputerDomain $cs.Domain)
    }
    network = @(Get-NetworkInventory)
    software = $softwarePayload
}

$json = $payload | ConvertTo-Json -Depth 8 -Compress
$body = [System.Text.Encoding]::UTF8.GetBytes($json)
$headers = @{
    'X-Inventory-Token' = $Token
    'X-Inventory-Agent' = 'intranet-inventory-ps/2.2.3'
}

try {
    $response = Invoke-RestMethod -Uri $Endpoint -Method Post -Headers $headers -Body $body -ContentType 'application/json; charset=utf-8' -TimeoutSec $TimeoutSec
    if ($response.ok -ne $true) {
        throw "La intranet rechazo el inventario: $($response.error)"
    }
    Write-Host "Inventario enviado: $($response.equipo) ($($response.software_count) programas)."
    if ($RunDeploymentsEnabled) {
        Invoke-IntranetDeployments -InventoryEndpoint $Endpoint -ConfiguredAgentEndpoint $AgentEndpoint -InventoryToken $Token -EquipmentName ([string]$env:COMPUTERNAME) -Limit $DeploymentLimit -HttpTimeoutSec $TimeoutSec
    }
} catch {
    $errorMessage = [string]$_.Exception.Message
    $inner = $_.Exception.InnerException
    while ($null -ne $inner) {
        $errorMessage += " $($inner.Message)"
        $inner = $inner.InnerException
    }

    if ($errorMessage -match 'SSL|TLS|certif|trust|confianza') {
        Write-Error "No se pudo enviar el inventario a $Endpoint. El certificado HTTPS no es de confianza para este equipo. Prueba con -IgnoreTlsErrors o instala el certificado de la intranet en el almacen de entidades raiz de confianza. Detalle: $errorMessage"
    } else {
        Write-Error "No se pudo enviar el inventario a $Endpoint. $errorMessage"
    }
    exit 1
}
