<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Multilingual;

use Ekesto\CacheInvalidator\Core\Multilingual\MultilingualProviderRegistry;

/**
 * Computes multilingual fanout behavior from runtime trigger context.
 *
 * Generic fanout math, target-language resolution, and URL expansion are
 * Lite-safe and degrade to single-language / identity behavior without a
 * resolver. The premium fanout-mode override is delegated to the registered
 * multilingual provider (a Lite-safe no-op unless the premium implementation is
 * present); its override filter is applied only inside the premium provider.
 */
final class FanoutPolicy
{
    public const MODE_SAME_LANGUAGE = 'same_language';
    public const MODE_ALL_LANGUAGES = 'all_languages';

    /**
     * Resolve effective fanout mode for the given trigger context.
     *
     * Context keys:
     * - trigger_class (string)
     * - trigger_kind (string)
     * - multilingual_fanout_post_triggers_enabled (bool)
     * - multilingual_fanout_non_post_triggers_enabled (bool)
     * - resolver_available (bool, optional)
     * - trigger_language_available (bool, optional)
     *
     * @param array<string,mixed> $context Runtime context.
     * @return string One of MODE_* constants.
     */
    public static function resolveMode(array $context): string
    {
        $postToggle = !empty($context['multilingual_fanout_post_triggers_enabled']);
        $nonPostToggle = !empty($context['multilingual_fanout_non_post_triggers_enabled']);

        $isPostLike = self::isPostLikeTrigger($context);
        $mode = ($isPostLike ? $postToggle : $nonPostToggle)
            ? self::MODE_ALL_LANGUAGES
            : self::MODE_SAME_LANGUAGE;

        $resolverAvailable = array_key_exists('resolver_available', $context)
            ? (bool) $context['resolver_available']
            : true;
        $triggerLanguageAvailable = array_key_exists('trigger_language_available', $context)
            ? (bool) $context['trigger_language_available']
            : (is_string($context['trigger_language'] ?? null)
                && trim((string) ($context['trigger_language'] ?? '')) !== '');

        // Without an active resolver, cross-language fanout cannot be executed.
        if (!$resolverAvailable) {
            $mode = self::MODE_SAME_LANGUAGE;
        }

        // Same-language mode is meaningful only when a trigger language exists.
        if ($mode === self::MODE_SAME_LANGUAGE && !$triggerLanguageAvailable) {
            $mode = self::MODE_SAME_LANGUAGE;
        }

        // Premium fanout-mode override (no-op in Lite). The override filter is
        // applied only inside the premium multilingual provider.
        $filtered = MultilingualProviderRegistry::current()->filterFanoutMode($mode, $context);

        return self::normalizeMode((string) $filtered, $mode);
    }

    /**
     * Resolve target languages for the current purge operation.
     *
     * @param LanguageResolverInterface|null $resolver Active resolver.
     * @param string|null $triggerLanguage Trigger language code.
     * @param array<string,mixed> $context Runtime context.
     * @return string[]
     */
    public static function resolveTargetLanguages(
        ?LanguageResolverInterface $resolver,
        ?string $triggerLanguage,
        array $context
    ): array {
        if ($resolver === null) {
            return [];
        }

        $triggerLanguage = is_string($triggerLanguage) ? trim($triggerLanguage) : '';
        $mode = self::resolveMode($context);

        if ($mode === self::MODE_SAME_LANGUAGE && $triggerLanguage === '') {
            return [];
        }

        $resolverContext = $context;
        $resolverContext['fanout_mode'] = $mode;

        if ($triggerLanguage !== '') {
            $resolverContext['trigger_language'] = $triggerLanguage;
        } elseif ($mode === self::MODE_ALL_LANGUAGES) {
            $defaultLanguage = trim((string) $resolver->getDefaultLanguage());
            if ($defaultLanguage !== '') {
                $resolverContext['trigger_language'] = $defaultLanguage;
            }
        }

        $resolved = $resolver->resolveTargetLanguages($resolverContext);
        return self::normalizeLanguages(is_array($resolved) ? $resolved : []);
    }

    /**
     * Expand URL targets across the resolved language set.
     *
     * @param string[] $urls Absolute URLs.
     * @param LanguageResolverInterface|null $resolver Active resolver.
     * @param string[] $languages Target languages.
     * @return string[]
     */
    public static function expandUrls(
        array $urls,
        ?LanguageResolverInterface $resolver,
        array $languages
    ): array {
        $urls = array_values(array_unique(array_filter(array_map(
            static fn($url): string => trim((string) $url),
            $urls
        ))));

        if ($resolver === null || empty($urls)) {
            return $urls;
        }

        $languages = self::normalizeLanguages($languages);
        if (empty($languages)) {
            return $urls;
        }

        $expanded = [];
        foreach ($urls as $url) {
            $expanded[] = $url;

            foreach ($languages as $lang) {
                $mapped = $resolver->mapPermalink($url, $lang);
                $mapped = trim((string) $mapped);
                if ($mapped !== '') {
                    $expanded[] = $mapped;
                }
            }
        }

        return array_values(array_unique(array_filter($expanded)));
    }

    /**
     * @param array<string,mixed> $context Runtime context.
     * @return bool
     */
    private static function isPostLikeTrigger(array $context): bool
    {
        $triggerClass = sanitize_key((string) ($context['trigger_class'] ?? ''));
        $triggerKind = sanitize_key((string) ($context['trigger_kind'] ?? ''));

        if (in_array($triggerClass, ['post', 'synced_pattern'], true)) {
            return true;
        }

        return in_array($triggerKind, ['post', 'taxonomy_assign'], true);
    }

    /**
     * @param array<int,mixed> $languages
     * @return string[]
     */
    private static function normalizeLanguages(array $languages): array
    {
        $normalized = [];
        foreach ($languages as $language) {
            if (!is_string($language)) {
                continue;
            }
            $language = trim($language);
            if ($language === '') {
                continue;
            }
            $normalized[] = $language;
        }

        return array_values(array_unique($normalized));
    }

    /**
     * @param string $mode Candidate mode.
     * @param string $fallback Fallback mode.
     * @return string
     */
    private static function normalizeMode(string $mode, string $fallback): string
    {
        if (in_array($mode, [self::MODE_SAME_LANGUAGE, self::MODE_ALL_LANGUAGES], true)) {
            return $mode;
        }

        return $fallback;
    }
}
