<?php

namespace objects;

use classes\db;
use classes\email;
use classes\gatewayapi;
use classes\object_property;
use classes\pdf_generator;
use classes\response;
use classes\slack;
use classes\wash_certificate_store;
use classes\wordpress_bookings_remote;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
use traits\db_object_t;

class bookings_o extends db
{
    use db_object_t;

    public object_property $customer_number;
    public object_property $wash_type;
    public object_property $contact_email;
    public object_property $reference_number;
    public object_property $regNrTraekker;
    public object_property $regNrTrailer;
    public object_property $washCertificateEmail;
    public object_property $date;
    public object_property $department;
    public object_property $pickup_bool;
    public object_property $notes;
    public object_property $washCertificateStatus;
    public object_property $washCertificateUrl;
    public object_property $wash_certificate_pdf;
    public object_property $status;
    public object_property $data;
    public object_property $virtual_cart;

    public function structure(): void
    {
        $this->setTable('bookings');
    }

    public function objectChanged(): void
    {
        // Clear the cache
        redis->clear_department_booking_count($this->department->value());
    }

    public function getCustomerBookingsPaginated(int $customer_number, int $page = 1, int $limit = 10, array $order = ['id' => 'DESC'], string $search = null, array $filters = null): array
    {
        global /** @var response $response */
        $db, $response;
        // Add the customer number to the filters
        $filters['customer_number'] = $customer_number;
        // List the objects with pagination
        $array = $this->listObjectsWithPagination($page, $limit, $search, $filters, $order);
        // Get the total number of objects
        $total = $this->getTotalObjects($search, $filters);
        $response->paginate($page, $limit, $total);
        return $array;
    }

    public function getDepartmentBookingsUnfulfilledCount(int $department_id): int
    {
        global /** @var response $response */
        $db, $response;
        // Check if the count is cached
        if (redis->get_department_booking_count($department_id)) {
            $response->add_meta('cached', true);
            return redis->get_department_booking_count($department_id);
        }
        $sql = "SELECT COUNT(*) as count FROM $this->table WHERE department = $department_id AND status = 'pending'";
        $result = $db->query($sql);
        $row = $db->fetch_assoc($result);
        // Cache the count
        redis->cache_department_booking_count($department_id, $row['count']);
        return $row['count'];
    }

    public function delete(int $id): void
    {
        $this->id = $id;
        $this->getObjectProperties();
        // Set the status to cancelled
        $this->status->set('cancelled');
        // Remove the cache
        redis->clear_department_booking_count($this->department->value());
    }

    public function getObjectProperties(): void
    {
        $this->customer_number = new object_property($this->table, $this->id, 'customer_number', 'int', true);
        $this->wash_type = new object_property($this->table, $this->id, 'wash_type', 'string', true);
        $this->contact_email = new object_property($this->table, $this->id, 'contact_email', 'string', true);
        $this->reference_number = new object_property($this->table, $this->id, 'reference_number', 'string', true);
        $this->regNrTraekker = new object_property($this->table, $this->id, 'regNrTraekker', 'string', true);
        $this->regNrTrailer = new object_property($this->table, $this->id, 'regNrTrailer', 'string', true);
        $this->washCertificateEmail = new object_property($this->table, $this->id, 'washCertificateEmail', 'string', true);
        $this->date = new object_property($this->table, $this->id, 'date', 'string', true);
        $this->department = new object_property($this->table, $this->id, 'department', 'int', true);
        $this->pickup_bool = new object_property($this->table, $this->id, 'pickup_bool', 'int', true);
        $this->notes = new object_property($this->table, $this->id, 'notes', 'string', true);
        $this->washCertificateStatus = new object_property($this->table, $this->id, 'washCertificateStatus', 'string', true);
        $this->washCertificateUrl = new object_property($this->table, $this->id, 'washCertificateUrl', 'string', true);
        $this->wash_certificate_pdf = new object_property($this->table, $this->id, 'wash_certificate_pdf', 'string', true);
        $this->status = new object_property($this->table, $this->id, 'status', 'string', true);
        $this->data = new object_property($this->table, $this->id, 'data', 'string', true);
        $this->virtual_cart = new object_property($this->table, $this->id, 'virtual_cart', 'int', false);
    }

    public function parseBookings(array $listObjectsWithPaginationIfSet): array
    {
        // Parse the customer numbers
        $bookings = $this->parseCustomerNumbers($listObjectsWithPaginationIfSet);
        return $bookings;
    }

    public function parseCustomerNumbers(array $listObjectsWithPaginationIfSet): array
    {
        // Parse the customer numbers
        foreach ( $listObjectsWithPaginationIfSet as $key => $value ) {
            $listObjectsWithPaginationIfSet[$key]['customer_name'] = (new users_o())->getCustomerName($value['customer_number']);
        }
        return $listObjectsWithPaginationIfSet;

    }

    public function syncBookings(): array
    {
        global /** @var response $response */
        $db, $response;
        $wordpress_bookings_remote = new wordpress_bookings_remote();
        // Get all the bookings from the remote API
        $bookings = $wordpress_bookings_remote->get_all_bookings()['data']['all_wash_bookings'];
        // Parse the bookings
        $parsed_bookings = $wordpress_bookings_remote->parse_bookings($bookings);
        // Remove the cancelled bookings from the $parsed_bookings array
        $cancelled_bookings = $this->getAllCancelledOrCompletedBookings();
        foreach ( $cancelled_bookings as $cancelled_booking ) {
            // Remove the cancelled booking from the parsed bookings
            foreach ( $parsed_bookings as $key => $parsed_booking ) {
                if ((int)$parsed_booking['id'] === (int)$cancelled_booking['id']) {
                    unset($parsed_bookings[$key]);
                }
            }
        }
        // Sync the bookings
        foreach ( $parsed_bookings as $booking ) {
            // Add or update the booking
            $this->addOrUpdate(
                $booking['id'],
                $booking['customer_number'],
                $booking['wash_type'],
                $booking['contact_email'],
                $booking['reference_number'],
                $booking['regNrTraekker'],
                $booking['regNrTrailer'],
                $booking['washCertificateEmail'],
                $booking['date'],
                $booking['department'],
                $booking['pickup_bool'] ? 1 : 0,
                $booking['notes'],
                $booking['washCertificateStatus'],
                $booking['washCertificateUrl'],
                $booking['status']
            );
        }
        return [
            "bookings" => count($bookings),
            "cancelled" => count($cancelled_bookings),
            "parsed" => count($parsed_bookings),
        ];
    }

    public function getAllCancelledOrCompletedBookings(): array
    {
        global $db;
        // Get all the cancelled bookings
        $sql = "SELECT * FROM $this->table WHERE status = 'cancelled' OR status = 'completed'";
        $result = $db->query($sql);
        return $db->fetch_all($result);
    }

    public function addOrUpdate(int $id, $customer_number, $wash_type, $contact_email, $reference_number, $regNrTraekker, $regNrTrailer, $washCertificateEmail, $date, $department, $pickup_bool, $notes, $washCertificateStatus, $washCertificateUrl, $status): void
    {
        global $db;
        // Avoid SQL injection
        $wash_type = $db->escape_string($wash_type);
        $contact_email = $db->escape_string($contact_email);
        $reference_number = $db->escape_string($reference_number);
        $regNrTraekker = $db->escape_string($regNrTraekker);
        $regNrTrailer = $db->escape_string($regNrTrailer);
        $washCertificateEmail = $db->escape_string($washCertificateEmail);
        $date = $db->escape_string($date);
        // Parse the department name to id
        $department = $this->getDepartmentIdByLegacyName($department);
        $notes = $db->escape_string($notes);
        $washCertificateStatus = $db->escape_string($washCertificateStatus);
        $washCertificateUrl = $db->escape_string($washCertificateUrl);
        $status = $db->escape_string($status);
        // Check if the entry already exists
        $sql = "SELECT * FROM $this->table WHERE id = $id";
        $result = $db->query($sql);
        if ($db->num_rows($result) === 0) {
            // Send a department webhook if the booking is new
            $slack = new slack();
            try {
                $slack->send_department_booking_notification($department, $slack->format_new_booking(
                    $id,
                    $customer_number,
                    $wash_type,
                    $contact_email,
                    $reference_number,
                    $regNrTraekker,
                    $regNrTrailer,
                    $washCertificateEmail,
                    $date,
                    $department,
                    $pickup_bool,
                    $notes,
                    $washCertificateStatus,
                    $washCertificateUrl,
                    $status
                ));
            } catch (Exception $e) {
                // Log the error
                $logs = new logs_o();
                $logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage());
            }
        }
        // Create a new record in the database ( Replace the code, if an entry already exists )
        $sql = "INSERT INTO $this->table (id, customer_number, wash_type, contact_email, reference_number, regNrTraekker, regNrTrailer, washCertificateEmail, date, department, pickup_bool, notes, washCertificateStatus, washCertificateUrl, status) VALUES ($id, $customer_number, '$wash_type', '$contact_email', '$reference_number', '$regNrTraekker', '$regNrTrailer', '$washCertificateEmail', '$date', '$department', $pickup_bool, '$notes', '$washCertificateStatus', '$washCertificateUrl', '$status') ON DUPLICATE KEY UPDATE customer_number = $customer_number, wash_type = '$wash_type', contact_email = '$contact_email', reference_number = '$reference_number', regNrTraekker = '$regNrTraekker', regNrTrailer = '$regNrTrailer', washCertificateEmail = '$washCertificateEmail', date = '$date', department = '$department', pickup_bool = $pickup_bool, notes = '$notes', washCertificateStatus = '$washCertificateStatus', washCertificateUrl = '$washCertificateUrl', status = '$status'";
        $db->query($sql);
        // Clear the cache
        redis->clear_department_booking_count($department);
    }

    public function getDepartmentIdByLegacyName(string $departmentName): int
    {
        // Get the department id by the legacy name
        $departmentLegacyNames = [
            'køge' => 4,
            'taastrup' => 2,
            'aarhusc' => 5,
            'roskilde' => 6,
            'hvidovre' => 1,
            'glostrup' => 3,
            'taulov' => 7,
        ];
        return $departmentLegacyNames[strtolower($departmentName)] ?? 0;
    }

    /**
     * Add a new booking
     * @throws Exception If the object could not be created
     */
    public function add(int $customer_number, string $wash_type, string $contact_email, string $reference_number, string $regNrTraekker, string $regNrTrailer, string $washCertificateEmail, string $date, int $department, int $pickup_bool, string $notes, string $washCertificateStatus, string $washCertificateUrl, string $status, array $data = []): void
    {
        global $db;
        $tmp_id = self::add_object([
            'customer_number' => (int)$db->escape_string($customer_number),
            'wash_type' => (string)$db->escape_string($wash_type),
            'contact_email' => (string)$db->escape_string($contact_email),
            'reference_number' => (string)$db->escape_string($reference_number),
            'regNrTraekker' => (string)$db->escape_string($regNrTraekker),
            'regNrTrailer' => (string)$db->escape_string($regNrTrailer),
            'washCertificateEmail' => (string)$washCertificateEmail ? $db->escape_string($washCertificateEmail) : '',
            'date' => (string)$db->escape_string($date),
            'department' => (int)$db->escape_string($department),
            'pickup_bool' => (int)$db->escape_string($pickup_bool),
            'notes' => (string)$db->escape_string($notes),
            'washCertificateStatus' => (string)$db->escape_string($washCertificateStatus),
            'washCertificateUrl' => (string)$db->escape_string($washCertificateUrl),
            'status' => (string)$db->escape_string($status),
            'data' => json_encode($data),
        ]);
        // Check if the booking was created successfully
        self::select((int)$tmp_id);
        self::requireSelected();
        self::notifyNewBooking();
    }

    /**
     * Notify the department about a new booking
     * @throws Exception If the object is not selected
     * @throws ClientExceptionInterface If the request to the API fails
     */
    public function notifyNewBooking(): void
    {
        self::requireSelected();
        // Get the department from the booking
        $department = (new departments_o())->select((int)$this->department->value());
        if (!$department->exists()) {
            throw new Exception('Department not found');
        }
        // Get the branding from the department
        $branding = (new branding_o())->select((int)$department->branding->value());
        if (!$branding->exists()) {
            throw new Exception('Branding not found');
        }
        // Get the customer from the booking
        $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
        $department_array = $department->asArray();
        $customer_array = $customer->asArray();
        // Define delivery methods
        $deliverSlack = (boolean)!empty($department->slack_webhook->value());
        $deliverSMS = true;
        $deliverEmail = (
            !empty($this->contact_email->value()) &&
            !$deliverSlack // Only send email if slack is not available
        );
        // Check if the department has a slack webhook
        if ($deliverSlack) {
            // Send a notification to the department
            $slack = new slack();
            $slack->send_department_booking_notification($department->id, $slack->format_new_booking(
                $this->id,
                $customer_array['customer_number'],
                self::formatWashTypeFromServices(json_decode($this->data->value(), true)),
                $this->contact_email->value(),
                $this->reference_number->value() ?: '-',
                $this->regNrTraekker->value(),
                $this->regNrTrailer->value(),
                $this->washCertificateEmail->value(),
                $this->date->value(),
                $department->id,
                (bool)$this->pickup_bool->value(),
                $this->notes->value(),
                $this->washCertificateStatus->value(),
                $this->washCertificateUrl->value(),
                $this->status->value()
            ));
        }

        if ($deliverSMS) {
            // Send an SMS notification to the department
            $gatewayapi = new gatewayapi();
            try {
                $gatewayapi->send(
                    [
                        // Add all the phone numbers from the department
                        ...$department->notificationSmsPhoneNumbers()
                    ],
                    'New booking from ' . $customer->getCustomerName($customer_array['customer_number']) . ' (' . $this->id . ')',
                );
            } catch (Exception $e) {
                // Log the error
                $logs = new logs_o();
                $logs->add('gatewayapi', 0, 3, 0, 'SEND_DEPARTMENT_BOOKING_NOTIFICATION', $e->getMessage());
            }
        }

        if ($deliverEmail) {
            // Send an email notification to the department
            $email = new email();
            $email->sendBookingNotification($this->id);
        }
    }

    /**
     * Convert the object to an array
     * @options include_customer_name Include the customer name
     * @options include_department_name Include the department name
     * @options include_parsed_services Include the parsed services
     * @param array $options [@options] The options to include in the array
     * @return array The object as an array
     * @throws Exception If the object is not selected
     */
    public function asArray(array $options = []): array
    {
        self::requireSelected();
        $options = array_merge([
            'include_customer_name' => false,
            'include_department_name' => false,
            'include_parsed_services' => false,
        ], $options);
        $washCertificatePDF = $this->wash_certificate_pdf->value();
        $washCertificateStatus = $this->washCertificateStatus->value();
        $status = $this->status->value();
        // If the wash certificate PDF is not empty, but the status is not 'completed', set the status to 'completed'
        if (!empty($washCertificatePDF) && $status !== 'completed') {
            $this->status->set('completed');
            $status = 'completed';
        }
        if ($washCertificateStatus === 'pending' && $status === 'completed') {
            $this->washCertificateStatus->set('completed');
            $washCertificateStatus = 'completed';
        }
        $tmp_array = [
            'id' => (int)$this->id,
            'customer_number' => (int)$this->customer_number->value(),
            'wash_type' => $this->wash_type->value(),
            'contact_email' => $this->contact_email->value(),
            'reference_number' => $this->reference_number->value(),
            'regNrTraekker' => $this->regNrTraekker->value(),
            'regNrTrailer' => $this->regNrTrailer->value(),
            'washCertificateEmail' => $this->washCertificateEmail->value(),
            'date' => $this->date->value(),
            'department' => (int)$this->department->value(),
            'pickup_bool' => $this->pickup_bool->value() ? true : false,
            'notes' => $this->notes->value(),
            'washCertificateStatus' => $this->washCertificateStatus->value(),
            'washCertificateUrl' => $this->washCertificateUrl->value(),
            'wash_certificate_pdf' => $washCertificatePDF,
            'status' => $status,
            'data' => $this->data->value() ? json_decode($this->data->value(), true) : null,
        ];
        // Include the customer name
        if ($options['include_customer_name']) {
            $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
            $tmp_array['customer_name'] = $customer->getCustomerName($this->customer_number->value());
        }
        // Include the department name
        if ($options['include_department_name']) {
            $department = (new departments_o())->select((int)$this->department->value());
            if ($department->exists()) {
                $tmp_array['department_name'] = $department->name->value();
            }
        }
        // Include the parsed services
        if ($options['include_parsed_services']) {
            // Format the wash type from the services array as followed:
            // 1 => 'Udvendig sættevognstræk ( Trækker / trailer )',
            $tmp_array['parsed_services']['string'] = $this->formatWashTypeFromServices(
                $this->data->value() ? json_decode($this->data->value(), true) : []
            );
            $tmp_array['parsed_services']['array'] = $this->data->value() ? json_decode($this->data->value(), true) : [];
        }
        return $tmp_array;
    }

    /**
     * Format the wash type from the services array
     * @param array $services The services array
     * @return string The formatted wash type
     */
    private function formatWashTypeFromServices(array $services): string
    {
        // Format the wash type from the services array
        $wash_services = self::getWashServicesString(self::getWashServices());
        $string = '';
        foreach ( $services as $service ) {
            if (isset($wash_services[$service])) {
                $string .= $wash_services[$service] . ', ';
            }
        }
        // Remove the last comma and space
        return rtrim($string, ', ');
    }

    private static function getWashServicesString(array $services): array
    {
        // Format the wash services as a string (product1, product2, product3)
        $string = [];
        foreach ( $services as $key => $service ) {
            if (isset($service['overrides']['name'])) {
                $string[$key] = $service['overrides']['name'];
            } else {
                $string[$key] = $service['name'];
            }
        }
        return $string;
    }

    /**
     * @throws Exception
     */
    private function getWashServices(): array
    {
        // Define the product function


        // Get the wash services from the database
        return [
            // Exterior wash "Sættevognstræk"
            1 => self::defineProduct(1, 3, [
                'name' => 'Udvendig sættevognstræk ( Trækker / trailer )',
                'description' => 'Udvendig vask af sættevognstræk',
            ]),
            // Exterior trailer wash "Udvendig trailer vask"
            2 => self::defineProduct(2, 2, [
                'name' => 'Udvendig trailer vask',
                'description' => 'Udvendig vask af trailer',
            ]),
            3 => self::defineProduct(3, 10, [
                'name' => 'Indvendig trailer vask',
                'description' => 'Indvendig vask af trailer',
            ]),
            4 => self::defineProduct(41, 41),
        ];
    }

    /**
     * Define a product for the booking
     * @param int $id
     * @param int $product_id
     * @param array $overrides
     * @param array $options
     * @param array $addons
     * @return array
     * @throws Exception
     * @see getWashServices()
     * @see getWashServicesString()
     */
    private function defineProduct(int $id, int $product_id, array $overrides = [], array $options = [], array $addons = []): array
    {

        // Check if the product exists
        if (!empty($product_id)) {
            $product_obj = (new products_o())->select((int)$product_id);
            if ($product_obj->exists()) {
                $default_options = $product_obj->asArray();
            }
        }

        // Return the product
        return [
            'id' => (int)$id,
            'product_id' => (int)$product_id,
            'addons' => [...$addons],
            ...($default_options ?? []),
            'overrides' => [
                'name' => null,
                'quantity' => null,
                'description' => null,
                'notes' => null,
                'reference' => null,
                ...$overrides,
            ],
            ...$options,
        ];
    }

    public function checkUnfulfilledBookings(): void
    {
        $unfulfilled_bookings = [];
        function addUnfulfilledBooking(int $department, array $arr): array
        {
            // Check if the department has a count in the unfulfilled bookings array
            if (!isset($arr[$department])) {
                $arr[$department] = 0;
            }
            // Add the unfulfilled booking to the department count
            $arr[$department]++;
            return $arr;
        }

        global $db;
        // Get all the unfulfilled bookings
        $bookings = $this->listObjectsWithPagination(1, 100000, null, ['status' => 'pending']);
        // Check if the booking has been fulfilled
        foreach ( $bookings as $booking ) {
            if ($this->isCancelled($booking['id'])) {
                echo "Booking with ID $booking[id] has been cancelled\n";
                continue;
            }
            // Check if the booking is scheduled for the future
            if (strtotime($booking['date']) > time()) {
                continue;
            }
            // Check if the booking has been fulfilled
            $fulfilled = $this->checkBookingFulfilled($booking['id']);
            if (!$fulfilled) {
                $unfulfilled_bookings = addUnfulfilledBooking($booking['department'], $unfulfilled_bookings);
                echo "Booking with ID $booking[id] has not been fulfilled\n";
            }
        }
        // Send a department webhook if the booking has not been fulfilled
        foreach ( $unfulfilled_bookings as $department => $count ) {
            $slack = new slack();
            try {
                $slack->send_department_booking_notification($department, 'There are ' . $count . ' unfulfilled bookings');
            } catch (Exception $e) {
                // Log the error
                $logs = new logs_o();
                $logs->add('slack', 0, 3, 0, 'SEND_DEPARTMENT_UNFULFILLED_BOOKINGS_NOTIFICATION', $e->getMessage());
            }
        }
    }

    private static function isCancelled(int $id): bool
    {
        global $db;
        // Check if the booking has been cancelled
        $sql = "SELECT status FROM bookings WHERE id = $id";
        $result = $db->query($sql);
        $row = $db->fetch_assoc($result);
        return $row['status'] === 'cancelled';
    }

    public function checkBookingFulfilled(int $booking_id): bool
    {
        // Check if the booking has a wash certificate
        $wash_certificate_store = new wash_certificate_store();
        return $wash_certificate_store->washCertificateExists($booking_id);
    }

    public function completeWashWithoutWashCertificate(int $id): void
    {
        global $db;
        // Set the status to completed
        $sql = "UPDATE $this->table SET status = 'completed', washCertificateStatus = 'cancelled' WHERE id = $id";
        $db->query($sql);
    }

    /**
     * Generate a wash certificate
     * @param int|null $safety_seal The safety seal number
     * @param string|null $operator The operator name
     * @throws Exception If the object is not selected
     * @throws Exception If the booking already has a wash certificate
     */
    public function generateWashCertificate(int|null $safety_seal = null, string|null $operator = null): void
    {
        self::requireSelected();
        // Check if the booking already has a wash certificate
        if ($this->hasWashCertificate()) {
            throw new Exception('Booking already has a wash certificate');
        }
        // Generate the wash certificate
        // Get the department from the booking
        $department = (new departments_o())->select((int)$this->department->value());
        if (!$department->exists()) {
            throw new Exception('Department not found');
        }
        // Get the branding from the department
        $branding = (new branding_o())->select((int)$department->branding->value());
        if (!$branding->exists()) {
            throw new Exception('Branding not found');
        }
        // Get the customer from the booking
        $customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
        $department_array = $department->asArray();
        $customer_array = $customer->asArray();
        $booking_array = self::asArray();
        $department_array['branding'] = $branding->asArray();
        //print_r($department_array);
        //print_r($customer_array);
        //print_r($booking_array);
        $pdf_generator = new pdf_generator();
        $pdf_generator->add_html($pdf_generator->templates->getTemplate('wash_certificate')
            ->setCompany([
                'name' => 'Truck Wash',
                'address' => 'Letland Allé 2',
                'zip' => 2630,
                'city' => 'Taastrup',
                'phone_prefix' => 45,
                'phone' => 43717886,
                'email' => 'cph@truckwash.dk',
                'website' => 'www.truckwash.dk',
                'images' => [
                    'logo' => '/truckwash-banner-png.png',
                    'banner' => '/truckwash-banner-png.png',
                    'signature' => '/truckwash-underskrift.png',
                ],
            ])
            ->addData([
                'booking_number' => $this->id,
                'seal_number' => ($safety_seal ?? null),
                'reg_1' => $booking_array['regNrTraekker'],
                'reg_2' => $booking_array['regNrTrailer'],
                'date' => date('d-m-Y'),
                'time' => date('H:i'),
                'carried_out_by' => ($operator ?? null),
                'department_id' => $department->id,
                'department_name' => $department_array['branding']['name'] ?: $department_array['name'],
                'department_address' => $department_array['branding']['address'] ?: $department_array['description'],
                'customer_name' => $customer->getCustomerName($customer_array['customer_number']),
                'customer_address' => $customer->getCustomerEcocomicData($customer_array['customer_number'])->economic_customer->address ?: '-',
                'type' => $booking_array['wash_type'],
            ])
            ->getHtml()
        );
        $pdf_path = $pdf_generator->generate_pdf();
        // Set the PDF in the booking
        $this->wash_certificate_pdf->set($pdf_path);
        // Add the wash certificate to the bookings data (If it does not exist)
        if (!self::hasServiceInBookingData(4)) {
            self::addServiceToBookingData(4);
        }
    }

    /**
     * Check if the booking has a wash certificate
     * @throws Exception If the object is not selected
     */
    public function hasWashCertificate(): bool
    {
        self::requireSelected();
        // Check if the booking has a wash certificate
        if ($this->wash_certificate_pdf->value() !== null) {
            return true;
        }
        return false;
    }

    /**
     * Check if the booking data has a service
     * @param int $service_id The service id
     * @return bool True if the service exists, false otherwise
     * @throws Exception If the object is not selected
     */
    public function hasServiceInBookingData(int $service_id): bool
    {
        self::requireSelected();
        // Get the current data
        $data = $this->data->value() ? json_decode($this->data->value(), true) : [];
        // Check if the service exists
        return in_array($service_id, $data);
    }

    /**
     * Add a service to the booking data
     * @param int $service_id The service id
     * @throws Exception If the object is not selected
     */
    public function addServiceToBookingData(int $service_id): void
    {
        self::requireSelected();
        // Get the current data
        $data = $this->data->value() ? json_decode($this->data->value(), true) : [];
        // Add the service to the data
        $data[] = (int)$service_id;
        // Set the data
        $this->data->set(json_encode($data));
    }

    /**
     * Send the wash certificate to the customer
     * @throws Exception If the object is not selected
     * @throws ClientExceptionInterface
     */
    public function sendWashCertificateToCustomer(): void
    {
        self::requireSelected();
        // Check if the booking has a wash certificate
        if (!$this->hasWashCertificate()) {
            throw new Exception('Booking does not have a wash certificate');
        }
        // Send the wash certificate to the customer
        $email = new email();
        $email->sendWashCertificateEmail(
            $this->id,
            (new users_o())->getCustomerName((int)$this->customer_number->value()),
            $this->contact_email->value(),
            $this->reference_number->value() ?: '-',
            $this->wash_certificate_pdf->value(),
            $this->washCertificateEmail->value(),
        );

    }

    /**
     * @throws Exception
     */
    public function createTransaction(): orders_o
    {
        global $response;
        self::requireSelected();
        // Get the services from the booking
        $services = $this->data->value() ? json_decode($this->data->value(), true) : [];
        // Get the department from the booking
        $department = (new departments_o())->select((int)$this->department->value());
        if (!$department->exists()) {
            throw new Exception('Department not found');
        }
        // Create a new order
        $order = new orders_o();
        $order->add(
            (int)$this->customer_number->value(),
            (int)$this->id,
            (string)$this->reference_number->value(),
            (string)$this->notes->value(),
            (int)$this->department->value(),
            (string)$this->regNrTraekker->value(),
            (string)$this->regNrTrailer->value(),
        );
        // Set the orders booking id
        $order->booking_id->set((int)$this->id);
        // Define the relation order item ids, when applicable.
        $relations = [
            // This is the order item id for the primary product, the wash certificate applies to.
            // This can be left null, if the product does not exist.
            4 => null,
        ];
        // Sort the services, so the primary product is first, and the services with relations are last.
        $services_sorted = [];
        $services_not_relation = [];
        $services_relation = [];
        foreach ( $services as $service ) {
            // Get the service id
            $service_id = (int)$service;
            // Check if the service has the posibility to have a relation (null values are allowed)
            if (in_array($service_id, array_keys($relations))) {
                $services_relation[] = $service_id;
            } else {
                $services_not_relation[] = $service_id;
            }
        }
        // Sort the services, so the primary product is first, and the services with (potential) relations are last.
        $services_sorted = [
            ...$services_not_relation,
            ...$services_relation,
        ];
        // Add the services to the order
        foreach ( $services_sorted as $service_id ) {
            $service = self::getWashServiceProduct((int)$service_id);
            // Add the service to the order
            $tmp_order_item = new order_items_o();
            $tmp_order_item->addItemToOrder(
                $order->id,
                (int)$service->id,
                (int)$response->get_user()->id,
                (int)1,
                $relations[(int)$service_id] ?? null,
            );
            // Check if the service allows for a wash certificate
            if ((int)$service_id === 3) {
                // Set the relation id for the wash certificate
                $relations[4] = (int)$tmp_order_item->id;
            }
        }
        // Return the order
        return $order;
    }

    /**
     * @throws Exception
     */
    private function getWashServiceProduct(int $service_id): products_o
    {
        // Get the product id from the service id
        $services = self::getWashServices();
        if (!isset($services[$service_id])) {
            throw new Exception('Service not found');
        }
        $product_id = $services[$service_id]['product_id'];
        // Get the wash service product from the database
        $product = (new products_o())->select($product_id);
        if (!$product->exists()) {
            throw new Exception('Product not found');
        }
        return $product;
    }

}