<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Integrations\Caching\Adapters;

use Ekesto\CacheInvalidator\Core\EmissionHelper;
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\UrlPurgeCapableAdapterInterface;
use Ekesto\CacheInvalidator\Support\Debug;

/**
 * FlyingPress adapter.
 *
 * Uses FlyingPress purge API methods when available and falls back to
 * documented purge actions.
 */
final class FlyingPressAdapter implements CacheAdapterInterface, UrlPurgeCapableAdapterInterface
{
    use EmitsFullInvalidationFromCacheClear;

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

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

    /**
     * Returns whether FlyingPress purge APIs are available.
     *
     * @return bool
     */
    public function isAvailable(): bool
    {
        return $this->isPluginLoaded()
            && $this->canPurgeUrl()
            && $this->canPurgeAll();
    }

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

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

    /**
     * Registers adapter-specific hooks.
     *
     * @return void
     */
    public function registerHooks(): void
    {
        add_action('flying_press_purge_everything:after', [$this, 'handleFullClear'], 10, 0);
        add_action('flying_press_purge_url:after', [$this, 'handleManualSingleTargetPurge'], 10, 1);
    }

    /**
     * Handle full cache clear events from FlyingPress.
     *
     * @return void
     */
    public function handleFullClear(): void
    {
        // Safety: only react to :after hooks (contract requirement)
        $hook = (string) current_filter();
        if (strpos($hook, ':after') === false) {
            return;
        }

        if (!AdapterSignalGuard::shouldProcessSignal('flyingpress', $hook, 'global_invalidation')) {
            return;
        }

        $this->emitFullInvalidationFromCacheClear('flyingpress');
    }

    /**
     * Handle manual single-target URL purge events from FlyingPress.
     *
     * @param mixed $url Purged URL payload.
     * @return void
     */
    public function handleManualSingleTargetPurge($url): void
    {
        $hook = (string) current_filter();
        if (strpos($hook, ':after') === false) {
            $this->logIgnoredSingleTarget($hook, 'invalid_payload', [
                'request_type' => 'rest_purge_current_page',
                'classification_path' => 'none',
                'rest_mode' => $this->isRestRequest(),
                'request_uri' => $this->getRequestUri(),
            ]);
            return;
        }

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

        if (!$this->isManualSingleTargetRequest()) {
            $this->logIgnoredSingleTarget($hook, 'not_manual', [
                'request_type' => 'rest_purge_current_page',
                'classification_path' => (string) $classification['type'],
                'rest_mode' => $this->isRestRequest(),
                'request_uri' => $this->getRequestUri(),
            ]);
            return;
        }

        $validationFailureReason = '';
        $validated = $this->validateManualSignal($classification, $validationFailureReason);
        if ($validated === null) {
            $this->logIgnoredSingleTarget($hook, $validationFailureReason !== '' ? $validationFailureReason : 'invalid_payload', [
                'request_type' => 'rest_purge_current_page',
                'classification_path' => (string) $classification['type'],
                'rest_mode' => $this->isRestRequest(),
                'request_uri' => $this->getRequestUri(),
            ]);
            return;
        }

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

        // If adapter introduces batch purge APIs in future, DO NOT reuse this path.
        // Trusted logic is strictly single-target only.
        $targetUrls = [$validated['url']];
        if (count($targetUrls) !== 1) {
            $this->logIgnoredSingleTarget($hook, 'invalid_url_count', [
                'request_type' => 'rest_purge_current_page',
                'classification_path' => (string) $validated['mode'],
                'url_count' => count($targetUrls),
                'rest_mode' => $this->isRestRequest(),
                'request_uri' => $this->getRequestUri(),
            ]);
            return;
        }

        // Guard dedupe identity depends on (adapter + hook + signal_type + url hash).
        // Do not alter $targetUrls shape or ordering before this call.
        if (!AdapterSignalGuard::shouldProcessSignal('flyingpress', $hook, 'targeted', $targetUrls)) {
            $this->logIgnoredSingleTarget($hook, 'guard_rejected', [
                'request_type' => 'rest_purge_current_page',
                'classification_path' => $validated['mode'],
                'url' => $validated['url'],
                'post_id' => 0,
                'rest_mode' => $this->isRestRequest(),
                'request_uri' => $this->getRequestUri(),
            ]);
            return;
        }

        Debug::log('cache_plugin_single_target_detected', [
            'source' => 'flyingpress',
            'hook' => $hook,
            'accepted' => true,
            'request_type' => 'rest_purge_current_page',
            'classification_path' => $validated['mode'],
            'mode' => $validated['mode'],
            'url' => $validated['url'],
            'post_id' => 0,
            'rest_mode' => $this->isRestRequest(),
            'request_uri' => $this->getRequestUri(),
            'trusted' => true,
        ]);

        EmissionHelper::emit($targetUrls, [
            'trigger_class' => 'cache_adapter',
            'trigger_event' => 'flyingpress_manual_single_target_purge',
            'trigger_post_id' => 0,
            'trigger_post_type' => '',
            'reason' => 'supported_cache_plugin_single_target_purge',
            'source' => 'flyingpress',
            'trusted_source' => 'cache_adapter_single_target',
            'resolution' => 'TARGETED',
            'intent' => 'TARGETED',
        ]);
    }

    /**
     * Classify a FlyingPress manual purge signal.
     *
     * @param mixed $url Purged URL payload.
     * @return array{type:string,urls:array<int,mixed>,post_id:int|null}|null
     */
    private function classifyManualSignal($url): ?array
    {
        if (is_array($url) && isset($url['url']) && is_scalar($url['url'])) {
            return [
                'type' => 'url',
                'urls' => [(string) $url['url']],
                'post_id' => null,
            ];
        }

        if (!is_scalar($url)) {
            return null;
        }

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

    /**
     * Determine whether this request matches FlyingPress manual current-page REST flow.
     *
     * @return bool
     */
    private function isManualSingleTargetRequest(): bool
    {
        if (!$this->isRestRequest()) {
            return false;
        }

        return $this->isCurrentPagePurgeRoute($this->getRequestUri());
    }

    /**
     * Determine whether the current request is a REST request.
     *
     * @return bool
     */
    private function isRestRequest(): bool
    {
        return defined('REST_REQUEST') && REST_REQUEST;
    }

    /**
     * Determine whether the request URI targets FlyingPress current-page purge endpoint.
     *
     * @param string $requestUri Request URI.
     * @return bool
     */
    private function isCurrentPagePurgeRoute(string $requestUri): bool
    {
        $requestUri = trim($requestUri);
        if ($requestUri === '') {
            return false;
        }

        $needle = 'flying-press/purge-current-page';
        $uriLower = strtolower($requestUri);
        if (str_contains($uriLower, $needle)) {
            return true;
        }

        $parts = wp_parse_url($requestUri);
        if (!is_array($parts) || !isset($parts['query']) || !is_string($parts['query'])) {
            return false;
        }

        parse_str($parts['query'], $queryArgs);
        if (!isset($queryArgs['rest_route']) || !is_string($queryArgs['rest_route'])) {
            return false;
        }

        $restRoute = strtolower(trim($queryArgs['rest_route']));
        $restRoute = trim($restRoute, '/');

        return $restRoute === trim($needle, '/');
    }

    /**
     * Resolve the current request URI from server globals.
     *
     * @return string
     */
    private function getRequestUri(): string
    {
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.NonceVerification.Recommended -- Read-only classification of a third-party REST route; no mutation is performed from this value.
        if (!isset($_SERVER['REQUEST_URI']) || !is_scalar($_SERVER['REQUEST_URI'])) {
            return '';
        }

        return trim(sanitize_text_field(wp_unslash((string) $_SERVER['REQUEST_URI'])));
    }

    /**
     * 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,
     *   mode:string
     * }|null
     */
    private function validateManualSignal(array $classification, string &$failureReason = ''): ?array
    {
        if ($classification['type'] !== 'url' || count($classification['urls']) !== 1) {
            return null;
        }

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

        return [
            'urls' => [$normalizedUrl],
            'url' => $normalizedUrl,
            'mode' => 'url',
        ];
    }

    /**
     * Log an ignored manual single-target signal.
     *
     * @param string               $hook Hook name.
     * @param string               $reason Ignore reason key.
     * @param array<string,mixed> $context Additional context.
     * @return void
     */
    private function logIgnoredSingleTarget(string $hook, string $reason, array $context = []): void
    {
        if (Debug::verboseEnabled()) {
            Debug::log('cache_plugin_single_target_ignored', array_merge([
                'source' => 'flyingpress',
                'hook' => $hook,
                'accepted' => false,
                'reason' => $reason,
                'url' => '',
                'post_id' => 0,
            ], $context));
        }
    }

    /**
     * Purges cache for a specific post ID.
     *
     * @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->canPurgePost()) {
            return false;
        }

        if (is_callable(['\\FlyingPress\\Purge', 'post'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function () use ($postId): bool {
                \FlyingPress\Purge::post($postId);
                return true;
            });
        }

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

        if (is_callable(['\\FlyingPress\\Purge', 'purge_urls'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function () use ($url): bool {
                \FlyingPress\Purge::purge_urls([$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->canPurgeUrl()) {
            return false;
        }

        if (is_callable(['\\FlyingPress\\Purge', 'url'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function () use ($url): bool {
                \FlyingPress\Purge::url($url);
                return true;
            });
        }

        if (is_callable(['\\FlyingPress\\Purge', 'purge_urls'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function () use ($url): bool {
                \FlyingPress\Purge::purge_urls([$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->canPurgeAll()) {
            return false;
        }

        if (is_callable(['\\FlyingPress\\Purge', 'purge_everything'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function (): bool {
                \FlyingPress\Purge::purge_everything();
                return true;
            });
        }

        if (is_callable(['\\FlyingPress\\Purge', 'all'])) {
            return AdapterSignalGuard::runWithSuppressedSignals(static function (): bool {
                \FlyingPress\Purge::all();
                return true;
            });
        }

        return false;
    }

    /**
     * Returns whether FlyingPress supports post-level purge.
     *
     * @return bool
     */
    private function canPurgePost(): bool
    {
        return is_callable(['\\FlyingPress\\Purge', 'post'])
            || $this->canPurgeUrl();
    }

    /**
     * Returns whether FlyingPress supports URL-level purge.
     *
     * @return bool
     */
    private function canPurgeUrl(): bool
    {
        return is_callable(['\\FlyingPress\\Purge', 'url'])
            || is_callable(['\\FlyingPress\\Purge', 'purge_urls']);
    }

    /**
     * Returns whether FlyingPress supports full purge.
     *
     * @return bool
     */
    private function canPurgeAll(): bool
    {
        return is_callable(['\\FlyingPress\\Purge', 'purge_everything'])
            || is_callable(['\\FlyingPress\\Purge', 'all']);
    }

    /**
     * Returns whether FlyingPress is currently loaded.
     *
     * @return bool
     */
    private function isPluginLoaded(): bool
    {
        return defined('FLYING_PRESS_VERSION')
            || class_exists('\\FlyingPress\\Purge');
    }

}
