Private/Core/Get-IpGeoData.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Get-IpGeoData {
    [CmdletBinding()]
    param(
        [string[]]$IpAddresses = @(),

        [int]$BatchSize = 100,
        [int]$MaxRequestsPerMinute = 15
    )

    $results = @{}
    $requestTimestamps = [System.Collections.Generic.List[datetime]]::new()

    # Deduplicate
    $uniqueIps = @($IpAddresses | Sort-Object -Unique | Where-Object { $_ })
    if ($uniqueIps.Count -eq 0) { return $results }

    # Split into batches
    $batches = [System.Collections.Generic.List[string[]]]::new()
    for ($i = 0; $i -lt $uniqueIps.Count; $i += $BatchSize) {
        $end = [Math]::Min($i + $BatchSize, $uniqueIps.Count)
        $batch = $uniqueIps[$i..($end - 1)]
        $batches.Add($batch)
    }

    $totalBatches = $batches.Count
    Write-Verbose "GeoIP: $($uniqueIps.Count) unique IPs in $totalBatches batch(es)"

    for ($batchNum = 0; $batchNum -lt $totalBatches; $batchNum++) {
        $batch = $batches[$batchNum]

        # Rate limiting: check sliding window
        $now = [datetime]::UtcNow
        $windowStart = $now.AddSeconds(-60)
        $recentRequests = @($requestTimestamps | Where-Object { $_ -gt $windowStart })

        if ($recentRequests.Count -ge $MaxRequestsPerMinute) {
            $oldest = $recentRequests[0]
            $waitSeconds = [Math]::Ceiling(($oldest.AddSeconds(60) - $now).TotalSeconds) + 1
            if ($waitSeconds -gt 0) {
                Write-Verbose "GeoIP rate limit: waiting ${waitSeconds}s before next batch..."
                Start-Sleep -Seconds $waitSeconds
            }
        }

        # Build batch request body
        $batchBody = $batch | ForEach-Object {
            @{ query = $_; fields = 'status,countryCode,isp,org,hosting,lat,lon,query' }
        }
        $jsonBody = $batchBody | ConvertTo-Json -Compress
        # Ensure it's an array even for single item
        if ($batch.Count -eq 1) { $jsonBody = "[$jsonBody]" }

        $requestTimestamps.Add([datetime]::UtcNow)

        try {
            $response = Invoke-RestMethod -Uri 'http://ip-api.com/batch' `
                -Method Post `
                -Body $jsonBody `
                -ContentType 'application/json' `
                -ErrorAction Stop

            foreach ($entry in $response) {
                if ($entry.status -eq 'success') {
                    $results[$entry.query] = @{
                        CountryCode = $entry.countryCode
                        ISP         = $entry.isp
                        Org         = $entry.org
                        IsHosting   = [bool]$entry.hosting
                        Latitude    = [double]$entry.lat
                        Longitude   = [double]$entry.lon
                    }
                } else {
                    $results[$entry.query] = $null
                }
            }
        } catch {
            Write-Warning "GeoIP batch request failed: $_. Retrying once..."
            Start-Sleep -Seconds 5
            try {
                $response = Invoke-RestMethod -Uri 'http://ip-api.com/batch' `
                    -Method Post `
                    -Body $jsonBody `
                    -ContentType 'application/json' `
                    -ErrorAction Stop

                foreach ($entry in $response) {
                    if ($entry.status -eq 'success') {
                        $results[$entry.query] = @{
                            CountryCode = $entry.countryCode
                            ISP         = $entry.isp
                            Org         = $entry.org
                            IsHosting   = [bool]$entry.hosting
                            Latitude    = [double]$entry.lat
                            Longitude   = [double]$entry.lon
                        }
                    } else {
                        $results[$entry.query] = $null
                    }
                }
            } catch {
                Write-Warning "GeoIP batch retry failed: $_. Skipping $($batch.Count) IPs."
                foreach ($ip in $batch) {
                    $results[$ip] = $null
                }
            }
        }

        Write-Progress -Activity 'GeoIP Enrichment' `
            -Status "Batch $($batchNum + 1) of $totalBatches ($($results.Count) IPs resolved)" `
            -PercentComplete ([Math]::Round(($batchNum + 1) / $totalBatches * 100))
    }

    Write-Progress -Activity 'GeoIP Enrichment' -Completed
    return $results
}