<?php

namespace App\Services;

use App\Enums\AppointmentStatus;
use App\Exceptions\InvalidPushTokenException;
use App\Exceptions\NotificationSkippedException;
use App\Models\Appointment;
use App\Models\DeviceToken;
use App\Models\NotificationLog;
use App\Models\NotificationPreference;
use App\Notifications\AppointmentReminderNotification;
use App\Services\Providers\EmailProvider;
use App\Services\Providers\PushProvider;
use App\Services\Providers\WhatsAppProvider;
use Illuminate\Support\Facades\Cache;
use Throwable;

class NotificationService
{
    public function __construct(
        private readonly PushProvider $push,
        private readonly WhatsAppProvider $whatsapp,
        private readonly EmailProvider $email,
    ) {}

    public function send(NotificationLog $log): void
    {
        $lock = Cache::lock('notification-send:'.hash('sha256', $log->idempotency_key), 120);

        $lock->block(5, function () use ($log) {
            $log = NotificationLog::query()->find($log->id);
            if (! $log || in_array($log->status, ['sent', 'skipped', 'superseded'], true)) {
                return;
            }

            $log->update([
                'status' => 'processing',
                'attempts' => $log->attempts + 1,
                'error_text' => null,
            ]);

            $log->loadMissing(['appointment.patient.user', 'appointment.doctor.user', 'appointment.location', 'patient']);
            $appointment = $log->appointment;

            if (! $appointment) {
                $this->fail($log, 'Appointment no longer exists.');

                return;
            }

            if (in_array($appointment->status, [
                AppointmentStatus::CANCELLED,
                AppointmentStatus::NO_SHOW,
                AppointmentStatus::COMPLETED,
            ], true)) {
                $log->update([
                    'status' => 'superseded',
                    'error_text' => 'Appointment is no longer eligible for reminders.',
                ]);

                return;
            }

            $patient = $appointment->patient;
            $preferences = NotificationPreference::query()
                ->where('organization_id', $appointment->organization_id)
                ->where(function ($query) use ($patient) {
                    $query->where('patient_id', $patient->id);
                    if ($patient->user_id) {
                        $query->orWhere('user_id', $patient->user_id);
                    }
                })
                ->first();

            try {
                $providerMessageId = match ($log->channel) {
                    'push' => $this->sendPush($appointment, $preferences),
                    'whatsapp' => $this->sendWhatsApp($appointment, $preferences),
                    'email' => $this->sendEmail($appointment, $preferences),
                    'in_app' => $this->sendInApp($appointment, $preferences, $log->type),
                    default => throw new \RuntimeException("Unsupported notification channel: {$log->channel}"),
                };

                $log->update([
                    'status' => 'sent',
                    'provider_message_id' => $providerMessageId,
                    'sent_at' => now(),
                    'error_text' => null,
                ]);
            } catch (NotificationSkippedException $e) {
                $log->update([
                    'status' => 'skipped',
                    'error_text' => mb_substr($e->getMessage(), 0, 5000),
                ]);
            } catch (Throwable $e) {
                $this->fail($log, $e->getMessage());
                throw $e;
            }
        });
    }

    private function sendPush(Appointment $appointment, ?NotificationPreference $preferences): ?string
    {
        if ($preferences && ! $preferences->push_enabled) {
            throw new NotificationSkippedException('Push notifications are disabled by patient preference.');
        }

        $user = $appointment->patient?->user;
        if (! $user) {
            throw new NotificationSkippedException('Patient does not have an authenticated user account for push notifications.');
        }

        $tokens = DeviceToken::query()
            ->where('user_id', $user->id)
            ->where('is_active', true)
            ->get();

        if ($tokens->isEmpty()) {
            throw new NotificationSkippedException('No active push token is registered.');
        }

        $lastId = null;
        $successes = 0;
        $errors = [];

        foreach ($tokens as $device) {
            try {
                $lastId = $this->push->send(
                    $device,
                    'Appointment Reminder',
                    "Your appointment {$appointment->appointment_code} is coming up.",
                    ['appointment_id' => $appointment->id]
                );
                $device->update(['last_used_at' => now()]);
                $successes++;
            } catch (InvalidPushTokenException $e) {
                $device->update(['is_active' => false]);
                $errors[] = $e->getMessage();
            } catch (Throwable $e) {
                $errors[] = $e->getMessage();
            }
        }

        if ($successes === 0 && $errors !== []) {
            throw new \RuntimeException('All push deliveries failed: '.implode('; ', array_unique($errors)));
        }

        return $lastId;
    }

    private function sendWhatsApp(Appointment $appointment, ?NotificationPreference $preferences): ?string
    {
        $patient = $appointment->patient;

        if (! $patient->has_whatsapp || ! $patient->whatsapp) {
            throw new NotificationSkippedException('Patient has not enabled WhatsApp for this number.');
        }

        if ($preferences && ! $preferences->whatsapp_enabled) {
            throw new NotificationSkippedException('WhatsApp notifications are disabled by patient preference.');
        }

        if (! $preferences?->consent_at) {
            throw new NotificationSkippedException('WhatsApp consent has not been recorded for this patient.');
        }

        return $this->whatsapp->sendAppointmentReminder($patient->whatsapp, $appointment);
    }

    private function sendEmail(Appointment $appointment, ?NotificationPreference $preferences): ?string
    {
        if (! $appointment->patient?->email) {
            throw new NotificationSkippedException('Patient email address is missing.');
        }

        if ($preferences && ! $preferences->email_enabled) {
            throw new NotificationSkippedException('Email notifications are disabled by patient preference.');
        }

        $this->email->sendAppointmentReminder($appointment->patient->email, $appointment);

        return null;
    }

    private function sendInApp(Appointment $appointment, ?NotificationPreference $preferences, string $type): ?string
    {
        if ($preferences && ! $preferences->in_app_enabled) {
            throw new NotificationSkippedException('In-app notifications are disabled by patient preference.');
        }

        $user = $appointment->patient?->user;
        if (! $user) {
            throw new NotificationSkippedException('Patient does not have an authenticated account for in-app notifications.');
        }

        $user->notify(new AppointmentReminderNotification($appointment, $type));

        return null;
    }

    private function fail(NotificationLog $log, string $error): void
    {
        $log->update([
            'status' => 'failed',
            'error_text' => mb_substr($error, 0, 5000),
        ]);
    }
}
