<?php

namespace App\Services\Maintenance;

use App\Mail\MaintenanceAnnouncementMail;
use App\Mail\MaintenanceCancelledMail;
use App\Models\Customer;
use App\Models\MaintenanceNotification;
use App\Models\MaintenanceWindow;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;

/**
 * Ledger-guarded delivery of maintenance emails. Shared by the admin console and
 * the mail-event listeners so idempotency and the announcement/cancellation
 * pairing are enforced in exactly one place.
 */
class MaintenanceNotifier
{
    /** A pending notification is presumed failed (retryable) after this long. */
    public const RETRY_AFTER_MIN = 60;

    public function announce(MaintenanceWindow $window): void
    {
        foreach ($window->affectedCustomers() as $customer) {
            $this->deliver($window, $customer, 'announcement', new MaintenanceAnnouncementMail($window, $customer));
        }
    }

    /** Notify customers who actually received the announcement that it's cancelled. */
    public function notifyCancellation(MaintenanceWindow $window): void
    {
        $ids = MaintenanceNotification::query()
            ->where('maintenance_window_id', $window->id)
            ->where('event', 'announcement')
            ->whereNotNull('sent_at')
            ->pluck('customer_id');

        foreach (Customer::query()->whereIn('id', $ids)->get() as $customer) {
            $this->deliver($window, $customer, 'cancelled', new MaintenanceCancelledMail($window, $customer));
        }
    }

    /** Queue one mail per (window, customer, event), idempotently and retryably. */
    public function deliver(MaintenanceWindow $window, Customer $customer, string $event, Mailable $mail): void
    {
        $keys = ['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event];

        // firstOrCreate can still race two first-time inserts; the unique index
        // makes one lose — catch it and re-fetch so the operation stays idempotent.
        try {
            $delivery = MaintenanceNotification::query()->firstOrCreate($keys, ['email' => $customer->email]);
            $wasNew = $delivery->wasRecentlyCreated;
        } catch (UniqueConstraintViolationException) {
            $delivery = MaintenanceNotification::query()->where($keys)->firstOrFail();
            $wasNew = false;
        }
        $priorUpdatedAt = $delivery->updated_at;

        // Claim under a row lock so two workers can't both pass a stale-retry
        // check and double-send. The winner stamps updated_at; the loser re-reads
        // a fresh updated_at and backs off.
        $claimed = DB::transaction(function () use ($delivery, $wasNew) {
            $row = MaintenanceNotification::query()->whereKey($delivery->id)->lockForUpdate()->first();
            if ($row === null || $row->sent_at !== null) {
                return false; // already delivered — final
            }
            if (! $wasNew && $row->updated_at->gt(now()->subMinutes(self::RETRY_AFTER_MIN))) {
                return false; // still pending/in-flight — don't duplicate
            }
            $row->forceFill(['updated_at' => now()])->save();

            return true;
        });
        if (! $claimed) {
            return;
        }

        $mail->notificationId = $delivery->id;
        try {
            // Mail::to() alone resolves the DEFAULT mailer. Illuminate\Mail\Mailer::queue()
            // then runs `$view->mailer($this->name)->queue(...)` — that overwrites the
            // mailable's own purpose mailer (set in its constructor, e.g. 'cp_maintenance')
            // with the default's name before the job is even built. Naming the mailer here
            // makes that overwrite a no-op: $this->name on the resolved Mailer is already
            // $mail->mailer, so it writes back the same value.
            Mail::mailer($mail->mailer)
                ->to($customer->email)
                ->locale($customer->locale ?: config('app.locale'))
                ->queue($mail);
        } catch (\Throwable $e) {
            // Release the claim so a resend retries immediately.
            $wasNew ? $delivery->delete() : MaintenanceNotification::query()->whereKey($delivery->id)->update(['updated_at' => $priorUpdatedAt]);

            throw $e;
        }
    }
}
