<?php
declare(strict_types=1);

namespace ProRank\SEO\Modules\Performance;

use ProRank\SEO\Modules\BaseModule;
use WP_REST_Request;
use WP_REST_Response;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * REST GET response cache.
 */
class RestGetCacheModule extends BaseModule {

    /** @var array<string,string> */
    private array $request_keys = [];

    public function __construct() {
        $this->slug = 'rest_get_cache';
        $this->name = 'REST GET Cache';
        $this->description = 'Cache safe public REST GET responses.';
        $this->parent_slug = 'performance';
    }

    public function init_hooks(): void {
        add_filter('rest_pre_dispatch', [$this, 'serve_cached_response'], 9, 3);
        add_filter('rest_post_dispatch', [$this, 'store_response'], 20, 3);
        add_action('save_post', [$this, 'purge_cache']);
        add_action('deleted_post', [$this, 'purge_cache']);
        add_action('updated_option', [$this, 'maybe_purge_on_option_update'], 10, 3);
    }

    public function serve_cached_response($result, $server, WP_REST_Request $request) {
        if ($result !== null || !$this->can_cache_request($request)) {
            return $result;
        }

        $cache_key = $this->get_cache_key($request);
        $this->request_keys[spl_object_hash($request)] = $cache_key;
        $cached = get_transient($cache_key);
        if (!is_array($cached) || !array_key_exists('data', $cached)) {
            return $result;
        }

        $response = new WP_REST_Response($cached['data'], (int) ($cached['status'] ?? 200));
        foreach ((array) ($cached['headers'] ?? []) as $name => $value) {
            if (is_string($name) && $name !== '' && is_scalar($value)) {
                $response->header($name, (string) $value);
            }
        }
        $response->header('X-ProRank-REST-Cache', 'HIT');

        return $response;
    }

    public function store_response($response, $server, WP_REST_Request $request) {
        if (!$response instanceof WP_REST_Response || !$this->can_cache_request($request)) {
            return $response;
        }

        $status = (int) $response->get_status();
        if ($status < 200 || $status >= 300) {
            return $response;
        }

        $data = $response->get_data();
        if ($data === null) {
            return $response;
        }

        $cache_key = $this->request_keys[spl_object_hash($request)] ?? $this->get_cache_key($request);
        $headers = [];
        foreach ($response->get_headers() as $name => $value) {
            if (is_string($name) && is_scalar($value)) {
                $headers[$name] = (string) $value;
            }
        }

        set_transient($cache_key, [
            'status' => $status,
            'headers' => $headers,
            'data' => $data,
            'stored_at' => time(),
        ], $this->get_ttl());

        $response->header('X-ProRank-REST-Cache', 'MISS');
        return $response;
    }

    public function purge_cache(): void {
        global $wpdb;
        if (!isset($wpdb->options)) {
            return;
        }

        $like = $wpdb->esc_like('_transient_prorank_rest_get_') . '%';
        $timeout_like = $wpdb->esc_like('_transient_timeout_prorank_rest_get_') . '%';
        $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $like, $timeout_like));
    }

    public function maybe_purge_on_option_update(string $option, $old_value, $value): void {
        if (str_starts_with($option, 'prorank_') || str_starts_with($option, 'woocommerce_')) {
            $this->purge_cache();
        }
    }

    private function can_cache_request(WP_REST_Request $request): bool {
        if (!$this->is_enabled() || !$this->get_setting('rest_get_cache_enabled', false)) {
            return false;
        }

        if (strtoupper($request->get_method()) !== 'GET') {
            return false;
        }

        if (is_user_logged_in() && !$this->get_setting('rest_get_cache_logged_in', false)) {
            return false;
        }

        if (is_user_logged_in() && get_current_user_id() <= 0) {
            return false;
        }

        $route = (string) $request->get_route();
        if ($route === '' || str_contains($route, '/users') || str_contains($route, '/settings') || str_contains($route, '/application-passwords')) {
            return false;
        }

        if (!$this->is_view_context_request($request) || !$this->is_public_status_request($request)) {
            return false;
        }

        foreach ($this->get_allowed_route_prefixes() as $prefix) {
            if (str_starts_with($route, $prefix)) {
                return true;
            }
        }

        return false;
    }

    private function get_allowed_route_prefixes(): array {
        $raw = (string) $this->get_setting('rest_get_cache_namespaces', '/wp/v2/posts,/wp/v2/pages,/wp/v2/categories,/wp/v2/tags,/wc/store/');
        $prefixes = array_filter(array_map('trim', explode(',', $raw)));
        return array_values(array_unique($prefixes));
    }

    private function get_cache_key(WP_REST_Request $request): string {
        $params = $request->get_query_params();
        ksort($params);

        $variant = '';
        if (is_user_logged_in() && $this->get_setting('rest_get_cache_logged_in', false)) {
            $variant = '|user:' . get_current_user_id();
        }

        return 'prorank_rest_get_' . md5($request->get_route() . '|' . wp_json_encode($params) . $variant);
    }

    private function is_view_context_request(WP_REST_Request $request): bool {
        $params = $request->get_query_params();
        if (!array_key_exists('context', $params)) {
            return true;
        }

        foreach ($this->split_param_values($params['context']) as $context) {
            if (strtolower((string) $context) !== 'view') {
                return false;
            }
        }

        return true;
    }

    private function is_public_status_request(WP_REST_Request $request): bool {
        $params = $request->get_query_params();
        if (!array_key_exists('status', $params)) {
            return true;
        }

        foreach ($this->split_param_values($params['status']) as $status) {
            if (strtolower((string) $status) !== 'publish') {
                return false;
            }
        }

        return true;
    }

    /**
     * @param mixed $value
     * @return array<int,mixed>
     */
    private function split_param_values($value): array {
        if (is_array($value)) {
            return array_values($value);
        }

        return array_values(array_filter(array_map('trim', explode(',', (string) $value)), static fn($item) => $item !== ''));
    }

    private function get_ttl(): int {
        $ttl = (int) $this->get_setting('rest_get_cache_ttl', 300);
        return max(60, min(DAY_IN_SECONDS, $ttl));
    }
}
