<?php

namespace App\Domain\Booking;

use App\Domain\Booking\Exceptions\SlotUnavailableException;
use App\Enums\AppointmentStatus;
use App\Models\Appointment;
use App\Models\Dentist;
use App\Models\Patient;
use Carbon\CarbonImmutable;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;

/**
 * The ONLY entry point for creating/moving appointments — used by reception,
 * the public booking page, the API, and (in M5+) WhatsApp automation, so
 * validation and double-booking prevention exist in exactly one place.
 *
 * Two layers of protection:
 *  1. Availability check + serialization inside the transaction.
 *  2. The active_slot_key UNIQUE index — even a request that slips past
 *     layer 1 cannot commit a duplicate slot.
 */
class BookingService
{
    public function __construct(protected SlotEngine $slots)
    {
    }

    public function book(
        Dentist $dentist,
        Patient $patient,
        CarbonImmutable $startUtc,
        string $source = 'reception',
        ?string $reason = null,
        ?int $createdBy = null,
    ): Appointment {
        if ($startUtc->isPast()) {
            throw SlotUnavailableException::inThePast();
        }

        try {
            return DB::transaction(function () use ($dentist, $patient, $startUtc, $source, $reason, $createdBy) {
                // Serialize concurrent bookings for this dentist.
                Appointment::query()
                    ->where('dentist_id', $dentist->id)
                    ->where('active_slot_key', $dentist->id.'|'.$startUtc->format('YmdHi'))
                    ->lockForUpdate()
                    ->get();

                if (! $this->slots->isAvailable($dentist, $startUtc)) {
                    throw SlotUnavailableException::taken();
                }

                $appointment = Appointment::create([
                    'dentist_id' => $dentist->id,
                    'patient_id' => $patient->id,
                    'starts_at' => $startUtc,
                    'ends_at' => $startUtc->addMinutes($dentist->slot_duration_min),
                    'status' => $source === 'online'
                        ? AppointmentStatus::Pending
                        : AppointmentStatus::Confirmed,
                    'source' => $source,
                    'reason' => $reason,
                    'created_by' => $createdBy,
                ]);

                if ($appointment->status === AppointmentStatus::Confirmed) {
                    $this->sendConfirmation($appointment);
                }

                return $appointment;
            });
        } catch (QueryException $e) {
            if ($this->isDuplicateKey($e)) {
                throw SlotUnavailableException::taken();
            }
            throw $e;
        }
    }

    public function reschedule(Appointment $appointment, CarbonImmutable $newStartUtc): Appointment
    {
        if ($newStartUtc->isPast()) {
            throw SlotUnavailableException::inThePast();
        }
        if (! $appointment->status->holdsSlot() || $appointment->status === AppointmentStatus::Completed) {
            throw SlotUnavailableException::taken();
        }

        try {
            return DB::transaction(function () use ($appointment, $newStartUtc) {
                $dentist = $appointment->dentist;

                if (! $this->slots->isAvailable($dentist, $newStartUtc)) {
                    throw SlotUnavailableException::taken();
                }

                $appointment->update([
                    'starts_at' => $newStartUtc,
                    'ends_at' => $newStartUtc->addMinutes($dentist->slot_duration_min),
                ]);

                return $appointment->refresh();
            });
        } catch (QueryException $e) {
            if ($this->isDuplicateKey($e)) {
                throw SlotUnavailableException::taken();
            }
            throw $e;
        }
    }

    public function changeStatus(Appointment $appointment, AppointmentStatus $to, ?string $reason = null): Appointment
    {
        abort_unless(
            $appointment->status->canTransitionTo($to),
            422,
            __('Cannot change status from :from to :to.', [
                'from' => $appointment->status->label(),
                'to' => $to->label(),
            ])
        );

        $appointment->update([
            'status' => $to,
            'cancellation_reason' => $to === AppointmentStatus::Cancelled ? $reason : $appointment->cancellation_reason,
        ]);

        if ($to === AppointmentStatus::Completed) {
            $this->recordVisit($appointment);
        }
        if ($to === AppointmentStatus::Confirmed) {
            $this->sendConfirmation($appointment);
        }

        return $appointment;
    }

    protected function sendConfirmation(Appointment $appointment): void
    {
        $appointment->loadMissing(['patient', 'dentist', 'tenant']);
        if (! $appointment->patient) {
            return;
        }
        $tz = $appointment->tenant?->timezone ?? config('app.timezone');
        $local = \Carbon\CarbonImmutable::parse($appointment->starts_at)->setTimezone($tz);

        app(\App\Domain\WhatsApp\WhatsAppManager::class)->queueTemplate(
            patient: $appointment->patient,
            templateKey: \App\Enums\WaTemplateKey::Confirmation->value,
            tokens: [
                'dentist' => $appointment->dentist->name,
                'date' => $local->format('Y-m-d'),
                'time' => $local->format('H:i'),
            ],
            related: $appointment,
        );
    }

    protected function recordVisit(Appointment $appointment): void
    {
        $appointment->patient()->update([
            'total_visits' => DB::raw('total_visits + 1'),
            'last_visit_at' => $appointment->starts_at,
        ]);
    }

    protected function isDuplicateKey(QueryException $e): bool
    {
        return in_array($e->errorInfo[1] ?? null, [1062, 19], true) // MySQL, SQLite
            || str_contains($e->getMessage(), 'UNIQUE constraint failed')
            || str_contains($e->getMessage(), 'Duplicate entry');
    }
}
