<?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\Console\Watchers;

use WP_CLI;
use cli\Notify;
use cli\Streams;

class AggregateWatcher extends Notify
{
    /**
     * Holds the command options.
     *
     * @var array<mixed>
     */
    public $options;

    /**
     * The object cache instance.
     *
     * @var \RedisCachePro\ObjectCaches\MeasuredObjectCacheInterface
     */
    public $cache;

    /**
     * Whether Relay is being used.
     *
     * @var bool
     */
    public $usingRelay;

    /**
     * Holds the timestamp of the next aggregate.
     *
     * @var int
     */
    protected $next;

    /**
     * The measurements to display.
     *
     * @var \RedisCachePro\Metrics\Measurements|null
     */
    protected $measurements;

    /**
     * Holds the default metrics.
     *
     * @var array<string>
     */
    protected $defaultMetrics = [
        'ms-total',
        'ms-cache',
        'ms-cache-avg',
        'ms-cache-ratio',
        'hits',
        'misses',
        'hit-ratio',
        'store-reads',
        'store-writes',
        'store-hits',
        'store-misses',
        'sql-queries',
        'redis-hit-ratio',
        'redis-ops-per-sec',
        'redis-memory-ratio',
        'relay-hits',
        'relay-misses',
        'relay-memory-ratio',
        'relay-adaptive-loadfactor',
    ];

    /**
     * Prints the metrics to the screen.
     *
     * @param  bool  $finish
     * @return void
     */
    public function display($finish = false)
    {
        if ($this->_current === 1) {
            Streams::line(WP_CLI::colorize($this->_message));
        }

        if (! $this->measurements || ! $this->measurements->count()) {
            return;
        }

        $metrics = empty($this->options['metrics'])
            ? $this->defaultMetrics
            : $this->options['metrics'];

        $data = [
            $this->format('measurements', $this->measurements->count()),
        ];

        foreach ($metrics as $metric) {
            $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $metric)));

            if (! method_exists($this, $method)) {
                WP_CLI::error("Invalid metric name: {$metric}.");
            }

            $data[] = $this->format($metric, $this->{$method}());
        }

        /** @var array<string> $data */
        $data = array_filter($data);

        Streams::line(implode(' ', $data));

        $this->measurements = null;
    }

    /**
     * Prepare the metrics.
     *
     * @return void
     */
    public function prepare()
    {
        $now = time();
        $window = $this->options['seconds'];

        if (! $this->next) {
            $this->next = $now + $window;
        }

        if ($now < $this->next) {
            return;
        }

        $this->next = $now + $window;

        $max = $now - 2;
        $min = $max - $window;

        $this->measurements = $this->cache->measurements($min, "({$max}");
    }

    /**
     * Format the given measurement in log format.
     *
     * @param  string  $metric
     * @param  mixed  $value
     * @return string|void
     */
    protected function format(string $metric, $value)
    {
        if (is_null($value)) {
            return;
        }

        $format = "\e[%sm%s\e[0m\e[2m=\e[0m%s";

        switch (strstr($metric, '-', true) ?: $metric) {
            case 'redis':
                return sprintf($format, 31, $metric, $value);
            case 'relay':
                return sprintf($format, 35, $metric, $value);
            case 'ms':
                return sprintf($format, 36, $metric, $value);
            case 'sql':
                return sprintf($format, 33, $metric, $value);
            case 'measurements':
                return sprintf($format, 32, $metric, $value);
            default:
                return sprintf($format, 34, $metric, $value);
        }
    }

    /**
     * @return int
     */
    protected function getHits()
    {
        return (int) round($this->measurements->median('wp->hits'));
    }

    /**
     * @return int
     */
    protected function getMisses()
    {
        return (int) round($this->measurements->median('wp->misses'));
    }

    /**
     * @return string|null
     */
    protected function getHitRatio()
    {
        $hitRatioMedian = $this->measurements->median('wp->hitRatio');

        return is_null($hitRatioMedian) ? null : number_format($hitRatioMedian, 1);
    }

    /**
     * @return int
     */
    protected function getBytes()
    {
        return (int) round($this->measurements->median('wp->bytes'));
    }

    /**
     * @return int
     */
    protected function getPrefetches()
    {
        return (int) round($this->measurements->median('wp->prefetches'));
    }

    /**
     * @return int
     */
    protected function getStoreReads()
    {
        return (int) round($this->measurements->median('wp->storeReads'));
    }

    /**
     * @return int
     */
    protected function getStoreWrites()
    {
        return (int) round($this->measurements->median('wp->storeWrites'));
    }

    /**
     * @return int
     */
    protected function getStoreHits()
    {
        return (int) round($this->measurements->median('wp->storeHits'));
    }

    /**
     * @return int
     */
    protected function getStoreMisses()
    {
        return (int) round($this->measurements->median('wp->storeMisses'));
    }

    /**
     * @return int
     */
    protected function getSqlQueries()
    {
        return (int) round($this->measurements->median('wp->sqlQueries'));
    }

    /**
     * @return string|null
     */
    protected function getMsTotal()
    {
        $msTotalMedian = $this->measurements->median('wp->msTotal');

        return is_null($msTotalMedian) ? null : number_format($msTotalMedian, 2, '.', '');
    }

    /**
     * @return string|null
     */
    protected function getMsCache()
    {
        $msCacheMedian = $this->measurements->median('wp->msCache');

        return is_null($msCacheMedian) ? null : number_format($msCacheMedian, 2, '.', '');
    }

    /**
     * @return string|null
     */
    protected function getMsCacheAvg()
    {
        $msCacheAvg = $this->measurements->median('wp->msCacheAvg');

        return is_null($msCacheAvg) ? null : number_format($msCacheAvg, 4, '.', '');
    }

    /**
     * @return string|null
     */
    protected function getMsCacheRatio()
    {
        $msCacheRatioMedian = $this->measurements->median('wp->msCacheRatio');

        return is_null($msCacheRatioMedian) ? null : number_format($msCacheRatioMedian, 2, '.', '');
    }

    /**
     * @return int
     */
    protected function getRedisHits()
    {
        return (int) round($this->measurements->median('redis->hits'));
    }

    /**
     * @return int
     */
    protected function getRedisMisses()
    {
        return (int) round($this->measurements->median('redis->misses'));
    }

    /**
     * @return string|null
     */
    protected function getRedisHitRatio()
    {
        $hitRatioMedian = $this->measurements->median('redis->hitRatio');

        return is_null($hitRatioMedian) ? null : number_format($hitRatioMedian, 1);
    }

    /**
     * @return int|null
     */
    protected function getRedisOpsPerSec()
    {
        $opsPerSec = $this->measurements->latest('redis->opsPerSec');

        return is_null($opsPerSec) ? null : (int) round($opsPerSec);
    }

    /**
     * @return int|null
     */
    protected function getRedisEvictedKeys()
    {
        $evictedKeys = $this->measurements->latest('redis->evictedKeys');

        return is_null($evictedKeys) ? null : (int) round($evictedKeys);
    }

    /**
     * @return int|null
     */
    protected function getRedisUsedMemory()
    {
        $usedMemory = $this->measurements->latest('redis->usedMemory');

        return is_null($usedMemory) ? null : (int) round($usedMemory);
    }

    /**
     * @return int|null
     */
    protected function getRedisUsedMemoryRss()
    {
        $usedMemoryRss = $this->measurements->latest('redis->usedMemoryRss');

        return is_null($usedMemoryRss) ? null : (int) round($usedMemoryRss);
    }

    /**
     * @return string|null
     */
    protected function getRedisMemoryRatio()
    {
        $memoryRatio = $this->measurements->latest('redis->memoryRatio');

        return is_null($memoryRatio) ? null : number_format($memoryRatio, 1);
    }

    /**
     * @return string|null
     */
    protected function getRedisMemoryFragmentationRatio()
    {
        $memoryFragmentationRatio = $this->measurements->latest('redis->memoryFragmentationRatio');

        return is_null($memoryFragmentationRatio) ? null : number_format($memoryFragmentationRatio, 1);
    }

    /**
     * @return int|null
     */
    protected function getRedisConnectedClients()
    {
        $connectedClients = $this->measurements->latest('redis->connectedClients');

        return is_null($connectedClients) ? null : (int) round($connectedClients);
    }

    /**
     * @return int|null
     */
    protected function getRedisTrackingClients()
    {
        $trackingClients = $this->measurements->latest('redis->trackingClients');

        return is_null($trackingClients) ? null : (int) round($trackingClients);
    }

    /**
     * @return int|null
     */
    protected function getRedisRejectedConnections()
    {
        $rejectedConnections = $this->measurements->latest('redis->rejectedConnections');

        return is_null($rejectedConnections) ? null : (int) round($rejectedConnections);
    }

    /**
     * @return int|null
     */
    protected function getRedisKeys()
    {
        $keys = $this->measurements->latest('redis->keys');

        return is_null($keys) ? null : (int) round($keys);
    }

    /**
     * @return int|void
     */
    protected function getRelayHits()
    {
        if ($this->usingRelay) {
            $hits = $this->measurements->latest('relay->hits');

            return is_null($hits) ? null : (int) round($hits);
        }
    }

    /**
     * @return int|void
     */
    protected function getRelayMisses()
    {
        if ($this->usingRelay) {
            $misses = $this->measurements->latest('relay->misses');

            return is_null($misses) ? null : (int) round($misses);
        }
    }

    /**
     * @return int|void
     */
    protected function getRelayMemoryUsed()
    {
        if ($this->usingRelay) {
            $memoryUsed = $this->measurements->latest('relay->memoryUsed');

            return is_null($memoryUsed) ? null : (int) round($memoryUsed);
        }
    }

    /**
     * @return int|void
     */
    protected function getRelayMemoryTotal()
    {
        if ($this->usingRelay) {
            $memoryTotal = $this->measurements->latest('relay->memoryTotal');

            return is_null($memoryTotal) ? null : (int) round($memoryTotal);
        }
    }

    /**
     * @return string|void
     */
    protected function getRelayMemoryRatio()
    {
        if ($this->usingRelay) {
            $memoryRatio = $this->measurements->latest('relay->memoryRatio');

            return is_null($memoryRatio) ? null : number_format($memoryRatio, 1);
        }
    }
}
