<?php

namespace App\Livewire\Admin;

use App\Models\Incident;
use App\Models\IncidentUpdate;
use App\Services\Status\ServiceHealth;
use App\Support\LocalTime;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Validate;
use Livewire\Component;

/**
 * Writing down a disruption while it is happening.
 *
 * The public page can only be as good as what somebody types here, so the form
 * is deliberately short: a title, what it touches, how bad it is, and the first
 * sentence. Everything else is a further update, which is also the only way to
 * change what the record says — an update is a statement made at a time, and
 * editing it afterwards is precisely what this record exists to prevent.
 *
 * Nothing is public until it is published. An operator two minutes into an
 * outage should be able to write carefully rather than publish a sentence they
 * then have to correct in front of everybody.
 */
#[Layout('layouts.admin')]
class Incidents extends Component
{
    #[Validate('required|string|max:160')]
    public string $title = '';

    #[Validate('required|in:degraded,down,maintenance')]
    public string $impact = Incident::IMPACT_DEGRADED;

    /** @var array<int, string> */
    #[Validate('required|array|min:1')]
    public array $components = [];

    #[Validate('required|date')]
    public string $startedAt = '';

    #[Validate('required|string|max:2000')]
    public string $firstUpdate = '';

    /** The incident an update is being written for, by uuid. */
    public ?string $updatingUuid = null;

    #[Validate('nullable|in:investigating,identified,monitoring,resolved')]
    public string $updateStage = 'identified';

    #[Validate('nullable|string|max:2000')]
    public string $updateBody = '';

    public function mount(): void
    {
        // Now, in the operator's own time — they are reporting something that
        // is happening, not filing a historical record (R19).
        $this->startedAt = LocalTime::toField(now());
    }

    public function create(): void
    {
        $this->authorize('maintenance.manage');

        $this->validateOnly('title');
        $this->validateOnly('impact');
        $this->validateOnly('components');
        $this->validateOnly('startedAt');
        $this->validateOnly('firstUpdate');

        // One transaction: an incident with no first update is a headline with
        // nothing under it, and that is what a reader would see if the second
        // insert failed.
        DB::transaction(function () {
            $incident = Incident::query()->create([
                'title' => $this->title,
                'impact' => $this->impact,
                'components' => array_values(array_intersect(ServiceHealth::COMPONENTS, $this->components)),
                'started_at' => LocalTime::fromField($this->startedAt),
                'published_at' => now(),
                'created_by' => Auth::guard('operator')->user()?->email,
            ]);

            // The first entry is always "wird untersucht" — that is what a
            // first report is. The form says so, so nobody looks for a stage
            // selector that is not there.
            $incident->updates()->create([
                'status' => 'investigating',
                'body' => $this->firstUpdate,
                'created_by' => Auth::guard('operator')->user()?->email,
            ]);
        });

        $this->reset(['title', 'impact', 'components', 'firstUpdate']);
        $this->startedAt = LocalTime::toField(now());
        $this->dispatch('notify', message: __('admin_incidents.created'));
    }

    /** Open the update box for one incident. */
    public function startUpdate(string $uuid): void
    {
        $this->authorize('maintenance.manage');
        $this->updatingUuid = $uuid;
        $this->updateStage = 'identified';
        $this->updateBody = '';
    }

    public function cancelUpdate(): void
    {
        $this->reset(['updatingUuid', 'updateStage', 'updateBody']);
    }

    public function postUpdate(): void
    {
        $this->authorize('maintenance.manage');

        $this->validate([
            'updateStage' => 'required|in:investigating,identified,monitoring,resolved',
            'updateBody' => 'required|string|max:2000',
        ]);

        $incident = Incident::query()->where('uuid', $this->updatingUuid)->firstOrFail();

        DB::transaction(function () use ($incident) {
            $incident->updates()->create([
                'status' => $this->updateStage,
                'body' => $this->updateBody,
                'created_by' => Auth::guard('operator')->user()?->email,
            ]);

            // "Resolved" is the update AND the closing of the incident. Two
            // separate actions is how a status page ends up with a resolved
            // note under an incident that still reports as ongoing.
            if ($this->updateStage === 'resolved' && ! $incident->isResolved()) {
                $incident->forceFill(['resolved_at' => now()])->save();
            }
        });

        $this->cancelUpdate();
        $this->dispatch('notify', message: __('admin_incidents.update_posted'));
    }

    /**
     * Remove an incident entirely — the whole record, updates and all.
     *
     * Deliberately different from editing an update, which there is still no
     * way to do: an update is a statement made at a time, and rewriting one
     * afterwards is what this record exists to prevent. Removing the WHOLE
     * incident is a different act. It is how a test entry, or one published by
     * mistake against the wrong service, is taken back — and taking it back
     * leaves nothing half-standing behind.
     *
     * Owner-level, and confirmed in a modal (R23), because on a page whose
     * whole value is that it is believed, deleting the record is the one action
     * that cannot be undone by writing more.
     */
    #[On('incident-delete-confirmed')]
    public function delete(string $uuid): void
    {
        $this->authorize('maintenance.manage');

        Incident::query()->where('uuid', $uuid)->first()?->delete();

        if ($this->updatingUuid === $uuid) {
            $this->cancelUpdate();
        }

        $this->dispatch('notify', message: __('admin_incidents.deleted'));
    }

    /** @return array<int, string> */
    public function componentOptions(): array
    {
        return ServiceHealth::COMPONENTS;
    }

    public function render()
    {
        $this->authorize('maintenance.manage');

        return view('livewire.admin.incidents', [
            'ongoing' => Incident::query()->ongoing()->with('updates')->orderByDesc('started_at')->get(),
            'resolved' => Incident::query()
                ->whereNotNull('resolved_at')
                ->with('updates')
                ->orderByDesc('started_at')
                ->limit(25)
                ->get(),
        ]);
    }
}
