<?php

namespace App\Listeners;

use App\Events\ServerStatusChanged;
use App\Services\FlapDetector;
use App\Services\IncidentManagerService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

/**
 * Auto-creates / auto-resolves incidents whenever a server's consensus
 * status flips. The ConsensusService fires ServerStatusChanged events
 * after each multi-location vote, so this is the canonical hook for
 * "server went down" → "create incident → page on-call".
 *
 * Behavior:
 * - status flipped TO offline/warning → call IncidentManagerService::openOrUpdate
 *   (idempotent — won't create duplicates if an open incident already exists)
 * - status flipped TO online → call ::autoResolve which closes any open
 *   incidents flagged with this detector
 * - status flipped within "flapping" pattern → suppress the create/resolve,
 *   we'll just log it and let FlapDetector raise its own alert if it
 *   crosses the flap threshold
 *
 * The detector key 'consensus.status' identifies incidents this listener
 * created so autoResolve only closes its own incidents (not ones a human
 * opened manually).
 *
 * Queued because the listener triggers a chain (save incident → fire
 * IncidentOpened → notify assignee → dispatch via channels). Doing all
 * that synchronously inside the consensus recompute would slow down
 * status updates noticeably during a big outage.
 */
class CreateIncidentFromStatusChange implements ShouldQueue
{
    public string $queue = 'default';

    public function __construct(
        protected IncidentManagerService $incidents,
        protected FlapDetector $flapDetector,
    ) {}

    public function handle(ServerStatusChanged $event): void
    {
        $server = $event->server;
        $previous = $event->previousStatus;
        $current = $server->status;

        // Skip transient flips — flap detector decides if we're in a
        // bouncy period and want to suppress new incidents temporarily.
        // The flap detector itself raises a separate alert if this server
        // crosses the threshold for sustained flapping.
        if ($this->flapDetector->isFlapping($server)) {
            Log::info("Server {$server->name} is flapping; skipping incident churn for {$previous}→{$current}");
            return;
        }

        if ($previous === $current) {
            return;
        }

        try {
            // Bad → bad transitions (e.g. warning → offline) update the
            // existing incident's severity, they don't need a new incident.
            // openOrUpdate handles both cases via the unique detector key.
            if (in_array($current, ['offline', 'warning'], true)) {
                $severity = $current === 'offline' ? 'critical' : 'warning';
                $title = $current === 'offline'
                    ? "Server down: {$server->name}"
                    : "Server degraded: {$server->name}";

                $this->incidents->openOrUpdate(
                    subject: $server,
                    title: $title,
                    severity: $severity,
                    detector: 'consensus.status',
                    meta: [
                        'previous_status' => $previous,
                        'current_status' => $current,
                        'consensus_status' => $server->consensus_status,
                        'locations_total' => $server->locations_total,
                        'locations_reporting_down' => $server->locations_reporting_down,
                        'detected_at' => now()->toIso8601String(),
                    ],
                );
                return;
            }

            // Bad → good transition closes the auto-created incident.
            if ($current === 'online' && in_array($previous, ['offline', 'warning'], true)) {
                $this->incidents->autoResolve($server, 'consensus.status');
                return;
            }
        } catch (\Throwable $e) {
            // Never let listener failures break the consensus pipeline.
            // The listener is queued, so failures are also visible in
            // the failed_jobs table for retry.
            Log::error('CreateIncidentFromStatusChange failed: ' . $e->getMessage(), [
                'server_id' => $server->id,
                'previous' => $previous,
                'current' => $current,
                'exception' => $e,
            ]);
            throw $e; // re-throw so queue worker records it as failed
        }
    }
}
