src/notifiers/NotifyDiscord.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
Discord webhook notifications for course alerts via embeds.

.DESCRIPTION
Sends course alerts to Discord via webhook with rich embeds.
Webhooks from IPSC_DISCORD_WEBHOOKS environment variable (comma-separated).
Supports retry logic, parallel posting, and error recovery.
#>


# ============================================================================
# CONSTANTS
# ============================================================================

$DISCORD_WEBHOOK_TIMEOUT = 30
$DISCORD_MAX_RETRIES = 3
$DISCORD_EMBED_COLOR_NEW = 3066993          # Green #10b981
$DISCORD_EMBED_COLOR_REDUCED = 16243689     # Orange #f59e0b
$DISCORD_EMBED_COLOR_SOLD_OUT = 15671588    # Red #ef4444
$DISCORD_EMBED_COLOR_DEFAULT = 3947580      # Blue #3b82f6

# ============================================================================
# PRIVATE FUNCTIONS (Helpers)
# ============================================================================

function _GetAlertEmoji {
    param([string]$AlertReason)

    switch ($AlertReason) {
        'NEW' { return '[NEW]' }
        'AVAILABILITY_REDUCED' { return '[REDUCED]' }
        'SOLD_OUT' { return '[SOLD_OUT]' }
        default { return '[ALERT]' }
    }
}

function _GetAlertColor {
    param([string]$AlertReason)

    switch ($AlertReason) {
        'NEW' { return $DISCORD_EMBED_COLOR_NEW }
        'AVAILABILITY_REDUCED' { return $DISCORD_EMBED_COLOR_REDUCED }
        'SOLD_OUT' { return $DISCORD_EMBED_COLOR_SOLD_OUT }
        default { return $DISCORD_EMBED_COLOR_DEFAULT }
    }
}

function _ValidateDiscordConfig {
    <# .SYNOPSIS Validate Discord configuration #>
    param([object]$Config)

    if (-not $Config) {
        Write-Log -Level WARN -Message "Discord config missing"
        return $false
    }

    if (-not $Config.enabled) {
        return $false
    }

    return $true
}

function _GetDiscordWebhookUrls {
    <# .SYNOPSIS Get Discord webhook URLs from env var or config #>
    param([object]$Config)

    # Try environment variable first (highest priority)
    $webhookUrlsEnv = $env:IPSC_DISCORD_WEBHOOKS
    if ($webhookUrlsEnv) {
        $urls = @($webhookUrlsEnv -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        if ($urls -and $urls.Count -gt 0) {
            Write-Log -Level DEBUG -Message "Discord webhooks from environment variable" `
                -Context @{ count = $urls.Count } | Out-Null
            return $urls
        }
    }

    # Fallback to config.json (backward compatibility)
    if ($Config.webhook_urls) {
        $urls = @($Config.webhook_urls | Where-Object { $_ })
        if ($urls -and $urls.Count -gt 0) {
            Write-Log -Level DEBUG -Message "Discord webhooks from config" `
                -Context @{ count = $urls.Count } | Out-Null
            return $urls
        }
    }

    Write-Log -Level WARN -Message "Discord notifier disabled: no webhook URLs configured" | Out-Null
    return @()
}

function _GroupAlertsByReason {
    <# .SYNOPSIS Group alerts by alert_reason (NEW, REDUCED, SOLD_OUT) #>
    param([object[]]$Alerts)

    if (-not $Alerts -or $Alerts.Count -eq 0) {
        return @{}
    }

    $grouped = @{}
    foreach ($alert in $Alerts) {
        $reason = $alert.alert_reason
        if (-not $reason) { $reason = 'OTHER' }

        if (-not $grouped[$reason]) {
            $grouped[$reason] = @()
        }
        $grouped[$reason] += $alert
    }

    return $grouped
}

function _BuildDiscordEmbeds {
    <# .SYNOPSIS Build Discord embed messages grouped by alert reason #>
    param([hashtable]$GroupedAlerts)

    [array]$embeds = @()
    $reasonOrder = @('NEW', 'AVAILABILITY_REDUCED', 'SOLD_OUT', 'OTHER')

    foreach ($reason in $reasonOrder) {
        if (-not $GroupedAlerts[$reason] -or $GroupedAlerts[$reason].Count -eq 0) {
            continue
        }

        $alerts = $GroupedAlerts[$reason]
        $emoji = _GetAlertEmoji -AlertReason $reason
        $color = _GetAlertColor -AlertReason $reason
        $count = $alerts.Count

        $title = switch ($reason) {
            'NEW' { "$emoji NEW Courses ($count available)" }
            'AVAILABILITY_REDUCED' { "$emoji Availability Reduced ($count courses)" }
            'SOLD_OUT' { "$emoji Sold Out ($count courses)" }
            default { "$emoji Course Alerts ($count)" }
        }

        $description = switch ($reason) {
            'NEW' { "New IPSC courses available on shooting-store.ch" }
            'AVAILABILITY_REDUCED' { "Available slots decreased for these courses" }
            'SOLD_OUT' { "These courses are no longer available" }
            default { "Course status updates" }
        }

        # Build fields (one per course)
        $fields = @()
        foreach ($alert in $alerts) {
            $value = "[$($alert.name)]($($alert.url)) | $($alert.price)`n$($alert.date) | $($alert.time) | **$($alert.availability) Slots**"

            $fields += @{
                name = " "
                value = $value
                inline = $false
            }
        }

        # Create embed
        $embed = @{
            title = $title
            description = $description
            color = $color
            fields = $fields
            footer = @{
                text = "IPSC Kurs Watcher"
            }
            timestamp = ([DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ'))
        }

        $embeds += $embed
    }

    return ,$embeds
}

function _PostToWebhook {
    <# .SYNOPSIS Post message to single Discord webhook with retry logic #>
    param(
        [string]$WebhookUrl,
        [array]$Embeds,
        [int]$TimeoutSeconds,
        [int]$MaxRetries
    )

    # Apply defaults if not provided
    if (-not $TimeoutSeconds -or $TimeoutSeconds -le 0) { $TimeoutSeconds = 30 }
    if (-not $MaxRetries -or $MaxRetries -le 0) { $MaxRetries = 3 }

    if (-not $WebhookUrl -or -not $Embeds -or $Embeds.Count -eq 0) {
        return @{ success = $false; error = "Invalid webhook URL or embeds" }
    }

    $payload = @{ embeds = @($Embeds) }
    $jsonPayload = $payload | ConvertTo-Json -Depth 10 -Compress

    $attempt = 0
    while ($attempt -lt $MaxRetries) {
        $attempt++
        try {
            $params = @{
                Uri = $WebhookUrl
                Method = 'POST'
                Headers = @{ 'Content-Type' = 'application/json' }
                Body = $jsonPayload
                TimeoutSeconds = $TimeoutSeconds
            }

            Invoke-SecureWebRequest @params | Out-Null
            Write-Log -Level DEBUG -Message "Discord webhook POST successful" `
                -Context @{ attempt = $attempt; webhook = $WebhookUrl } | Out-Null

            return @{ success = $true; status = 'sent' }
        }
        catch {
            if ($attempt -lt $MaxRetries) {
                $waitSeconds = [Math]::Pow(2, $attempt - 1)
                Write-Log -Level WARN -Message "Discord webhook retry attempt $attempt/$MaxRetries" `
                    -Context @{ webhook = $WebhookUrl; wait_seconds = $waitSeconds; error = $_.Exception.Message } | Out-Null
                Start-Sleep -Seconds $waitSeconds
            }
            else {
                Write-Log -Level ERROR -Message "Discord webhook failed after $MaxRetries attempts" `
                    -Context @{ webhook = $WebhookUrl; error = $_.Exception.Message } | Out-Null
                return @{ success = $false; error = $_.Exception.Message }
            }
        }
    }

    return @{ success = $false; error = "Unknown error" }
}

function _SendDiscordWebhooks {
    <# .SYNOPSIS Send Discord embeds to all webhooks #>
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification='Parameters used in job scriptblock with Using: scope')]
    param(
        [string[]]$WebhookUrls,
        [array]$Embeds,
        [int]$TimeoutSeconds = 30,
        [int]$MaxRetries = 3
    )

    if (-not $WebhookUrls -or $WebhookUrls.Count -eq 0) {
        return @{ sent = 0; failed = 0; results = @() }
    }

    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    $results = @()
    $successCount = 0
    $failureCount = 0

    # Send to all webhooks (serial to avoid job startup overhead for small batches)
    $results = @()
    foreach ($url in $WebhookUrls) {
        $result = _PostToWebhook -WebhookUrl $url -Embeds $Embeds `
            -TimeoutSeconds $TimeoutSeconds -MaxRetries $MaxRetries
        $results += $result

        if ($result.success) {
            $successCount++
        }
        else {
            $failureCount++
        }
    }

    $stopwatch.Stop()

    Write-Log -Level INFO -Message "Discord notifications sent" `
        -Context @{
            webhook_count = $WebhookUrls.Count
            embeds_sent = $Embeds.Count
            success = $successCount
            failed = $failureCount
            duration_ms = $stopwatch.ElapsedMilliseconds
        } | Out-Null

    return @{ sent = $successCount; failed = $failureCount; results = $results }
}

# ============================================================================
# PUBLIC FUNCTIONS (Exported API)
# ============================================================================

function Send-DiscordNotification {
    <#
    .SYNOPSIS
    Send Discord webhook notifications for course alerts.

    .DESCRIPTION
    Sends course alerts to Discord via webhook with rich embeds.
    Webhooks from IPSC_DISCORD_WEBHOOKS environment variable or config.json.
    Supports retry logic, parallel posting, and graceful error handling.

    .PARAMETER Alerts
    Array of alert objects (from pipeline, containing alert_reason, name, date, time, availability, price, url)

    .PARAMETER Config
    Discord configuration from config.json (enabled, retry_attempts, timeout_seconds)

    .EXAMPLE
    $alerts = @(...course alerts...)
    Send-DiscordNotification -Alerts $alerts -Config $config.notifiers.discord
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [ValidateNotNull()][object[]]$Alerts,
        [ValidateNotNull()][object]$Config
    )

    # Validation
    if (-not (_ValidateDiscordConfig -Config $Config)) {
        return
    }

    if (-not $Alerts -or $Alerts.Count -eq 0) {
        Write-Log -Level DEBUG -Message "Discord: no alerts to send" | Out-Null
        return
    }

    if (-not $PSCmdlet.ShouldProcess("Discord webhooks", "Send $($Alerts.Count) course alerts")) {
        return
    }

    # Get webhook URLs
    $webhookUrls = _GetDiscordWebhookUrls -Config $Config
    if (-not $webhookUrls -or $webhookUrls.Count -eq 0) {
        return
    }

    # Group alerts by reason and build embeds
    $grouped = _GroupAlertsByReason -Alerts $Alerts
    $embeds = _BuildDiscordEmbeds -GroupedAlerts $grouped

    if (-not $embeds -or $embeds.Count -eq 0) {
        Write-Log -Level WARN -Message "Discord: no embeds to send" | Out-Null
        return
    }

    # Send to all webhooks
    $timeoutSeconds = $Config.timeout_seconds -as [int]
    if (-not $timeoutSeconds -or $timeoutSeconds -le 0) { $timeoutSeconds = $DISCORD_WEBHOOK_TIMEOUT }

    $maxRetries = $Config.retry_attempts -as [int]
    if (-not $maxRetries -or $maxRetries -le 0) { $maxRetries = $DISCORD_MAX_RETRIES }

    _SendDiscordWebhooks -WebhookUrls $webhookUrls -Embeds $embeds `
        -TimeoutSeconds $timeoutSeconds -MaxRetries $maxRetries
}