<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Mail\IncidentDetectedMail;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;

/**
 * Сканирует failed_webhook_jobs и failed_jobs за скользящее окно.
 *
 * failed_webhook_jobs: одно правило — spike ≥ threshold (200).
 * failed_jobs: три правила:
 *   - spike: кол-во за окно одного job-класса ≥ threshold-spike (10) → high
 *   - daily-total: за 24ч одного job-класса ≥ threshold-daily (50) → medium
 *   - persistent: один exception повторяется > persistent-hours часов → medium
 *
 * Дедуп: если открытый инцидент с той же сигнатурой создан < dedup-window мин —
 * пропускаем. Письмо на kdv1@bk.ru только для severity=high.
 */
class IncidentsWatchFailures extends Command
{
    private const DB_CONNECTION = 'pgsql_supplier';

    protected $signature = 'incidents:watch-failures
        {--window=10                  : Окно сканирования в минутах}
        {--threshold=200              : Порог спайка для failed_webhook_jobs}
        {--threshold-spike=10         : Порог спайка для failed_jobs (за окно)}
        {--threshold-daily=50         : Порог суммы за 24ч для failed_jobs}
        {--persistent-hours=3         : Порог возраста persistent-exception для failed_jobs}
        {--dedup-window=60            : Окно дедупа открытых инцидентов в минутах}
        {--threshold-single-lead=1000 : Порог storm detection: failures одного supplier_lead_id за окно}';

    protected $description = 'Сканирует failed_webhook_jobs и failed_jobs, создаёт incidents_log на превышение порогов';

    public function handle(): int
    {
        $windowMinutes = (int) $this->option('window');
        $threshold = (int) $this->option('threshold');
        $thresholdSpike = (int) $this->option('threshold-spike');
        $thresholdDaily = (int) $this->option('threshold-daily');
        $persistentHours = (int) $this->option('persistent-hours');
        $dedupMinutes = (int) $this->option('dedup-window');

        $thresholdSingleLead = (int) $this->option('threshold-single-lead');

        $since = Carbon::now()->subMinutes($windowMinutes);
        $since24h = Carbon::now()->subHours(24);
        $dedupAt = Carbon::now()->subMinutes($dedupMinutes);
        $now = Carbon::now();

        // --- Проверяем наличие SaaS-администратора (FK NOT NULL) ---
        $adminId = DB::connection(self::DB_CONNECTION)
            ->table('saas_admin_users')
            ->where('is_active', true)
            ->whereNull('deleted_at')
            ->value('id');

        if ($adminId === null) {
            $this->warn('No active saas_admin_users found — skipping incident creation (warn-only).');

            return self::SUCCESS;
        }

        $created = 0;

        // ===== БЛОК 1: failed_webhook_jobs (исходная логика) =====
        $webhookGroups = DB::connection(self::DB_CONNECTION)
            ->table('failed_webhook_jobs')
            ->selectRaw('LEFT(exception, 180) AS sig, COUNT(*) AS cnt')
            ->whereNull('resolved_at')
            ->where('failed_at', '>=', $since)
            ->groupByRaw('LEFT(exception, 180)')
            ->havingRaw('COUNT(*) >= ?', [$threshold])
            ->get();

        foreach ($webhookGroups as $group) {
            $sig = $group->sig;
            $count = (int) $group->cnt;
            $dedupKey = substr($sig, 0, 80);

            if ($this->isDup($dedupKey, $dedupAt)) {
                $this->line("Skipping webhook (dedup): {$dedupKey}");

                continue;
            }

            $summary = "Автоматически: {$count} упавших webhook-джобов за {$windowMinutes} мин. Сигнатура: {$sig}";

            $this->createIncident($adminId, 'other', 'high', $summary, $since, $now, $dedupKey);
            $created++;
            $this->info("Webhook incident [high]: {$count} failures");
        }

        // ===== БЛОК 2: failed_jobs — spike =====
        $spikes = DB::connection(self::DB_CONNECTION)
            ->table('failed_jobs')
            ->selectRaw(
                "payload::json->>'displayName' AS job_class, ".
                'LEFT(exception, 80) AS exc_sig, '.
                'COUNT(*) AS cnt'
            )
            ->where('failed_at', '>=', $since)
            ->groupByRaw("payload::json->>'displayName', LEFT(exception, 80)")
            ->havingRaw('COUNT(*) >= ?', [$thresholdSpike])
            ->get();

        foreach ($spikes as $row) {
            $jobClass = (string) $row->job_class;
            $excSig = (string) $row->exc_sig;
            $cnt = (int) $row->cnt;
            $dedupKey = "spike:{$jobClass}:{$excSig}";

            if ($this->isDup($dedupKey, $dedupAt)) {
                $this->line("Skipping spike (dedup): {$dedupKey}");

                continue;
            }

            $summary = "Автоматически: spike {$cnt} failures job={$jobClass} за {$windowMinutes} мин. Exc: {$excSig}";
            $this->createIncident($adminId, 'other', 'high', $summary, $since, $now, $dedupKey);
            $created++;
            $this->info("Job spike [high]: {$jobClass} — {$cnt}");
        }

        // ===== БЛОК 3: failed_jobs — daily-total =====
        $daily = DB::connection(self::DB_CONNECTION)
            ->table('failed_jobs')
            ->selectRaw(
                "payload::json->>'displayName' AS job_class, ".
                'COUNT(*) AS cnt'
            )
            ->where('failed_at', '>=', $since24h)
            ->groupByRaw("payload::json->>'displayName'")
            ->havingRaw('COUNT(*) >= ?', [$thresholdDaily])
            ->get();

        foreach ($daily as $row) {
            $jobClass = (string) $row->job_class;
            $cnt = (int) $row->cnt;
            $dedupKey = "daily:{$jobClass}";

            if ($this->isDup($dedupKey, $dedupAt)) {
                $this->line("Skipping daily (dedup): {$dedupKey}");

                continue;
            }

            $summary = "Автоматически: daily-total {$cnt} failures job={$jobClass} за 24ч";
            $this->createIncident($adminId, 'other', 'medium', $summary, $since24h, $now, $dedupKey);
            $created++;
            $this->info("Job daily [medium]: {$jobClass} — {$cnt}");
        }

        // ===== БЛОК 4: failed_jobs — persistent =====
        $persistentSince = Carbon::now()->subHours($persistentHours);

        $persistent = DB::connection(self::DB_CONNECTION)
            ->table('failed_jobs')
            ->selectRaw(
                "payload::json->>'displayName' AS job_class, ".
                'LEFT(exception, 80) AS exc_sig, '.
                'MIN(failed_at) AS oldest_at, '.
                'COUNT(*) AS cnt'
            )
            ->where('failed_at', '<=', $persistentSince)
            ->groupByRaw("payload::json->>'displayName', LEFT(exception, 80)")
            ->get();

        foreach ($persistent as $row) {
            $jobClass = (string) $row->job_class;
            $excSig = (string) $row->exc_sig;
            $dedupKey = "persistent:{$jobClass}:{$excSig}";

            if ($this->isDup($dedupKey, $dedupAt)) {
                $this->line("Skipping persistent (dedup): {$dedupKey}");

                continue;
            }

            $summary = "Автоматически: persistent exception job={$jobClass} повторяется >{$persistentHours}ч. Exc: {$excSig}";
            $this->createIncident($adminId, 'other', 'medium', $summary, Carbon::parse($row->oldest_at), $now, $dedupKey);
            $created++;
            $this->info("Job persistent [medium]: {$jobClass}");
        }

        // ===== БЛОК 5: single-lead storm detection =====
        // Detects случай когда один supplier_lead_id генерирует >= threshold
        // failures за окно — классический шторм от застрявшего лида (Finding 2,
        // 2026-05-29). Создаём severity=high инцидент per lead_id.
        if ($thresholdSingleLead > 0) {
            $stormLeads = DB::connection(self::DB_CONNECTION)
                ->table('failed_webhook_jobs')
                ->selectRaw("raw_payload->>'supplier_lead_id' AS lead_id, COUNT(*) AS cnt")
                ->whereNull('resolved_at')
                ->where('failed_at', '>=', $since)
                ->whereRaw("raw_payload ?? 'supplier_lead_id'")
                ->groupByRaw("raw_payload->>'supplier_lead_id'")
                ->havingRaw('COUNT(*) >= ?', [$thresholdSingleLead])
                ->get();

            foreach ($stormLeads as $row) {
                $leadId = $row->lead_id;
                $cnt = (int) $row->cnt;
                $dedupKey = "single-lead-storm:{$leadId}";

                if ($this->isDup($dedupKey, $dedupAt)) {
                    $this->line("Skipping single-lead-storm (dedup): {$dedupKey}");

                    continue;
                }

                $summary = "Автоматически: single-lead-storm {$cnt} failures supplier_lead_id={$leadId} за {$windowMinutes} мин. Вероятная причина: terminal error без fast-fail guard.";
                $this->createIncident($adminId, 'other', 'high', $summary, $since, $now, $dedupKey);
                $created++;
                $this->info("Single-lead storm [high]: lead_id={$leadId} — {$cnt}");
            }
        }

        $this->info("Done. Created {$created} incident(s).");

        return self::SUCCESS;
    }

    private function isDup(string $dedupKey, Carbon $dedupAt): bool
    {
        // Сигнатура сохраняется в root_cause для надёжного дедупа
        return DB::connection(self::DB_CONNECTION)
            ->table('incidents_log')
            ->where('root_cause', $dedupKey)
            ->whereNull('resolved_at')
            ->where('detected_at', '>=', $dedupAt)
            ->exists();
    }

    private function createIncident(
        int $adminId,
        string $type,
        string $severity,
        string $summary,
        Carbon $startedAt,
        Carbon $now,
        string $dedupKey = '',
    ): void {
        DB::connection(self::DB_CONNECTION)->table('incidents_log')->insert([
            'type' => $type,
            'severity' => $severity,
            'summary' => $summary,
            'root_cause' => $dedupKey !== '' ? $dedupKey : null,
            'started_at' => $startedAt,
            'detected_at' => $now,
            'resolved_at' => null,
            'created_by_admin_id' => $adminId,
            'created_at' => $now,
            'updated_at' => $now,
        ]);

        if ($severity === 'high') {
            Mail::to('kdv1@bk.ru')->send(new IncidentDetectedMail($summary, $severity));
        }
    }
}
