<?php

declare(strict_types=1);

namespace Plugin\tnt_stabila_shop\src\Helper;

/**
 * Pure decision helper that routes an alert to the notification channels whose
 * merchant-configured minimum severity is met.
 *
 * Each channel (in-shop notification, email, WhatsApp, Slack, webhook, …) carries
 * its own minimum-severity threshold, so a merchant can, for example, receive
 * every event as an in-shop notice, an email only from "warning" up and a Slack
 * ping only for "fatal". The severity scale is the SAME one used by
 * {@see AlertSeverityDecider} — this helper does NOT invent a second scale; it
 * delegates the ordering and the ">=" boundary to that decider.
 *
 * A channel with no/blank/unresolvable configuration is treated as disabled and
 * never fires, so enabling any channel is fully opt-in. Unknown channel keys in
 * the configuration are ignored. The returned list is always in the canonical
 * {@see self::CHANNEL_ORDER}, independent of the configuration map's key order,
 * so routing is deterministic. The function is pure: no I/O, globals or clock.
 */
class AlertChannelRouter
{
    /** In-shop admin notification channel key. */
    public const CHANNEL_NOTIFICATION = 'notification';
    /** Email channel key. */
    public const CHANNEL_MAIL = 'mail';
    /** WhatsApp channel key (routing supported; outbound delivery out of scope). */
    public const CHANNEL_WHATSAPP = 'whatsapp';
    /** Slack channel key (routing supported; outbound delivery out of scope). */
    public const CHANNEL_SLACK = 'slack';
    /** Generic outbound webhook channel key (routing supported; delivery out of scope). */
    public const CHANNEL_WEBHOOK = 'webhook';

    /**
     * Canonical, deterministic channel order used for the returned list.
     *
     * @var list<string>
     */
    public const CHANNEL_ORDER = [
        self::CHANNEL_NOTIFICATION,
        self::CHANNEL_MAIL,
        self::CHANNEL_WHATSAPP,
        self::CHANNEL_SLACK,
        self::CHANNEL_WEBHOOK,
    ];

    /**
     * Decide which channels should fire for an alert of the given severity.
     *
     * @param string               $severity          Severity label (e.g. "warning", "error", "fatal").
     * @param array<string, mixed> $channelMinSeverity Map of channel key => minimum severity
     *                                                  (int rank 0-4, severity label string, or
     *                                                  null/blank = channel disabled).
     * @return list<string> Channel keys whose threshold is met, in {@see self::CHANNEL_ORDER}.
     */
    public static function route(string $severity, array $channelMinSeverity): array
    {
        $fired = [];
        foreach (self::CHANNEL_ORDER as $channel) {
            if (\array_key_exists($channel, $channelMinSeverity) === false) {
                continue;
            }
            $minRank = self::minRankOf($channelMinSeverity[$channel]);
            // Disabled/blank/unresolvable configuration: the channel never fires.
            if ($minRank === null) {
                continue;
            }
            // Reuse the existing severity scale, ">=" boundary and fail-open-on-unknown rules.
            if (AlertSeverityDecider::shouldAlert($severity, $minRank)) {
                $fired[] = $channel;
            }
        }

        return $fired;
    }

    /**
     * Whether a single channel would fire for the given severity and configuration.
     *
     * @param string $severity    Severity label.
     * @param mixed  $minSeverity Channel configuration (int rank, label string, or null/blank).
     * @return bool True when the channel is enabled and the severity meets its threshold.
     */
    public static function channelFires(string $severity, mixed $minSeverity): bool
    {
        $minRank = self::minRankOf($minSeverity);
        if ($minRank === null) {
            return false;
        }

        return AlertSeverityDecider::shouldAlert($severity, $minRank);
    }

    /**
     * Resolve a per-channel configuration value to a numeric minimum rank.
     *
     * Accepts an int rank (0-4), a numeric string, or a severity label; a null,
     * blank or unresolvable value yields null, meaning the channel is disabled.
     *
     * @param mixed $config Raw channel configuration value.
     * @return int|null Minimum severity rank, or null when the channel is disabled.
     */
    public static function minRankOf(mixed $config): ?int
    {
        if (\is_int($config)) {
            return $config;
        }
        if (\is_string($config) === false) {
            return null;
        }
        $trimmed = \trim($config);
        if ($trimmed === '') {
            return null;
        }
        if (\is_numeric($trimmed)) {
            return (int)$trimmed;
        }

        return AlertSeverityDecider::rankOf($trimmed);
    }
}
