<?php

namespace App\Services\Booking;

use App\Enums\BookingStatus;
use App\Events\BookingCreated;
use App\Events\BookingCancelled;
use App\Models\Booking;
use App\Models\CashMovement;
use App\Models\Coupon;
use App\Models\Customer;
use App\Models\Employee;
use App\Models\Promotion;
use App\Models\Service;
use App\Support\Tenant;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class BookingService
{
    public function __construct(
        protected AvailabilityService $availability,
        protected CustomerService $customerSvc,
    ) {}

    /**
     * Crea una reserva de forma atómica protegiendo contra doble booking.
     *
     * Estrategia: dentro de una transacción, hacemos `lockForUpdate` sobre
     * cualquier reserva activa del mismo empleado en el mismo día. Si dos
     * requests intentan crear simultáneamente, uno espera al commit del otro
     * y revalida la disponibilidad antes de insertar.
     *
     * @throws RuntimeException si el slot ya no está disponible.
     */
    public function create(array $data): Booking
    {
        return DB::transaction(function () use ($data) {

            /** @var Service $service */
            $service  = Service::findOrFail($data['service_id']);
            $employee = isset($data['employee_id']) ? Employee::findOrFail($data['employee_id']) : null;

            if ($employee && ! $service->employees()->whereKey($employee->id)->exists()) {
                throw new RuntimeException('El profesional seleccionado no realiza este servicio.');
            }

            $date     = Carbon::parse($data['booking_date']);
            $startStr = $data['start_time'];
            $duration = (int) ($data['duration_minutes'] ?? $service->duration_minutes);

            $scheduledAt = $date->copy()->setTimeFromTimeString($startStr);
            $endsAt      = $scheduledAt->copy()->addMinutes($duration);

            // Lock pesimista sobre reservas del empleado para ese día
            if ($employee) {
                Booking::query()
                    ->where('employee_id', $employee->id)
                    ->whereDate('booking_date', $date->toDateString())
                    ->whereIn('status', ['pending', 'confirmed'])
                    ->lockForUpdate()
                    ->get();
            }

            // Revalidar disponibilidad ahora que tenemos el lock
            $available = $this->availability->slotsFor(
                $service,
                $employee?->id,
                $date->copy(),
            )->contains(fn ($slot) => $slot['start'] === substr($startStr, 0, 5));

            if (! $available) {
                throw new RuntimeException('El horario seleccionado ya no está disponible.');
            }

            // Cliente: invitado o existente
            $customer = $data['customer_id'] ?? null
                ? Customer::findOrFail($data['customer_id'])
                : $this->customerSvc->findOrCreate([
                    'company_id' => $data['company_id'] ?? Tenant::id(),
                    'first_name' => $data['customer_first_name'] ?? 'Cliente',
                    'last_name'  => $data['customer_last_name'] ?? null,
                    'email'      => $data['customer_email'] ?? null,
                    'phone'      => $data['customer_phone'],
                ]);

            $price = (float) $service->effectivePrice();
            $promotion = $this->resolvePromotion($data['promotion_id'] ?? null, $service);
            $promotionDiscount = $promotion?->calculateDiscount($price) ?? 0.0;
            $couponBase = max(0, $price - $promotionDiscount);
            $coupon = $this->resolveCoupon($data['coupon_id'] ?? null, $customer, $couponBase);
            $couponDiscount = $coupon?->calculateDiscount($couponBase) ?? 0.0;
            $discountAmount = min($price, round($promotionDiscount + $couponDiscount, 2));

            $booking = Booking::create([
                'company_id'        => $customer->company_id,
                'branch_id'         => $employee?->branch_id,
                'customer_id'       => $customer->id,
                'service_id'        => $service->id,
                'employee_id'       => $employee?->id,
                'status'            => $data['status'] ?? BookingStatus::Pending->value,
                'source'            => $data['source'] ?? 'public',
                'booking_date'      => $date->toDateString(),
                'start_time'        => $scheduledAt->format('H:i:s'),
                'end_time'          => $endsAt->format('H:i:s'),
                'scheduled_at'      => $scheduledAt,
                'duration_minutes'  => $duration,
                'service_price'     => $price,
                'discount_amount'   => $discountAmount,
                'total_amount'      => max(0, $price - $discountAmount),
                'customer_notes'    => $data['customer_notes'] ?? null,
                'internal_notes'    => $data['internal_notes'] ?? null,
                'currency'          => $service->company->currency ?? 'COP',
                'coupon_id'         => $coupon?->id,
                'promotion_id'      => $promotion?->id,
            ]);

            $coupon?->increment('used_count');
            $promotion?->increment('used_count');

            event(new BookingCreated($booking));

            return $booking->fresh(['service', 'employee', 'customer']);
        });
    }

    public function confirm(Booking $booking): Booking
    {
        $booking->update(['status' => BookingStatus::Confirmed]);
        return $booking;
    }

    public function complete(Booking $booking, string $paymentMethod = 'cash'): Booking
    {
        return DB::transaction(function () use ($booking, $paymentMethod) {
            $booking->update(['status' => BookingStatus::Completed]);

            $booking = $booking->fresh(['service:id,name', 'customer:id,first_name,last_name']);
            $this->registerCashIncome($booking, $paymentMethod);

            return $booking;
        });
    }

    public function cancel(Booking $booking, ?string $reason = null): Booking
    {
        $booking->update([
            'status'              => BookingStatus::Cancelled,
            'cancellation_reason' => $reason,
            'cancelled_at'        => $booking->cancelled_at ?? now(),
            'cancelled_by'        => $booking->cancelled_by ?? auth()->id(),
        ]);
        event(new BookingCancelled($booking));
        return $booking;
    }

    public function markNoShow(Booking $booking): Booking
    {
        $booking->update(['status' => BookingStatus::NoShow]);
        return $booking;
    }

    public function archive(Booking $booking): Booking
    {
        $booking->update(['status' => BookingStatus::Archived]);
        return $booking;
    }

    public function archiveExpired(int $graceMinutes = 0): int
    {
        $count = 0;

        Booking::query()
            ->whereIn('status', ['pending', 'confirmed'])
            ->whereRaw('DATE_ADD(scheduled_at, INTERVAL duration_minutes MINUTE) <= ?', [now()->subMinutes(max(0, $graceMinutes))])
            ->eachById(function (Booking $booking) use (&$count) {
                $booking->update(['status' => BookingStatus::NoShow]);
                $count++;
            });

        return $count;
    }

    protected function resolvePromotion(mixed $promotionId, Service $service): ?Promotion
    {
        if (! $promotionId) {
            return null;
        }

        $promotion = Promotion::query()
            ->active()
            ->whereKey($promotionId)
            ->first();

        return $promotion?->appliesToService($service->id) ? $promotion : null;
    }

    protected function registerCashIncome(Booking $booking, string $paymentMethod): void
    {
        $amount = (float) $booking->total_amount;

        if ($amount <= 0) {
            return;
        }

        $paymentMethod = in_array($paymentMethod, ['cash', 'card', 'transfer', 'online', 'other'], true)
            ? $paymentMethod
            : 'cash';

        $alreadyExists = CashMovement::query()
            ->withoutGlobalScopes()
            ->where('booking_id', $booking->id)
            ->where('type', 'income')
            ->exists();

        if ($alreadyExists) {
            return;
        }

        CashMovement::create([
            'company_id' => $booking->company_id,
            'user_id' => auth()->id(),
            'booking_id' => $booking->id,
            'type' => 'income',
            'payment_method' => $paymentMethod,
            'status' => 'confirmed',
            'category' => 'Servicios',
            'reference' => $booking->code,
            'description' => 'Cita atendida: '.($booking->service?->name ?? 'Servicio'),
            'amount' => $amount,
            'currency' => $booking->currency ?? 'COP',
            'occurred_at' => $booking->completed_at ?? now(),
            'notes' => trim('Ingreso automático por reserva '.$booking->code.' - '.($booking->customer?->fullName() ?? 'Cliente')),
        ]);
    }

    protected function resolveCoupon(mixed $couponId, Customer $customer, float $amount): ?Coupon
    {
        if (! $couponId) {
            return null;
        }

        $coupon = Coupon::query()
            ->active()
            ->where('scope', 'company')
            ->whereKey($couponId)
            ->first();

        if (! $coupon || ! $coupon->isAvailable() || $coupon->calculateDiscount($amount) <= 0) {
            return null;
        }

        if ($coupon->usage_limit_per_customer) {
            $customerUses = Booking::query()
                ->where('customer_id', $customer->id)
                ->where('coupon_id', $coupon->id)
                ->count();

            if ($customerUses >= $coupon->usage_limit_per_customer) {
                return null;
            }
        }

        return $coupon;
    }
}
