<?php

namespace App\Services;

use App\Enums\BookingStatus;
use App\Enums\InvoiceStatus;
use App\Enums\QuotationStatus;
use App\Enums\ReservationStatus;
use App\Models\Booking;
use App\Models\BookingSpace;
use App\Models\Invoice;
use App\Models\Notification;
use App\Models\Quotation;
use App\Models\User;
use App\Support\PropertyDate;
use Illuminate\Support\Collection;

class NotificationService
{
    /**
     * @param  array<string, mixed>|null  $data
     */
    public function create(
        int $userId,
        string $type,
        string $title,
        string $body,
        ?string $relatedType = null,
        ?int $relatedId = null,
        ?array $data = null,
    ): Notification {
        return Notification::query()->create([
            'user_id' => $userId,
            'type' => $type,
            'title' => $title,
            'body' => $body,
            'related_type' => $relatedType,
            'related_id' => $relatedId,
            'data' => $data !== null ? json_encode($data) : null,
            'created_at' => now(),
        ]);
    }

    public function markRead(Notification $notification): Notification
    {
        if ($notification->read_at === null) {
            $notification->update(['read_at' => now()]);
        }

        return $notification->fresh();
    }

    public function markAllRead(int $userId): int
    {
        return Notification::query()
            ->where('user_id', $userId)
            ->whereNull('read_at')
            ->update(['read_at' => now()]);
    }

    public function unreadCount(int $userId): int
    {
        return Notification::query()
            ->where('user_id', $userId)
            ->whereNull('read_at')
            ->count();
    }

    /**
     * @return Collection<int, Notification>
     */
    public function forUser(int $userId, int $limit = 20): Collection
    {
        return Notification::query()
            ->where('user_id', $userId)
            ->orderByDesc('created_at')
            ->limit($limit)
            ->get();
    }

    public function generateOperationalAlerts(): int
    {
        $users = User::query()
            ->where('is_active', true)
            ->whereHas('roles.permissions', fn ($q) => $q->where('slug', 'notifications.view'))
            ->get();

        if ($users->isEmpty()) {
            return 0;
        }

        $created = 0;
        $today = PropertyDate::now()->startOfDay();
        $todayEnd = PropertyDate::now()->endOfDay();
        $holdHorizon = PropertyDate::now()->addHours(48);
        $quoteHorizon = PropertyDate::now()->addDays(7)->endOfDay();
        $invoiceHorizon = PropertyDate::now()->addDays(7)->endOfDay();

        foreach ($users as $user) {
            $created += $this->alertExpiringHolds($user, $holdHorizon);
            $created += $this->alertUpcomingEvents($user, $today);
            $created += $this->alertTodayEvents($user, $today, $todayEnd);
            $created += $this->alertExpiringQuotations($user, $today, $quoteHorizon);
            $created += $this->alertDueInvoices($user, $today, $invoiceHorizon);
            $created += $this->alertOverdueInvoices($user, $today);
        }

        return $created;
    }

    private function alertExistsToday(int $userId, string $type, ?string $relatedType, ?int $relatedId): bool
    {
        $todayStart = PropertyDate::now()->startOfDay();
        $todayEnd = PropertyDate::now()->endOfDay();

        return Notification::query()
            ->where('user_id', $userId)
            ->where('type', $type)
            ->where('related_type', $relatedType)
            ->where('related_id', $relatedId)
            ->whereBetween('created_at', [$todayStart, $todayEnd])
            ->exists();
    }

    private function alertExpiringHolds(User $user, $horizon): int
    {
        $created = 0;
        $holds = BookingSpace::query()
            ->with(['booking', 'space'])
            ->where('reservation_status', ReservationStatus::Provisional)
            ->whereNotNull('hold_expires_at')
            ->whereBetween('hold_expires_at', [PropertyDate::now(), $horizon])
            ->get();

        foreach ($holds as $hold) {
            if ($this->alertExistsToday($user->id, 'hold_expiring', BookingSpace::class, $hold->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'hold_expiring',
                'Hold expiring soon',
                sprintf(
                    'Hold on %s for %s expires %s.',
                    $hold->space?->name ?? 'space',
                    $hold->booking?->event_name ?? 'booking',
                    PropertyDate::formatDateTime($hold->hold_expires_at),
                ),
                BookingSpace::class,
                $hold->id,
                ['booking_id' => $hold->booking_id],
            );
            $created++;
        }

        return $created;
    }

    private function alertUpcomingEvents(User $user, $today): int
    {
        $created = 0;
        $tomorrow = PropertyDate::now()->addDay()->startOfDay();
        $dayAfter = PropertyDate::now()->addDays(2)->endOfDay();

        $bookings = Booking::query()
            ->whereNotIn('status', [BookingStatus::Cancelled, BookingStatus::Completed, BookingStatus::NoShow])
            ->whereBetween('event_start', [$tomorrow, $dayAfter])
            ->get();

        foreach ($bookings as $booking) {
            if ($this->alertExistsToday($user->id, 'upcoming_event', Booking::class, $booking->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'upcoming_event',
                'Upcoming event',
                sprintf('%s starts %s.', $booking->event_name, PropertyDate::formatDateTime($booking->event_start)),
                Booking::class,
                $booking->id,
            );
            $created++;
        }

        return $created;
    }

    private function alertTodayEvents(User $user, $todayStart, $todayEnd): int
    {
        $created = 0;

        $bookings = Booking::query()
            ->whereNotIn('status', [BookingStatus::Cancelled, BookingStatus::NoShow])
            ->whereDate('event_start', $todayStart)
            ->get();

        foreach ($bookings as $booking) {
            if ($this->alertExistsToday($user->id, 'today_event', Booking::class, $booking->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'today_event',
                "Today's event",
                sprintf('%s is scheduled today at %s.', $booking->event_name, PropertyDate::formatDateTime($booking->event_start)),
                Booking::class,
                $booking->id,
            );
            $created++;
        }

        return $created;
    }

    private function alertExpiringQuotations(User $user, $today, $horizon): int
    {
        $created = 0;

        $quotations = Quotation::query()
            ->where('status', QuotationStatus::Sent)
            ->whereBetween('expires_at', [$today, $horizon])
            ->get();

        foreach ($quotations as $quotation) {
            if ($this->alertExistsToday($user->id, 'quotation_expiring', Quotation::class, $quotation->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'quotation_expiring',
                'Quotation expiring',
                sprintf('Quotation %s expires %s.', $quotation->quotation_number, PropertyDate::format($quotation->expires_at)),
                Quotation::class,
                $quotation->id,
            );
            $created++;
        }

        return $created;
    }

    private function alertDueInvoices(User $user, $today, $horizon): int
    {
        $created = 0;

        $invoices = Invoice::query()
            ->whereNotIn('status', [InvoiceStatus::Draft, InvoiceStatus::Void, InvoiceStatus::Paid, InvoiceStatus::Refunded])
            ->where('outstanding_balance', '>', 0)
            ->whereBetween('due_date', [$today, $horizon])
            ->get();

        foreach ($invoices as $invoice) {
            if ($this->alertExistsToday($user->id, 'invoice_due', Invoice::class, $invoice->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'invoice_due',
                'Invoice due soon',
                sprintf('Invoice %s is due %s.', $invoice->invoice_number, PropertyDate::format($invoice->due_date)),
                Invoice::class,
                $invoice->id,
            );
            $created++;
        }

        return $created;
    }

    private function alertOverdueInvoices(User $user, $today): int
    {
        $created = 0;

        $invoices = Invoice::query()
            ->whereNotIn('status', [InvoiceStatus::Draft, InvoiceStatus::Void, InvoiceStatus::Paid, InvoiceStatus::Refunded])
            ->where('outstanding_balance', '>', 0)
            ->where('due_date', '<', $today)
            ->get();

        foreach ($invoices as $invoice) {
            if ($this->alertExistsToday($user->id, 'invoice_overdue', Invoice::class, $invoice->id)) {
                continue;
            }

            $this->create(
                $user->id,
                'invoice_overdue',
                'Overdue invoice',
                sprintf('Invoice %s is overdue (due %s).', $invoice->invoice_number, PropertyDate::format($invoice->due_date)),
                Invoice::class,
                $invoice->id,
            );
            $created++;
        }

        return $created;
    }
}
