<?php

namespace helpers;

use classes\xlvask;
use Exception;
use objects\departments_o;
use objects\orders_o;
use objects\users_o;
use objects\xlvask_potential_order_matches_o;

class xlvask_usage_log extends xlvask_helper
{
    /**
     * Example of a usage log object:
     * [WashId] => 8cb0b5a7-ab4b-4dd5-b805-869733c23e54
     * [CustomerId] => 59440200
     * [Customer] => DITOBUS EXCURSIONS A/S
     * [VatNumber] => 31171520
     * [Location] => Hvidovre
     * [Hall] => Hvidovre_1
     * [HallId] => 845d29a1-a7d2-4e3b-bbc3-2b13242d744a
     * [StartTime] => 2025-06-02T15:20:14.950
     * [FinishTime] => 2025-06-02T15:25:55.377
     * [RegistrationNumber] => CZ92915
     * [VehicleType] => Bus/autocamper, M
     * [IdentificationType] => LPR
     * [IdentificationId] => CZ92915
     * [Info] => CZ92915
     * [Updated] =>
     * [Prepaid] =>
     * [FinishStatus] => 1
     * [CustomerGuid] => 21ba156a-b2d2-44be-8398-4b67d66003d6
     * [VehicleId] => e6679209-e52f-4376-9660-d56cb46f523b
     * [WashItems] => Array[washItem] => Array
     */
    public static array $default_customers = [
        'CTW',
        'Personalebiler Truckwash ', // The space at the end is intentional
        'XL Gønge Transport ApS', // This is a test customer
    ];

    /**
     * The wash ID of the usage log entry
     * @var int|string|null $WashId
     */
    public int|string|null $WashId;
    /**
     * The ID of the customer (XLVask ID)
     * @var int|string|null $CustomerId
     */
    public int|string|null $CustomerId;
    /**
     * The name of the customer
     * @var int|string|null $Customer
     */
    public int|string|null $Customer;
    /**
     * The VAT number of the customer
     * @var int|string|null $VatNumber
     */
    public int|string|null $VatNumber;
    /**
     * The location of the wash
     * @var int|string|null $Location
     */
    public int|string|null $Location;
    /**
     * The hall where the wash took place
     * @var int|string|null $Hall
     */
    public int|string|null $Hall;
    /**
     * The ID of the hall where the wash took place
     * @var int|string|null $HallId
     */
    public int|string|null $HallId;
    /**
     * The start time of the wash
     * @var string|int|null $StartTime
     */
    public string|int|null $StartTime;
    /**
     * The finish time of the wash
     * @var string|int|null $FinishTime
     */
    public string|int|null $FinishTime;
    /**
     * The registration number of the vehicle
     * @var int|string|null $RegistrationNumber
     */
    public int|string|null $RegistrationNumber;
    /**
     * The type of the vehicle
     * @var string|int|null $VehicleType
     */
    public string|int|null $VehicleType;
    /**
     * The type of identification used for the wash
     * @var string|int|null $IdentificationType
     */
    public string|int|null $IdentificationType;
    /**
     * The ID of the identification used for the wash
     * @var string|int|null $IdentificationId
     */
    public string|int|null $IdentificationId;
    /**
     * Additional information about the wash
     * @var string|int|null $Info
     */
    public string|int|null $Info;
    /**
     * The last updated time of the wash entry
     * @var string|int|null $Updated
     */
    public string|int|null $Updated;
    /**
     * Indicates if the wash was prepaid
     * @var int|string|null $Prepaid
     */
    public int|string|null $Prepaid;
    /**
     * The finish status of the wash
     * @var int|string|null $FinishStatus
     */
    public int|string|null $FinishStatus;
    /**
     * The GUID of the customer in XL Vask
     * @var string|int|null $CustomerGuid
     */
    public string|int|null $CustomerGuid;
    /**
     * The ID of the vehicle associated with the wash
     * @var string|int|null $vehicleId
     */
    public string|int|null $VehicleId;
    /**
     * The wash items associated with the wash
     * @var array[washItem] $washItems
     * @see xlvask_wash_item
     */
    public array $WashItems;

    private string $default_string = 'DEFAULT_STRING_1';
    private string $default_int = 'DEFAULT_INT_1';
    private string $default_bool = 'DEFAULT_BOOL_1';
    private string $default_int_nullable = 'DEFAULT_INT_NULLABLE_1';
    private string $default_string_nullable = 'DEFAULT_STRING_NULLABLE_1';
    private string $default_bool_nullable = 'DEFAULT_BOOL_NULLABLE_1';

    /**
     * Constructor to initialize the customer object with default values
     * @throws Exception
     */
    public function __construct(array $data = [])
    {
        $this->default_string = 'DEFAULT_STRING_1';
        $this->default_int = 'DEFAULT_INT_1';
        $this->default_bool = 'DEFAULT_BOOL_1';
        $this->default_int_nullable = 'DEFAULT_INT_NULLABLE_1';
        $this->default_string_nullable = 'DEFAULT_STRING_NULLABLE_1';
        $this->default_bool_nullable = 'DEFAULT_BOOL_NULLABLE_1';
        // Initialize the customer object with default values
        $this::reset();
        $this::setProperties($data);
    }

    /**
     * Reset this object to its default values.
     * This method clears all properties of the usage log object,
     * effectively resetting it to a clean state.
     * @return void
     */
    private function reset(): void
    {
        $this->WashId = $this->default_string;
        $this->CustomerId = $this->default_string;
        $this->Customer = $this->default_string;
        $this->VatNumber = $this->default_string;
        $this->Location = $this->default_string;
        $this->Hall = $this->default_string;
        $this->HallId = $this->default_string;
        $this->StartTime = $this->default_string;
        $this->FinishTime = $this->default_string;
        $this->RegistrationNumber = $this->default_string;
        $this->VehicleType = $this->default_string;
        $this->IdentificationType = $this->default_string;
        $this->IdentificationId = $this->default_string;
        $this->Info = $this->default_string;
        $this->Updated = $this->default_string_nullable;
        $this->Prepaid = $this->default_bool;
        $this->FinishStatus = $this->default_int;
        $this->CustomerGuid = $this->default_string;
        $this->VehicleId = $this->default_string;
        $this->WashItems = []; // Initialize as an empty array
    }

    /**
     * Set the properties of the object based on the provided data.
     * @note This method DOES NOT reset the object before setting the properties.
     * @param array $data
     * @return self
     * @throws Exception
     * @see reset()
     */
    public function setProperties(array $data): self
    {
        $default_property_values = [
            'WashId' => $this->default_string,
            'CustomerId' => $this->default_string,
            'Customer' => $this->default_string,
            'VatNumber' => $this->default_string,
            'Location' => $this->default_string,
            'Hall' => $this->default_string,
            'HallId' => $this->default_string,
            'StartTime' => $this->default_string,
            'FinishTime' => $this->default_string,
            'RegistrationNumber' => $this->default_string,
            'VehicleType' => $this->default_string,
            'IdentificationType' => $this->default_string,
            'IdentificationId' => $this->default_string,
            'Info' => $this->default_string,
            'Updated' => $this->default_string_nullable,
            'Prepaid' => $this->default_bool,
            'FinishStatus' => $this->default_int,
            'CustomerGuid' => $this->default_string,
            'VehicleId' => $this->default_string,
        ];
        foreach ( $data as $key => $value ) {
            if (property_exists(self::class, $key)) {
                switch ($default_property_values[$key] ?? null) {
                    case $this->default_string:
                    case $this->default_string_nullable:
                        $nullable = $default_property_values[$key] === $this->default_string_nullable;
                        // Check if the value is a string or can be converted to a string
                        if (is_string($value) || is_numeric($value)) {
                            $value = (string)$value; // Ensure the value is a string
                        } elseif ($nullable) {
                            $value = null; // If nullable, set to null
                        } else {
                            $value = ''; // If not nullable, set to empty string
                        }
                        break;
                    case $this->default_int:
                    case $this->default_int_nullable:
                        $nullable = $default_property_values[$key] === $this->default_int_nullable;
                        // Check if the value is numeric and convert it to an integer or null
                        $value = is_numeric($value) ? (int)$value : ($nullable ? null : 0); // Ensure the value is an integer or null
                        break;
                    case $this->default_bool:
                    case $this->default_bool_nullable:
                        $nullable = $default_property_values[$key] === $this->default_bool_nullable;
                        // Check if the value is a boolean or can be converted to a boolean
                        $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
                        if ($value === null && !$nullable) {
                            $value = false; // If the value is not a boolean and not nullable, set it to false
                        }
                        break;
                    case null:
                        break;
                    default:
                        // If the value is not a default string, keep it as is
                        echo "No conversion needed for property '$key' with value '$value'." . PHP_EOL;
                        break;
                }
                // Set the property value
                $this->{$key} = $value;
            } else {
                // If the property does not exist, throw an exception
                // Debug print all properties of the class
                //$properties = get_class_vars(self::class);
                //echo "Available properties in " . self::class . ": " . implode(', ', array_keys($properties)) . PHP_EOL;
                throw new Exception("Property '$key' does not exist in " . self::class);
            }
        }
        // If the washItems property is set, ensure it is an array
        if (isset($data['WashItems']) && is_array($data['WashItems'])) {
            //echo 'Setting WashItems with ' . count($this->WashItems) . ' items.' . PHP_EOL;
            $this->WashItems = self::generateWashItems($data['WashItems']);
            //print_r($this->WashItems);
        } else {
            //echo 'No washItems provided or not an array. Initializing as empty array.' . PHP_EOL;
            //print_r($data);
        }
        // After setting all properties, nullify nullable properties
        $this->unsetNullifiableProperties();
        // Return the populated object
        return $this;
    }

    /**
     * Get the user associated with this usage log.
     * This method retrieves the user object based on the CustomerId (externId).
     * If the CustomerId is not set or the user cannot be found, it returns null.
     * @return users_o|null
     */
    public function getUser(): ?users_o
    {
        if (empty($this->CustomerId)) {
            return null;
        }
        try {
            $user = new users_o();
            $user->getUserByCustomerNumber((int)$this->CustomerId);
            return $user;
        } catch (Exception $e) {
            // Handle the exception if needed
            return null;
        }
    }

    /**
     * Generate wash items from the provided array.
     * This method takes an array of wash items and converts them into instances of xlvask_wash_item.
     * @throws Exception
     */
    private static function generateWashItems(array $washItems): array
    {
        // Generate wash items from the provided array
        $generatedItems = [];
        foreach ( $washItems as $item ) {
            if (is_array($item)) {
                $generatedItems[] = new xlvask_wash_item($item);
            } elseif ($item instanceof xlvask_wash_item) {
                $generatedItems[] = $item; // If it's already an instance, add it
            } else {
                throw new Exception("Invalid wash item type: " . gettype($item));
            }
        }
        // Sort the generated items with the sortWashItems method
        return self::sortWashItems($generatedItems);
    }

    private static function sortWashItems(array $generatedItems): array
    {
        // Make sure the primary wash item is always first
        $primaryItems = [
            'Stor bil',
            'Lille bil',
            'Vask udført',
            'Hænger',
        ];
        usort($generatedItems, function ($a, $b) use ($primaryItems) {
            // Check if the item is a primary item
            $aIsPrimary = in_array($a->OriginalProductName, $primaryItems, true);
            $bIsPrimary = in_array($b->OriginalProductName, $primaryItems, true);
            // If both are primary items, keep their order
            if ($aIsPrimary && $bIsPrimary) {
                return 0;
            }
            // If only one is a primary item, it should come first
            return $aIsPrimary ? -1 : 1;
        });
        // Return the sorted array of wash items
        return $generatedItems;
    }

    private function unsetNullifiableProperties(): void
    {
        // Unset properties that are null or empty strings
        $properties = [
            'WashId', 'CustomerId', 'Customer', 'VatNumber', 'Location',
            'Hall', 'HallId', 'StartTime', 'FinishTime', 'RegistrationNumber',
            'VehicleType', 'IdentificationType', 'IdentificationId', 'Info',
            'Updated', 'Prepaid', 'FinishStatus', 'CustomerGuid', 'VehicleId'
        ];
        foreach ( $properties as $property ) {
            if ($this->isEmptyOrDefault($this->{$property})) {
                $tmp_value = $this->{$property};
                if ($tmp_value === $this->default_string || $tmp_value === $this->default_string_nullable) {
                    $this->{$property} = ''; // Set to null if it matches the default string
                } elseif ($tmp_value === $this->default_int || $tmp_value === $this->default_int_nullable) {
                    if ($tmp_value === $this->default_int_nullable) {
                        $this->{$property} = null; // Set to null if it matches the default int nullable
                    } else {
                        $this->{$property} = 0; // Set to 0 if it matches the default int
                    }
                } elseif ($tmp_value === $this->default_bool || $tmp_value === $this->default_bool_nullable) {
                    $this->{$property} = false; // Set to null if it matches the default bool
                } else {
                    $this->{$property} = null; // Otherwise, set to null
                }
            }
        }
    }

    private function isEmptyOrDefault($param): bool
    {
        // Check if the parameter is empty or matches the default values
        return $param === null || $param === '' || $param === $this->default_string || $param === $this->default_int || $param === $this->default_bool || $param === $this->default_int_nullable || $param === $this->default_string_nullable || $param === $this->default_bool_nullable;
    }


    /**
     * Get the formatted date of the wash start time.
     * @return string The formatted date of the wash start time. YYYY-MM-DD HH:MM:SS format.
     * @note Returns an empty string if StartTime is not set.
     */
    public function getFormattedDate(): string
    {
        // Format the date in a human-readable format
        if ($this->StartTime) {
            return date('Y-m-d H:i:s', strtotime($this->StartTime));
        }
        return '';
    }

    /**
     * Get the formatted finish date of the wash.
     * @return string The formatted finish date of the wash. YYYY-MM-DD HH:MM:SS format.
     * @note Returns an empty string if FinishTime is not set.
     */
    public function getFormattedFinishDate(): string
    {
        // Format the finish date in a human-readable format
        if ($this->FinishTime) {
            return date('Y-m-d H:i:s', strtotime($this->FinishTime));
        }
        return '';
    }

    /**
     * Get the primary wash item from the usage log.
     * This method returns the first wash item in the WashItems array as the primary item.
     * @throws Exception
     */
    public function getPrimaryItem(): ?xlvask_wash_item
    {
        // Return the first wash item as the primary item
        if (isset($this->WashItems[0]) && $this->WashItems[0] instanceof xlvask_wash_item) {
            $allowed_primary_items = [
                'Stor bil',
                'Lille bil',
                'Vask udført',
                'Hænger',
            ];
            // Check if the first item is an allowed primary item
            if (in_array($this->WashItems[0]->OriginalProductName, $allowed_primary_items, true)) {
                // If it is, return it
                return $this->WashItems[0];
            }
            //echo "Returning primary wash item: " . $this->WashItems[0]->OriginalProductName . PHP_EOL;
            //echo "Available items: " . implode(', ', array_map(fn($item) => $item->OriginalProductName, $this->WashItems)) . PHP_EOL;
            return null; // There's no actual primary wash item in the usage log
        }
        throw new Exception("No primary wash item found in the usage log.");
    }

    /**
     * Is the wash linked to an order?
     * @return bool
     * @note This method checks if an order exists with the "wash_id" field matching the WashId of this usage log.
     * @throws Exception
     */
    public function isLinkedToOrder(): bool
    {
        // Check if the wash is linked to an order by checking the "wash_id" field in the orders table
        $order = (new orders_o())->selectByWashId($this->WashId);
        return !empty($order);
    }

    /**
     * Get the order associated with this wash.
     * @return orders_o|null
     * @throws Exception
     */
    public function getOrder(): ?orders_o
    {
        // Get the order associated with this wash by checking the "wash_id" field in the orders table
        $order = (new orders_o())->selectByWashId($this->WashId);
        if ($order) {
            return $order;
        }
        return null; // Return null if no order is found
    }

    /**
     * Get the department associated with this wash.
     * @throws Exception If the department cannot be determined.
     */
    public function getDepartment(): ?departments_o
    {
        // Since the department / hall XLVASK API endpoints are not available,
        // We will try to get the department from the hall name.
        // These are formatted as "DepartmentName_HallNumber" (E.g. "Hvidovre_1").
        if (!empty($this->Hall)) {
            $parts = explode('_', $this->Hall);
            if (count($parts) > 1) {
                $departmentName = $parts[0];
                // If there's a capitalized letter in the department name, add a space before it (e.g. "AarhusC" -> "Aarhus C")
                $departmentName = preg_replace('/([a-z])([A-Z])/', '$1 $2', $departmentName);
                // Remove any trailing underscores or spaces
                $departmentName = trim($departmentName, '_ ');
                return (new departments_o())->selectByName($departmentName);
            }
        }
        // If the hall is not set or does not contain a department name, return null
        return null;
    }

    public function getLane(): ?int
    {
        // Since the department / hall XLVASK API endpoints are not available,
        // We will try to get the lane from the hall name.
        // These are formatted as "DepartmentName_HallNumber" (E.g. "Hvidovre_1").
        if (!empty($this->Hall)) {
            $parts = explode('_', $this->Hall);
            if (count($parts) > 1 && is_numeric($parts[1])) {
                return (int)$parts[1]; // Return the lane number as an integer
            }
        }
        // If the hall is not set or does not contain a lane number, return null
        return null;
    }

    /**
     * @throws Exception
     */
    public function getCustomer(): xlvask_customer
    {
        $xlvask = new xlvask();
        $matches = $xlvask->getCustomers(
            $this->CustomerId,
        );
        if (isset($matches[0]) && $matches[0] instanceof xlvask_customer) {
            return $matches[0];
        } else {
            throw new Exception("No customer found with ID: " . $this->CustomerId);
        }
    }

    /**
     * @throws Exception
     */
    public function getPotentialOrder(): ?orders_o
    {
        // To prevent duplicate orders, we will check if an order that looks like it belongs to this wash already exists.
        // There will be false positives, but we will try to minimize them.
        // Usually, vehicles aren't washed more than once a day, so we will check for orders with the same license plate and within a 24-hour period.
        $date = [
            'start' => date('Y-m-d H:i:s', strtotime($this->StartTime) - 86400), // 24 hours before the start time
            'end' => date('Y-m-d H:i:s', strtotime($this->FinishTime) + 86400), // 24 hours after the finish time
        ];
        //echo 'Looking for potential order for vehicle ' . $this->RegistrationNumber . ' between ' . $date['start'] . ' and ' . $date['end'] . PHP_EOL;
        $order = (new orders_o())->selectByRegistrationNumberAndDateRange(
            $this->RegistrationNumber,
            $date['start'],
            $date['end'],
        );
        if ($order) {
            // Make sure the order duplicate is not ignored
            if (!(new xlvask_potential_order_matches_o())->shouldIgnoreDuplicate($this->WashId)) {
                // If an order is found, return it
                return $order;
            }
        }
        // If no order is found, return null
        return null;
    }

    public function getTimestampStart(): string
    {
        // Return the start time as a timestamp
        return date('Y-m-d H:i:s', strtotime($this->StartTime));
    }

    public function isEligibleForAutomaticContinuance(bool $allowPrepaid = false): bool
    {
        // Check if the wash is eligible for automatic continuance
        // This can be based on various criteria, such as the finish status or other business rules
        return
            $this->isCompleted() &&  // Ensure the wash is completed
            $this->hasBillableCustomer() && // Ensure the wash has a billable customer
            (
                $this->getTotalPrice() > 0 || $allowPrepaid
                // Allow washes with a total price greater than 0 or allow prepaid washes if $allowPrepaid is true
            ) &&
            (
                !$this->isPrepaid() || $allowPrepaid
            ); // Allow prepaid washes if $allowPrepaid is true)
    }

    /**
     * Check if the wash is completed.
     * @note This method checks the FinishStatus property to determine if the wash is completed.
     * * A wash is considered completed if the FinishStatus is set to 1.
     * @return bool
     */
    public function isCompleted(): bool
    {
        // Check if the wash is completed based on the FinishStatus
        return $this->FinishStatus === 1; // Assuming 1 indicates a completed wash
    }

    public function hasBillableCustomer(): bool
    {
        // Check if the wash has a non-default customer with a valid external ID
        return !$this->hasDefaultCustomer() && $this->hasExternalId();
    }

    public function hasDefaultCustomer(): bool
    {
        // Check if the customer is a default customer (e.g., "Unknown" or similar)
        return in_array($this->Customer, $this::$default_customers, true);
    }

    public function hasExternalId(): bool
    {
        // Check if the wash has an external ID (e.g., CustomerId)
        return !empty($this->CustomerId) && $this->CustomerId !== '0';
    }

    /**
     * Get the total price of the wash.
     * This method calculates the total price of the wash by summing up the prices of all wash items.
     * @return float The total price of the wash.
     * @throws Exception If any of the wash items do not have a valid price.
     */
    public function getTotalPrice(): float
    {
        // Calculate the total price of the wash by summing up the prices of all wash items
        $totalPrice = 0.0;
        foreach ( $this->WashItems as $item ) {
            /** @var xlvask_wash_item $item */
            if (isset($item->PriceIncVat) && is_numeric($item->PriceIncVat)) {
                // The reason why we use the PriceIncVat is that it is the final price, and the price without VAT is not always available / correct.
                $totalPrice += ((float)$item->PriceIncVat - (float)$item->Vat);
            } else {
                //print_r($item);
                throw new Exception("Invalid price for wash item: " . json_encode($item));
            }
        }
        return $totalPrice;
    }

    public function isPrepaid(): bool
    {
        // Check if the wash is prepaid
        return !empty($this->Prepaid) && $this->Prepaid === 1; // Assuming 1 indicates a prepaid wash
    }
}