<?php

declare(strict_types=1);

namespace App\Services;

use Illuminate\Support\Facades\DB;
use Throwable;

/**
 * Трекер пульса планировщика задач (hole #6).
 *
 * Оборачивает каждую cron-задачу: фиксирует время запуска, длительность,
 * результат (успех / ошибка) и consecutive_failures в scheduler_heartbeats.
 *
 * Использует pgsql_supplier (BYPASSRLS, crm_supplier_worker) — SaaS-level таблица,
 * RLS не применяется. Паттерн аналогичен IncidentsWatchFailures.
 */
final class SchedulerHeartbeatTracker
{
    private const DB_CONNECTION = 'pgsql_supplier';

    /**
     * Ожидаемые интервалы cron-задач в минутах.
     * Используется SchedulerCheckHeartbeats для детекции пропавшего пульса.
     */
    public const EXPECTED_INTERVALS = [
        'projects:reset-delivered-today' => 1440,  // daily
        'projects:reset-monthly' => 43200, // monthly (~30 days)
        'partitions:create-months' => 1440,  // daily
        'App\Jobs\Supplier\RefreshSupplierSessionJob@hourly' => 60,
        'App\Jobs\Supplier\RefreshSupplierSessionJob@daily' => 1440,
        'App\Jobs\Supplier\SyncSupplierProjectsJob' => 1440,
        'App\Jobs\Supplier\CleanupInactiveSupplierProjectsJob' => 1440,
        'supplier:retry-failed' => 60,    // hourly
        'App\Jobs\Supplier\CsvReconcileJob' => 30,    // every 30 min
        'incidents:watch-failures' => 10,    // every 10 min
        'audit:verify-chains' => 1440,  // daily
        'partitions:drop-expired' => 10080, // weekly (Sunday 03:00 MSK)
        'scheduler:check-heartbeats' => 60,    // hourly (self-check)
    ];

    /**
     * Выполняет $work, записывает heartbeat (успех или ошибку).
     * Исключение пробрасывается наружу после сохранения.
     * Используется в тестах и при прямой инвокации.
     */
    public function recordRun(string $name, callable $work): void
    {
        $startedAt = now();
        $error = null;

        try {
            $work();
        } catch (Throwable $e) {
            $error = substr($e->getMessage(), 0, 2000);
            throw $e;
        } finally {
            $runtimeMs = (int) ($startedAt->diffInMilliseconds(now()));
            $this->saveHeartbeat($name, $startedAt, $error, $runtimeMs);
        }
    }

    /**
     * Записывает результат запуска напрямую (без обёртки callable).
     * Используется из before/after/onFailure хуков routes/console.php.
     *
     * @param  bool  $success  true = успех, false = ошибка
     * @param  string|null  $errorMsg  сообщение ошибки при $success=false
     * @param  int|null  $runtimeMs  длительность в мс (null если неизвестна)
     */
    public function recordRunResult(
        string $name,
        bool $success,
        ?string $errorMsg,
        ?int $runtimeMs,
    ): void {
        $this->saveHeartbeat($name, now(), $success ? null : $errorMsg, $runtimeMs ?? 0);
    }

    private function saveHeartbeat(
        string $name,
        \DateTimeInterface $startedAt,
        ?string $error,
        int $runtimeMs,
    ): void {
        $now = now();
        $db = DB::connection(self::DB_CONNECTION);

        if ($error === null) {
            // Успех: сбрасываем consecutive_failures
            $db->statement(
                <<<'SQL'
                INSERT INTO scheduler_heartbeats
                    (command_name, last_run_at, last_success_at, last_error, runtime_ms, consecutive_failures, created_at, updated_at)
                VALUES
                    (?, ?, ?, NULL, ?, 0, ?, ?)
                ON CONFLICT (command_name) DO UPDATE SET
                    last_run_at          = EXCLUDED.last_run_at,
                    last_success_at      = EXCLUDED.last_success_at,
                    last_error           = NULL,
                    runtime_ms           = EXCLUDED.runtime_ms,
                    consecutive_failures = 0,
                    updated_at           = EXCLUDED.updated_at
                SQL,
                [$name, $startedAt, $now, $runtimeMs, $now, $now],
            );
        } else {
            // Ошибка: инкрементируем consecutive_failures
            $db->statement(
                <<<'SQL'
                INSERT INTO scheduler_heartbeats
                    (command_name, last_run_at, last_success_at, last_error, runtime_ms, consecutive_failures, created_at, updated_at)
                VALUES
                    (?, ?, NULL, ?, ?, 1, ?, ?)
                ON CONFLICT (command_name) DO UPDATE SET
                    last_run_at          = EXCLUDED.last_run_at,
                    last_error           = EXCLUDED.last_error,
                    runtime_ms           = EXCLUDED.runtime_ms,
                    consecutive_failures = scheduler_heartbeats.consecutive_failures + 1,
                    updated_at           = EXCLUDED.updated_at
                SQL,
                [$name, $startedAt, $error, $runtimeMs, $now, $now],
            );
        }
    }
}
