<?php

namespace helpers;

use classes\slack;
use Exception;
use objects\products_o;

class xlvask_wash_item extends xlvask_helper
{
    /**
     * Example of a wash item object:
     * [WashItemId] => 0774be08-6777-490f-add2-0ccff70771ee
     * [ExternalProductId] =>
     * [ExternalProductName] =>
     * [OriginalProductName] => Stor bil
     * [Unit] => stk
     * [UnitPrice] => 559
     * [Count] => 1
     * [Discount] => 65
     * [PriceExVat] => 559
     * [Vat] => 48.91
     * [PriceIncVat] => 244.56
     */

    /**
     * The ID of the wash item
     * @var string|int|float|null $WashItemId
     */
    public string|int|float|null $WashItemId;
    /**
     * The external product ID of the wash item
     * @var string|int|float|null $ExternalProductId
     */
    public string|int|float|null $ExternalProductId;
    /**
     * The external product name of the wash item
     * @var string|int|float|null $ExternalProductName
     */
    public string|int|float|null $ExternalProductName;
    /**
     * The original product name of the wash item
     * @var string|int|float|null $OriginalProductName
     */
    public string|int|float|null $OriginalProductName;
    /**
     * The Unit of the wash item
     * @var string|int|float|null $Unit
     */
    public string|int|float|null $Unit;
    /**
     * The Unit price of the wash item
     * @var string|int|float|null $UnitPrice
     */
    public string|int|float|null $UnitPrice;
    /**
     * The count of the wash item
     * @var string|int|float|null $Count
     */
    public string|int|float|null $Count;
    /**
     * The discount applied to the wash item
     * @var string|int|float|null $Discount
     */
    public string|int|float|null $Discount;
    /**
     * The price excluding VAT of the wash item
     * @var string|int|float|null $PriceExVat
     */
    public string|int|float|null $PriceExVat;
    /**
     * The VAT amount of the wash item
     * @var string|int|float|null $Vat
     */
    public string|int|float|null $Vat;
    /**
     * The price including VAT of the wash item
     * @var string|int|float|null $PriceIncVat
     */
    public string|int|float|null $PriceIncVat;

    protected string $default_string = 'DEFAULT_STRING_1';
    protected string $default_int = 'DEFAULT_INT_1';
    protected string $default_bool = 'DEFAULT_BOOL_1';
    protected string $default_int_nullable = 'DEFAULT_INT_NULLABLE_1';
    protected string $default_string_nullable = 'DEFAULT_STRING_NULLABLE_1';
    protected 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->WashItemId = $this->default_string;
        $this->ExternalProductId = $this->default_string;
        $this->ExternalProductName = $this->default_string;
        $this->OriginalProductName = $this->default_string;
        $this->Unit = $this->default_string;
        $this->UnitPrice = $this->default_int; // Assuming Unit price is a float, but using default_int for initialization
        $this->Count = $this->default_int; // Assuming count is an integer
        $this->Discount = $this->default_int; // Assuming discount is a float, but using default_int for initialization
        $this->PriceExVat = $this->default_int; // Assuming priceExVat is a float, but using default_int for initialization
        $this->Vat = $this->default_int; // Assuming vat is a float, but using default_int for initialization
        $this->PriceIncVat = $this->default_int; // Assuming priceIncVat is a float, but using default_int for initialization
    }

    /**
     * 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 = [
            'WashItemId' => $this->default_string,
            'ExternalProductId' => $this->default_string,
            'ExternalProductName' => $this->default_string,
            'OriginalProductName' => $this->default_string,
            'Unit' => $this->default_string,
            'UnitPrice' => $this->default_int, // Assuming Unit price is a float, but using default_int for initialization
            'Count' => $this->default_int, // Assuming count is an integer
            'Discount' => $this->default_int, // Assuming discount is a float, but using default_int for initialization
            'PriceExVat' => $this->default_int, // Assuming priceExVat is a float, but using default_int for initialization
            'Vat' => $this->default_int, // Assuming vat is a float, but using default_int for initialization
            'PriceIncVat' => $this->default_int, // Assuming priceIncVat is a float, but using default_int for initialization
        ];
        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
                throw new Exception("Property '$key' does not exist in " . self::class);
            }
        }
        // After setting all properties, nullify nullable properties
        $this->unsetNullifiableProperties();
        // Return the populated object
        return $this;
    }

    private function unsetNullifiableProperties(): void
    {
        // Unset properties that are null or empty strings
        $properties = [
            'WashItemId', 'ExternalProductId', 'ExternalProductName', 'OriginalProductName',
            'Unit', 'UnitPrice', 'Count', 'Discount', 'PriceExVat', 'Vat', 'PriceIncVat'
        ];
        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 product associated with this wash item.
     * @throws Exception If the OriginalProductName is not set or if the product cannot be found.
     */
    public function getProduct(xlvask_usage_log $xlvask_usage_log): products_o
    {
        // Since the ExternalProductId is rarely set, we must use the OriginalProductName to find the product.
        // The OriginalProductName is the name of the service in the xlvask module, this is ususally very ambiguous.
        // Example: "Stor bil" represents a large car/vehicle (And doesn't specify the type of vehicle, this could be a truck, bus, etc.)
        if (empty($this->OriginalProductName)) {
            $this->slackMessage('The OriginalProductName is not set. Please provide a valid product name.');
            throw new Exception('The OriginalProductName is not set. Please provide a valid product name.');
        }

        // Check if a parser exists for the OriginalProductName
        $parser_class = 'helpers\xlvask_parser_' . $this->getParserName();

        if (!class_exists($parser_class)) {
            $this->slackMessage("No parser found for the OriginalProductName: {$this->OriginalProductName} (Parsed as: {$this->getParserName()})" . PHP_EOL .
                'WashItemId: ' . $this->WashItemId . PHP_EOL .
                'ExternalProductId: ' . $this->ExternalProductId . PHP_EOL .
                'ExternalProductName: ' . $this->ExternalProductName . PHP_EOL .
                'OriginalProductName: ' . $this->OriginalProductName . PHP_EOL .
                'Unit: ' . $this->Unit . PHP_EOL .
                'UnitPrice: ' . $this->UnitPrice . PHP_EOL .
                'Count: ' . $this->Count . PHP_EOL .
                'Discount: ' . $this->Discount . PHP_EOL .
                'PriceExVat: ' . $this->PriceExVat . PHP_EOL .
                'Vat: ' . $this->Vat . PHP_EOL .
                'PriceIncVat: ' . $this->PriceIncVat . PHP_EOL);
            throw new Exception("No parser found for the OriginalProductName: {$this->OriginalProductName} (Parsed as: {$this->getParserName()}). Please ensure the parser class exists.");
        }

        $parser = new $parser_class();
        if (!$parser instanceof xlvask_product_parser) {
            $this->slackMessage('The xlvask product parser class is not an instance of xlvask_product_parser. Please ensure it implements the correct interface.');
            throw new Exception('The xlvask product parser class is not an instance of xlvask_product_parser. Please ensure it implements the correct interface.');
        }
        return $parser->parseProduct($this, $xlvask_usage_log);
    }

    private function slackMessage(string $message, $function = null, $class_name = null): void
    {
        // Get the class name and function name if not provided
        $class_name = $class_name ?? __CLASS__;
        $function_name = $function ?? debug_backtrace()[1]['function'] ?? 'unknown_function';

        // Log the error message to the console
        //error_log("Error in {$class_name}::{$function_name} - {$message}");

        // Send an error message to Slack
        $slack = new slack();
        $slack->send_message(
            $message,
            $class_name . '::' . $function_name,
        );
    }

    /**
     * Get the parser name based on the OriginalProductName.
     * This method converts the OriginalProductName to a valid parser name.
     * @return string
     */
    private function getParserName(): string
    {
        // Convert the OriginalProductName to a valid parser name
        // Example: "Stor bil" becomes "stor_bil", "2-b\u00f8rstevask" becomes "2_br_stevask", "ingen_bu00f8rster_foran_no_brush_wash_front" => "ingen_b_rster_foran_no_brush_wash_front"
        // Remove all non-alphanumeric characters and replace spaces with underscores
        if (empty($this->OriginalProductName)) {
            $this->slackMessage('Experienced an error parsing a product name: MISSING_ORIGINAL_PRODUCT_NAME ( WashItemId: ' . $this->WashItemId . ' )');
            throw new Exception('The OriginalProductName is not set. Please provide a valid product name.');
        }
        // Use preg_replace to replace non-alphanumeric characters with underscores
        $result = preg_replace('/[^a-z0-9]+/i', '_', $this->OriginalProductName);
        // Remove leading and trailing underscores
        $result = trim($result, '_');
        // Convert to lowercase
        if ($result === '') {
            $this->slackMessage('Experienced an error parsing a product name: EMPTY_ORIGINAL_PRODUCT_NAME_AFTER_PROCESSING ( WashItemId: ' . $this->WashItemId . ' )');
            throw new Exception('The OriginalProductName is empty after processing. Please provide a valid product name.');
        }
        // Check if there's any "uxxxx" pattern in the string and replace it with the "_" character
        $result = preg_replace('/u([0-9a-fA-F]{4})/', '_', $result);
        // Remove any special characters that might still be present
        $result = preg_replace('/[^a-z0-9_]/i', '', $result);
        // Remove any double underscores
        $result = preg_replace('/_+/', '_', $result);
        return strtolower($result);
    }

    /**
     * Check if the wash item should be included in the order.
     * This method checks if the wash item has a count above zero or if the price excluding VAT is greater than zero.
     * @note This method was added to accommodate the need to check if the wash item should be included in the order.
     * @return bool
     * @see getPriceExVat()
     * @see isCountAboveZero()
     */
    public function shouldIncludeInOrder(): bool
    {
        // Check if the wash item should be included in the order
        // This is based on whether the count is above zero or the price excluding VAT is greater than zero.
        return ($this->isCountAboveZero() || $this->getPriceExVat() > 0);
    }

    /**
     * Check if the count of the wash item is above zero.
     * @note This method was added to accommodate the need to check if the count is above zero. (Since some, but not all wash items can have a count of 0, which is not valid for processing)
     * @return bool
     */
    public function isCountAboveZero(): bool
    {
        // Check if the count is above zero
        return is_numeric($this->Count) && (float)$this->Count > 0;
    }

    public function getPriceExVat(): float
    {
        // Calculate the price excluding VAT
        if (is_numeric($this->PriceIncVat) && is_numeric($this->Count)) {
            return (float)$this->PriceIncVat - (float)$this->Vat;
        }
        // If either PriceIncVat or Count is not numeric, return 0
        return 0.0;
    }

    public function getUnitPriceExVat(): float
    {
        // Calculate the unit price excluding VAT
        if (!is_numeric($this->Count) || $this->Count <= 0) {
            // If Count is not numeric or less than or equal to zero, return 0
            return 0.0;
        }
        return $this->getPriceExVat() / (float)$this->Count;
    }
}