<?php

namespace Modules\Bms\Services\Booking;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Bms\Entities\BmsAddon;
use Modules\Bms\Entities\BmsBooking;
use Modules\Bms\Entities\BmsBookingHall;
use Modules\Bms\Entities\BmsHall;
use Modules\Bms\Entities\BmsHallAvailability;
use Modules\Bms\Entities\BmsLead;
use Modules\Bms\Entities\BmsPackage;
use Modules\Bms\Entities\BmsPackageItem;
use Modules\Bms\Services\Contracts\InvoiceServiceContract;

/**
 * Turns a qualified lead's hall selection into a banquet booking, locking the
 * hall-date(s) at the database layer, and confirms it (→ ERP invoice).
 */
class BookingService
{
    /**
     * Create a (tentative) booking from a lead's selected halls.
     *
     * @throws HoldConflictException  if a chosen hall-date is already taken
     * @throws \RuntimeException      if no event date is available
     */
    public function createFromLead(BmsLead $lead, ?string $eventDate = null, ?int $createdBy = null): BmsBooking
    {
        $date = $eventDate
            ?: (is_array($lead->candidate_dates) ? ($lead->candidate_dates[0] ?? null) : null);
        if (! $date) {
            throw new \RuntimeException('An event date is required to create a booking.');
        }
        $date = Carbon::parse($date)->toDateString();

        $selected = $lead->selectedHalls()->get();
        if ($selected->isEmpty()) {
            throw new \RuntimeException('The lead has no selected halls.');
        }

        $businessId = (int) $lead->business_id;
        $hallIds = $selected->pluck('hall_id')->all();
        $halls = BmsHall::whereIn('id', $hallIds)->get()->keyBy('id');
        $locationId = optional($halls->first())->location_id;

        return DB::transaction(function () use ($lead, $selected, $halls, $date, $businessId, $locationId, $createdBy) {
            // Lock each hall-date first (DB-level double-booking guard).
            foreach ($selected as $sel) {
                $existing = BmsHallAvailability::where('hall_id', $sel->hall_id)
                    ->where('date', $date)->where('slot', 'full_day')->lockForUpdate()->first();
                if ($existing) {
                    $expiredHold = $existing->status === 'hold' && $existing->hold_expires_at && $existing->hold_expires_at->isPast();
                    if (! $expiredHold) {
                        throw new HoldConflictException("Hall #{$sel->hall_id} is already booked or held on {$date}.");
                    }
                    $existing->delete();
                }
            }

            $total = 0;
            foreach ($selected as $sel) {
                $total += $this->lineTotal($sel, $halls->get($sel->hall_id));
            }

            $booking = BmsBooking::create([
                'business_id'  => $businessId,
                'booking_no'   => $this->nextBookingNo($businessId),
                'contact_id'   => $lead->contact_id,
                'lead_id'      => $lead->id,
                'location_id'  => $locationId,
                'status'       => 'tentative',
                'event_type'   => $lead->event_type,
                'event_date'   => $date,
                'attendees'    => $lead->attendees,
                'total_amount' => $total,
                'created_by'   => $createdBy,
            ]);

            foreach ($selected as $sel) {
                $price = $this->lineTotal($sel, $halls->get($sel->hall_id));
                BmsBookingHall::create([
                    'business_id' => $businessId,
                    'booking_id'  => $booking->id,
                    'hall_id'     => $sel->hall_id,
                    'event_date'  => $date,
                    'slot'        => 'full_day',
                    'package_id'   => $sel->package_id,
                    'addons'        => $sel->addons,
                    'package_items' => $sel->package_items,
                    'price'         => $price,
                ]);

                BmsHallAvailability::create([
                    'business_id' => $businessId,
                    'hall_id'     => $sel->hall_id,
                    'date'        => $date,
                    'slot'        => 'full_day',
                    'status'      => 'booked',
                    'booking_id'  => $booking->id,
                    'created_by'  => $createdBy,
                ]);
            }

            // Advance the lead stage.
            $lead->update(['stage' => 'won', 'last_activity_at' => now()]);

            return $booking;
        });
    }

    /**
     * Confirm a booking → mark confirmed, raise the ERP invoice, auto-generate
     * the per-department task sets, and notify the client through the matrix.
     * Returns the ERP transaction id.
     */
    public function confirm(BmsBooking $booking, ?int $userId = null): ?int
    {
        $booking->update(['status' => 'confirmed', 'confirmed_at' => now()]);
        $booking = $booking->fresh();

        $invoiceId = app(InvoiceServiceContract::class)->createOrUpdateFromBooking($booking);

        // Structured payment plan (deposit / balance) for the deposit gate + collections.
        app(\Modules\Bms\Services\Finance\PaymentPlanService::class)->generateForBooking($booking);

        // Auto task sets (logistics / kitchen / operations / collections).
        app(\Modules\Bms\Services\Tasks\TaskService::class)->generateForBooking($booking);

        \Modules\Bms\Entities\BmsBookingEvent::record($booking, 'confirmed', [
            'erp_invoice_id' => $invoiceId,
            'total'          => (float) $booking->total_amount,
        ], $userId);

        // Client notification through the configured channels.
        $this->notifyContact($booking, 'booking_confirmed',
            'Booking confirmed',
            "Your banquet booking {$booking->booking_no} is confirmed for " . optional($booking->event_date)->toDateString() . '.');

        return $invoiceId;
    }

    /**
     * Advance a confirmed booking to "contracted" (the signed-contract state).
     * Honours the deposit gate when the business enforces it.
     *
     * @throws \RuntimeException when the deposit gate is enforced and unmet.
     */
    public function markContracted(BmsBooking $booking, ?int $userId = null): BmsBooking
    {
        $plan = app(\Modules\Bms\Services\Finance\PaymentPlanService::class);
        if ($plan->depositGateEnforced((int) $booking->business_id) && ! $plan->depositMet($booking)) {
            throw new \RuntimeException('The deposit must be collected before the booking can be contracted.');
        }

        $booking->update(['status' => 'contracted', 'contracted_at' => now()]);
        \Modules\Bms\Entities\BmsBookingEvent::record($booking, 'contracted', [], $userId);

        return $booking->fresh();
    }

    /** Close out a booking after the event (optionally QC-gated by the caller). */
    public function markCompleted(BmsBooking $booking, ?int $userId = null): BmsBooking
    {
        $booking->update(['status' => 'completed', 'completed_at' => now()]);
        \Modules\Bms\Entities\BmsBookingEvent::record($booking, 'completed', [], $userId);

        return $booking->fresh();
    }

    /** Send a matrix-routed notification to the booking's contact. */
    public function notifyContact(BmsBooking $booking, string $updateType, string $subject, string $body): void
    {
        $contact = \App\Contact::find($booking->contact_id);
        if (! $contact) {
            return;
        }
        $to = array_values(array_filter([$contact->mobile, $contact->email]));
        if (empty($to)) {
            return;
        }
        try {
            app(\Modules\Bms\Services\Notifications\NotificationRouter::class)->notify(
                (int) $booking->business_id, $updateType, $to, $subject, $body,
                ['booking_no' => $booking->booking_no],
                $updateType . ':' . $booking->id
            );
        } catch (\Throwable $e) {
            \Log::warning('[BMS] booking notification failed: ' . $e->getMessage());
        }
    }

    /**
     * Price one selected hall the same way the client funnel does, so the
     * booking total matches the quote the guest agreed to:
     *   package price (or hall base)
     * + global add-ons (price × qty)
     * + package-item add-ons the guest kept (addon_price × qty; per-guest qty
     *   was captured at selection time)
     * + variant swaps (price delta, recomputed from the item's options for
     *   integrity — inclusions always apply, add-ons only when included).
     */
    /** Public quote for one selected hall (same maths as a booking line). */
    public function estimateHall($leadHall): float
    {
        $hall = $leadHall->hall_id ? BmsHall::find($leadHall->hall_id) : null;
        return $this->lineTotal($leadHall, $hall);
    }

    protected function lineTotal($leadHall, ?BmsHall $hall): float
    {
        $base = 0.0;
        if ($leadHall->package_id) {
            $base = (float) optional(BmsPackage::find($leadHall->package_id))->price;
        }
        if ($base <= 0 && $hall) {
            $base = (float) ($hall->base_price ?? $hall->min_budget ?? 0);
        }

        $addonsTotal = 0.0;
        foreach ((array) $leadHall->addons as $a) {
            $price = (float) optional(BmsAddon::find($a['addon_id'] ?? null))->price;
            $addonsTotal += $price * (int) ($a['qty'] ?? 1);
        }

        $itemsTotal = 0.0;
        $variantsTotal = 0.0;
        foreach ((array) $leadHall->package_items as $pi) {
            $item = BmsPackageItem::find($pi['item_id'] ?? null);
            if (! $item) {
                continue;
            }
            $included = ! empty($pi['included']);

            // Paid add-on lines the guest kept (sale price × selected qty).
            if ($item->is_addon && $included) {
                $price = $item->addon_price !== null ? (float) $item->addon_price : 0.0;
                $itemsTotal += $price * (float) ($pi['qty'] ?? $item->quantity);
            }

            // Variant swap difference (recomputed for integrity from the
            // effective options — the item's own override, else the linked
            // product's variations resolved via ProductPresentation).
            $applies = $item->is_addon ? $included : true;
            $vi = (int) ($pi['variant_index'] ?? 0);
            if ($applies && $vi > 0) {
                $options = (is_array($item->options) && ! empty($item->options))
                    ? $item->options
                    : (app(\Modules\Bms\Services\Catalog\ProductPresentation::class)
                        ->for((int) $item->business_id, $item->product_id, $item->variation_id)['variants'] ?? []);
                if (isset($options[$vi - 1])) {
                    $variantsTotal += (float) ($options[$vi - 1]['price_delta'] ?? 0);
                }
            }

            // Buffet-content substitutions: replace N units of a content row with
            // a swap option. Per-unit delta × replaced qty, recomputed from the
            // item's curated contents (the swap_options carry the price).
            if ($applies && is_array($item->contents)) {
                foreach ((array) ($pi['content_swaps'] ?? []) as $cs) {
                    $ci = (int) ($cs['index'] ?? -1);
                    $ti = (int) ($cs['to_index'] ?? 0);
                    $opt = $item->contents[$ci]['swap_options'][$ti - 1] ?? null;
                    if ($ti > 0 && $opt) {
                        $rowQty = (float) ($item->contents[$ci]['quantity'] ?? 1);
                        $qty = min(max((float) ($cs['qty'] ?? $rowQty), 0), $rowQty);
                        $variantsTotal += (float) ($opt['price_delta'] ?? 0) * $qty;
                    }
                }
            }
        }

        return $base + $addonsTotal + $itemsTotal + $variantsTotal;
    }

    protected function nextBookingNo(int $businessId): string
    {
        $seq = BmsBooking::where('business_id', $businessId)->count() + 1;
        return 'BQ' . str_pad((string) $seq, 5, '0', STR_PAD_LEFT);
    }
}
