<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Listeners;

use Ekesto\CacheInvalidator\Integrations\Caching\AdapterManager;
use Ekesto\CacheInvalidator\Core\EmissionBuilder;
use Ekesto\CacheInvalidator\Core\EmissionHelper;
use Ekesto\CacheInvalidator\Core\InvalidationEngine;
use Ekesto\CacheInvalidator\Core\LifecycleProbe;
use Ekesto\CacheInvalidator\InvalidatorRuntime;
use Ekesto\CacheInvalidator\Core\PurgeCoordinator;
use Ekesto\CacheInvalidator\Core\TrustDecider;
use Ekesto\CacheInvalidator\Integrations\Multilingual\LanguageResolverFactory;
use Ekesto\CacheInvalidator\Resolvers\RelationshipGraphResolver;
use Ekesto\CacheInvalidator\Support\Debug;

/**
 * Listens to filtered post meta changes and invalidates affected targets.
 */
final class MetaChangeInvalidationListener
{
    /** @var array<string,true> Request-local dedupe tokens. */
    private static array $processedTokens = [];

    /**
     * Register post meta hooks.
     *
     * @return void
     */
    public static function registerHooks(): void
    {
        add_action('added_post_meta', [self::class, 'handleAddedPostMeta'], 25, 4);
        add_action('updated_post_meta', [self::class, 'handleUpdatedPostMeta'], 25, 4);
        add_action('deleted_post_meta', [self::class, 'handleDeletedPostMeta'], 25, 4);
    }

    /**
     * Handle added post meta.
     *
     * @param int    $metaId
     * @param int    $postId
     * @param string $metaKey
     * @param mixed  $metaValue
     * @return void
     */
    public static function handleAddedPostMeta(int $metaId, int $postId, string $metaKey, $metaValue): void
    {
        unset($metaId, $metaValue);
        self::invalidateForMetaChange($postId, $metaKey, 'added_post_meta');
    }

    /**
     * Handle updated post meta.
     *
     * @param int    $metaId
     * @param int    $postId
     * @param string $metaKey
     * @param mixed  $metaValue
     * @return void
     */
    public static function handleUpdatedPostMeta(int $metaId, int $postId, string $metaKey, $metaValue): void
    {
        unset($metaId, $metaValue);
        self::invalidateForMetaChange($postId, $metaKey, 'updated_post_meta');
    }

    /**
     * Handle deleted post meta.
     *
     * @param int[]  $metaIds
     * @param int    $postId
     * @param string $metaKey
     * @param mixed  $metaValue
     * @return void
     */
    public static function handleDeletedPostMeta(array $metaIds, int $postId, string $metaKey, $metaValue): void
    {
        unset($metaIds, $metaValue);
        self::invalidateForMetaChange($postId, $metaKey, 'deleted_post_meta');
    }

    /**
     * Invalidate targets for a filtered post meta change.
     *
     * @param int    $postId
     * @param string $metaKey
     * @param string $event
     * @return void
     */
    private static function invalidateForMetaChange(int $postId, string $metaKey, string $event): void
    {
        if (!InvalidatorRuntime::settingsRepository()->isInvalidationEnabled()) {
            return;
        }

        if ($postId <= 0 || $metaKey === '' || InvalidationEngine::wasProcessedInRequest($postId)) {
            return;
        }

        $post = get_post($postId);
        if (!$post instanceof \WP_Post || $post->post_status !== 'publish') {
            return;
        }

        $rules = self::getMetaChangeRules();
        if (empty($rules)) {
            return;
        }

        $postType = sanitize_key((string) $post->post_type);
        if ($postType === '' || !isset($rules[$postType])) {
            return;
        }

        $metaKey = (string) $metaKey;
        $metaKeyMatchedRules = self::metaKeyMatchesRules($metaKey, $rules[$postType]);
        if (!$metaKeyMatchedRules) {
            return;
        }

        $token = $postId . '|' . $postType . '|' . $metaKey;
        if (isset(self::$processedTokens[$token])) {
            return;
        }
        self::$processedTokens[$token] = true;

        $settingsRepository = InvalidatorRuntime::settingsRepository();
        $candidateConfiguredPostTypes = array_values(array_unique(array_merge(
            [$postType],
            RelationshipGraphResolver::resolveAffectedHosts($postType)
        )));

        $activeConfiguredPostTypes = [];
        $selectedTargetPageIds = [];
        $targetPageIds = [];

        foreach ($candidateConfiguredPostTypes as $configuredPostType) {
            $postTypeConfig = $settingsRepository->getPostTypeConfig((string) $configuredPostType);
            if ($postTypeConfig === null || !($postTypeConfig['enabled'] ?? true)) {
                continue;
            }

            $activeConfiguredPostTypes[] = (string) $configuredPostType;

            $targetMode = $settingsRepository->getPostTypeTargetMode((string) $configuredPostType);
            if ($targetMode === 'selected') {
                $configuredSelectedPageIds = $settingsRepository->getPostTypeTargetPageIds((string) $configuredPostType);
                $selectedTargetPageIds = array_merge($selectedTargetPageIds, $configuredSelectedPageIds);
                $targetPageIds = array_merge($targetPageIds, $configuredSelectedPageIds);
                continue;
            }

            if ($targetMode === 'all') {
                $targetPageIds = array_merge(
                    $targetPageIds,
                    $settingsRepository->getEffectivePostTypeTargetPageIds((string) $configuredPostType)
                );
            }
        }

        $activeConfiguredPostTypes = array_values(array_unique(array_filter($activeConfiguredPostTypes)));
        if (empty($activeConfiguredPostTypes)) {
            return;
        }

        $selectedTargetPageIds = array_values(array_unique(array_filter(array_map('absint', $selectedTargetPageIds), static fn(int $id): bool => $id > 0)));
        $targetPageIds = array_values(array_unique(array_filter($targetPageIds, static fn(int $id): bool => $id > 0)));
        $extraUrls = [];

        $postTypeObject = get_post_type_object($postType);
        if ($postTypeObject instanceof \WP_Post_Type && $postTypeObject->publicly_queryable) {
            $permalink = (string) get_permalink($postId);
            if ($permalink !== '') {
                $extraUrls[] = $permalink;
            }
        }

        if (empty($targetPageIds) && empty($extraUrls)) {
            return;
        }

        $adapter = AdapterManager::getActiveAdapter();
        if ($adapter === null) {
            return;
        }

        $resolver = LanguageResolverFactory::create();
        $triggerLanguage = $resolver ? $resolver->getTriggerLanguage($postId) : null;
        $result = PurgeCoordinator::purgeTargets(
            $targetPageIds,
            $extraUrls,
            $adapter,
            $resolver,
            $triggerLanguage,
            [
                'trigger_class' => 'meta',
                'trigger_kind' => 'meta',
                'trigger_post_id' => $postId,
                'trigger_post_type' => $postType,
                'trigger_event' => $event,
                'reason' => 'meta_change',
            ]
        );
        $emittedUrls = $result['emitted_urls'] ?? [];
        if (empty($emittedUrls)) {
            return;
        }
        $fanoutLanguages = TrustDecider::normalizeStringList((array) ($result['fanout_languages'] ?? []));
        $trustedUrls = TrustDecider::trustedUrlsFromOriginUrls([
            TrustDecider::ORIGIN_SELECTED => TrustDecider::resolveTrustedUrlsForSelectedPostIds(
                $selectedTargetPageIds,
                $resolver,
                $fanoutLanguages
            ),
        ]);
        $trustedUrls = TrustDecider::intersectWithUrls($trustedUrls, $emittedUrls);
        Debug::verbose('[ECI] trust classification', [
            'trusted' => count($trustedUrls),
            'origin' => 'selected',
        ]);
        if (!empty($extraUrls)) {
            Debug::verbose('[ECI] untrusted origin', [
                'origin' => 'derived',
                'count' => count($extraUrls),
            ]);
        }

        $context = [
            'source' => $adapter->getKey(),
            'source_label' => $adapter->getLabel(),
            'reason' => 'meta_change',
            'trigger_class' => 'meta',
            'trigger_event' => $event,
            'trigger_post_id' => $postId,
            'trigger_post_type' => $postType,
            'trigger_meta_key' => $metaKey,
            'groups' => $activeConfiguredPostTypes,
            'post_ids' => $targetPageIds,
            'urls' => $extraUrls,
            'archives' => [],
            'purged_urls_directly' => $result['purged_urls_directly'] ?? [],
            'purge_skipped_urls' => $result['purge_skipped_urls'] ?? [],
            'fanout_mode' => $result['fanout_mode'] ?? 'same_language',
            'fanout_languages' => $fanoutLanguages,
            'synchronous_targeted_resolution' => true,
            'trusted_urls' => $trustedUrls,
        ];
        $emissionPayload = EmissionBuilder::build((array) ($context['trusted_urls'] ?? []), $emittedUrls);
        $context['trusted_urls'] = $emissionPayload['trusted_urls'];
        LifecycleProbe::log('meta_change_invalidation_emit', $post, [
            'hook_name' => $event,
            'reason' => 'meta_change',
            'dedupe_system' => 'meta_change_request_local',
            'dedupe_decisions' => [
                'token' => $token,
                'outcome' => 'emitted',
            ],
            'invalidation_emitted' => true,
            'emitted_url_count' => count($emissionPayload['urls']),
            'structural_claimed' => false,
            'generic_lifecycle_must_stop' => false,
        ]);
        EmissionHelper::emit($emissionPayload['urls'], $context);
    }

    /**
     * Resolve configured post-type meta-key map.
     *
     * @return array<string,array<int,string>> [post_type => [meta_key|prefix_*]]
     */
    private static function getMetaChangeRules(): array
    {
        /** @var array<string,array<int,mixed>> $raw */
        $raw = (array) apply_filters('ekesto_ci_meta_change_post_types', []);
        $out = [];

        foreach ($raw as $postType => $keys) {
            $postType = sanitize_key((string) $postType);
            if ($postType === '' || !is_array($keys)) {
                continue;
            }

            $normalizedKeys = [];
            foreach ($keys as $key) {
                $key = trim((string) $key);
                if ($key === '') {
                    continue;
                }
                $normalizedKeys[] = $key;
            }

            if (!empty($normalizedKeys)) {
                $out[$postType] = array_values(array_unique($normalizedKeys));
            }
        }

        return $out;
    }

    /**
     * Check whether a meta key matches configured exact or prefix patterns.
     *
     * @param string            $metaKey
     * @param array<int,string> $patterns
     * @return bool
     */
    private static function metaKeyMatchesRules(string $metaKey, array $patterns): bool
    {
        foreach ($patterns as $pattern) {
            if (str_ends_with($pattern, '*')) {
                $prefix = substr($pattern, 0, -1);
                if ($prefix !== '' && str_starts_with($metaKey, $prefix)) {
                    return true;
                }
                continue;
            }

            if ($metaKey === $pattern) {
                return true;
            }
        }

        return false;
    }
}
