<?php
/**
 * Copyright © 2019-2025 Rhubarb Tech Inc. All Rights Reserved.
 *
 * The Object Cache Pro Software and its related materials are property and confidential
 * information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
 * of the Object Cache Pro Software and its related materials, in whole or in part,
 * is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
 *
 * In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
 * Software and its related materials, in whole or in part, is subject to the End-User License
 * Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
 */

declare(strict_types=1);

namespace RedisCachePro\Plugin\Api;

use WP_Error;
use WP_REST_Server;

use RedisCachePro\ObjectCaches\ObjectCacheInterface;
use RedisCachePro\ObjectCaches\MeasuredObjectCacheInterface;

use RedisCachePro\Metrics\RedisMetrics;
use RedisCachePro\Metrics\RelayMetrics;
use RedisCachePro\Metrics\WordPressMetrics;

class Analytics extends Controller
{
    /**
     * The resource name of this controller's route.
     *
     * @var string
     */
    protected $resource_name = 'analytics';

    /**
     * The default interval, in seconds.
     *
     * @var int
     */
    protected static $interval = 60;

    /**
     * The default intervals.
     *
     * The keys represent the resolution in seconds
     * and the values are the number of intervals.
     *
     * @var array<int, int>
     */
    protected static $intervals = [
        10 => 30,
        60 => 30,
        300 => 24,
    ];

    /**
     * Returns the default interval, in seconds.
     *
     * @return int
     */
    public static function interval()
    {
        /**
         * Filters the default interval for object cache analytics.
         *
         * @param  int  $interval  The interval, in seconds.
         */
        return (int) apply_filters('objectcache_analytics_interval', static::$interval);
    }

    /**
     * Returns the supported intervals.
     *
     * @return array<int, int>
     */
    public static function intervals()
    {
        /**
         * Filters the intervals for object cache analytics.
         *
         * The array keys represent the resolution in seconds
         * and the values are the number of intervals.
         *
         * @param  array  $intervals  The intervals.
         */
        return (array) apply_filters('objectcache_analytics_intervals', static::$intervals);
    }

    /**
     * Register all REST API routes.
     *
     * @return void
     */
    public function register_routes()
    {
        register_rest_route($this->namespace, "/{$this->resource_name}", [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_items'],
                'permission_callback' => [$this, 'get_items_permissions_check'],
                'args' => $this->get_collection_params(),
            ],
            'schema' => [$this, 'get_public_item_schema'],
        ]);
    }

    /**
     * Retrieves the query params for the posts collection.
     *
     * @return array<string, mixed>
     */
    public function get_collection_params()
    {
        $params = parent::get_collection_params();
        $params['per_page']['default'] = 30;
        $params['context']['default'] = 'compute';

        unset($params['search']);

        $params['interval'] = [
            'description' => 'The interval in seconds.',
            'type' => 'integer',
            'required' => false,
            'minimum' => 1,
            'default' => static::interval(),
        ];

        return $params;
    }

    /**
     * Returns the REST API response for the request.
     *
     * @param  \WP_REST_Request  $request
     * @return \WP_REST_Response|\WP_Error
     */
    public function get_items($request)
    {
        global $wp_object_cache;

        if (! $wp_object_cache instanceof ObjectCacheInterface) {
            return $this->notSupportedError();
        }

        if (! $wp_object_cache instanceof MeasuredObjectCacheInterface) {
            return new WP_Error(
                'objectcache_analytics_unsupported',
                'The object cache does not support analytics.',
                ['status' => 400]
            );
        }

        if (! $wp_object_cache->connection()) {
            return $this->notConnectedError();
        }

        if (! $wp_object_cache->config()->analytics->enabled) {
            return new WP_Error(
                'objectcache_analytics_disabled',
                'Object cache analytics are disabled.',
                ['status' => 400]
            );
        }

        $page = $request->get_param('page');
        $per_page = $request->get_param('per_page');
        $interval = $request->get_param('interval');

        $now = microtime(true);

        $min = (int) $now - ($interval * $per_page * $page);
        $min = $min - $min % $interval;

        $max = (int) $now - ($interval * $per_page * ($page - 1));

        $intervals = $wp_object_cache->measurements((string) $min, (string) $max)
            ->intervals($interval);

        $range = array_slice(array_reverse(range($min, $max, $interval)), 0, 30, true);

        $collection = array_map(function ($timestamp) use ($intervals, $request) {
            return $this->prepare_item_for_response([
                'timestamp' => $timestamp,
                'measurements' => $intervals[$timestamp] ?? null,
            ], $request);
        }, $range);

        /** @var \WP_REST_Response $response */
        $response = rest_ensure_response($collection);
        $response->header('Cache-Control', 'no-store');

        return $response;
    }

    /**
     * Prepares a single interval output for response.
     *
     * @param  array  $item
     * @param  \WP_REST_Request  $request
     * @return array
     */
    public function prepare_item_for_response($item, $request) // @phpstan-ignore-line
    {
        if ($this->fieldIsIncluded('count', $request)) {
            $item['count'] = count($item['measurements'] ?? []);
        }

        $rfc3339 = 'Y-m-d\TH:i:s';

        if ($this->fieldIsIncluded('date', $request)) {
            $item['date'] = get_date_from_gmt("@{$item['timestamp']}", $rfc3339);
        }

        if ($this->fieldIsIncluded('date_gmt', $request)) {
            $item['date_gmt'] = date($rfc3339, $item['timestamp']);
        }

        if ($this->fieldIsIncluded('date_display', $request)) {
            $item['date_display'] = [
                'date' => wp_date('D jS', $item['timestamp']),
                'time' => sprintf(
                    '%s - %s %s',
                    wp_date('H:i', $item['timestamp']),
                    wp_date('H:i', $item['timestamp'] + $request->get_param('interval')),
                    current_datetime()->format('T')
                ),
            ];
        }

        $hasMeasurements = ! empty($item['measurements']);

        foreach ($this->get_metrics() as $id => $metric) {
            if ($this->fieldIsIncluded($id, $request)) {
                foreach ($metric['computations'] as $computation) {
                    if ($hasMeasurements) {
                        $name = $metric['group'] === 'wp' ? $id : str_replace($metric['group'], '', $id);
                        $item[$id][$computation] = $item['measurements']->{$computation}("{$metric['group']}->{$name}");
                    } else {
                        $item[$id][$computation] = null;
                    }
                }
            }
        }

        if ($this->fieldIsIncluded('measurements', $request)) {
            $item['measurements'] = array_map(static function ($measurement) {
                return $measurement->toArray();
            }, $item['measurements'] ? $item['measurements']->all() : []);
        }

        $item = $this->add_additional_fields_to_object($item, $request);
        $item = $this->filter_response_by_context($item, $request['context']);

        return $item;
    }

    /**
     * Determine whether the provided field should
     * be included in the response body.
     *
     * @param  string  $field
     * @param  \WP_REST_Request  $request
     * @return bool
     */
    protected function fieldIsIncluded($field, $request)
    {
        static $fields = null;

        if ($fields === null) {
            $fields = $this->get_fields_for_response($request);
        }

        static $cache = [];

        if (! isset($cache[$field])) {
            $cache[$field] = rest_is_field_included($field, $fields);
        }

        return $cache[$field];
    }

    /**
     * Returns the metrics for each group and their supported computations.
     *
     * @return array<string, mixed>
     */
    protected function get_metrics()
    {
        $metrics = array_merge(
            WordPressMetrics::schema(),
            RedisMetrics::schema(),
            RelayMetrics::schema()
        );

        return array_map(static function ($metric) {
            $metric['computations'] = [
                'max',
                'mean',
                'median',
                'p90',
                'p95',
                'p99',
            ];

            return $metric;
        }, $metrics);
    }

    /**
     * Retrieves the endpoint's schema, conforming to JSON Schema.
     *
     * @return array<string, mixed>
     */
    public function get_item_schema()
    {
        if ($this->schema) {
            return $this->add_additional_fields_schema($this->schema);
        }

        $properties = [
            'max' => ['type' => ['integer', 'float', 'null']],
            'mean' => ['type' => ['integer', 'float', 'null']],
            'median' => ['type' => ['integer', 'float', 'null']],
            'p90' => ['type' => ['integer', 'float', 'null']],
            'p95' => ['type' => ['integer', 'float', 'null']],
            'p99' => ['type' => ['integer', 'float', 'null']],
        ];

        $schema = [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'objectcache_analytics',
            'type' => 'object',
            'properties' => [
                'timestamp' => [
                    'description' => 'The timestamp of the interval.',
                    'type' => 'integer',
                    'context' => ['raw', 'compute'],
                ],
                'date' => [
                    'description' => "The date of the interval, in the site's timezone.",
                    'type' => 'string',
                    'format' => 'date-time',
                    'context' => ['compute'],
                ],
                'date_gmt' => [
                    'description' => 'The date of the interval, as GMT.',
                    'type' => 'string',
                    'format' => 'date-time',
                    'context' => ['compute'],
                ],
                'date_display' => [
                    'description' => 'The displayable date of the interval.',
                    'type' => 'object',
                    'context' => ['compute'],
                ],
                'count' => [
                    'description' => 'The amount of measurements taken in the interval.',
                    'type' => 'integer',
                    'context' => ['raw', 'compute'],
                ],
                'measurements' => [
                    'description' => 'The measurements taken in the interval.',
                    'type' => 'array',
                    'context' => ['raw'],
                ],
            ],
        ];

        foreach ($this->get_metrics() as $id => $metric) {
            $schema['properties'][$id] = [
                'title' => $metric['title'],
                'description' => $metric['description'],
                'type' => 'object',
                'context' => ['compute'],
                'properties' => $properties,
            ];
        }

        $this->schema = $schema;

        return $this->add_additional_fields_schema($this->schema);
    }
}
