<?php
declare(strict_types=1);

namespace Ekesto\CacheInvalidator\Settings;

use Ekesto\CacheInvalidator\InvalidatorRuntime;
use Ekesto\CacheInvalidator\Settings;
use Ekesto\CacheAutopilot\AccessControl;

/**
 * Registers and handles Settings REST endpoints.
 */
final class SettingsRestController
{
    private SettingsRepository $repository;
    private SettingsPayloadBuilder $payloadBuilder;
    private SettingsSanitizer $sanitizer;

    /**
     * @param SettingsRepository     $repository Settings repository for option IO.
     * @param SettingsPayloadBuilder $payloadBuilder Settings payload builder for REST/UI payloads.
     * @param SettingsSanitizer      $sanitizer Sanitizer + validation rules.
     */
    public function __construct(
        SettingsRepository $repository,
        SettingsPayloadBuilder $payloadBuilder,
        SettingsSanitizer $sanitizer
    ) {
        $this->repository = $repository;
        $this->payloadBuilder = $payloadBuilder;
        $this->sanitizer = $sanitizer;
    }

    /**
     * Register REST API endpoints for admin settings.
     *
     * @return void
     */
    public function registerRestRoutes(): void
    {
        if (defined('LSCWP_V')) {
            add_filter('rest_pre_dispatch', [$this, 'preventLscCaching'], 10, 3);
        }

        register_rest_route(Settings::REST_NAMESPACE, Settings::REST_SETTINGS_ROUTE, [
            'methods' => 'GET',
            'callback' => [$this, 'getSettingsRest'],
            'permission_callback' => static fn(): bool => AccessControl::userCanManage(),
        ]);

        register_rest_route(Settings::REST_NAMESPACE, Settings::REST_SETTINGS_ROUTE, [
            'methods' => 'POST',
            'callback' => [$this, 'updateSettingsRest'],
            'permission_callback' => static fn(): bool => AccessControl::userCanManage(),
            'args' => [
                'post_types' => [
                    'type' => 'object',
                    'required' => true,
                ],
                'include_non_public_post_types' => [
                    'type' => 'boolean',
                    'required' => false,
                ],
                'invalidation_behavior' => [
                    'type' => 'object',
                    'required' => false,
                ],
            ],
        ]);

    }

    /**
     * Prevent LiteSpeed Cache from caching REST responses.
     *
     * @param mixed            $result Response to replace the requested version with.
     * @param \WP_REST_Server  $server Server instance.
     * @param \WP_REST_Request $request Request used to generate the response.
     * @return mixed
     */
    public function preventLscCaching($result, $server, $request)
    {
        $route = $request->get_route();
        if (str_starts_with($route, '/' . Settings::REST_NAMESPACE)) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache third-party control hook.
            do_action('litespeed_control_set_nocache', 'ekesto-cache-invalidator REST API');
        }

        return $result;
    }

    /**
     * REST GET handler.
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function getSettingsRest(\WP_REST_Request $request): \WP_REST_Response
    {
        unset($request);

        $payload = $this->payloadBuilder->buildSettingsPayload();
        $response = new \WP_REST_Response($payload, 200);

        return $this->applyRestNoCache($response);
    }

    /**
     * REST POST handler.
     *
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function updateSettingsRest(\WP_REST_Request $request): \WP_REST_Response
    {
        $postTypesIn = $request->get_param('post_types');
        $globalInvalidationIn = $request->get_param('global_invalidation');
        $timedInvalidationIn = $request->get_param('timed_invalidation');
        $invalidationBehaviorIn = $request->get_param('invalidation_behavior');
        $requestParams = $request->get_params();
        $current = $this->repository->getOption();
        $includeNonPublicPostTypes = array_key_exists('include_non_public_post_types', $requestParams)
            ? $this->sanitizeBoolean($requestParams['include_non_public_post_types'])
            : $this->sanitizeBoolean($current['include_non_public_post_types'] ?? false);
        $toSave = [
            'post_types' => $postTypesIn ?? ($current['post_types'] ?? []),
            'global_invalidation' => $globalInvalidationIn ?? ($current['global_invalidation'] ?? []),
            'timed_invalidation' => $timedInvalidationIn ?? ($current['timed_invalidation'] ?? []),
            'invalidation_behavior' => $invalidationBehaviorIn
                ?? ($current['invalidation_behavior'] ?? $this->repository->getInvalidationBehavior()),
            'include_non_public_post_types' => $includeNonPublicPostTypes,
        ];

        // Lite must not change premium-only settings. Freeze them to the stored
        // values (preserved but inert at runtime) so a crafted/partial POST can
        // neither reset nor inject Pro keys. Pro builds save normally.
        $toSave = $this->freezeProKeys($toSave, $current);

        $errors = $this->sanitizer->validatePostTypes($toSave['post_types']);
        if (!empty($errors)) {
            $response = new \WP_REST_Response(
                [
                    'code' => 'validation_error',
                    'message' => __('Post type validation failed.', 'cache-autopilot'),
                    'errors' => $errors,
                ],
                400
            );

            return $this->applyRestNoCache($response);
        }

        $this->repository->updateOption($toSave);

        $payload = $this->payloadBuilder->buildSettingsPayload();
        $response = new \WP_REST_Response($payload, 200);

        return $this->applyRestNoCache($response);
    }

    /**
     * Freeze premium-only settings keys to their stored values for Lite builds.
     *
     * Incoming changes to these keys are ignored; the stored values are kept
     * (preserved, never deleted) and remain inert because the premium runtime is
     * stripped. Keys with no stored value are dropped so Lite cannot introduce
     * them. All other (Lite-safe) settings are passed through untouched.
     *
     * Frozen keys: post_types[*].relationships, post_types[*].expand_post_types,
     * timed_invalidation, global_invalidation.multilingual_fanout_post_triggers_enabled,
     * global_invalidation.multilingual_fanout_non_post_triggers_enabled.
     *
     * @param array<string,mixed> $toSave  Candidate settings about to be saved.
     * @param array<string,mixed> $current Currently stored option.
     * @return array<string,mixed>
     */
    private function freezeProKeys(array $toSave, array $current): array
    {
        return $this->sanitizer->preserveStoredLitePremiumSettings($toSave, $current);
    }

    /**
     * Apply no-cache signals to an authenticated REST response.
     *
     * @param \WP_REST_Response $response
     * @return \WP_REST_Response
     */
    private function applyRestNoCache(\WP_REST_Response $response): \WP_REST_Response
    {
        if (!defined('DONOTCACHEPAGE')) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin global constant used to prevent caching of authenticated REST responses.
            define('DONOTCACHEPAGE', true);
        }

        add_filter('rest_send_nocache_headers', '__return_true', PHP_INT_MAX);

        $response->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0, private');
        $response->header('Pragma', 'no-cache');
        $response->header('Expires', '0');

        $headers = $response->get_headers();
        $vary = isset($headers['Vary']) ? (string) $headers['Vary'] : '';
        if ($vary === '') {
            $response->header('Vary', 'Cookie');
        } elseif (stripos($vary, 'Cookie') === false) {
            $response->header('Vary', $vary . ', Cookie');
        }

        return $response;
    }

    /**
     * Sanitize a boolean-like REST value.
     *
     * @param mixed $value
     * @param bool  $default
     * @return bool
     */
    private function sanitizeBoolean($value, bool $default = false): bool
    {
        $normalized = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
        if ($normalized === null) {
            return $default;
        }

        return $normalized;
    }
}
