<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Caching\Adapters;

use Ekesto\CacheInvalidator\Integrations\Caching\AdapterSignalGuard;
use Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterInterface;
use Ekesto\CacheInvalidator\Integrations\Caching\CacheAdapterUrlNormalizer;
use Ekesto\CacheInvalidator\Integrations\Caching\EmitsFullInvalidationFromCacheClear;
use Ekesto\CacheInvalidator\Integrations\Caching\EmitsSingleTargetCacheAdapterInvalidation;
use Ekesto\CacheInvalidator\Integrations\Caching\UrlPurgeCapableAdapterInterface;

/**
 * Cache Enabler adapter.
 *
 * Uses Cache Enabler public static methods for URL and full cache purge.
 */
final class CacheEnablerAdapter implements CacheAdapterInterface, UrlPurgeCapableAdapterInterface
{
    use EmitsFullInvalidationFromCacheClear;
    use EmitsSingleTargetCacheAdapterInvalidation;

    /**
     * Returns a human-readable adapter label.
     *
     * @return string
     */
    public function getLabel(): string
    {
        return 'Cache Enabler';
    }

    /**
     * Returns a unique adapter key.
     *
     * @return string
     */
    public function getKey(): string
    {
        return 'cache-enabler';
    }

    /**
     * Returns whether Cache Enabler purge APIs are available.
     *
     * @return bool
     */
    public function isAvailable(): bool
    {
        return defined('CE_VERSION')
            && class_exists('Cache_Enabler')
            && method_exists('Cache_Enabler', 'clear_page_cache_by_url')
            && method_exists('Cache_Enabler', 'clear_total_cache');
    }

    /**
     * Returns whether Cache Enabler supports runtime cache-status detection.
     *
     * @return bool
     */
    public function supportsCacheStatusDetection(): bool
    {
        return false;
    }

    /**
     * Returns whether Cache Enabler cache should be treated as enabled.
     *
     * @return bool
     */
    public function isCacheEnabled(): bool
    {
        return true;
    }

    /**
     * Registers adapter-specific hooks.
     *
     * @return void
     */
    public function registerHooks(): void
    {
        // Settings button path:
        // options.php -> validate_settings() -> clear_site_cache().
        // The submit button adds cache_enabler[clear_site_cache_on_saved_settings].
        add_filter('cache_enabler_settings_before_validation', function ($settings) {
            if (is_array($settings) && !empty($settings['clear_site_cache_on_saved_settings'])) {
                $this->emitFullInvalidationFromCacheClear('cache_enabler');
            }

            return $settings;
        }, 10, 1);

        add_action('cache_enabler_complete_cache_cleared', function (): void {
            $this->emitFullInvalidationFromCacheClear('cache_enabler');
        }, 10, 0);

        add_action('cache_enabler_site_cache_cleared', function (): void {
            $this->emitFullInvalidationFromCacheClear('cache_enabler');
        }, 10, 0);

        // Backward compatibility for older Cache Enabler versions.
        add_action('ce_action_cache_cleared', function (): void {
            $this->emitFullInvalidationFromCacheClear('cache_enabler');
        }, 10, 0);

        add_action('cache_enabler_page_cache_cleared', [$this, 'handleManualSingleTargetPurge'], 10, 2);
    }

    /**
     * Handle Cache Enabler page-level purge events.
     *
     * @param mixed $url Purged URL payload.
     * @param mixed $postId Purged post ID payload.
     * @return void
     */
    public function handleManualSingleTargetPurge($url, $postId = null): void
    {
        $hook = (string) current_filter();

        if (!$this->isManualSingleTargetRequest()) {
            $this->logIgnoredSingleTarget($hook, 'not_manual', [
                'request_type' => 'admin_bar_clear_page',
                'classification_path' => 'none',
            ]);
            return;
        }

        $classification = $this->classifyManualSignal($url, $postId);
        if ($classification === null) {
            $this->logIgnoredSingleTarget($hook, 'ambiguous', [
                'request_type' => 'admin_bar_clear_page',
                'classification_path' => 'none',
            ]);
            return;
        }

        $validationFailureReason = '';
        $validated = $this->validateManualSignal($classification, $validationFailureReason);
        if ($validated === null) {
            $this->logIgnoredSingleTarget($hook, $validationFailureReason !== '' ? $validationFailureReason : 'invalid_payload', [
                'request_type' => 'admin_bar_clear_page',
                'classification_path' => (string) $classification['type'],
                'post_id' => isset($classification['post_id']) && is_int($classification['post_id']) ? $classification['post_id'] : 0,
            ]);
            return;
        }

        if (!isset($validated['url']) || !is_string($validated['url']) || $validated['url'] === '') {
            $this->logIgnoredSingleTarget($hook, 'invalid_payload', [
                'request_type' => 'admin_bar_clear_page',
                'classification_path' => (string) $validated['mode'],
                'post_id' => $validated['post_id'],
            ]);
            return;
        }

        $this->emitSingleTargetInvalidation(
            $hook,
            [$validated['url']],
            $validated,
            [
                'request_type' => 'admin_bar_clear_page',
                'classification_path' => $validated['mode'],
            ],
            [
                'trigger_class' => 'cache_adapter',
                'trigger_event' => 'cache_enabler_manual_single_target_purge',
                'trigger_post_id' => $validated['post_id'],
                'trigger_post_type' => $validated['post_type'],
                'reason' => 'supported_cache_plugin_single_target_purge',
                'source' => 'cache_enabler',
                'trusted_source' => 'cache_adapter_single_target',
                'resolution' => 'TARGETED',
                'intent' => 'TARGETED',
            ]
        );
    }

    /**
     * Classify a Cache Enabler manual purge signal.
     *
     * @param mixed $url Purged URL payload.
     * @param mixed $postId Purged post ID payload.
     * @return array{type:string,urls:array<int,mixed>,post_id:int|null}|null
     */
    private function classifyManualSignal($url, $postId): ?array
    {
        if (!is_scalar($url)) {
            return null;
        }

        $resolvedPostId = is_scalar($postId) ? absint($postId) : 0;
        if ($resolvedPostId <= 0) {
            $resolvedPostId = null;
        }

        return [
            'type' => 'url',
            'urls' => [(string) $url],
            'post_id' => $resolvedPostId,
        ];
    }

    /**
     * Determine whether this request matches Cache Enabler manual clear-page flow.
     *
     * @return bool
     */
    private function isManualSingleTargetRequest(): bool
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Cache Enabler request classification; capability and its nonce are validated below.
        $cache = isset($_GET['_cache']) && is_string($_GET['_cache']) ? sanitize_key(wp_unslash($_GET['_cache'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Cache Enabler request classification; capability and its nonce are validated below.
        $action = isset($_GET['_action']) && is_string($_GET['_action']) ? sanitize_key(wp_unslash($_GET['_action'])) : '';

        if ($cache !== 'cache-enabler' || $action !== 'clearurl') {
            return false;
        }

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Cache Enabler third-party capability filter.
        $canClearCache = (bool) apply_filters('cache_enabler_user_can_clear_cache', current_user_can('manage_options'));
        if (!$canClearCache) {
            return false;
        }

        $nonce = isset($_GET['_wpnonce']) && is_string($_GET['_wpnonce'])
            ? sanitize_text_field(wp_unslash($_GET['_wpnonce']))
            : '';

        return $nonce !== ''
            && wp_verify_nonce($nonce, 'cache_enabler_clear_cache_nonce') !== false;
    }

    /**
     * Validate one classified signal and resolve one normalized target URL.
     *
     * @param array{type:string,urls:array<int,mixed>,post_id:int|null} $classification Classified signal.
     * @return array{
     *   urls:array<int,string>,
     *   url:string,
     *   post_id:int,
     *   post_type:string,
     *   mode:string
     * }|null
     */
    private function validateManualSignal(array $classification, string &$failureReason = ''): ?array
    {
        if ($classification['type'] !== 'url' || count($classification['urls']) !== 1) {
            return null;
        }

        $url = CacheAdapterUrlNormalizer::normalize($classification['urls'][0]);
        if ($url === '') {
            $failureReason = 'invalid_or_cross_origin_url';
            return null;
        }

        $postId = 0;
        $postType = '';
        if (isset($classification['post_id']) && is_int($classification['post_id']) && $classification['post_id'] > 0) {
            $postId = $classification['post_id'];
            $resolvedType = get_post_type($postId);
            $postType = is_string($resolvedType) ? sanitize_key($resolvedType) : '';
        }

        return [
            'urls' => [$url],
            'url' => $url,
            'post_id' => $postId,
            'post_type' => $postType,
            'mode' => 'url',
        ];
    }

    /**
     * Returns the adapter source key used in single-target signal logs.
     *
     * @return string
     */
    protected function getSingleTargetSignalSource(): string
    {
        return 'cache_enabler';
    }

    /**
     * Purges cache for a specific post ID by resolving and purging its permalink URL.
     *
     * @param int $postId Post/page ID to purge.
     * @return bool True when purge call was executed.
     */
    public function purgePostId(int $postId): bool
    {
        $postId = absint($postId);
        if ($postId <= 0) {
            return false;
        }

        if (!$this->isAvailable()) {
            return false;
        }

        $url = get_permalink($postId);
        if (!is_string($url) || $url === '') {
            return false;
        }

        $url = trim($url);
        if ($url === '') {
            return false;
        }

        if (is_callable(['Cache_Enabler', 'clear_page_cache_by_url'])) {
            AdapterSignalGuard::runWithSuppressedSignals(static function () use ($url): void {
                \Cache_Enabler::clear_page_cache_by_url($url);
            });
            return true;
        }

        return false;
    }

    /**
     * Purges cache for a specific absolute URL.
     *
     * @param string $url Absolute URL.
     * @return bool True when purge call was executed.
     */
    public function purgeUrl(string $url): bool
    {
        $url = trim($url);
        if ($url === '') {
            return false;
        }

        if (!$this->isAvailable()) {
            return false;
        }

        if (is_callable(['Cache_Enabler', 'clear_page_cache_by_url'])) {
            AdapterSignalGuard::runWithSuppressedSignals(static function () use ($url): void {
                \Cache_Enabler::clear_page_cache_by_url($url);
            });
            return true;
        }

        return false;
    }

    /**
     * Purges all cache entries for the active site.
     *
     * @return bool True when purge call was executed.
     */
    public function purgeAll(): bool
    {
        if (!$this->isAvailable()) {
            return false;
        }

        if (is_callable(['Cache_Enabler', 'clear_total_cache'])) {
            AdapterSignalGuard::runWithSuppressedSignals(static function (): void {
                \Cache_Enabler::clear_total_cache();
            });
            return true;
        }

        return false;
    }
}
