<?php
namespace EasyChat\Library\Messenger;









class MaxCronHeartbeatRepository {

    public const SETTING_KEY = 'chat_max_cron_heartbeat_history';
    private const DOCUMENT_VERSION = 2;
    private const HISTORY_LIMIT = 30;
    private const QUARTER_SECONDS = 900;
    private const QUARTER_LIMIT = 32;
    private const HOUR_SECONDS = 3600;
    private const HOUR_LIMIT = 168;

    private $db;
    private $prefix;

    public function __construct($registry) {
        $this->db = $registry->get('db');
        $this->prefix = function_exists('easychat_db_prefix') ? easychat_db_prefix() : DB_PREFIX;
    }

    public function start(): string {
        try {
            $run_id = bin2hex(random_bytes(12));
        } catch (\Throwable $e) {
            return '';
        }
        $this->mutate(function (array $document) use ($run_id): array {
            $now = time();
            $document['history'][] = [
                'run_id' => $run_id,
                'started_at' => $now,
                'finished_at' => 0,
                'status' => 'running',
                'exit_code' => null,
                'duration_ms' => 0,
                'version' => defined('EASYCHAT_VERSION') ? (string)EASYCHAT_VERSION : '',
            ];
            $document['profile']['first_seen_at'] = $this->firstSeenAt($document['profile'], $now);
            $document['profile']['quarters'] = $this->incrementBucket(
                $document['profile']['quarters'],
                self::QUARTER_SECONDS,
                $now,
                1,
                0,
                0
            );
            $document['profile']['hours'] = $this->incrementBucket(
                $document['profile']['hours'],
                self::HOUR_SECONDS,
                $now,
                1,
                0,
                0
            );
            return $document;
        });
        return $run_id;
    }

    public function finish(string $run_id, bool $success, int $exit_code, int $duration_ms): void {
        if (!preg_match('/^[a-f0-9]{24}$/', $run_id)) {
            return;
        }
        $this->mutate(function (array $document) use ($run_id, $success, $exit_code, $duration_ms): array {
            $finished_at = time();
            $started_at = 0;
            foreach ($document['history'] as &$record) {
                if (!is_array($record) || !hash_equals((string)($record['run_id'] ?? ''), $run_id)) {
                    continue;
                }
                 
                if ((string)($record['status'] ?? '') !== 'running') {
                    return $document;
                }
                $started_at = max(0, (int)($record['started_at'] ?? 0));
                $record['finished_at'] = $finished_at;
                $record['status'] = $success ? 'success' : 'failed';
                $record['exit_code'] = $exit_code;
                $record['duration_ms'] = max(0, min(3600000, $duration_ms));
                break;
            }
            unset($record);
            if ($started_at <= 0) {
                return $document;
            }
            $success_delta = $success ? 1 : 0;
            $failure_delta = $success ? 0 : 1;
            $document['profile']['quarters'] = $this->incrementBucket(
                $document['profile']['quarters'],
                self::QUARTER_SECONDS,
                $started_at,
                0,
                $success_delta,
                $failure_delta
            );
            $document['profile']['hours'] = $this->incrementBucket(
                $document['profile']['hours'],
                self::HOUR_SECONDS,
                $started_at,
                0,
                $success_delta,
                $failure_delta
            );
            return $document;
        });
    }

    public function history(): array {
        return $this->snapshot()['history'];
    }

     
    public function snapshot(): array {
        try {
            $query = $this->db->query(
                "SELECT `value` FROM `{$this->prefix}settings`
                 WHERE `key` = '" . $this->db->escape(self::SETTING_KEY) . "' LIMIT 1"
            );
            $document = $this->decodeDocument((string)($query->row['value'] ?? ''));
            return [
                'history' => $document['history'],
                'profile' => $document['profile'],
            ];
        } catch (\Throwable $e) {
            return ['history' => [], 'profile' => $this->emptyProfile()];
        }
    }

    private function mutate(callable $callback): void {
        try {
            $this->db->query('START TRANSACTION');
            $this->db->query(
                "INSERT INTO `{$this->prefix}settings` (`key`, `value`)
                 VALUES ('" . $this->db->escape(self::SETTING_KEY) . "', '[]')
                 ON DUPLICATE KEY UPDATE `value` = `value`"
            );
            $query = $this->db->query(
                "SELECT `value` FROM `{$this->prefix}settings`
                 WHERE `key` = '" . $this->db->escape(self::SETTING_KEY) . "' LIMIT 1 FOR UPDATE"
            );
            $document = call_user_func($callback, $this->decodeDocument((string)($query->row['value'] ?? '')));
            $document = $this->normalizeDocument(is_array($document) ? $document : []);
            $encoded = json_encode($document, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (!is_string($encoded)) {
                throw new \RuntimeException('MAX heartbeat JSON encoding failed');
            }
            $this->db->query(
                "UPDATE `{$this->prefix}settings`
                 SET `value` = '" . $this->db->escape($encoded) . "'
                 WHERE `key` = '" . $this->db->escape(self::SETTING_KEY) . "'"
            );
            $this->db->query('COMMIT');
        } catch (\Throwable $e) {
            try {
                $this->db->query('ROLLBACK');
            } catch (\Throwable $rollback_error) {
                 
            }
        }
    }

    private function decodeDocument(string $json): array {
        $decoded = json_decode($json, true);
        if (!is_array($decoded)) {
            return $this->normalizeDocument([]);
        }
         
         
        if (array_values($decoded) === $decoded) {
            return $this->normalizeDocument(['history' => $decoded]);
        }
        return $this->normalizeDocument($decoded);
    }

    private function normalizeDocument(array $document): array {
        $history = is_array($document['history'] ?? null) ? array_values($document['history']) : [];
        $history = array_values(array_filter($history, static function ($record): bool {
            return is_array($record) && max(0, (int)($record['started_at'] ?? 0)) > 0;
        }));
        usort($history, static function (array $left, array $right): int {
            return (int)($left['started_at'] ?? 0) <=> (int)($right['started_at'] ?? 0);
        });
        $history = array_slice($history, -self::HISTORY_LIMIT);

        $profile = is_array($document['profile'] ?? null) ? $document['profile'] : [];
        $first_seen_at = max(0, (int)($profile['first_seen_at'] ?? 0));
        if ($first_seen_at <= 0 && $history) {
            $first_seen_at = max(0, (int)($history[0]['started_at'] ?? 0));
        }
        return [
            'version' => self::DOCUMENT_VERSION,
            'history' => $history,
            'profile' => [
                'first_seen_at' => $first_seen_at,
                'quarters' => $this->normalizeBuckets($profile['quarters'] ?? [], self::QUARTER_LIMIT),
                'hours' => $this->normalizeBuckets($profile['hours'] ?? [], self::HOUR_LIMIT),
            ],
        ];
    }

    private function emptyProfile(): array {
        return ['first_seen_at' => 0, 'quarters' => [], 'hours' => []];
    }

    private function firstSeenAt(array $profile, int $now): int {
        $current = max(0, (int)($profile['first_seen_at'] ?? 0));
        return $current > 0 ? min($current, $now) : $now;
    }

    



    private function incrementBucket(array $buckets, int $seconds, int $timestamp, int $started, int $success, int $failed): array {
        $bucket_start = intdiv(max(0, $timestamp), $seconds) * $seconds;
        $found = false;
        foreach ($buckets as &$bucket) {
            if (!is_array($bucket) || (int)($bucket[0] ?? 0) !== $bucket_start) {
                continue;
            }
            $bucket = [
                $bucket_start,
                max(0, (int)($bucket[1] ?? 0) + $started),
                max(0, (int)($bucket[2] ?? 0) + $success),
                max(0, (int)($bucket[3] ?? 0) + $failed),
            ];
            $found = true;
            break;
        }
        unset($bucket);
        if (!$found) {
            $buckets[] = [$bucket_start, max(0, $started), max(0, $success), max(0, $failed)];
        }
        $limit = $seconds === self::QUARTER_SECONDS ? self::QUARTER_LIMIT : self::HOUR_LIMIT;
        return $this->normalizeBuckets($buckets, $limit);
    }

    private function normalizeBuckets($buckets, int $limit): array {
        if (!is_array($buckets)) {
            return [];
        }
        $normalized = [];
        foreach ($buckets as $bucket) {
            if (!is_array($bucket)) {
                continue;
            }
            $started_at = max(0, (int)($bucket[0] ?? 0));
            if ($started_at <= 0) {
                continue;
            }
            $normalized[$started_at] = [
                $started_at,
                max(0, (int)($bucket[1] ?? 0)),
                max(0, (int)($bucket[2] ?? 0)),
                max(0, (int)($bucket[3] ?? 0)),
            ];
        }
        ksort($normalized, SORT_NUMERIC);
        return array_slice(array_values($normalized), -$limit);
    }
}
