<?php

namespace Modules\Bms\Services\Booking;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Bms\Entities\BmsBooking;
use Modules\Bms\Entities\BmsBookingEvent;
use Modules\Bms\Entities\BmsHallAvailability;
use Modules\Bms\Entities\BmsPaymentSchedule;
use Modules\Bms\Entities\BmsSetting;
use Modules\Bms\Entities\BmsTask;
use Modules\Bms\Services\Contracts\InvoiceServiceContract;
use Modules\Bms\Services\Contracts\PaymentServiceContract;

/**
 * Cancellation & reschedule policy engine.
 *
 * Penalty is a percentage of the booking total, chosen from the editable
 * day/percent tiers (the most-severe matching tier wins). On cancel the
 * retained penalty becomes the invoice total and the over-collected balance is
 * refunded; the hall-date is released. On reschedule the same penalty is
 * charged as an added rebooking fee, the date is moved (with the same
 * double-booking guard as a fresh booking), and all task/schedule due-dates
 * re-anchor. An authorised user may override the penalty percentage.
 */
class CancellationService
{
    public function __construct(
        protected InvoiceServiceContract $invoices,
        protected PaymentServiceContract $payments,
    ) {
    }

    /** Effective policy for a business (bms_settings over config fallback). */
    public function policy(int $businessId): array
    {
        $policy = BmsSetting::get($businessId, 'cancellation_policy', null);
        if (! is_array($policy) || empty($policy)) {
            $policy = (array) config('bms.cancellation_policy', []);
        }
        return $policy;
    }

    /** Editable cancellation reason list (code + localized label). */
    public function reasons(int $businessId): array
    {
        $r = BmsSetting::get($businessId, 'cancellation_reasons', null);
        return (is_array($r) && ! empty($r)) ? $r : (array) config('bms.cancellation_reasons', []);
    }

    /** Whole days from "now" (or a given moment) to the event date. */
    public function daysToEvent(BmsBooking $booking, ?Carbon $when = null): int
    {
        if (! $booking->event_date) {
            return 0;
        }
        $when = ($when ?: now())->copy()->startOfDay();
        return $when->diffInDays(Carbon::parse($booking->event_date)->startOfDay(), false);
    }

    /**
     * Auto penalty percent for a booking at a moment in time. The most severe
     * tier (smallest days_before that still contains daysToEvent) wins; outside
     * every tier the default applies.
     */
    public function penaltyPercent(BmsBooking $booking, ?Carbon $when = null): float
    {
        $policy = $this->policy((int) $booking->business_id);
        if (! ($policy['enabled'] ?? true)) {
            return 0.0;
        }

        $days = $this->daysToEvent($booking, $when);
        $tiers = collect($policy['tiers'] ?? [])
            ->map(fn ($t) => ['days_before' => (int) $t['days_before'], 'penalty_percent' => (float) $t['penalty_percent']])
            ->sortBy('days_before') // ascending = most severe first
            ->values();

        foreach ($tiers as $tier) {
            if ($days <= $tier['days_before']) {
                return (float) $tier['penalty_percent'];
            }
        }
        return (float) ($policy['default_penalty_percent'] ?? 0);
    }

    /**
     * Preview the financial outcome of cancelling now (or on reschedule).
     *
     * @param float|null $overridePercent  caller-supplied (already permission-checked)
     */
    public function quote(BmsBooking $booking, ?float $overridePercent = null, ?Carbon $when = null): array
    {
        $percent = $overridePercent !== null
            ? max(0.0, min(100.0, $overridePercent))
            : $this->penaltyPercent($booking, $when);

        $total = round((float) $booking->total_amount, 2);
        $penalty = round($total * $percent / 100, 2);

        $status = $this->invoices->statusForBooking($booking);
        $paid = (float) ($status['paid'] ?? 0);
        $refund = round(max(0.0, $paid - $penalty), 2);

        return [
            'days_to_event'   => $this->daysToEvent($booking, $when),
            'penalty_percent' => $percent,
            'penalty_amount'  => $penalty,
            'total'           => $total,
            'paid'            => $paid,
            'refund_amount'   => $refund,
            'overridden'      => $overridePercent !== null,
        ];
    }

    /**
     * Cancel a booking: retain the penalty (invoice reduced to it), refund the
     * over-collected balance, free the hall-date, and cancel pending work.
     */
    public function cancel(BmsBooking $booking, array $opts = []): array
    {
        if ($booking->status === 'cancelled') {
            throw new \RuntimeException('Booking is already cancelled.');
        }

        $override = array_key_exists('override_penalty_percent', $opts) && $opts['override_penalty_percent'] !== null
            ? (float) $opts['override_penalty_percent'] : null;
        $userId = $opts['user_id'] ?? null;

        $quote = $this->quote($booking, $override);

        return DB::transaction(function () use ($booking, $quote, $opts, $userId) {
            // 1) Reduce the invoice to the retained penalty.
            $this->invoices->adjustInvoiceTotal(
                $booking, $quote['penalty_amount'],
                'Cancellation — retained ' . $quote['penalty_percent'] . '% (' . number_format($quote['penalty_amount'], 2) . ')'
            );

            // 2) Refund the over-collected balance.
            if ($quote['refund_amount'] > 0) {
                $this->payments->refund(
                    (int) $booking->business_id, $booking->id, $quote['refund_amount'],
                    'Cancellation refund — booking ' . $booking->booking_no
                );
            }

            // 3) Free the hall-date(s) so they can be rebooked.
            BmsHallAvailability::where('booking_id', $booking->id)->delete();

            // 4) Cancel still-open work.
            BmsTask::where('booking_id', $booking->id)->where('status', '!=', 'done')
                ->update(['status' => 'cancelled']);
            BmsPaymentSchedule::where('booking_id', $booking->id)->where('status', '!=', 'paid')
                ->update(['status' => 'cancelled']);

            // 5) Stamp the booking.
            $booking->update([
                'status'              => 'cancelled',
                'cancelled_at'        => now(),
                'cancelled_by'        => $userId,
                'cancellation_reason' => $opts['reason'] ?? null,
                'cancellation_reason_code' => $opts['reason_code'] ?? null,
                'penalty_percent'     => $quote['penalty_percent'],
                'penalty_amount'      => $quote['penalty_amount'],
                'refund_amount'       => $quote['refund_amount'],
            ]);

            BmsBookingEvent::record($booking, 'cancelled', $quote + [
                'reason'     => $opts['reason'] ?? null,
                'overridden' => $quote['overridden'],
            ], $userId);

            app(BookingService::class)->notifyContact(
                $booking, 'booking_cancelled', 'Booking cancelled',
                'Your banquet booking ' . $booking->booking_no . ' has been cancelled.'
                . ($quote['refund_amount'] > 0 ? ' A refund of ' . number_format($quote['refund_amount'], 2) . ' is being processed.' : '')
            );

            return $quote;
        });
    }

    /**
     * Move a booking to a new event date. Charges the policy rebooking fee
     * (added to the invoice), locks the new hall-date with the same guard as a
     * fresh booking, and re-anchors tasks + the payment schedule.
     *
     * @throws HoldConflictException when the new date is already taken.
     */
    public function reschedule(BmsBooking $booking, string $newDate, array $opts = []): array
    {
        if (in_array($booking->status, ['cancelled', 'completed'], true)) {
            throw new \RuntimeException('A ' . $booking->status . ' booking cannot be rescheduled.');
        }

        $policy = $this->policy((int) $booking->business_id);
        $usesPolicy = (bool) ($policy['reschedule_uses_policy'] ?? true);
        $override = array_key_exists('override_penalty_percent', $opts) && $opts['override_penalty_percent'] !== null
            ? (float) $opts['override_penalty_percent'] : null;
        $userId = $opts['user_id'] ?? null;

        $oldDate = optional($booking->event_date)->toDateString();
        $newDate = Carbon::parse($newDate)->toDateString();

        $percent = $override !== null
            ? max(0.0, min(100.0, $override))
            : ($usesPolicy ? $this->penaltyPercent($booking) : 0.0);
        $fee = round((float) $booking->total_amount * $percent / 100, 2);

        return DB::transaction(function () use ($booking, $oldDate, $newDate, $percent, $fee, $userId) {
            // 1) Lock + claim the new hall-date(s); guard against double-booking.
            foreach ($booking->halls as $bh) {
                $clash = BmsHallAvailability::where('hall_id', $bh->hall_id)
                    ->where('date', $newDate)->where('slot', $bh->slot ?: 'full_day')
                    ->where('booking_id', '!=', $booking->id)
                    ->lockForUpdate()->first();
                if ($clash) {
                    $expiredHold = $clash->status === 'hold' && $clash->hold_expires_at && $clash->hold_expires_at->isPast();
                    if (! $expiredHold) {
                        throw new HoldConflictException("Hall #{$bh->hall_id} is already booked or held on {$newDate}.");
                    }
                    $clash->delete();
                }
            }

            // 2) Move availability rows + booking-hall dates.
            BmsHallAvailability::where('booking_id', $booking->id)->update(['date' => $newDate]);
            foreach ($booking->halls as $bh) {
                $bh->forceFill(['event_date' => $newDate])->save();
            }

            // 3) Charge the rebooking fee (added to the invoice).
            if ($fee > 0) {
                $status = $this->invoices->statusForBooking($booking);
                $this->invoices->adjustInvoiceTotal(
                    $booking, (float) ($status['total'] ?? $booking->total_amount) + $fee,
                    'Reschedule fee ' . $percent . '% (' . number_format($fee, 2) . ')'
                );
            }

            // 4) Re-anchor tasks (shift by the date delta) + the payment schedule.
            $deltaDays = Carbon::parse($oldDate ?: $newDate)->diffInDays(Carbon::parse($newDate), false);
            if ($deltaDays !== 0) {
                foreach (BmsTask::where('booking_id', $booking->id)->where('status', '!=', 'done')->get() as $task) {
                    if ($task->due_at) {
                        $task->forceFill(['due_at' => Carbon::parse($task->due_at)->addDays($deltaDays)])->save();
                    }
                }
            }
            app(\Modules\Bms\Services\Finance\PaymentPlanService::class)->reanchor($booking, Carbon::parse($newDate));

            // 5) Stamp the booking.
            $booking->update([
                'event_date'       => $newDate,
                'rescheduled_from' => $oldDate,
                'reschedule_count' => (int) $booking->reschedule_count + 1,
            ]);

            $summary = [
                'from'            => $oldDate,
                'to'              => $newDate,
                'penalty_percent' => $percent,
                'fee_amount'      => $fee,
            ];
            BmsBookingEvent::record($booking, 'rescheduled', $summary, $userId);

            app(BookingService::class)->notifyContact(
                $booking, 'booking_rescheduled', 'Booking rescheduled',
                'Your banquet booking ' . $booking->booking_no . ' has been moved to ' . $newDate . '.'
                . ($fee > 0 ? ' A rebooking fee of ' . number_format($fee, 2) . ' applies.' : '')
            );

            return $summary;
        });
    }
}
