<?php
declare(strict_types=1);

namespace Ekesto\CacheWarmup\Core;

/**
 * Resolves user-meaningful trigger reason labels from stored run metadata.
 *
 * Display-only normalization — no DB access, no side effects.
 */
final class TriggerReasonResolver
{
    /**
     * Resolve a user-meaningful trigger reason label.
     *
     * @param string              $triggerKey Stored trigger key (e.g. 'cache_invalidation', 'manual:settings_general').
     * @param array<string,mixed> $details    Decoded log details JSON.
     * @param string              $mode       Run mode ('single', 'full').
     * @return string
     */
    public static function resolve(string $triggerKey, array $details, string $mode = ''): string
    {
        $primaryPostType = sanitize_key((string) ($details['primary_trigger_post_type'] ?? ''));
        $primaryEvent = self::normalizeTriggerLookupKey((string) ($details['primary_trigger_event'] ?? ''));
        $primaryClass = sanitize_key((string) ($details['primary_trigger_class'] ?? ''));

        $label = null;
        if ($primaryClass === 'post' && $primaryEvent === 'deleted') {
            $label = self::resolveDeletedPostLabel($primaryPostType);
        }
        if ($label === null) {
            $label = self::resolveTemplateIdentity($primaryEvent, $primaryPostType);
        }
        if ($label === null) {
            $label = TriggerLabelMap::forPostType($primaryPostType);
        }
        if ($label === null) {
            $label = self::resolvePrimaryEvent($primaryEvent);
        }
        if ($label === null) {
            $label = self::resolvePrimaryClass($primaryClass);
        }
        if (
            $label === null
            && $primaryClass === 'system'
            && $primaryEvent === 'cache_plugin_cleared'
        ) {
            // Explicit guard so system cache clears never collapse into generic
            // cache_invalidation fallback labels.
            $label = 'Cache plugin cleared';
        }
        if ($label === null) {
            $label = ($triggerKey === 'cache_invalidation')
                ? 'Content update'
                : self::resolveNonEciTrigger($triggerKey);
        }

        if ($mode === 'full') {
            $label .= ' (full)';
        }

        return $label;
    }

    /**
     * Normalize trigger/event keys for label-map lookups.
     *
     * Keep ":" and "_" to preserve exact map keys (for example
     * "maintenance:plugin_activated", "manual_settings_general").
     *
     * @param string $value Raw lookup key.
     * @return string
     */
    private static function normalizeTriggerLookupKey(string $value): string
    {
        $value = strtolower(trim($value));
        if ($value === '') {
            return '';
        }

        $normalized = preg_replace('/[^a-z0-9:_-]/', '', $value);
        return is_string($normalized) ? $normalized : '';
    }

    /**
     * Resolve labels from locked primary trigger event.
     *
     * @param string $event Primary trigger event key.
     * @return string|null
     */
    private static function resolvePrimaryEvent(string $event): ?string
    {
        return TriggerKeyLabelMap::get($event);
    }

    /**
     * Resolve labels from template-related trigger identities.
     *
     * @param string $event Primary trigger event key.
     * @param string $postType Primary post type key.
     * @return string|null
     */
    private static function resolveTemplateIdentity(string $event, string $postType): ?string
    {
        $globalWidgetEvents = [
            'elementor_global_widget_change' => true,
            'elementor_global_widget_change_fallback' => true,
        ];

        if (isset($globalWidgetEvents[$event])) {
            return 'Global widget update';
        }

        $templateEvents = [
            'template_change' => true,
            'elementor_template_change' => true,
            'elementor_template_change_fallback' => true,
        ];

        if (isset($templateEvents[$event])) {
            return 'Template update';
        }

        $templatePostTypes = [
            'wp_template' => true,
            'wp_template_part' => true,
            'elementor_library' => true,
        ];

        return isset($templatePostTypes[sanitize_key($postType)])
            ? 'Template update'
            : null;
    }

    /**
     * Resolve a deletion label from the same post-type label source used for updates.
     *
     * @param string $postType Primary post type key.
     * @return string
     */
    private static function resolveDeletedPostLabel(string $postType): string
    {
        $postType = sanitize_key($postType);
        $postTypeLabel = TriggerLabelMap::forPostType($postType);
        if ($postTypeLabel !== null) {
            $deletedLabel = preg_replace('/\s+update$/i', ' deleted', $postTypeLabel);
            if (is_string($deletedLabel) && $deletedLabel !== '' && $deletedLabel !== $postTypeLabel) {
                return $deletedLabel;
            }

            return $postTypeLabel . ' deleted';
        }

        $postTypeObject = $postType !== '' ? get_post_type_object($postType) : null;
        if ($postTypeObject instanceof \WP_Post_Type) {
            $label = trim((string) ($postTypeObject->labels->singular_name ?? ''));
            if ($label === '') {
                $label = trim((string) ($postTypeObject->label ?? ''));
            }
            if ($label !== '') {
                return $label . ' deleted';
            }
        }

        return 'Content deleted';
    }

    /**
     * Resolve labels from locked primary trigger class.
     *
     * @param string $triggerClass Primary trigger class key.
     * @return string|null
     */
    private static function resolvePrimaryClass(string $triggerClass): ?string
    {
        switch ($triggerClass) {
            case 'post':
                return 'Content update';

            case 'taxonomy':
                return 'Content update';

            case 'comment':
                return 'Comment update';

            case 'meta':
                return 'Content update';

            case 'form':
                return 'Form update';

            case 'async':
                return 'Related content refresh';

            case 'timed':
                return 'Scheduled cache refresh';

            case 'presentation':
                return 'Layout change';

            case 'manual':
                return 'Manual cache refresh';

            default:
                return null;
        }
    }

    /**
     * Resolve labels for non-ECI trigger keys.
     *
     * IMPORTANT:
     * This method must only be called when NO primary identity exists.
     * TriggerKeyLabelMap must never override ECI-derived identity labels.
     *
     * @param string $triggerKey Trigger key.
     * @return string
     */
    private static function resolveNonEciTrigger(string $triggerKey): string
    {
        $mappedLabel = TriggerKeyLabelMap::get($triggerKey);
        if ($mappedLabel !== null) {
            return $mappedLabel;
        }

        switch ($triggerKey) {
            // @deprecated Fallback for legacy rows without primary trigger identity.
            // @remove-after 6.0.0
            case 'manual:settings_general':
            // @deprecated Fallback for legacy rows without primary trigger identity.
            // @remove-after 6.0.0
            case 'manual:admin_bar':
                return 'Manual cache refresh';

            case 'purge_all':
            case 'purge_pages':
                return 'Full cache refresh';

            case 'purge_single_url':
            case 'purge_front_page':
                return 'Targeted cache refresh';

            default:
                return $triggerKey;
        }
    }
}
