<?php

namespace App\Domain\Communication\Services;

use App\Domain\Communication\Enums\MessageStatus;
use App\Domain\Communication\Enums\NotificationChannel;
use App\Domain\Communication\Models\NotificationMessage;
use App\Domain\Communication\Models\NotificationPreference;
use App\Domain\Communication\Models\NotificationTemplate;
use App\Domain\Identity\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Sending, and recording, everything the agency tells people (§56, §57).
 *
 * Two rules shape this class. Every attempt is logged, including the ones that
 * were deliberately not sent, so "did we tell them?" always has an answer. And
 * a message the recipient opted out of is skipped — unless it is critical, in
 * which case it goes anyway, because a visa rejection is not marketing.
 */
class Notifier
{
    public function __construct(private readonly ChannelDispatcher $dispatcher) {}

    /**
     * Queues a templated message on every channel the recipient allows.
     *
     * Never throws into the business action that triggered it: an SMS gateway
     * being down must not stop a visa being recorded.
     *
     * @param  array<string, mixed>  $data
     * @return Collection<int, NotificationMessage>
     */
    public function send(
        string $templateCode,
        Model $recipient,
        array $data = [],
        ?Model $subject = null,
        ?User $actor = null,
    ): Collection {
        $companyId = $recipient->company_id ?? $subject?->company_id;

        if ($companyId === null) {
            return collect();
        }

        $template = NotificationTemplate::where('company_id', $companyId)
            ->where('code', $templateCode)
            ->active()
            ->first();

        if ($template === null) {
            // A missing template is a configuration gap worth knowing about,
            // not a reason to fail the thing that triggered it.
            Log::warning("No active notification template [{$templateCode}] for company {$companyId}.");

            return collect();
        }

        $messages = collect();

        foreach ($template->channelCases() as $channel) {
            $messages->push($this->queueOne($template, $channel, $recipient, $data, $subject, $actor, $companyId));
        }

        return $messages->filter()->values();
    }

    /**
     * Whether this recipient accepts this channel. Opting out is recorded per
     * channel; anything not recorded is treated as allowed.
     */
    public function allows(Model $recipient, NotificationChannel $channel): bool
    {
        $preference = NotificationPreference::where('owner_type', $recipient->getMorphClass())
            ->where('owner_id', $recipient->getKey())
            ->where('channel', $channel->value)
            ->first();

        return $preference === null || $preference->is_enabled;
    }

    /** @param array<string, bool> $channels */
    public function setPreferences(Model $recipient, array $channels): void
    {
        $companyId = $recipient->company_id ?? null;

        DB::transaction(function () use ($recipient, $channels, $companyId) {
            foreach ($channels as $channel => $enabled) {
                if (NotificationChannel::tryFrom($channel) === null) {
                    continue;
                }

                NotificationPreference::updateOrCreate(
                    [
                        'owner_type' => $recipient->getMorphClass(),
                        'owner_id' => $recipient->getKey(),
                        'channel' => $channel,
                    ],
                    ['company_id' => $companyId, 'is_enabled' => (bool) $enabled],
                );
            }
        });
    }

    /**
     * Marks an in-app message read. Only the recipient's own messages, and
     * reading is not something that can be undone into unread history.
     */
    public function markRead(NotificationMessage $message): NotificationMessage
    {
        if ($message->read_at === null) {
            $message->read_at = now();
            $message->save();
        }

        return $message;
    }

    /** @param array<string, mixed> $data */
    private function queueOne(
        NotificationTemplate $template,
        NotificationChannel $channel,
        Model $recipient,
        array $data,
        ?Model $subject,
        ?User $actor,
        int $companyId,
    ): ?NotificationMessage {
        $destination = $this->destinationFor($recipient, $channel);

        // A channel with nowhere to send to is recorded as skipped rather than
        // silently dropped, so the gap in someone's contact details is visible.
        $skipReason = match (true) {
            $channel->needsDestination() && blank($destination) => 'No '.$channel->label().' address on file.',
            ! $template->is_critical && ! $this->allows($recipient, $channel) => 'The recipient has opted out of '.$channel->label().'.',
            default => null,
        };

        $body = $template->bodyFor($channel, $data);

        $message = NotificationMessage::create([
            'company_id' => $companyId,
            'notification_template_id' => $template->getKey(),
            'template_code' => $template->code,
            'channel' => $channel->value,
            'status' => $skipReason === null ? MessageStatus::Queued->value : MessageStatus::Skipped->value,
            'recipient_type' => $recipient->getMorphClass(),
            'recipient_id' => $recipient->getKey(),
            'recipient_name' => $recipient->full_name ?? $recipient->name ?? null,
            'destination' => $destination,
            'subject_type' => $subject?->getMorphClass(),
            'subject_id' => $subject?->getKey(),
            'subject_line' => filled($template->subject)
                ? $template->render($template->subject, $data)
                : null,
            'body' => $body,
            'payload' => $data === [] ? null : $data,
            'queued_at' => now(),
            'failure_reason' => $skipReason,
            'triggered_by' => $actor?->getKey(),
        ]);

        if ($skipReason !== null) {
            return $message;
        }

        $this->dispatcher->dispatch($message);

        return $message->fresh();
    }

    private function destinationFor(Model $recipient, NotificationChannel $channel): ?string
    {
        $field = $channel->addressField();

        if ($field === null) {
            return null;
        }

        if ($field === 'email') {
            return $recipient->email ?? null;
        }

        // A WhatsApp number may differ from the number that takes SMS.
        return match ($channel) {
            NotificationChannel::WhatsApp => $recipient->whatsapp ?? $recipient->mobile ?? $recipient->phone ?? null,
            default => $recipient->mobile ?? $recipient->phone ?? null,
        };
    }
}
