<?php

namespace Detektivo\Helper;

class RunnerState extends \Lime\Helper {

    protected const STATE_COLLECTION = 'detektivo/indexer_state';
    protected const LOG_COLLECTION = 'detektivo/indexer';

    public function indexerHealthConfig(): array {

        $config = $this->app->retrieve('detektivo.indexer', []);

        if (!\is_array($config)) {
            $config = [];
        }

        return \array_replace([
            // runner never left queue (async bootstrap issue)
            'queued_timeout' => 120,
            // runner stopped sending heartbeats/logs (likely crashed/deadlocked)
            'running_timeout' => 10800,
        ], $config);
    }

    public function normalizeIndexerState($name, ?array $state = null): array {

        $state = $state ?? $this->app->dataStorage->getKey(self::STATE_COLLECTION, $name, []);
        $status = $state['status'] ?? 'idle';

        if (!\in_array($status, ['queued', 'running'], true)) {
            return $state;
        }

        $config = $this->indexerHealthConfig();
        $now = \time();
        $updated = (int) ($state['heartbeat'] ?? $state['updated'] ?? $state['started'] ?? $state['queued'] ?? $state['time'] ?? 0);

        if ($updated <= 0) {
            return $state;
        }

        if ($status === 'queued') {

            $timeout = \max(5, (int) ($config['queued_timeout'] ?? 120));

            if (($now - $updated) > $timeout) {

                $message = 'Indexer startup timeout';

                $this->app->dataStorage->rpush(self::LOG_COLLECTION, $name, [
                    'time' => $now,
                    'msg' => "Indexing failed: {$message}",
                    'type' => 'error',
                ]);

                return $this->setIndexerState($name, 'failed', ['error' => $message, 'stale' => true]);
            }

            return $state;
        }

        $timeout = \max(60, (int) ($config['running_timeout'] ?? 10800));

        if (($now - $updated) > $timeout) {

            $message = 'Indexer heartbeat timeout';

            $this->app->dataStorage->rpush(self::LOG_COLLECTION, $name, [
                'time' => $now,
                'msg' => "Indexing failed: {$message}",
                'type' => 'error',
            ]);

            return $this->setIndexerState($name, 'failed', ['error' => $message, 'stale' => true]);
        }

        return $state;
    }

    public function getIndexerState($name): array {

        $state = $this->app->dataStorage->getKey(self::STATE_COLLECTION, $name, []);

        if (!\is_array($state)) {
            $state = [];
        }

        $state = \array_replace([
            'status' => 'idle',
            'time' => 0,
            'updated' => 0,
        ], $state);

        return $this->normalizeIndexerState($name, $state);
    }

    public function setIndexerState($name, string $status, array $meta = []): array {

        $current = $this->app->dataStorage->getKey(self::STATE_COLLECTION, $name, []);

        if (!\is_array($current)) {
            $current = [];
        }

        $now = \time();
        $state = \array_replace($current, $meta);

        $state['status'] = $status;
        $state['time'] = $now;
        $state['updated'] = $now;

        if ($status === 'queued') {
            $state['queued'] = $state['queued'] ?? $now;
            $state['heartbeat'] = $now;
            unset($state['started'], $state['finished']);
        }

        if ($status === 'running') {
            $state['started'] = $state['started'] ?? $now;
            $state['heartbeat'] = $now;
            unset($state['finished']);
        }

        if (\in_array($status, ['finished', 'failed', 'stopped'], true)) {
            $state['finished'] = $now;
        }

        if ($status !== 'failed') {
            unset($state['error'], $state['stale']);
        }

        $this->app->dataStorage->setKey(self::STATE_COLLECTION, $name, $state);

        return $state;
    }

    public function queueIndexer($name): string|false {

        if (!$this->app->module('detektivo')->exists($name)) {
            return false;
        }

        $state = $this->getIndexerState($name);

        if (\in_array($state['status'] ?? 'idle', ['queued', 'running'], true)) {
            return false;
        }

        $runId = \uniqid('idxrun_', true);

        // reset previous run log before queueing a new run
        $this->app->dataStorage->setKey(self::LOG_COLLECTION, $name, []);
        $this->setIndexerState($name, 'queued', ['runId' => $runId, 'queued' => \time()]);

        return $runId;
    }

    public function beginIndexer($name, array $meta = []): bool {

        if (!$this->app->module('detektivo')->exists($name)) {
            return false;
        }

        $state = $this->getIndexerState($name);
        $status = $state['status'] ?? 'idle';
        $runId = $meta['runId'] ?? null;
        $stateRunId = $state['runId'] ?? null;

        if ($status === 'running') {
            return false;
        }

        // Prevent stale async workers from starting a canceled/replaced run.
        if ($runId && $stateRunId && $runId !== $stateRunId) {
            return false;
        }

        if ($runId && $stateRunId && $runId === $stateRunId && \in_array($status, ['stopped', 'failed', 'finished'], true)) {
            return false;
        }

        $this->setIndexerState($name, 'running', [
            'runId' => $runId ?: ($stateRunId ?? \uniqid('idxrun_', true)),
            'started' => \time(),
            'heartbeat' => \time(),
        ]);

        return true;
    }

    public function touchIndexer($name, array $meta = []): array {

        if (!$this->app->module('detektivo')->exists($name)) {
            return ['status' => 'idle', 'time' => 0, 'updated' => 0];
        }

        $state = $this->getIndexerState($name);

        if (($state['status'] ?? 'idle') !== 'running') {
            return $state;
        }

        $runId = $meta['runId'] ?? null;

        if ($runId && isset($state['runId']) && $state['runId'] !== $runId) {
            return $state;
        }

        $now = \time();
        $next = \array_replace($state, $meta, [
            'status' => 'running',
            'time' => $now,
            'updated' => $now,
            'heartbeat' => $now,
        ]);

        $this->app->dataStorage->setKey(self::STATE_COLLECTION, $name, $next);

        return $next;
    }

    public function finishIndexer($name, string $status = 'finished', array $meta = []): array {
        return $this->setIndexerState($name, $status, $meta);
    }

    public function stopIndexer($name): bool {

        if (!$this->app->module('detektivo')->exists($name)) {
            return false;
        }

        $this->setIndexerState($name, 'stopped', ['stopped' => \time()]);

        return true;
    }

    public function isIndexerRunning($name): bool {
        $status = $this->getIndexerState($name)['status'] ?? 'idle';
        return \in_array($status, ['queued', 'running'], true);
    }

    public function getIndexerLog($name): array {
        return $this->app->dataStorage->getKey(self::LOG_COLLECTION, $name, []);
    }
}
