<?php

namespace objects;

use classes\db;
use classes\object_property;
use Exception;
use traits\db_object_t;

class order_items_o extends db
{
    use db_object_t;

    /**
     * The associated order id
     * @var object_property
     */
    public object_property $order_id;
    /**
     * The associated product id
     * @var object_property
     */
    public object_property $product_id;
    /**
     * The text reference assigned to the product at the time of the order
     * @var object_property
     */
    public object_property $reference;
    /**
     * The notes added to the order item
     * @var object_property
     */
    public object_property $notes;
    /**
     * The cashier (id) who added the item to the order
     * @var object_property
     */
    public object_property $cashier_id;
    /**
     * The price of the product at the time of the order
     * @var object_property
     */
    public object_property $price;
    /**
     * The quantity of the product ordered
     * @var object_property
     */
    public object_property $quantity;
    /**
     * The id of the related item, if it exists
     * @var object_property
     */
    public object_property $related_item_id;

    /**
     * The include in invoice property
     * @var object_property
     */
    public object_property $include_in_invoice;

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

    public function getOrderItemById(int $id): order_items_o
    {
        global $db;
        // Get the record from the database
        $sql = "SELECT * FROM $this->table WHERE id = $id";
        $result = $db->query($sql);
        if ($result->num_rows > 0) {
            $this->id = $id;
            $this->getObjectProperties();
        }
        return $this;
    }

    public function getObjectProperties(): void
    {
        $this->order_id = new object_property($this->table, $this->id, 'order_id', 'int', true);
        $this->product_id = new object_property($this->table, $this->id, 'product_id', 'int', true);
        $this->reference = new object_property($this->table, $this->id, 'reference', 'string', true);
        $this->notes = new object_property($this->table, $this->id, 'notes', 'string', false);
        $this->cashier_id = new object_property($this->table, $this->id, 'cashier_id', 'int', true);
        $this->price = new object_property($this->table, $this->id, 'price', 'int', true);
        $this->quantity = new object_property($this->table, $this->id, 'quantity', 'int', true);
        $this->related_item_id = new object_property($this->table, $this->id, 'related_item_id', 'int', false);
        $this->include_in_invoice = new object_property($this->table, $this->id, 'include_in_invoice', 'bool', false);
    }

    public function add(int $order_id, int $product_id, string $reference, string $notes, int $cashier_id, int $price, int $quantity, $related_item_id = null): void
    {
        global $db, $response;
        try {
            // Avoid SQL injection
            $reference = $db->escape_string($reference);
            $notes = $db->escape_string($notes);
            // Create a new record in the database
            $sql = "INSERT INTO $this->table (order_id, product_id, reference, notes, cashier_id, price, quantity) VALUES ($order_id, $product_id, '$reference', '$notes', $cashier_id, $price, $quantity)";
            $db->query($sql);

            // Get the id of the new record
            $this->id = $db->insert_id();

            // Set the values of the object properties
            $this->getObjectProperties();
            // Set the related item id, if it is set
            if ($related_item_id) {
                $this->related_item_id->set($related_item_id);
            }
            // Inform the order object that a new item has been added
            $this->getOrder()->objectChanged();
        } catch (Exception $e) {
            $response->error($e->getMessage());
        }
    }

    /**
     * @throws Exception
     */
    public function objectChanged(): void
    {
        // This method is called when the object is changed, to inform the order object
        // that an item has been added, edited or removed.
        // This is used to update the order total and other related properties.
        $order = $this->getOrder();
        $order->objectChanged();
    }

    /**
     * Get the order object associated with this order item
     * @return orders_o The order object associated with this order item
     * @throws Exception If the order item is not selected
     * @throws Exception If no order is selected
     */
    public function getOrder(): orders_o
    {
        self::requireSelected();
        return (new orders_o())->select((int)$this->order_id->value());
    }

    public function edit(int $id, int $order_id, int $product_id, string $reference, string $notes, int $cashier_id, int $price, int $quantity): void
    {
        global $db, $response;
        $this->id = $id;
        try {
            // Avoid SQL injection
            $reference = $db->escape_string($reference);
            $notes = $db->escape_string($notes);
            // Update the record in the database
            $sql = "UPDATE $this->table SET order_id = $order_id, product_id = $product_id, reference = '$reference', notes = '$notes', cashier_id = $cashier_id, price = $price, quantity = $quantity WHERE id = $this->id";
            $db->query($sql);

            // Set the values of the object properties
            $this->getObjectProperties();
            // Inform the order object that an item has been edited
            $this->objectChanged();
        } catch (Exception $e) {
            $response->error($e->getMessage());
        }
    }

    public function addItemToOrder(int $order_id, int $product_id, int $cashier_id, int $quantity, $related_item_id = null, $notes = null, $forcePrice = null): void
    {
        global $db, $response;
        try {
            // Get the order
            $order = (new orders_o())->getOrderById($order_id);
            // Get the product price
            $price = (new products_o())->getProductById($product_id)->getDepartmentPrice((int)$order->department_id->value());

            // Check if the user has a discount on the product, or category
            $customer = (new orders_o())->getOrderCustomer($order_id);
            $discount = $customer->getCustomPrice($product_id, false);
            if ($discount) {
                $price = $price - ($price * $discount / 100);
            }

            // If the price is forced, set the price to the forced price
            if ($forcePrice) {
                $price = (int)$forcePrice;
            }

            // Create a new record in the database
            $sql = "INSERT INTO $this->table (order_id, product_id, price, cashier_id, quantity) VALUES ($order_id, $product_id, $price, $cashier_id, $quantity)";
            $db->query($sql);
            // Get the id of the new record
            $this->id = $db->insert_id();
            // Set the values of the object properties
            $this->getObjectProperties();
            // Set the related item id, if it is set
            if ($related_item_id) {
                $this->related_item_id->set($related_item_id);
            }
            // Set the notes, if it is set
            if ($notes) {
                $this->notes->set($notes);
            }
            // Invalidate the order cache
            $order->objectChanged();

        } catch (Exception $e) {
            $response->error($e->getMessage());
        }
    }


    /**
     * @throws Exception
     */
    public function removeOrderItem(int $id): void
    {
        // TODO: Implement delete() method instead
        global $db;
        $this->id = $id;
        $this->select($this->id);
        // Inform the order object that an item has been removed
        $this->getOrder()->objectChanged();
        $sql = "DELETE FROM $this->table WHERE id = $this->id or related_item_id = $this->id";
        $db->query($sql);
    }

    public function getItemAsArray(): array
    {
        return [
            'id' => (int)$this->id,
            'order_id' => (int)$this->order_id->value(),
            'product_id' => (int)$this->product_id->value(),
            'reference' => (string)$this->reference->value(),
            'notes' => (string)$this->notes->value(),
            'cashier_id' => (int)$this->cashier_id->value(),
            'price' => (int)$this->price->value(),
            'quantity' => (int)$this->quantity->value(),
            'related_item_id' => (int)$this->related_item_id->value(),
            'product' => (array)(new products_o())->getProductById($this->product_id->value())->asArray(),
            'cashier' => (array)(new users_o())->getUserById($this->cashier_id->value())->asArray(),
            'include_in_invoice' => (bool)$this->include_in_invoice->value(),
        ];
    }

    public function getAllItemsAsArray(int $orderId, ?array $onlySpecificColumns = null): array
    {
        global $db;
        // Get all items for the order
        if ($onlySpecificColumns) {
            $columns = implode(', ', $onlySpecificColumns);
        } else {
            $columns = '*';
        }
        $sql = "SELECT $columns FROM $this->table WHERE order_id = $orderId AND deleted_at IS NULL";
        $result = $db->query($sql);
        // Circumvent the repeated instantiation of the object, by just selecting the fields
        $items = [];
        if ($result->num_rows > 0) {
            // Fetch all rows
            return $result->fetch_all(MYSQLI_ASSOC);
        }
        return $items;
    }

    public function updateOrderItem(int $id, int $price, string $notes, string $reference, int $quantity): void
    {
        global $db, $response;
        $this->id = $id;
        try {
            // Avoid SQL injection
            $price = $db->escape_string($price);
            $notes = $db->escape_string($notes);
            $reference = $db->escape_string($reference);
            // Update the record in the database
            $sql = "UPDATE $this->table SET price = $price, notes = '$notes', reference = '$reference', quantity = $quantity WHERE id = $this->id";
            $db->query($sql);
            // Set the values of the object properties
            $this->getObjectProperties();
            $this->objectChanged();
        } catch (Exception $e) {
            $response->error($e->getMessage());
        }
    }

    /**
     * Get the list of order items based on the criteria.
     * @param int[] $department_ids
     * @param int[] $product_ids
     * @param int[] $customer_numbers
     * @param \DateTime|null $datetime_start
     * @param \DateTime|null $datetime_end
     * @return order_items_o[]
     */
    public static function getListByCriteria(
        array     $department_ids = [],
        array     $product_ids = [],
        array     $customer_numbers = [],
        \DateTime $datetime_start = null,
        \DateTime $datetime_end = null
    ): array
    {
        global $db;
        $sql = "SELECT oi.* FROM order_items oi 
                JOIN orders o ON oi.order_id = o.id 
                WHERE oi.deleted_at IS NULL AND o.deleted_at IS NULL";

        if (!empty($department_ids)) {
            $sql .= " AND o.department_id IN (" . implode(',', array_map('intval', $department_ids)) . ")";
        }

        if (!empty($product_ids)) {
            $sql .= " AND oi.product_id IN (" . implode(',', array_map('intval', $product_ids)) . ")";
        }

        if (!empty($customer_numbers)) {
            // customer_id in orders table matches customer_number in users table (at least it is used this way in many places)
            $sql .= " AND o.customer_id IN (" . implode(',', array_map('intval', $customer_numbers)) . ")";
        }

        if ($datetime_start) {
            $sql .= " AND o.created_at >= '" . $datetime_start->format('Y-m-d H:i:s') . "'";
        }

        if ($datetime_end) {
            $sql .= " AND o.created_at <= '" . $datetime_end->format('Y-m-d H:i:s') . "'";
        }

        $result = $db->query($sql);
        $items = [];
        if ($result && $result->num_rows > 0) {
            while ($row = $result->fetch_assoc()) {
                $item = new order_items_o();
                $item->id = (int)$row['id'];
                // We don't call getObjectProperties() here to avoid many SQL queries, 
                // but the count() method in goals_criteria_products only uses $item->quantity
                // Let's check if we can populate quantity directly.
                // Actually, goals_criteria_products uses $item->quantity. 
                // In this codebase, properties are often object_property objects.
                $item->getObjectProperties(); 
                $items[] = $item;
            }
        }
        return $items;
    }

    /**
     * @throws Exception
     */
    public function getProduct(): products_o
    {
        return (new products_o())->select((int)$this->product_id->value());
    }
}