<?php

namespace App\Livewire\Admin;

use App\Support\LocalTime;
use App\Models\Datacenter;
use App\Models\Host;
use App\Models\MaintenanceWindow;
use App\Services\Maintenance\MaintenanceNotifier;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('layouts.admin')]
class Maintenance extends Component
{
    #[Validate('required|string|max:255')]
    public string $title = '';

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

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

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

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

    /** @var array<int> */
    public array $hostIds = [];

    /** Toggle every host of one datacenter (select-all / deselect-all). */
    public function selectDatacenter(string $code): void
    {
        $this->authorize('maintenance.manage');
        $ids = Host::query()->where('datacenter', $code)->pluck('id')->map(fn ($id) => (string) $id)->all();
        $current = array_map('strval', $this->hostIds);

        $this->hostIds = empty(array_diff($ids, $current))
            ? array_values(array_diff($current, $ids))   // all selected → clear them
            : array_values(array_unique([...$current, ...$ids]));
    }

    /**
     * Set the end from the start. Typing a full timestamp by hand is the
     * fiddliest part of this form, and the end is almost always "start plus a
     * round number of minutes".
     */
    public function setDuration(int $minutes): void
    {
        $this->authorize('maintenance.manage');

        $start = $this->parsed($this->startsAt);
        if ($start === null) {
            // No start yet: assume the next half hour, which is what someone
            // scheduling a window in a hurry means anyway.
            $start = now()->addMinutes(30 - (now()->minute % 30))->startOfMinute();
            $this->startsAt = LocalTime::toField($start);
        }

        $this->endsAt = LocalTime::toField($start->copy()->addMinutes($minutes));
    }

    /** Minutes between start and end, or null while either is unusable. */
    public function durationMinutes(): ?int
    {
        $start = $this->parsed($this->startsAt);
        $end = $this->parsed($this->endsAt);

        if ($start === null || $end === null || $end->lessThanOrEqualTo($start)) {
            return null;
        }

        return (int) $start->diffInMinutes($end);
    }

    /** What the operator typed, as UTC. Half-typed input is normal here. */
    private function parsed(string $value): ?\Illuminate\Support\Carbon
    {
        return LocalTime::fromField($value);
    }

    public function saveDraft(): void
    {
        $this->authorize('maintenance.manage');
        $this->persist('draft');
    }

    public function publish(): void
    {
        $this->authorize('maintenance.manage');
        $window = $this->persist('scheduled');
        if ($window !== null) {
            app(MaintenanceNotifier::class)->announce($window);
        }
    }

    private function persist(string $state): ?MaintenanceWindow
    {
        $data = $this->validate([
            'title' => 'required|string|max:255',
            'publicDescription' => 'nullable|string|max:2000',
            'internalNotes' => 'nullable|string|max:2000',
            'startsAt' => 'required|date',
            'endsAt' => 'required|date',
            'hostIds' => 'array',
            'hostIds.*' => 'integer|exists:hosts,id',
        ]);

        $starts = LocalTime::fromField($data['startsAt']);
        $ends = LocalTime::fromField($data['endsAt']);

        if ($ends->lessThanOrEqualTo($starts)) {
            $this->addError('endsAt', __('maintenance.end_after_start'));

            return null;
        }
        // A window always needs a host — otherwise a hostless draft can never be
        // published (there is no edit-hosts action) and is stuck.
        if (empty($this->hostIds)) {
            $this->addError('hostIds', __('maintenance.need_host'));

            return null;
        }
        if ($state === 'scheduled' && $ends->isPast()) {
            $this->addError('endsAt', __('maintenance.end_future'));

            return null;
        }

        // Create the window and attach hosts atomically so a bad id can't leave
        // an orphaned scheduled window behind.
        $window = DB::transaction(function () use ($data, $starts, $ends, $state) {
            $window = MaintenanceWindow::create([
                'title' => $data['title'],
                'public_description' => $data['publicDescription'] ?: null,
                'internal_notes' => $data['internalNotes'] ?: null,
                'starts_at' => $starts,
                'ends_at' => $ends,
                'state' => $state,
                'created_by' => Auth::guard('operator')->id(),
                'published_at' => $state === 'scheduled' ? now() : null,
            ]);
            $window->hosts()->sync(array_map('intval', $data['hostIds'] ?? []));

            return $window;
        });

        $this->reset('title', 'publicDescription', 'internalNotes', 'startsAt', 'endsAt', 'hostIds');
        $this->dispatch('notify', message: __($state === 'scheduled' ? 'maintenance.published' : 'maintenance.draft_saved'));

        return $window;
    }

    public function publishExisting(string $uuid): void
    {
        $this->authorize('maintenance.manage');
        $window = MaintenanceWindow::query()->where('uuid', $uuid)->first();
        if ($window === null || $window->state !== 'draft') {
            return;
        }
        if ($window->hosts()->count() === 0 || $window->ends_at->isPast()) {
            $this->dispatch('notify', message: __('maintenance.need_host'));

            return;
        }
        $window->update(['state' => 'scheduled', 'published_at' => now()]);
        app(MaintenanceNotifier::class)->announce($window);
        $this->dispatch('notify', message: __('maintenance.published'));
    }

    /**
     * Re-run announcements for a published window. Idempotent (ledger-guarded),
     * so it only fills gaps — the retry path when a transient queue outage left
     * some affected customers un-notified.
     */
    public function resend(string $uuid): void
    {
        $this->authorize('maintenance.manage');
        $window = MaintenanceWindow::query()->where('uuid', $uuid)->first();
        // Only announce for a window that has not yet ended (derived state).
        if ($window === null || ! in_array($window->derivedState(), ['upcoming', 'active'], true)) {
            return;
        }
        app(MaintenanceNotifier::class)->announce($window);
        $this->dispatch('notify', message: __('maintenance.notified'));
    }

    public function cancel(string $uuid): void
    {
        $this->authorize('maintenance.manage');
        $window = MaintenanceWindow::query()->where('uuid', $uuid)->first();
        // Cannot cancel what is already cancelled or has already completed.
        if ($window === null || in_array($window->derivedState(), ['cancelled', 'completed'], true)) {
            return;
        }
        $wasPublished = $window->state === 'scheduled';
        $window->update(['state' => 'cancelled', 'cancelled_at' => now()]);

        // Customers who received an announcement are told it's cancelled. The
        // MessageSending listener suppresses still-pending announcements, and the
        // MessageSent listener catches the race (an announcement that delivers
        // just as we cancel) by queuing a catch-up cancellation for it.
        if ($wasPublished) {
            app(MaintenanceNotifier::class)->notifyCancellation($window);
        }

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

    public function render()
    {
        $windows = MaintenanceWindow::query()
            ->withCount('hosts')
            ->orderByDesc('starts_at')
            ->get()
            ->map(fn (MaintenanceWindow $w) => [
                'uuid' => $w->uuid,
                'title' => $w->title,
                'starts_at' => $w->starts_at,
                'ends_at' => $w->ends_at,
                'state' => $w->derivedState(),
                'hosts' => $w->hosts_count,
                'affected' => $w->affectedCustomers()->count(),
                'is_draft' => $w->state === 'draft',
                'notifiable' => $w->state === 'scheduled' && in_array($w->derivedState(), ['upcoming', 'active'], true),
                'cancellable' => in_array($w->derivedState(), ['draft', 'upcoming', 'active'], true),
            ]);

        return view('livewire.admin.maintenance', [
            'windows' => $windows,
            'datacenters' => Datacenter::query()->orderBy('name')->get(),
            'hosts' => Host::query()->orderBy('datacenter')->orderBy('name')->get(),
            'durationMinutes' => $this->durationMinutes(),
        ]);
    }
}
