<?php

namespace Database\Seeders;

use App\Enums\BookingStatus;
use App\Enums\ChargeBasis;
use App\Enums\CustomerType;
use App\Enums\InvoiceStatus;
use App\Enums\PackagePricingMethod;
use App\Enums\PackageStatus;
use App\Enums\PackageType;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Enums\QuotationStatus;
use App\Enums\ReservationStatus;
use App\Enums\SpaceBlockType;
use App\Enums\SpaceOperationalStatus;
use App\Models\Booking;
use App\Models\BookingItem;
use App\Models\BookingSpace;
use App\Models\Customer;
use App\Models\EventType;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Notification;
use App\Models\Package;
use App\Models\PackageItem;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductCategory;
use App\Models\Quotation;
use App\Models\QuotationItem;
use App\Models\Role;
use App\Models\Space;
use App\Models\SpaceBlock;
use App\Models\SpaceLayout;
use App\Models\SpaceType;
use App\Models\User;
use App\Services\PricingService;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

/**
 * Rich fictional demo data for exercising every major feature.
 *
 * Idempotent: safe to re-run. Uses DEMO-* codes / DEMO-* document numbers.
 *
 *   php artisan db:seed --class=DemoSeeder
 *   php artisan venue:seed-demo
 */
class DemoSeeder extends Seeder
{
    private PricingService $pricing;

    private ?User $admin = null;

    private ?User $finance = null;

    private string $tz = 'Africa/Nairobi';

    public function run(): void
    {
        $this->pricing = app(PricingService::class);
        $this->tz = (string) (config('app.timezone') ?: 'Africa/Nairobi');

        $this->seedStaffUsers();
        $customers = $this->seedCustomers();
        $spaces = $this->seedSpaces();
        $products = $this->seedProducts();
        $packages = $this->seedPackages($products);

        $this->seedSpaceBlock($spaces['garden']);
        $this->seedScenarioDeck($customers, $spaces, $products, $packages);
        $this->seedNotifications();

        $this->command?->info('Demo data ready. Login: admin@venue.local / password');
        $this->command?->info('Also seeded: sales@venue.local, finance@venue.local, reservations@venue.local (password)');
    }

    private function seedStaffUsers(): void
    {
        $this->admin = User::query()->where('email', 'admin@venue.local')->first();

        $staff = [
            ['email' => 'sales@venue.local', 'name' => 'Amina Sales', 'role' => 'events-sales-officer'],
            ['email' => 'finance@venue.local', 'name' => 'Brian Finance', 'role' => 'finance-officer'],
            ['email' => 'reservations@venue.local', 'name' => 'Carol Reservations', 'role' => 'reservations-officer'],
        ];

        foreach ($staff as $row) {
            $user = User::query()->updateOrCreate(
                ['email' => $row['email']],
                [
                    'name' => $row['name'],
                    'password' => Hash::make('password'),
                    'is_active' => true,
                    'email_verified_at' => now(),
                ],
            );

            $role = Role::query()->where('slug', $row['role'])->first();
            if ($role !== null) {
                $user->roles()->sync([$role->id]);
            }

            if ($row['email'] === 'finance@venue.local') {
                $this->finance = $user;
            }
        }

        if ($this->admin === null) {
            $this->admin = User::query()->where('email', 'admin@venue.local')->first()
                ?? User::query()->first();
        }
    }

    /**
     * @return array<string, Customer>
     */
    private function seedCustomers(): array
    {
        $rows = [
            'acme' => [
                'customer_type' => CustomerType::Company,
                'name' => 'Acme Conferencing Ltd',
                'contact_person' => 'Jane Wanjiku',
                'phone' => '+254700000001',
                'email' => 'jane@acme-demo.test',
                'physical_address' => 'Westlands, Nairobi',
                'billing_address' => 'Westlands, Nairobi',
                'tax_id' => 'P051234567X',
                'preferred_payment_terms' => 'Net 30',
            ],
            'green' => [
                'customer_type' => CustomerType::Ngo,
                'name' => 'Green Horizons Foundation',
                'contact_person' => 'Peter Otieno',
                'phone' => '+254700000002',
                'email' => 'peter@greenhorizons-demo.test',
                'physical_address' => 'Kilimani, Nairobi',
                'billing_address' => 'Kilimani, Nairobi',
                'preferred_payment_terms' => 'Net 14',
            ],
            'mary' => [
                'customer_type' => CustomerType::Individual,
                'name' => 'Mary Achieng',
                'contact_person' => 'Mary Achieng',
                'phone' => '+254700000003',
                'email' => 'mary.achieng@example.test',
                'physical_address' => 'Lavington, Nairobi',
                'billing_address' => 'Lavington, Nairobi',
            ],
            'ministry' => [
                'customer_type' => CustomerType::Government,
                'name' => 'Ministry of Trade — Training Unit',
                'contact_person' => 'Samuel Kariuki',
                'phone' => '+254700000004',
                'email' => 'training@trade-demo.go.ke.test',
                'physical_address' => 'Upper Hill, Nairobi',
                'billing_address' => 'P.O. Box 30400, Nairobi',
                'tax_id' => 'P000000001A',
                'preferred_payment_terms' => 'Net 45',
            ],
            'planner' => [
                'customer_type' => CustomerType::EventPlanner,
                'name' => 'Celebrate Events Kenya',
                'contact_person' => 'Grace Njeri',
                'phone' => '+254700000005',
                'email' => 'grace@celebrate-demo.test',
                'physical_address' => 'Karen, Nairobi',
                'billing_address' => 'Karen, Nairobi',
                'preferred_payment_terms' => 'Deposit 50%',
            ],
            'school' => [
                'customer_type' => CustomerType::School,
                'name' => 'Ridgeview Academy',
                'contact_person' => 'Head of Academics',
                'phone' => '+254700000006',
                'email' => 'events@ridgeview-demo.test',
                'physical_address' => 'Runda, Nairobi',
                'billing_address' => 'Runda, Nairobi',
            ],
            'parish' => [
                'customer_type' => CustomerType::Religious,
                'name' => 'St. Clare Parish Social Committee',
                'contact_person' => 'Fr. Joseph Mutua',
                'phone' => '+254700000007',
                'email' => 'parish@stclare-demo.test',
                'physical_address' => 'South B, Nairobi',
                'billing_address' => 'South B, Nairobi',
            ],
        ];

        $out = [];
        foreach ($rows as $key => $data) {
            $data['is_active'] = true;
            $out[$key] = Customer::query()->updateOrCreate(
                ['email' => $data['email']],
                $data,
            );
        }

        return $out;
    }

    /**
     * @return array{grand: Space, board: Space, garden: Space}
     */
    private function seedSpaces(): array
    {
        $hallType = SpaceType::query()->where('name', 'like', '%Banquet%')->first()
            ?? SpaceType::query()->first();
        $boardType = SpaceType::query()->where('name', 'like', '%Boardroom%')->first() ?? $hallType;
        $outdoorType = SpaceType::query()->where('name', 'like', '%Outdoor%')->first()
            ?? SpaceType::query()->where('name', 'like', '%Garden%')->first()
            ?? $hallType;

        $grand = Space::query()->updateOrCreate(
            ['code' => 'DEMO-GRAND'],
            [
                'space_type_id' => $hallType?->id,
                'name' => 'Grand Ballroom',
                'description' => 'Flagship banquet and conference ballroom (demo).',
                'location' => 'Ground Floor',
                'min_capacity' => 50,
                'max_capacity' => 400,
                'standard_rate' => 85000,
                'hourly_rate' => 12000,
                'half_day_rate' => 45000,
                'full_day_rate' => 85000,
                'setup_duration_minutes' => 60,
                'breakdown_duration_minutes' => 60,
                'opening_time' => '07:00:00',
                'closing_time' => '23:00:00',
                'amenities' => 'Stage, AV booth, dance floor',
                'equipment' => 'Projector, PA system',
                'operational_status' => SpaceOperationalStatus::Active,
            ],
        );

        $board = Space::query()->updateOrCreate(
            ['code' => 'DEMO-BOARD'],
            [
                'space_type_id' => $boardType?->id,
                'name' => 'Cedar Boardroom',
                'description' => 'Executive boardroom (demo).',
                'location' => 'Mezzanine',
                'min_capacity' => 4,
                'max_capacity' => 16,
                'standard_rate' => 18000,
                'hourly_rate' => 3500,
                'half_day_rate' => 10000,
                'full_day_rate' => 18000,
                'setup_duration_minutes' => 30,
                'breakdown_duration_minutes' => 30,
                'opening_time' => '07:00:00',
                'closing_time' => '21:00:00',
                'amenities' => 'Video conferencing, whiteboard',
                'operational_status' => SpaceOperationalStatus::Active,
            ],
        );

        $garden = Space::query()->updateOrCreate(
            ['code' => 'DEMO-GARDEN'],
            [
                'space_type_id' => $outdoorType?->id,
                'name' => 'Sunset Garden Pavilion',
                'description' => 'Covered outdoor pavilion for cocktails and receptions (demo).',
                'location' => 'East Lawn',
                'min_capacity' => 30,
                'max_capacity' => 200,
                'standard_rate' => 55000,
                'hourly_rate' => 8000,
                'half_day_rate' => 30000,
                'full_day_rate' => 55000,
                'setup_duration_minutes' => 90,
                'breakdown_duration_minutes' => 90,
                'opening_time' => '08:00:00',
                'closing_time' => '22:00:00',
                'amenities' => 'String lights, patio heaters',
                'operational_status' => SpaceOperationalStatus::Active,
            ],
        );

        foreach ([['Theatre', 350], ['Banquet', 280], ['Cocktail', 400], ['Classroom', 200]] as $i => [$layout, $cap]) {
            SpaceLayout::query()->updateOrCreate(
                ['space_id' => $grand->id, 'layout_name' => $layout],
                ['capacity' => $cap, 'is_custom' => false, 'sort_order' => $i],
            );
        }

        foreach ([['U-Shape', 14], ['Boardroom', 16]] as $i => [$layout, $cap]) {
            SpaceLayout::query()->updateOrCreate(
                ['space_id' => $board->id, 'layout_name' => $layout],
                ['capacity' => $cap, 'is_custom' => false, 'sort_order' => $i],
            );
        }

        SpaceLayout::query()->updateOrCreate(
            ['space_id' => $garden->id, 'layout_name' => 'Cocktail'],
            ['capacity' => 180, 'is_custom' => false, 'sort_order' => 0],
        );

        return compact('grand', 'board', 'garden');
    }

    /**
     * @return array<string, Product>
     */
    private function seedProducts(): array
    {
        $cat = static fn (string $like) => ProductCategory::query()->where('name', 'like', $like)->first()
            ?? ProductCategory::query()->first();

        $defs = [
            'venue' => [
                'code' => 'DEMO-VH',
                'category' => '%Venue%',
                'name' => 'Venue Hire — Full Day',
                'unit' => 'day',
                'default_price' => 85000,
                'default_charge_basis' => ChargeBasis::OncePerEvent,
            ],
            'board_hire' => [
                'code' => 'DEMO-BOARD-HIRE',
                'category' => '%Venue%',
                'name' => 'Boardroom Hire — Full Day',
                'unit' => 'day',
                'default_price' => 18000,
                'default_charge_basis' => ChargeBasis::OncePerEvent,
            ],
            'lunch' => [
                'code' => 'DEMO-LUNCH',
                'category' => '%Cater%',
                'name' => 'Conference Lunch',
                'unit' => 'guest',
                'default_price' => 2500,
                'default_charge_basis' => ChargeBasis::PerGuest,
            ],
            'breakfast' => [
                'code' => 'DEMO-BF',
                'category' => '%Cater%',
                'name' => 'Working Breakfast',
                'unit' => 'guest',
                'default_price' => 1200,
                'default_charge_basis' => ChargeBasis::PerGuest,
            ],
            'water' => [
                'code' => 'DEMO-WATER',
                'category' => '%Cater%',
                'name' => 'Bottled Water (pair)',
                'unit' => 'guest',
                'default_price' => 200,
                'default_charge_basis' => ChargeBasis::PerGuest,
            ],
            'av' => [
                'code' => 'DEMO-AV',
                'category' => '%AV%',
                'name' => 'AV Package (projector + mics)',
                'unit' => 'day',
                'default_price' => 15000,
                'default_charge_basis' => ChargeBasis::OncePerEvent,
            ],
            'signage' => [
                'code' => 'DEMO-SIGN',
                'category' => '%Decor%',
                'name' => 'Directional Signage Set',
                'unit' => 'set',
                'default_price' => 8000,
                'default_charge_basis' => ChargeBasis::OncePerEvent,
            ],
            'decor' => [
                'code' => 'DEMO-DECOR',
                'category' => '%Decor%',
                'name' => 'Stage Floral Backdrop',
                'unit' => 'set',
                'default_price' => 22000,
                'default_charge_basis' => ChargeBasis::OncePerEvent,
            ],
            'tea' => [
                'code' => 'DEMO-TEA',
                'category' => '%Cater%',
                'name' => 'Afternoon Tea Break',
                'unit' => 'guest',
                'default_price' => 800,
                'default_charge_basis' => ChargeBasis::PerGuest,
            ],
        ];

        $out = [];
        foreach ($defs as $key => $def) {
            $out[$key] = Product::query()->updateOrCreate(
                ['code' => $def['code']],
                [
                    'category_id' => $cat($def['category'])?->id,
                    'name' => $def['name'],
                    'description' => 'Demo product — '.$def['name'],
                    'unit' => $def['unit'],
                    'default_price' => $def['default_price'],
                    'default_tax_rate' => 16,
                    'default_charge_basis' => $def['default_charge_basis'],
                    'min_qty' => 1,
                    'max_qty' => 5000,
                    'is_active' => true,
                ],
            );
        }

        return $out;
    }

    /**
     * @param  array<string, Product>  $products
     * @return array{confDay: Package, avGlobal: Package, cateringPg: Package, fixed: Package}
     */
    private function seedPackages(array $products): array
    {
        $confDay = Package::query()->updateOrCreate(
            ['code' => 'DEMO-CONF-DAY'],
            [
                'name' => 'Conference Day Package',
                'category' => 'Conference',
                'package_type' => PackageType::PerGuest,
                'description' => 'Venue hire + lunch (per guest pricing).',
                'min_guests' => 20,
                'max_guests' => 200,
                'status' => PackageStatus::Active,
                'pricing_method' => PackagePricingMethod::Calculated,
                'default_terms' => 'Demo terms — 50% deposit to confirm.',
            ],
        );
        $this->syncPackageItems($confDay, [
            [$products['venue'], 1, true],
            [$products['lunch'], 1, true],
            [$products['tea'], 1, false],
        ]);

        $avGlobal = Package::query()->updateOrCreate(
            ['code' => 'DEMO-AV-GLOBAL'],
            [
                'name' => 'AV & Signage Add-on',
                'category' => 'AV',
                'package_type' => PackageType::Global,
                'description' => 'Global (per reservation) AV and signage.',
                'min_guests' => 1,
                'max_guests' => 1000,
                'status' => PackageStatus::Active,
                'pricing_method' => PackagePricingMethod::Calculated,
                'default_terms' => null,
            ],
        );
        $this->syncPackageItems($avGlobal, [
            [$products['av'], 1, true],
            [$products['signage'], 1, true],
        ]);

        $cateringPg = Package::query()->updateOrCreate(
            ['code' => 'DEMO-CATER-PG'],
            [
                'name' => 'Full Day Catering (Per Guest)',
                'category' => 'Catering',
                'package_type' => PackageType::PerGuest,
                'description' => 'Breakfast, lunch, tea, and water per guest.',
                'min_guests' => 10,
                'max_guests' => 400,
                'status' => PackageStatus::Active,
                'pricing_method' => PackagePricingMethod::Calculated,
            ],
        );
        $this->syncPackageItems($cateringPg, [
            [$products['breakfast'], 1, true],
            [$products['lunch'], 1, true],
            [$products['tea'], 1, true],
            [$products['water'], 1, true],
        ]);

        $fixed = Package::query()->updateOrCreate(
            ['code' => 'DEMO-WEDDING-FIXED'],
            [
                'name' => 'Intimate Wedding Reception (Fixed)',
                'category' => 'Wedding',
                'package_type' => PackageType::Global,
                'description' => 'Fixed-price demo package for garden receptions.',
                'min_guests' => 50,
                'max_guests' => 150,
                'status' => PackageStatus::Active,
                'pricing_method' => PackagePricingMethod::Fixed,
                'fixed_price' => 350000,
                'default_terms' => 'Includes pavilion hire and basic décor.',
            ],
        );
        $this->syncPackageItems($fixed, [
            [$products['decor'], 1, true],
            [$products['signage'], 1, false],
        ]);

        return compact('confDay', 'avGlobal', 'cateringPg', 'fixed');
    }

    /**
     * @param  list<array{0: Product, 1: int, 2: bool}>  $items
     */
    private function syncPackageItems(Package $package, array $items): void
    {
        foreach ($items as $i => [$product, $qty, $mandatory]) {
            PackageItem::query()->updateOrCreate(
                ['package_id' => $package->id, 'product_id' => $product->id],
                [
                    'default_quantity' => $qty,
                    'is_mandatory' => $mandatory,
                    'is_optional' => ! $mandatory,
                    'is_complimentary' => false,
                    'sort_order' => $i + 1,
                ],
            );
        }
    }

    private function seedSpaceBlock(Space $garden): void
    {
        $start = $this->local(now()->addDays(21)->setTime(8, 0));
        $end = $this->local(now()->addDays(22)->setTime(18, 0));

        SpaceBlock::query()->updateOrCreate(
            [
                'space_id' => $garden->id,
                'reason' => 'DEMO: Lawn irrigation & lighting maintenance',
            ],
            [
                'block_type' => SpaceBlockType::Maintenance,
                'starts_at' => $start->copy()->utc(),
                'ends_at' => $end->copy()->utc(),
                'created_by' => $this->admin?->id,
            ],
        );
    }

    /**
     * @param  array<string, Customer>  $customers
     * @param  array{grand: Space, board: Space, garden: Space}  $spaces
     * @param  array<string, Product>  $products
     * @param  array{confDay: Package, avGlobal: Package, cateringPg: Package, fixed: Package}  $packages
     */
    private function seedScenarioDeck(array $customers, array $spaces, array $products, array $packages): void
    {
        $eventType = EventType::query()->first();

        // 1) Today — confirmed + issued invoice + partial payment (dashboard “today”)
        $todayStart = $this->local(now()->setTime(9, 0));
        $todayEnd = $this->local(now()->setTime(17, 0));
        $todayBooking = $this->upsertBooking('BKG-DEMO-TODAY', [
            'customer' => $customers['acme'],
            'event_name' => 'Acme Q3 Kick-off (Today)',
            'event_type_id' => $eventType?->id,
            'start' => $todayStart,
            'end' => $todayEnd,
            'guests' => 60,
            'status' => BookingStatus::InProgress,
            'space' => $spaces['grand'],
            'layout' => 'Theatre',
            'space_price' => 85000,
            'reservation_status' => ReservationStatus::Confirmed,
            'items' => [
                $this->lineFromProduct($products['lunch'], 60),
                $this->lineFromProduct($products['av'], 60),
            ],
        ]);
        $todayInv = $this->upsertInvoice('inv-DEMO-TODAY-0001', $todayBooking, InvoiceStatus::PartiallyPaid, [
            'invoice_date' => now()->subDays(3)->toDateString(),
            'due_date' => now()->addDays(7)->toDateString(),
        ]);
        $this->upsertPayment('RCT-DEMO-TODAY-001', $todayInv, round((float) $todayInv->grand_total * 0.4, 2), PaymentMethod::BankTransfer);

        // 2) Upcoming — confirmed + fully paid invoice
        $upStart = $this->local(now()->addDays(7)->setTime(9, 0));
        $upEnd = $this->local(now()->addDays(7)->setTime(17, 0));
        $upcoming = $this->upsertBooking('BKG-DEMO-UPCOMING', [
            'customer' => $customers['green'],
            'event_name' => 'Green Horizons Partners Forum',
            'event_type_id' => $eventType?->id,
            'start' => $upStart,
            'end' => $upEnd,
            'guests' => 80,
            'status' => BookingStatus::Confirmed,
            'space' => $spaces['grand'],
            'layout' => 'Banquet',
            'space_price' => 85000,
            'reservation_status' => ReservationStatus::Confirmed,
            'package_id' => $packages['confDay']->id,
            'items' => [
                $this->lineFromProduct($products['venue'], 80),
                $this->lineFromProduct($products['lunch'], 80),
                $this->lineFromProduct($products['tea'], 80),
            ],
        ]);
        $upInv = $this->upsertInvoice('inv-DEMO-UPCOMING-0001', $upcoming, InvoiceStatus::Paid, [
            'invoice_date' => now()->subDays(10)->toDateString(),
            'due_date' => now()->subDays(2)->toDateString(),
        ]);
        $this->upsertPayment('RCT-DEMO-UPCOMING-001', $upInv, (float) $upInv->grand_total, PaymentMethod::MobileMoney);

        // 3) Past completed + paid
        $pastStart = $this->local(now()->subDays(14)->setTime(9, 0));
        $pastEnd = $this->local(now()->subDays(14)->setTime(16, 0));
        $past = $this->upsertBooking('BKG-DEMO-PAST', [
            'customer' => $customers['ministry'],
            'event_name' => 'Trade Officers Capacity Workshop',
            'event_type_id' => $eventType?->id,
            'start' => $pastStart,
            'end' => $pastEnd,
            'guests' => 12,
            'status' => BookingStatus::Completed,
            'space' => $spaces['board'],
            'layout' => 'Boardroom',
            'space_price' => 18000,
            'reservation_status' => ReservationStatus::Confirmed,
            'items' => [
                $this->lineFromProduct($products['board_hire'], 12),
                $this->lineFromProduct($products['lunch'], 12),
                $this->lineFromProduct($products['water'], 12),
            ],
        ]);
        $pastInv = $this->upsertInvoice('inv-DEMO-PAST-0001', $past, InvoiceStatus::Paid, [
            'invoice_date' => now()->subDays(20)->toDateString(),
            'due_date' => now()->subDays(10)->toDateString(),
        ]);
        $this->upsertPayment('RCT-DEMO-PAST-001', $pastInv, (float) $pastInv->grand_total, PaymentMethod::Cheque);

        // 4) Provisional hold (active hold, no invoice yet)
        $holdStart = $this->local(now()->addDays(4)->setTime(10, 0));
        $holdEnd = $this->local(now()->addDays(4)->setTime(14, 0));
        $this->upsertBooking('BKG-DEMO-HOLD', [
            'customer' => $customers['planner'],
            'event_name' => 'Celebrate — Client site visit hold',
            'event_type_id' => $eventType?->id,
            'start' => $holdStart,
            'end' => $holdEnd,
            'guests' => 40,
            'status' => BookingStatus::Provisional,
            'space' => $spaces['garden'],
            'layout' => 'Cocktail',
            'space_price' => 30000,
            'reservation_status' => ReservationStatus::Provisional,
            'hold_expires_at' => now()->addDays(2),
            'items' => [
                $this->lineFromProduct($products['decor'], 40),
            ],
        ]);

        // 5) Enquiry + draft quotation (custom package)
        $enqStart = $this->local(now()->addDays(30)->setTime(9, 0));
        $enqEnd = $this->local(now()->addDays(30)->setTime(18, 0));
        $enquiry = $this->upsertBooking('BKG-DEMO-ENQUIRY', [
            'customer' => $customers['mary'],
            'event_name' => 'Achieng Family Reunion Dinner',
            'event_type_id' => $eventType?->id,
            'start' => $enqStart,
            'end' => $enqEnd,
            'guests' => 90,
            'status' => BookingStatus::Enquiry,
            'space' => $spaces['grand'],
            'layout' => 'Banquet',
            'space_price' => 85000,
            'reservation_status' => ReservationStatus::Provisional,
            'hold_expires_at' => now()->addDays(5),
            'items' => [
                $this->lineFromProduct($products['venue'], 90),
                $this->lineFromProduct($products['lunch'], 90),
                $this->lineFromProduct($products['decor'], 90),
            ],
        ]);
        $this->upsertQuotation('quo-DEMO-DRAFT-0001', $enquiry, QuotationStatus::Draft, [
            'quotation_date' => now()->toDateString(),
            'expires_at' => now()->addDays(14)->toDateString(),
        ]);

        // 6) Sent quotation (board day)
        $sentStart = $this->local(now()->addDays(12)->setTime(8, 30));
        $sentEnd = $this->local(now()->addDays(12)->setTime(17, 0));
        $sentBooking = $this->upsertBooking('BKG-DEMO-QUOTE-SENT', [
            'customer' => $customers['school'],
            'event_name' => 'Ridgeview Staff Development Day',
            'event_type_id' => $eventType?->id,
            'start' => $sentStart,
            'end' => $sentEnd,
            'guests' => 14,
            'status' => BookingStatus::AwaitingDeposit,
            'space' => $spaces['board'],
            'layout' => 'U-Shape',
            'space_price' => 18000,
            'reservation_status' => ReservationStatus::Provisional,
            'hold_expires_at' => now()->addDays(3),
            'package_id' => $packages['avGlobal']->id,
            'items' => [
                $this->lineFromProduct($products['board_hire'], 14),
                $this->lineFromProduct($products['av'], 14),
                $this->lineFromProduct($products['signage'], 14),
                $this->lineFromProduct($products['lunch'], 14),
            ],
        ]);
        $this->upsertQuotation('quo-DEMO-SENT-0001', $sentBooking, QuotationStatus::Sent, [
            'quotation_date' => now()->subDays(1)->toDateString(),
            'expires_at' => now()->addDays(10)->toDateString(),
            'issued_at' => now()->subDays(1),
        ]);

        // 7) Accepted quotation ready to convert
        $accStart = $this->local(now()->addDays(18)->setTime(9, 0));
        $accEnd = $this->local(now()->addDays(18)->setTime(17, 0));
        $acceptedBooking = $this->upsertBooking('BKG-DEMO-QUOTE-OK', [
            'customer' => $customers['parish'],
            'event_name' => 'Parish Harvest Thanksgiving Luncheon',
            'event_type_id' => $eventType?->id,
            'start' => $accStart,
            'end' => $accEnd,
            'guests' => 120,
            'status' => BookingStatus::AwaitingApproval,
            'space' => $spaces['grand'],
            'layout' => 'Banquet',
            'space_price' => 85000,
            'reservation_status' => ReservationStatus::Provisional,
            'package_id' => $packages['cateringPg']->id,
            'items' => [
                $this->lineFromProduct($products['venue'], 120),
                $this->lineFromProduct($products['breakfast'], 120),
                $this->lineFromProduct($products['lunch'], 120),
                $this->lineFromProduct($products['tea'], 120),
                $this->lineFromProduct($products['water'], 120),
            ],
        ]);
        $this->upsertQuotation('quo-DEMO-ACCEPTED-0001', $acceptedBooking, QuotationStatus::Accepted, [
            'quotation_date' => now()->subDays(5)->toDateString(),
            'expires_at' => now()->addDays(20)->toDateString(),
            'issued_at' => now()->subDays(5),
            'accepted_at' => now()->subDay(),
        ]);

        // 8) Converted quotation + matching invoice (numbering pattern)
        $convStart = $this->local(now()->addDays(25)->setTime(11, 0));
        $convEnd = $this->local(now()->addDays(25)->setTime(22, 0));
        $convertedBooking = $this->upsertBooking('BKG-DEMO-CONVERTED', [
            'customer' => $customers['planner'],
            'event_name' => 'Mwangi–Otieno Garden Cocktail',
            'event_type_id' => $eventType?->id,
            'start' => $convStart,
            'end' => $convEnd,
            'guests' => 100,
            'status' => BookingStatus::Confirmed,
            'space' => $spaces['garden'],
            'layout' => 'Cocktail',
            'space_price' => 55000,
            'reservation_status' => ReservationStatus::Confirmed,
            'package_id' => $packages['fixed']->id,
            'items' => [
                [
                    'description' => 'Intimate Wedding Reception (Fixed)',
                    'quantity' => 1,
                    'unit_price' => 350000,
                    'charge_basis' => ChargeBasis::FixedPackage,
                    'guest_count' => 100,
                    'billable_days' => 1,
                    'billable_hours' => 11,
                    'discount_amount' => 0,
                    'tax_rate' => 16,
                    'product_id' => null,
                    'package_id' => $packages['fixed']->id,
                    'sort_order' => 1,
                ],
            ],
        ]);
        $convertedQuote = $this->upsertQuotation('quo-DEMO-CONV-0001', $convertedBooking, QuotationStatus::Converted, [
            'quotation_date' => now()->subDays(12)->toDateString(),
            'expires_at' => now()->addDays(5)->toDateString(),
            'issued_at' => now()->subDays(12),
            'accepted_at' => now()->subDays(8),
        ]);
        $convInv = $this->upsertInvoice('inv-quo-DEMO-CONV-0001', $convertedBooking, InvoiceStatus::Issued, [
            'invoice_date' => now()->subDays(7)->toDateString(),
            'due_date' => now()->addDays(14)->toDateString(),
            'quotation_id' => $convertedQuote->id,
        ]);
        $convertedQuote->update(['converted_invoice_id' => $convInv->id]);

        // 9) Overdue unpaid invoice (past due, still outstanding)
        $overStart = $this->local(now()->subDays(5)->setTime(9, 0));
        $overEnd = $this->local(now()->subDays(5)->setTime(13, 0));
        $overdueBooking = $this->upsertBooking('BKG-DEMO-OVERDUE', [
            'customer' => $customers['acme'],
            'event_name' => 'Acme Interim Board Briefing',
            'event_type_id' => $eventType?->id,
            'start' => $overStart,
            'end' => $overEnd,
            'guests' => 8,
            'status' => BookingStatus::Completed,
            'space' => $spaces['board'],
            'layout' => 'Boardroom',
            'space_price' => 10000,
            'reservation_status' => ReservationStatus::Confirmed,
            'items' => [
                $this->lineFromProduct($products['board_hire'], 8, ['unit_price' => 10000]),
                $this->lineFromProduct($products['av'], 8),
            ],
        ]);
        $this->upsertInvoice('inv-DEMO-OVERDUE-0001', $overdueBooking, InvoiceStatus::Overdue, [
            'invoice_date' => now()->subDays(20)->toDateString(),
            'due_date' => now()->subDays(5)->toDateString(),
        ]);

        // 10) Cancelled booking (calendar / lists)
        $cancelStart = $this->local(now()->addDays(9)->setTime(9, 0));
        $cancelEnd = $this->local(now()->addDays(9)->setTime(12, 0));
        $this->upsertBooking('BKG-DEMO-CANCELLED', [
            'customer' => $customers['green'],
            'event_name' => 'Cancelled — Media training (demo)',
            'event_type_id' => $eventType?->id,
            'start' => $cancelStart,
            'end' => $cancelEnd,
            'guests' => 25,
            'status' => BookingStatus::Cancelled,
            'space' => $spaces['grand'],
            'layout' => 'Classroom',
            'space_price' => 45000,
            'reservation_status' => ReservationStatus::Cancelled,
            'items' => [],
        ]);

        // 11) Rejected / expired quotations for status filters
        $rejStart = $this->local(now()->addDays(40)->setTime(10, 0));
        $rejEnd = $this->local(now()->addDays(40)->setTime(16, 0));
        $rejBooking = $this->upsertBooking('BKG-DEMO-REJECTED', [
            'customer' => $customers['mary'],
            'event_name' => 'Rejected quote scenario',
            'event_type_id' => $eventType?->id,
            'start' => $rejStart,
            'end' => $rejEnd,
            'guests' => 50,
            'status' => BookingStatus::Cancelled,
            'space' => $spaces['garden'],
            'layout' => 'Cocktail',
            'space_price' => 55000,
            'reservation_status' => ReservationStatus::Cancelled,
            'items' => [
                $this->lineFromProduct($products['decor'], 50),
            ],
        ]);
        $this->upsertQuotation('quo-DEMO-REJECTED-0001', $rejBooking, QuotationStatus::Rejected, [
            'quotation_date' => now()->subDays(15)->toDateString(),
            'expires_at' => now()->subDays(1)->toDateString(),
            'issued_at' => now()->subDays(15),
        ]);

        $expStart = $this->local(now()->addDays(45)->setTime(9, 0));
        $expEnd = $this->local(now()->addDays(45)->setTime(15, 0));
        $expBooking = $this->upsertBooking('BKG-DEMO-EXPIRED', [
            'customer' => $customers['school'],
            'event_name' => 'Expired quote scenario',
            'event_type_id' => $eventType?->id,
            'start' => $expStart,
            'end' => $expEnd,
            'guests' => 20,
            'status' => BookingStatus::Enquiry,
            'space' => $spaces['board'],
            'layout' => 'Boardroom',
            'space_price' => 18000,
            'reservation_status' => ReservationStatus::Expired,
            'items' => [
                $this->lineFromProduct($products['board_hire'], 20),
            ],
        ]);
        $this->upsertQuotation('quo-DEMO-EXPIRED-0001', $expBooking, QuotationStatus::Expired, [
            'quotation_date' => now()->subDays(30)->toDateString(),
            'expires_at' => now()->subDays(3)->toDateString(),
            'issued_at' => now()->subDays(30),
        ]);

        // 12) Draft invoice (not yet issued)
        $draftStart = $this->local(now()->addDays(15)->setTime(9, 0));
        $draftEnd = $this->local(now()->addDays(15)->setTime(17, 0));
        $draftBooking = $this->upsertBooking('BKG-DEMO-INV-DRAFT', [
            'customer' => $customers['ministry'],
            'event_name' => 'Draft invoice — policy review',
            'event_type_id' => $eventType?->id,
            'start' => $draftStart,
            'end' => $draftEnd,
            'guests' => 10,
            'status' => BookingStatus::Confirmed,
            'space' => $spaces['board'],
            'layout' => 'U-Shape',
            'space_price' => 18000,
            'reservation_status' => ReservationStatus::Confirmed,
            'items' => [
                $this->lineFromProduct($products['board_hire'], 10),
                $this->lineFromProduct($products['av'], 10),
                $this->lineFromProduct($products['lunch'], 10),
            ],
        ]);
        $this->upsertInvoice('inv-DEMO-DRAFT-0001', $draftBooking, InvoiceStatus::Draft, [
            'invoice_date' => now()->toDateString(),
            'due_date' => now()->addDays(21)->toDateString(),
            'issued_at' => null,
        ]);
    }

    /**
     * @param  array{
     *   customer: Customer,
     *   event_name: string,
     *   event_type_id: int|null,
     *   start: Carbon,
     *   end: Carbon,
     *   guests: int,
     *   status: BookingStatus,
     *   space: Space,
     *   layout: string,
     *   space_price: float|int,
     *   reservation_status: ReservationStatus,
     *   hold_expires_at?: Carbon|null,
     *   package_id?: int|null,
     *   items: list<array<string, mixed>>
     * }  $data
     */
    private function upsertBooking(string $number, array $data): Booking
    {
        $start = $data['start']->copy()->utc();
        $end = $data['end']->copy()->utc();
        $setup = $start->copy()->subHour();
        $breakdown = $end->copy()->addHour();
        $days = $this->pricing->calculateBillableDays($data['start'], $data['end'], $this->tz);
        $hours = max(1, (float) $data['start']->diffInHours($data['end']));

        $spaceLine = [
            'unit_price' => $data['space_price'],
            'quantity' => 1,
            'guest_count' => $data['guests'],
            'billable_days' => $days,
            'billable_hours' => $hours,
            'discount_amount' => 0,
            'tax_rate' => 16,
            'charge_basis' => ChargeBasis::OncePerEvent,
        ];
        $spaceCalc = $this->pricing->calculateLineAmount($spaceLine);

        $itemLines = [];
        foreach ($data['items'] as $item) {
            $item['billable_days'] ??= $days;
            $item['billable_hours'] ??= $hours;
            $item['guest_count'] ??= $data['guests'];
            $calc = $this->pricing->calculateLineAmount($item);
            $itemLines[] = array_merge($item, [
                'tax_amount' => $calc['tax'],
                'line_total' => $calc['total'],
                'discount_amount' => $calc['discount'],
            ]);
        }

        $docLines = array_merge(
            [[
                'unit_price' => $data['space_price'],
                'quantity' => 1,
                'guest_count' => $data['guests'],
                'billable_days' => $days,
                'billable_hours' => $hours,
                'discount_amount' => 0,
                'tax_rate' => 16,
                'charge_basis' => ChargeBasis::OncePerEvent,
            ]],
            array_map(static fn (array $i): array => [
                'unit_price' => $i['unit_price'],
                'quantity' => $i['quantity'] ?? 1,
                'guest_count' => $i['guest_count'] ?? $data['guests'],
                'billable_days' => $i['billable_days'] ?? $days,
                'billable_hours' => $i['billable_hours'] ?? $hours,
                'discount_amount' => $i['discount_amount'] ?? 0,
                'tax_rate' => $i['tax_rate'] ?? 16,
                'charge_basis' => $i['charge_basis'],
                'fixed_amount' => $i['unit_price'] ?? 0,
            ], $data['items']),
        );
        $totals = $this->pricing->calculateDocumentTotals($docLines);

        $booking = Booking::query()->updateOrCreate(
            ['booking_number' => $number],
            [
                'customer_id' => $data['customer']->id,
                'contact_name' => $data['customer']->contact_person ?: $data['customer']->name,
                'contact_phone' => $data['customer']->phone,
                'contact_email' => $data['customer']->email,
                'event_name' => $data['event_name'],
                'event_type_id' => $data['event_type_id'],
                'description' => 'Demo booking — '.$data['event_name'],
                'event_start' => $start,
                'event_end' => $end,
                'guest_count' => $data['guests'],
                'billable_days' => $days,
                'billable_hours' => $hours,
                'coordinator_id' => $this->admin?->id,
                'status' => $data['status'],
                'subtotal' => $totals['subtotal'],
                'discount_amount' => $totals['discount'],
                'service_charge_amount' => $totals['service_charge'],
                'tax_amount' => $totals['tax'],
                'grand_total' => $totals['grand_total'],
                'amount_paid' => 0,
                'outstanding_balance' => $totals['grand_total'],
            ],
        );

        BookingSpace::query()->updateOrCreate(
            ['booking_id' => $booking->id, 'space_id' => $data['space']->id],
            [
                'setup_start' => $setup,
                'event_start' => $start,
                'event_end' => $end,
                'breakdown_end' => $breakdown,
                'seating_layout' => $data['layout'],
                'guest_count' => $data['guests'],
                'unit_price' => $data['space_price'],
                'charge_basis' => ChargeBasis::OncePerEvent,
                'billable_days' => $days,
                'billable_hours' => $hours,
                'line_total' => $spaceCalc['total'],
                'reservation_status' => $data['reservation_status'],
                'hold_expires_at' => $data['hold_expires_at'] ?? null,
            ],
        );

        BookingItem::query()->where('booking_id', $booking->id)->delete();
        foreach ($itemLines as $i => $item) {
            BookingItem::query()->create([
                'booking_id' => $booking->id,
                'product_id' => $item['product_id'] ?? null,
                'package_id' => $item['package_id'] ?? ($data['package_id'] ?? null),
                'description' => $item['description'],
                'quantity' => $item['quantity'] ?? 1,
                'unit_price' => $item['unit_price'],
                'charge_basis' => $item['charge_basis'],
                'guest_count' => $item['guest_count'] ?? $data['guests'],
                'billable_days' => $item['billable_days'] ?? $days,
                'billable_hours' => $item['billable_hours'] ?? $hours,
                'discount_amount' => $item['discount_amount'] ?? 0,
                'tax_rate' => $item['tax_rate'] ?? 16,
                'tax_amount' => $item['tax_amount'],
                'line_total' => $item['line_total'],
                'sort_order' => $item['sort_order'] ?? ($i + 1),
                'is_complimentary' => false,
            ]);
        }

        return $booking->fresh(['items', 'spaces', 'customer']) ?? $booking;
    }

    /**
     * @param  array<string, mixed>  $overrides
     */
    private function upsertQuotation(string $number, Booking $booking, QuotationStatus $status, array $overrides = []): Quotation
    {
        $items = $booking->items->map(fn (BookingItem $item): array => [
            'description' => $item->description,
            'quantity' => $item->quantity,
            'unit_price' => $item->unit_price,
            'charge_basis' => $item->charge_basis,
            'guest_count' => $item->guest_count,
            'billable_days' => $item->billable_days,
            'billable_hours' => $item->billable_hours,
            'discount_amount' => $item->discount_amount,
            'tax_rate' => $item->tax_rate,
            'tax_amount' => $item->tax_amount,
            'line_total' => $item->line_total,
            'product_id' => $item->product_id,
            'package_id' => $item->package_id,
            'sort_order' => $item->sort_order,
        ])->all();

        foreach ($booking->spaces as $space) {
            array_unshift($items, [
                'description' => 'Space hire — '.($space->space?->name ?? 'Venue'),
                'quantity' => 1,
                'unit_price' => $space->unit_price,
                'charge_basis' => $space->charge_basis,
                'guest_count' => $space->guest_count,
                'billable_days' => $space->billable_days,
                'billable_hours' => $space->billable_hours,
                'discount_amount' => 0,
                'tax_rate' => 16,
                'tax_amount' => 0,
                'line_total' => $space->line_total,
                'product_id' => null,
                'package_id' => null,
                'sort_order' => 0,
            ]);
        }

        $pricingLines = array_map(static fn (array $i): array => [
            'unit_price' => $i['unit_price'],
            'quantity' => $i['quantity'] ?? 1,
            'guest_count' => $i['guest_count'] ?? 1,
            'billable_days' => $i['billable_days'] ?? 1,
            'billable_hours' => $i['billable_hours'] ?? 1,
            'discount_amount' => $i['discount_amount'] ?? 0,
            'tax_rate' => $i['tax_rate'] ?? 16,
            'charge_basis' => $i['charge_basis'],
            'fixed_amount' => $i['unit_price'] ?? 0,
        ], $items);
        $totals = $this->pricing->calculateDocumentTotals($pricingLines);

        $quotation = Quotation::query()->updateOrCreate(
            ['quotation_number' => $number],
            array_merge([
                'version' => 1,
                'quotation_date' => now()->toDateString(),
                'expires_at' => now()->addDays(14)->toDateString(),
                'customer_id' => $booking->customer_id,
                'booking_id' => $booking->id,
                'customer_snapshot' => json_encode([
                    'name' => $booking->customer?->name,
                    'email' => $booking->customer?->email,
                    'phone' => $booking->customer?->phone,
                ]),
                'event_snapshot' => json_encode([
                    'event_name' => $booking->event_name,
                    'event_start' => $booking->event_start?->toIso8601String(),
                    'event_end' => $booking->event_end?->toIso8601String(),
                    'guest_count' => $booking->guest_count,
                ]),
                'spaces_snapshot' => json_encode($booking->spaces->map(fn ($s) => [
                    'space_id' => $s->space_id,
                    'layout' => $s->seating_layout,
                ])->values()->all()),
                'guest_count' => $booking->guest_count,
                'currency' => 'KES',
                'subtotal' => $totals['subtotal'],
                'discount_amount' => $totals['discount'],
                'service_charge_amount' => $totals['service_charge'],
                'tax_amount' => $totals['tax'],
                'grand_total' => $totals['grand_total'],
                'deposit_required' => round($totals['grand_total'] * 0.5, 2),
                'payment_terms' => '50% deposit on acceptance',
                'notes' => 'Demo quotation',
                'terms_and_conditions' => 'Demo terms and conditions only.',
                'status' => $status,
                'issued_at' => null,
                'accepted_at' => null,
                'created_by' => $this->admin?->id,
            ], $overrides),
        );

        QuotationItem::query()->where('quotation_id', $quotation->id)->delete();
        foreach ($items as $i => $item) {
            $calc = $this->pricing->calculateLineAmount([
                'unit_price' => $item['unit_price'],
                'quantity' => $item['quantity'] ?? 1,
                'guest_count' => $item['guest_count'] ?? 1,
                'billable_days' => $item['billable_days'] ?? 1,
                'billable_hours' => $item['billable_hours'] ?? 1,
                'discount_amount' => $item['discount_amount'] ?? 0,
                'tax_rate' => $item['tax_rate'] ?? 16,
                'charge_basis' => $item['charge_basis'],
                'fixed_amount' => $item['unit_price'] ?? 0,
            ]);
            QuotationItem::query()->create([
                'quotation_id' => $quotation->id,
                'description' => $item['description'],
                'quantity' => $item['quantity'] ?? 1,
                'unit_price' => $item['unit_price'],
                'charge_basis' => $item['charge_basis'],
                'guest_count' => $item['guest_count'] ?? 1,
                'billable_days' => $item['billable_days'] ?? 1,
                'billable_hours' => $item['billable_hours'] ?? 1,
                'discount_amount' => $calc['discount'],
                'tax_rate' => $item['tax_rate'] ?? 16,
                'tax_amount' => $calc['tax'],
                'line_total' => $calc['total'],
                'product_id' => $item['product_id'] ?? null,
                'package_id' => $item['package_id'] ?? null,
                'sort_order' => $item['sort_order'] ?? $i,
            ]);
        }

        return $quotation->fresh() ?? $quotation;
    }

    /**
     * @param  array<string, mixed>  $overrides
     */
    private function upsertInvoice(string $number, Booking $booking, InvoiceStatus $status, array $overrides = []): Invoice
    {
        $booking->loadMissing(['items', 'spaces.space', 'customer']);

        $items = $booking->items->map(fn (BookingItem $item): array => [
            'description' => $item->description,
            'quantity' => $item->quantity,
            'unit_price' => $item->unit_price,
            'charge_basis' => $item->charge_basis,
            'guest_count' => $item->guest_count,
            'billable_days' => $item->billable_days,
            'billable_hours' => $item->billable_hours,
            'discount_amount' => $item->discount_amount,
            'tax_rate' => $item->tax_rate,
            'product_id' => $item->product_id,
            'package_id' => $item->package_id,
            'sort_order' => $item->sort_order,
        ])->all();

        foreach ($booking->spaces as $space) {
            array_unshift($items, [
                'description' => 'Space hire — '.($space->space?->name ?? 'Venue'),
                'quantity' => 1,
                'unit_price' => $space->unit_price,
                'charge_basis' => $space->charge_basis,
                'guest_count' => $space->guest_count,
                'billable_days' => $space->billable_days,
                'billable_hours' => $space->billable_hours,
                'discount_amount' => 0,
                'tax_rate' => 16,
                'product_id' => null,
                'package_id' => null,
                'sort_order' => 0,
            ]);
        }

        $pricingLines = array_map(static fn (array $i): array => [
            'unit_price' => $i['unit_price'],
            'quantity' => $i['quantity'] ?? 1,
            'guest_count' => $i['guest_count'] ?? 1,
            'billable_days' => $i['billable_days'] ?? 1,
            'billable_hours' => $i['billable_hours'] ?? 1,
            'discount_amount' => $i['discount_amount'] ?? 0,
            'tax_rate' => $i['tax_rate'] ?? 16,
            'charge_basis' => $i['charge_basis'],
            'fixed_amount' => $i['unit_price'] ?? 0,
        ], $items);
        $totals = $this->pricing->calculateDocumentTotals($pricingLines);

        $issuedAt = array_key_exists('issued_at', $overrides)
            ? $overrides['issued_at']
            : ($status === InvoiceStatus::Draft ? null : now()->subDays(2));
        unset($overrides['issued_at']);

        $invoice = Invoice::query()->updateOrCreate(
            ['invoice_number' => $number],
            array_merge([
                'invoice_date' => now()->toDateString(),
                'due_date' => now()->addDays(14)->toDateString(),
                'quotation_id' => null,
                'booking_id' => $booking->id,
                'customer_id' => $booking->customer_id,
                'customer_snapshot' => json_encode([
                    'name' => $booking->customer?->name,
                    'email' => $booking->customer?->email,
                ]),
                'event_snapshot' => json_encode([
                    'event_name' => $booking->event_name,
                    'guest_count' => $booking->guest_count,
                ]),
                'spaces_snapshot' => json_encode($booking->spaces->pluck('space_id')),
                'guest_count' => $booking->guest_count,
                'currency' => 'KES',
                'subtotal' => $totals['subtotal'],
                'discount_amount' => $totals['discount'],
                'service_charge_amount' => $totals['service_charge'],
                'tax_amount' => $totals['tax'],
                'grand_total' => $totals['grand_total'],
                'deposit_required' => round($totals['grand_total'] * 0.5, 2),
                'amount_paid' => 0,
                'outstanding_balance' => $totals['grand_total'],
                'payment_terms' => 'Net 14',
                'notes' => 'Demo invoice',
                'terms_and_conditions' => 'Demo invoice terms.',
                'status' => $status,
                'issued_at' => $issuedAt,
                'created_by' => $this->finance?->id ?? $this->admin?->id,
            ], $overrides),
        );

        InvoiceItem::query()->where('invoice_id', $invoice->id)->delete();
        foreach ($items as $i => $item) {
            $calc = $this->pricing->calculateLineAmount([
                'unit_price' => $item['unit_price'],
                'quantity' => $item['quantity'] ?? 1,
                'guest_count' => $item['guest_count'] ?? 1,
                'billable_days' => $item['billable_days'] ?? 1,
                'billable_hours' => $item['billable_hours'] ?? 1,
                'discount_amount' => $item['discount_amount'] ?? 0,
                'tax_rate' => $item['tax_rate'] ?? 16,
                'charge_basis' => $item['charge_basis'],
                'fixed_amount' => $item['unit_price'] ?? 0,
            ]);
            InvoiceItem::query()->create([
                'invoice_id' => $invoice->id,
                'description' => $item['description'],
                'quantity' => $item['quantity'] ?? 1,
                'unit_price' => $item['unit_price'],
                'charge_basis' => $item['charge_basis'],
                'guest_count' => $item['guest_count'] ?? 1,
                'billable_days' => $item['billable_days'] ?? 1,
                'billable_hours' => $item['billable_hours'] ?? 1,
                'discount_amount' => $calc['discount'],
                'tax_rate' => $item['tax_rate'] ?? 16,
                'tax_amount' => $calc['tax'],
                'line_total' => $calc['total'],
                'product_id' => $item['product_id'] ?? null,
                'package_id' => $item['package_id'] ?? null,
                'sort_order' => $item['sort_order'] ?? $i,
            ]);
        }

        $this->syncBookingBalances($booking, $invoice);

        return $invoice->fresh() ?? $invoice;
    }

    private function upsertPayment(string $number, Invoice $invoice, float $amount, PaymentMethod $method): Payment
    {
        $payment = Payment::query()->updateOrCreate(
            ['payment_number' => $number],
            [
                'invoice_id' => $invoice->id,
                'booking_id' => $invoice->booking_id,
                'customer_id' => $invoice->customer_id,
                'payment_date' => now()->subDay()->toDateString(),
                'amount' => $amount,
                'payment_method' => $method,
                'transaction_reference' => 'DEMO-TXN-'.substr(md5($number), 0, 8),
                'received_by' => $this->finance?->id ?? $this->admin?->id,
                'notes' => 'Demo payment',
                'status' => PaymentStatus::Posted,
            ],
        );

        $paid = (float) Payment::query()
            ->where('invoice_id', $invoice->id)
            ->where('status', PaymentStatus::Posted)
            ->sum('amount');

        $grand = (float) $invoice->grand_total;
        $outstanding = max(0, round($grand - $paid, 2));
        $status = match (true) {
            $outstanding <= 0.009 => InvoiceStatus::Paid,
            $paid > 0 => InvoiceStatus::PartiallyPaid,
            default => $invoice->status,
        };

        if ($invoice->status === InvoiceStatus::Overdue && $outstanding > 0) {
            $status = InvoiceStatus::Overdue;
        }

        $invoice->update([
            'amount_paid' => round($paid, 2),
            'outstanding_balance' => $outstanding,
            'status' => $status,
        ]);

        if ($invoice->booking_id) {
            $booking = Booking::query()->find($invoice->booking_id);
            if ($booking !== null) {
                $this->syncBookingBalances($booking, $invoice->fresh() ?? $invoice);
            }
        }

        return $payment;
    }

    private function syncBookingBalances(Booking $booking, Invoice $invoice): void
    {
        if (! in_array($invoice->status, [
            InvoiceStatus::Issued,
            InvoiceStatus::PartiallyPaid,
            InvoiceStatus::Paid,
            InvoiceStatus::Overdue,
        ], true)) {
            return;
        }

        $paid = (float) Invoice::query()
            ->where('booking_id', $booking->id)
            ->whereIn('status', [
                InvoiceStatus::Issued->value,
                InvoiceStatus::PartiallyPaid->value,
                InvoiceStatus::Paid->value,
                InvoiceStatus::Overdue->value,
            ])
            ->sum('amount_paid');

        $outstanding = (float) Invoice::query()
            ->where('booking_id', $booking->id)
            ->whereIn('status', [
                InvoiceStatus::Issued->value,
                InvoiceStatus::PartiallyPaid->value,
                InvoiceStatus::Paid->value,
                InvoiceStatus::Overdue->value,
            ])
            ->sum('outstanding_balance');

        $booking->update([
            'amount_paid' => round($paid, 2),
            'outstanding_balance' => round($outstanding, 2),
            'grand_total' => max((float) $booking->grand_total, (float) $invoice->grand_total),
        ]);
    }

    /**
     * @param  array<string, mixed>  $overrides
     * @return array<string, mixed>
     */
    private function lineFromProduct(Product $product, int $guests, array $overrides = []): array
    {
        return array_merge([
            'description' => $product->name,
            'quantity' => 1,
            'unit_price' => (float) $product->default_price,
            'charge_basis' => $product->default_charge_basis,
            'guest_count' => $guests,
            'billable_days' => 1,
            'billable_hours' => 8,
            'discount_amount' => 0,
            'tax_rate' => (float) ($product->default_tax_rate ?? 16),
            'product_id' => $product->id,
            'package_id' => null,
            'sort_order' => 1,
        ], $overrides);
    }

    private function seedNotifications(): void
    {
        if ($this->admin === null) {
            return;
        }

        $samples = [
            [
                'type' => 'booking.provisional_hold',
                'title' => 'Provisional hold active',
                'body' => 'DEMO hold on Sunset Garden Pavilion expires soon.',
            ],
            [
                'type' => 'invoice.overdue',
                'title' => 'Overdue invoice',
                'body' => 'inv-DEMO-OVERDUE-0001 is past due — review collections.',
            ],
            [
                'type' => 'quotation.accepted',
                'title' => 'Quotation accepted',
                'body' => 'quo-DEMO-ACCEPTED-0001 is ready to convert to an invoice.',
            ],
        ];

        foreach ($samples as $i => $sample) {
            Notification::query()->updateOrCreate(
                [
                    'user_id' => $this->admin->id,
                    'type' => $sample['type'],
                    'title' => $sample['title'],
                ],
                [
                    'body' => $sample['body'],
                    'related_type' => null,
                    'related_id' => null,
                    'data' => json_encode(['demo' => true]),
                    'read_at' => $i === 2 ? now() : null,
                    'created_at' => now()->subHours($i + 1),
                ],
            );
        }
    }

    private function local(Carbon $dt): Carbon
    {
        return $dt->copy()->timezone($this->tz);
    }
}
