<?php

declare(strict_types=1);

namespace App\Filament\Resources\Bookings\Tables;

use App\Enums\BookingStatus;
use App\Enums\BookingType;
use App\Exceptions\SlotUnavailableException;
use App\Models\Booking;
use App\Models\Engineer;
use App\Services\Booking\BookingService;
use App\Services\Payments\PaymentService;
use Carbon\CarbonImmutable;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;

/**
 * The admin's main workspace.
 *
 * Ordered by soonest session rather than newest booking: what the studio
 * actually needs to see is what is coming up, not what was typed in last.
 */
class BookingsTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->defaultSort('starts_at')
            ->recordUrl(null)
            ->columns([
                TextColumn::make('reference')
                    ->label('Reference')
                    ->searchable()
                    ->copyable()
                    ->copyMessage('Reference copied')
                    ->weight(FontWeight::Bold)
                    // The customer types this into Revolut, so the admin
                    // spends a lot of time matching it against a statement.
                    ->description(fn (Booking $record) => $record->type->getLabel()),

                TextColumn::make('user.profile.artist_name')
                    ->label('Artist')
                    ->searchable()
                    ->description(fn (Booking $record) => $record->user->profile?->phone ?? $record->user->email)
                    ->placeholder('—'),

                TextColumn::make('room.name')
                    ->label('Room')
                    ->badge()
                    ->color('gray')
                    ->sortable(),

                TextColumn::make('starts_at')
                    ->label('When')
                    ->dateTime('D j M, H:i', config('studio.timezone'))
                    ->description(fn (Booking $record) => $record->ends_at
                        ->setTimezone(config('studio.timezone'))
                        ->format('H:i').' · '.self::duration($record))
                    ->sortable(),

                TextColumn::make('status')
                    ->label('Status')
                    ->badge()
                    // A hold that has run out is dead but not yet swept up by
                    // cron. Showing it as "Pending payment" would have the
                    // admin chasing money for a slot already back on sale.
                    ->formatStateUsing(fn (BookingStatus $state, Booking $record) => self::isStale($record)
                        ? 'Hold lapsed'
                        : $state->getLabel())
                    ->color(fn (BookingStatus $state, Booking $record) => self::isStale($record)
                        ? 'danger'
                        : $state->getColor())
                    ->description(fn (Booking $record) => self::holdCountdown($record)),

                TextColumn::make('price_total')
                    ->label('Total')
                    ->money('GBP')
                    ->alignEnd()
                    ->description(fn (Booking $record) => $record->isPaidByPackage() ? 'On a package' : null)
                    ->sortable(),

                TextColumn::make('assignedEngineer.name')
                    ->label('Engineer')
                    ->placeholder(fn (Booking $record) => $record->staff_required > 0
                        ? 'Unassigned'
                        : '—')
                    ->color(fn (Booking $record) => $record->staff_required > 0 && $record->assigned_engineer_id === null
                        ? 'warning'
                        : null)
                    ->description(fn (Booking $record) => $record->preferredEngineer
                        ? 'Asked for '.$record->preferredEngineer->name
                        : null)
                    ->toggleable(),
            ])
            // The workflow views — needs a decision, today, unassigned — are
            // tabs, not filters. Doing both would mean a default-on filter
            // silently narrowing the "Everything" tab, so an admin looking at
            // the full list would not actually be seeing it.
            ->filters([
                Filter::make('upcoming')
                    ->label('Upcoming only')
                    ->query(fn (Builder $query) => $query->where('starts_at', '>=', CarbonImmutable::now('UTC')))
                    ->toggle(),

                SelectFilter::make('status')
                    ->options(BookingStatus::class)
                    ->multiple(),

                SelectFilter::make('room')
                    ->relationship('room', 'name')
                    ->multiple(),

                SelectFilter::make('type')
                    ->label('Kind')
                    ->options(BookingType::class),
            ])
            ->recordActions([
                self::approveAction(),
                self::rejectAction(),
                ActionGroup::make([
                    ViewAction::make(),
                    self::assignEngineerAction(),
                    self::rescheduleAction(),
                    self::completeAction(),
                    self::cancelAction(),
                ])->label('More')->icon('heroicon-m-ellipsis-horizontal'),
            ])
            // No bulk delete. Deleting a booking row would orphan the customer
            // and lose the audit trail; cancelling is the correct verb, and it
            // has to run one at a time so a refund is calculated per booking.
            ->toolbarActions([]);
    }

    /**
     * Approving is the single most common thing the admin does, so it is a
     * first-class button rather than buried in a menu.
     */
    private static function approveAction(): Action
    {
        return Action::make('approve')
            ->label('Approve')
            ->icon('heroicon-m-check-circle')
            ->color('success')
            ->visible(fn (Booking $record) => in_array($record->status, [
                BookingStatus::PaymentSubmitted,
                BookingStatus::PendingPayment,
            ], true))
            ->requiresConfirmation()
            ->modalHeading('Confirm this booking')
            ->modalDescription(fn (Booking $record) => sprintf(
                'Check £%s arrived with reference %s before approving. A screenshot is not proof of payment.',
                number_format((float) $record->price_total, 2),
                $record->reference,
            ))
            ->modalSubmitActionLabel('Payment verified, confirm')
            // Through PaymentService, not BookingService: approving a
            // transfer settles the payment as well as confirming the booking,
            // and those two must never drift apart.
            ->action(function (Booking $record, PaymentService $payments) {
                $payments->approveTransfer($record, auth()->user());

                Notification::make()
                    ->success()
                    ->title('Booking confirmed')
                    ->body($record->reference.' is confirmed and the artist has been told.')
                    ->send();
            });
    }

    private static function rejectAction(): Action
    {
        return Action::make('reject')
            ->label('Reject')
            ->icon('heroicon-m-x-circle')
            ->color('danger')
            ->visible(fn (Booking $record) => in_array($record->status, [
                BookingStatus::PaymentSubmitted,
                BookingStatus::PendingPayment,
            ], true))
            ->schema([
                Textarea::make('reason')
                    ->label('Why is this being rejected?')
                    ->helperText('Goes into the email, so the artist is not left guessing.')
                    ->required()
                    ->rows(3),
            ])
            ->action(function (Booking $record, array $data, BookingService $bookings) {
                $bookings->cancel($record, $data['reason']);

                Notification::make()
                    ->warning()
                    ->title('Booking rejected')
                    ->body('The slot is back on sale.')
                    ->send();
            });
    }

    private static function cancelAction(): Action
    {
        return Action::make('cancel')
            ->label('Cancel booking')
            ->icon('heroicon-m-no-symbol')
            ->color('danger')
            ->visible(fn (Booking $record) => ! $record->status->isTerminal())
            ->schema([
                Textarea::make('reason')->label('Reason')->required()->rows(2),
            ])
            // The refund figure is calculated and shown before the admin
            // commits, because the 48 hour rule is exactly the kind of thing
            // that gets applied inconsistently when worked out by hand.
            ->modalDescription(fn (Booking $record) => sprintf(
                '%s notice. Refund due: £%s. The system records this — it does not move the money.',
                self::noticeGiven($record),
                app(BookingService::class)->refundDueFor($record),
            ))
            ->action(function (Booking $record, array $data, BookingService $bookings, PaymentService $payments) {
                $refund = $bookings->refundDueFor($record);
                $bookings->cancel($record, $data['reason'], $refund);

                // Written as a negative payment row so the ledger nets out,
                // rather than living only in a column somebody has to
                // remember to look at.
                $payments->recordRefund($record, $refund, auth()->user());

                Notification::make()
                    ->warning()
                    ->title('Booking cancelled')
                    ->body($refund === '0.00'
                        ? 'No refund due. The slot is back on sale.'
                        : "Refund £{$refund} manually via Revolut. The slot is back on sale.")
                    ->persistent()
                    ->send();
            });
    }

    private static function assignEngineerAction(): Action
    {
        return Action::make('assignEngineer')
            ->label('Assign engineer')
            ->icon('heroicon-m-user')
            ->visible(fn (Booking $record) => $record->staff_required > 0)
            ->schema(fn (Booking $record) => [
                Select::make('assigned_engineer_id')
                    ->label('Engineer')
                    ->options(Engineer::active()->pluck('name', 'id'))
                    ->default($record->preferred_engineer_id)
                    ->helperText($record->preferredEngineer
                        ? 'The artist asked for '.$record->preferredEngineer->name.'.'
                        : 'The artist has no preference.')
                    ->required(),
            ])
            ->action(function (Booking $record, array $data) {
                $record->update(['assigned_engineer_id' => $data['assigned_engineer_id']]);

                Notification::make()->success()->title('Engineer assigned')->send();
            });
    }

    private static function rescheduleAction(): Action
    {
        return Action::make('reschedule')
            ->label('Move to another time')
            ->icon('heroicon-m-calendar-days')
            ->visible(fn (Booking $record) => ! $record->status->isTerminal())
            ->schema([
                DateTimePicker::make('starts_at')
                    ->label('New start')
                    ->seconds(false)
                    ->minutesStep((int) config('studio.slot_minutes'))
                    ->timezone(config('studio.timezone'))
                    ->required(),
            ])
            ->modalDescription('The new time is checked for clashes exactly like a fresh booking.')
            ->action(function (Booking $record, array $data, BookingService $bookings) {
                try {
                    $bookings->reschedule(
                        $record,
                        CarbonImmutable::parse($data['starts_at'], config('studio.timezone')),
                    );

                    Notification::make()->success()->title('Booking moved')->send();
                } catch (SlotUnavailableException $e) {
                    Notification::make()
                        ->danger()
                        ->title('Could not move it')
                        ->body($e->getMessage())
                        ->send();
                }
            });
    }

    private static function completeAction(): Action
    {
        return Action::make('complete')
            ->label('Mark as done')
            ->icon('heroicon-m-check-badge')
            ->color('gray')
            // Only offered once the session has actually happened, so the
            // loyalty count cannot be inflated by clicking ahead of time.
            ->visible(fn (Booking $record) => $record->status === BookingStatus::Confirmed
                && $record->ends_at->isPast())
            ->requiresConfirmation()
            ->action(function (Booking $record, BookingService $bookings) {
                $bookings->complete($record);

                Notification::make()->success()->title('Session marked as done')->send();
            });
    }

    private static function duration(Booking $booking): string
    {
        $hours = $booking->slot_count * (int) config('studio.slot_minutes') / 60;

        return rtrim(rtrim(number_format($hours, 1), '0'), '.').'h';
    }

    /** A pending booking whose countdown has already run out. */
    private static function isStale(Booking $booking): bool
    {
        return $booking->status === BookingStatus::PendingPayment
            && $booking->hold_expires_at !== null
            && $booking->hold_expires_at->isPast();
    }

    private static function holdCountdown(Booking $booking): ?string
    {
        if ($booking->status !== BookingStatus::PendingPayment || $booking->hold_expires_at === null) {
            return null;
        }

        if ($booking->hold_expires_at->isPast()) {
            return 'Slot already released';
        }

        return 'Hold ends '.$booking->hold_expires_at->diffForHumans();
    }

    private static function noticeGiven(Booking $booking): string
    {
        $hours = (int) CarbonImmutable::now('UTC')->diffInHours($booking->starts_at, false);

        if ($hours < 0) {
            return 'Session already started';
        }

        return $hours >= 48 ? "{$hours} hours" : "Only {$hours} hours";
    }
}
