<?php

namespace FleekDash\Core;

/**
 * WordPress REST hooks for ETag, Cache-Control, and related pre/post dispatch behavior.
 *
 * @package FleekDash\Core
 */
final class RestApiCacheFilters
{
    private Logger $logger;

    public function __construct()
    {
        $this->logger = Logger::get_instance();
    }

    public function register(): void
    {
        /*
         * rest_pre_serve_request: WordPress runs every callback; the first argument is the
         * chained bool ($served). Run ETag/cache headers before SSE short-circuit so GET
         * cacheable routes are handled first; POST /ai/chat (SSE) is never an ETag candidate.
         */
        \add_filter('rest_pre_serve_request', [$this, 'filterRestPreServeCacheHeaders'], 5, 4);
        \add_filter('rest_pre_serve_request', [$this, 'filterRestPreServeAiSseStream'], 6, 4);
        \add_filter('rest_pre_dispatch', [$this, 'filterRestPreDispatchEtagShortCircuit'], 1, 3);
        // Store ETag transient BEFORE 304 fallback: maybeNotModifiedIfEtagMatches replaces the response
        // with an empty body (304), so running store after would persist an ETag of null and break validation.
        \add_filter('rest_post_dispatch', [$this, 'filterRestPostDispatchStoreEtagTransient'], 4, 3);
        \add_filter('rest_post_dispatch', [$this, 'filterRestPostDispatch304Fallback'], 5, 3);
        \add_filter('rest_post_dispatch', [$this, 'filterRestPostDispatchListCacheControl'], 15, 3);
        \add_filter('rest_post_dispatch', [$this, 'filterRestPostDispatchThemesNoStore'], 20, 3);
        \add_filter('rest_post_dispatch', [$this, 'filterRestPostDispatchAiPrivateNoStore'], 25, 3);
    }

    /**
     * Skip JSON body for AI chat SSE: stream bytes are already sent during dispatch.
     *
     * @param bool                                    $served  Chained: prior filters may already have set served.
     * @param \WP_HTTP_Response|\WP_REST_Response     $result  Response object.
     * @param \WP_REST_Request                        $request Request.
     * @param \WP_REST_Server                         $server  Server.
     */
    public function filterRestPreServeAiSseStream($served, $result, $request, $server): bool
    {
        if ($served) {
            return $served;
        }

        $out = $served;
        try {
            if (($result instanceof \WP_REST_Response)
                && ($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
            ) {
                $data = $result->get_data();
                if (\is_array($data) && !empty($data['_fleekdash_sse_stream_complete'])) {
                    $out = true;
                }
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->logger->warning('rest_pre_serve_request AI SSE filter: ' . $e->getMessage());
            }
        }

        return $out;
    }

    /**
     * ETag + Cache-Control for GET in our REST namespace (browser-safe paths only).
     *
     * @param bool                                    $served  Chained from previous rest_pre_serve_request callbacks.
     * @param \WP_HTTP_Response|\WP_REST_Response     $result  Response object.
     * @param \WP_REST_Request                        $request Request object.
     * @param \WP_REST_Server                         $server  REST server.
     */
    public function filterRestPreServeCacheHeaders($served, $result, $request, $server): bool
    {
        $out = $served;
        try {
            if ($this->isPreServeEtagCandidate($request, $server)) {
                $etag = RestApiCacheSupport::buildStrongEtagFromPayload(
                    $server->response_to_data($result, false),
                );
                RestApiCacheSupport::emitPreServeCacheControlAndEtagHeaders($etag);
                $ifNoneMatchRaw = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? (string) $_SERVER['HTTP_IF_NONE_MATCH'] : '';
                if ($ifNoneMatchRaw !== '' && RestApiCacheSupport::httpEtagHeaderMatchesStrongTag($ifNoneMatchRaw, $etag)) {
                    RestApiCacheSupport::emitPreServe304Headers($etag);
                    $out = true;
                }
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->logger->warning(__('Cache headers error: ', 'fleekdash') . $e->getMessage());
            }
        }

        return $out;
    }

    /**
     * @param mixed $request
     * @param mixed $server
     */
    private function isPreServeEtagCandidate($request, $server): bool
    {
        return ($request instanceof \WP_REST_Request)
            && ($server instanceof \WP_REST_Server)
            && strtoupper($request->get_method()) === 'GET'
            && RestApiCacheSupport::isFleekdashCacheableEtagRoute($request->get_route());
    }

    /**
     * Short-circuit GET when If-None-Match matches cached ETag transient.
     *
     * @param mixed            $result  Incoming result.
     * @param \WP_REST_Server  $server  REST server.
     * @param \WP_REST_Request $request Request.
     * @return mixed
     */
    public function filterRestPreDispatchEtagShortCircuit($result, $server, $request)
    {
        $out = $result;
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
            ) {
                $route = $request->get_route();
                $ifNoneMatchRaw = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? (string) $_SERVER['HTTP_IF_NONE_MATCH'] : '';
                if (RestApiCacheSupport::isFleekdashCacheableEtagRoute($route) && $ifNoneMatchRaw !== '') {
                    $cacheKey = RestApiCacheSupport::buildEtagTransientKey($route, $request->get_params());
                    $cachedEtag = \get_transient($cacheKey);
                    if (is_string($cachedEtag) && $cachedEtag !== ''
                        && RestApiCacheSupport::httpEtagHeaderMatchesStrongTag($ifNoneMatchRaw, $cachedEtag)
                    ) {
                        $out = RestApiCacheSupport::newNotModifiedRestResponse($cachedEtag);
                    }
                }
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->logger->warning('rest_pre_dispatch 304 short-circuit error: ' . $e->getMessage());
            }
        }

        return $out;
    }

    /**
     * Light Cache-Control for allowlisted list-style GET routes.
     *
     * @param \WP_HTTP_Response|\WP_REST_Response $response Response.
     * @param \WP_REST_Server                     $server   REST server.
     * @param \WP_REST_Request                    $request  Request.
     * @return \WP_HTTP_Response|\WP_REST_Response
     */
    public function filterRestPostDispatchListCacheControl($response, $server, $request)
    {
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
            ) {
                $route = $request->get_route();
                if (RestApiCacheSupport::shouldEmitPublicListCacheControl($request, $route) && !headers_sent()) {
                    $maxAge = 120;
                    if (\function_exists('apply_filters')) {
                        $maxAge = (int) \apply_filters('fleekdash_rest_cache_max_age', $maxAge, $request, $route);
                    }
                    header('Cache-Control: public, max-age=' . max(0, $maxAge) . ', must-revalidate');
                }
            }
        } catch (\Throwable $e) {
            // Silent fail
        }

        return $response;
    }

    /**
     * Persist response ETag in a transient for pre_dispatch short-circuit.
     * Must run before {@see filterRestPostDispatch304Fallback} (priority 4 vs 5): a 304 response has no body.
     *
     * @param \WP_HTTP_Response|\WP_REST_Response $response Response.
     * @param \WP_REST_Server                     $server   REST server.
     * @param \WP_REST_Request                    $request  Request.
     * @return \WP_HTTP_Response|\WP_REST_Response
     */
    public function filterRestPostDispatchStoreEtagTransient($response, $server, $request)
    {
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
            ) {
                $route = $request->get_route();
                if (RestApiCacheSupport::isFleekdashCacheableEtagRoute($route)) {
                    $payload = $response instanceof \WP_REST_Response ? $response->get_data() : $response;
                    $etag = RestApiCacheSupport::buildStrongEtagFromPayload($payload);
                    $cacheKey = RestApiCacheSupport::buildEtagTransientKey($route, $request->get_params());
                    \set_transient($cacheKey, $etag, 300);
                }
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->logger->warning(__('rest_post_dispatch ETag cache error: ', 'fleekdash') . $e->getMessage());
            }
        }

        return $response;
    }

    /**
     * Fallback 304 when If-None-Match matches computed response ETag.
     *
     * @param \WP_HTTP_Response|\WP_REST_Response $response Response.
     * @param \WP_REST_Server                     $server   REST server.
     * @param \WP_REST_Request                    $request  Request.
     * @return \WP_HTTP_Response|\WP_REST_Response
     */
    public function filterRestPostDispatch304Fallback($response, $server, $request)
    {
        $out = $response;
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
            ) {
                $out = RestApiCacheSupport::maybeNotModifiedIfEtagMatches($response, $request);
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                $this->logger->warning(__('rest_post_dispatch ETag handling error: ', 'fleekdash') . $e->getMessage());
            }
        }

        return $out;
    }

    /**
     * Theme/palette GET: do not allow browser/CDN caching (stale palettes after delete until hard refresh).
     *
     * @param \WP_HTTP_Response|\WP_REST_Response $response Response.
     * @param \WP_REST_Server                     $server   REST server.
     * @param \WP_REST_Request                    $request  Request.
     * @return \WP_HTTP_Response|\WP_REST_Response
     */
    public function filterRestPostDispatchThemesNoStore($response, $server, $request)
    {
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
                && RestApiCacheSupport::isThemesUserMutableRestRoute($request->get_route())
                && ($response instanceof \WP_REST_Response)
            ) {
                $response->header('Cache-Control', 'private, no-store, must-revalidate');
            }
        } catch (\Throwable $e) {
            // Silent fail
        }

        return $response;
    }

    /**
     * AI GET responses: never allow shared HTTP cache on the final response object.
     *
     * @param \WP_HTTP_Response|\WP_REST_Response $response Response.
     * @param \WP_REST_Server                     $server   REST server.
     * @param \WP_REST_Request                    $request  Request.
     * @return \WP_HTTP_Response|\WP_REST_Response
     */
    public function filterRestPostDispatchAiPrivateNoStore($response, $server, $request)
    {
        try {
            if (($request instanceof \WP_REST_Request)
                && ($server instanceof \WP_REST_Server)
                && strtoupper($request->get_method()) === 'GET'
                && RestApiCacheSupport::isSessionSensitiveRoute($request->get_route())
                && ($response instanceof \WP_REST_Response)
            ) {
                $response->header('Cache-Control', 'private, no-store, must-revalidate');
            }
        } catch (\Throwable $e) {
            // Silent fail
        }

        return $response;
    }
}
