<?php

namespace objects;

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

class department_daily_reports_o extends db
{
    use db_object_t;

    /**
     * The id of the department
     * @var object_property $department_id
     */
    public object_property $department_id;
    /**
     * The cm3 of water used in the department today
     * @var object_property $water_usage
     */
    public object_property $water_usage;
    /**
     * The cm3 of water used in the department today
     * @var object_property $water_usage_morning
     */
    public object_property $water_usage_morning;

    /**
     * Any notes for the report
     * @var object_property $notes
     */
    public object_property $notes;

    /**
     * The user (id) that filled the report
     * @var object_property $filled_by
     */
    public object_property $filled_by;
    /**
     * The timestamp of when the object was created
     * @var object_property $created_at
     */
    public object_property $created_at;


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

    public function asArray(): array
    {
        return [
            'id' => (int)$this->id,
            'department_id' => (int)$this->department_id->value(),
            'water_usage' => (int)$this->water_usage->value(),
            'water_usage_morning' => (int)$this->water_usage_morning->value(),
            'notes' => (string)$this->notes->value(),
            'filled_by' => (int)$this->filled_by->value(),
            'created_at' => (string)$this->created_at->value(),
        ];
    }

    /**
     * Get the amount of products sold on a given date
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param int $product_id The id of the product
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return int The amount of products sold on the given date (Not counting products from removed orders)
     * @throws Exception
     */
    public function getProductsSoldOnDate(string $date, int $department_id, int $product_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function () use ($date, $department_id, $product_id, $date_to) {
            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Method cache for 2 minutes
            $department = (new departments_o())->select($department_id);
            $department->requireSelected();
            return $department->getTotalAddonsSoldInDepartment([$product_id], $date, $date_to, $department_id);
        });
    }

    /**
     * Get the amount of products sold, where the product would be applicable as an addon
     * E.g. if the product is "Cheese" this would return the amount of "Pizza" sold
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param int $product_id The id of the product (the addon)
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return int The amount of products sold on the given date (Not counting products from removed orders)
     * @throws Exception
     */
    public function getProductsSoldWithAddonApplicable(string $date, int $department_id, int $product_id, string $date_to = null): int
    {
        global /** @var db $db */
        $db;
        // Get all the addons for the product
        $addons = (new product_options_o())->getOptionProducts($product_id);
        $addons = array_map(function ($addon) {
            return $addon['product_id'];
        }, $addons);
        $addons = implode(',', $addons);
        // If the date_to is null, set it to the date
        if ($date_to === null) {
            $date_to = $date; // Making the report for an entire day
        }
        $department = (new departments_o())->select($department_id);
        return $department->getTotalMaxAddonsInDepartment([$product_id], $date, $date_to, $department_id);
    }

    /**
     * Get the amount of products sold on a given date
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return int The amount of products sold on the given date (Not counting products from removed orders)
     */
    public function getTransactionProductsOnDateCount(string $date, int $department_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($date, $department_id, $date_to) {
            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Set the date time to cover the entire day
            $date = date('Y-m-d 00:00:00', strtotime($date));
            $date_to = date('Y-m-d 23:59:59', strtotime($date_to));
            $conn = $db->conn();
            $stmt = $conn->prepare(
                'SELECT SUM(oi.quantity) as amount FROM orders o
                        JOIN order_items oi ON o.id = oi.order_id
                        WHERE o.department_id = ? AND DATE(o.created_at) BETWEEN ? AND ? AND o.deleted_at IS NULL AND oi.deleted_at IS NULL'
            );

            if ($stmt) {
                $stmt->bind_param('iss', $department_id, $date, $date_to); // Bind parameters (i = integer, s = string)
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array

                // Access the "amount" field
                $amount = $data['amount'];

                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            return (int)$amount;
        });
    }

    /**
     * Get the amount of transactions on a given date
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return int The amount of transactions on the given date (Not counting products from removed orders)
     */
    public function getTransactionsOnDateCount(string $date, int $department_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($date, $department_id, $date_to) {
            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Set the date time to cover the entire day
                $date = date('Y-m-d 00:00:00', strtotime($date));
            $date_to = date('Y-m-d 23:59:59', strtotime($date_to));
            $conn = $db->conn();
            // Make sure only to count orders with at least one non-deleted order item
            $stmt = $conn->prepare(
                'SELECT COUNT(DISTINCT o.id) as amount FROM orders o
                        JOIN order_items oi ON o.id = oi.order_id
                        WHERE o.department_id = ? AND DATE(o.created_at) BETWEEN ? AND ? AND o.deleted_at IS NULL AND oi.deleted_at IS NULL'
            );

            if ($stmt) {
                $stmt->bind_param('iss', $department_id, $date, $date_to); // Bind parameters (i = integer, s = string)
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array

                // Access the "amount" field
                $amount = $data['amount'];

                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            return (int)$amount;
        });
    }

    /**
     * Select the department daily report for today
     * @param int $department_id The id of the department
     * @return department_daily_reports_o The department daily report
     * @throws Exception If a department daily report does not exist for today
     * @throws Exception If the object was not selected
     */
    public function selectDepartmentDailyReport(int $department_id): department_daily_reports_o
    {
        global $db;
        if (!$this->doesDepartmentDailyReportExist($department_id)) {
            throw new Exception('Department daily report does not exist');
        }
        $conn = $db->conn();
        $stmt = $conn->prepare(
            'SELECT id FROM department_daily_reports o
                    WHERE o.department_id = ? AND DATE(o.created_at) = CURDATE()'
        );
        // Check if the statement was prepared successfully
        if ($stmt) {
            $stmt->bind_param('i', $department_id); // Bind parameters (i = integer, s = string)
            $stmt->execute();
            $result = $stmt->get_result(); // Get the result set from the statement
            $data = $result->fetch_assoc(); // Fetch the result as an associative array

            // Access the "amount" field
            $id = $data['id'];

            $stmt->close(); // Close the statement
        } else {
            // Handle query preparation error
            die('Query preparation failed: ' . $conn->error);
        }
        $this->select($id);
        return $this;
    }

    /**
     * Check if a department daily report exists for today
     * @param int $department_id The id of the department
     * @param string $date The date to check for (YYYY-MM-DD)
     * @return bool True if the report exists, false otherwise
     */
    public function doesDepartmentDailyReportExist(int $department_id, string $date = 'today'): bool
    {
        global $db;
        if ($date === 'today') {
            $date = date('Y-m-d');
        }
        $conn = $db->conn();
        $stmt = $conn->prepare(
            'SELECT COUNT(*) as amount FROM department_daily_reports o
                    WHERE o.department_id = ? AND DATE(o.created_at) = ?'
        );

        if ($stmt) {
            $stmt->bind_param('is', $department_id, $date); // Bind parameters (i = integer, s = string)
            $stmt->execute();
            $result = $stmt->get_result(); // Get the result set from the statement
            $data = $result->fetch_assoc(); // Fetch the result as an associative array

            // Access the "amount" field
            $amount = $data['amount'];

            $stmt->close(); // Close the statement
        } else {
            // Handle query preparation error
            die('Query preparation failed: ' . $conn->error);
        }
        return (int)$amount > 0;
    }

    /**
     * Select the last department daily report for a department
     * @param int $department_id The id of the department
     * @return department_daily_reports_o The department daily report
     * @throws Exception If the object was not selected
     */
    public function selectLastDepartmentDailyReport(int $department_id): department_daily_reports_o
    {
        global $db;
        $conn = $db->conn();
        $stmt = $conn->prepare(
            'SELECT id FROM department_daily_reports o
                    WHERE o.department_id = ? ORDER BY o.created_at DESC LIMIT 1'
        );
        // Check if the statement was prepared successfully
        if ($stmt) {
            $stmt->bind_param('i', $department_id); // Bind parameters (i = integer, s = string)
            $stmt->execute();
            $result = $stmt->get_result(); // Get the result set from the statement
            $data = $result->fetch_assoc(); // Fetch the result as an associative array

            // Access the "id" field
            if (!$data) {
                throw new Exception('Department daily report does not exist');
            }
            $id = $data['id'];

            $stmt->close(); // Close the statement
        } else {
            // Handle query preparation error
            die('Query preparation failed: ' . $conn->error);
        }
        $this->select($id);
        return $this;
    }

    public function getTransactionsOnDateEarnings(string $date, int $department_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($date, $department_id, $date_to) {

            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Set the date time to cover the entire day
            $date = date('Y-m-d 00:00:00', strtotime($date));
            $date_to = date('Y-m-d 23:59:59', strtotime($date_to));
            $conn = $db->conn();
            // Get all the product prices, in all the orders (Not counting removed orders), for the given date and department, and sum them
            $stmt = $conn->prepare(
                'SELECT SUM(oi.price * oi.quantity) as amount FROM orders o
                        JOIN order_items oi ON o.id = oi.order_id
                        WHERE o.department_id = ? AND DATE(o.created_at) BETWEEN ? AND ? AND o.deleted_at IS NULL AND oi.deleted_at IS NULL'
            );
            if ($stmt) {
                $stmt->bind_param('iss', $department_id, $date, $date_to); // Bind parameters (i = integer, s = string)
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array

                // Access the "amount" field
                if (!$data) {
                    // If there are no orders, set the amount to 0
                    $amount = 0;
                } else {
                    $amount = $data['amount'];
                }
                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            return (int)$amount;
        });
    }

    /**
     * Check if a department daily report exists for today
     * @param int $department_id The id of the department
     * @return bool True if the report exists, false otherwise
     */
    public function hasReportForToday(int $department_id): bool
    {
        return $this->doesDepartmentDailyReportExist($department_id);
    }

    /**
     * Select the department daily report for a given date
     * @param int $department_id The id of the department
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @return department_daily_reports_o The department daily report
     * @throws Exception If a department daily report does not exist for today
     * @throws Exception If the object was not selected
     */
    public function selectDepartmentReportByDate(int $department_id, string $date): department_daily_reports_o
    {
        global $db;
        if (!$this->doesDepartmentDailyReportExist($department_id, $date)) {
            // Create a new report for the given date, if it does not exist
            return $this->add($department_id, 0, 0, '', 0, $date);
        }
        $conn = $db->conn();
        $stmt = $conn->prepare(
            'SELECT id FROM department_daily_reports o
                    WHERE o.department_id = ? AND DATE(o.created_at) = ?'
        );
        // Check if the statement was prepared successfully
        if ($stmt) {
            $stmt->bind_param('is', $department_id, $date); // Bind parameters (i = integer, s = string)
            $stmt->execute();
            $result = $stmt->get_result(); // Get the result set from the statement
            $data = $result->fetch_assoc(); // Fetch the result as an associative array

            // Access the "amount" field
            if (!$data) {
                throw new Exception('Department daily report does not exist');
            }
            $id = $data['id'];

            $stmt->close(); // Close the statement
        } else {
            // Handle query preparation error
            die('Query preparation failed: ' . $conn->error);
        }
        $this->select($id);
        return $this;
    }

    /**
     * Add a department daily report
     * @param int $department_id The id of the department
     * @param int $water_usage The cm3 of water used in the department today
     * @param int $water_usage_morning The cm3 of water used in the department (Checked in the morning)
     * @param string $notes Any notes for the report
     * @param int $filled_by The user (id) that filled the report
     * @return department_daily_reports_o
     * @throws Exception If the object was not created successfully
     */
    public function add(
        int    $department_id,
        int    $water_usage,
        int    $water_usage_morning,
        string $notes,
        int    $filled_by,
        string $created_at = null
    ): department_daily_reports_o
    {
        // Validate the created_at date, if provided
        if ($created_at !== null) {
            $dateTime = \DateTime::createFromFormat('Y-m-d', $created_at);
            if (!$dateTime || $dateTime->format('Y-m-d') !== $created_at) {
                throw new Exception('Invalid date format for created_at. Expected format: YYYY-MM-DD');
            }
        }
        $tmp_id = $this->add_object([
            'department_id' => (int)$department_id,
            'water_usage' => (int)$water_usage,
            'water_usage_morning' => (int)$water_usage_morning,
            'notes' => (string)$notes,
            'filled_by' => (int)$filled_by,
            'created_at' => $created_at ? $created_at : date('Y-m-d H:i:s'),
        ]);
        $this->id = $tmp_id;
        $this->getObjectProperties();
        $this->objectChanged();
        return $this;
    }

    public function getObjectProperties(): void
    {
        $this->department_id = new object_property($this->table, $this->id, 'department_id', 'int', false);
        $this->water_usage = new object_property($this->table, $this->id, 'water_usage', 'int', false);
        $this->water_usage_morning = new object_property($this->table, $this->id, 'water_usage_morning', 'int', false);
        $this->notes = new object_property($this->table, $this->id, 'notes', 'string', false);
        $this->filled_by = new object_property($this->table, $this->id, 'filled_by', 'int', false);
        $this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
    }

    public function objectChanged(): void
    {
        //TODO: Add cache invalidation
    }

    public function hasReport(int $department_id, string $date): bool
    {
        return $this->doesDepartmentDailyReportExist($department_id, $date);
    }

    public function getBookingsOnDateCount(string $date, int $department_id, string $status): int
    {
        global /** @var db $db */
        $db;
        $conn = $db->conn();
        $stmt = $conn->prepare(
            'SELECT COUNT(*) as amount FROM bookings_new o
                    WHERE o.department = ? AND DATE(o.created_at) = ? AND o.status = ?'
        );

        if ($stmt) {
            $stmt->bind_param('iss', $department_id, $date, $status); // Bind parameters (i = integer, s = string)
            $stmt->execute();
            $result = $stmt->get_result(); // Get the result set from the statement
            $data = $result->fetch_assoc(); // Fetch the result as an associative array

            // Access the "amount" field
            $amount = $data['amount'];

            $stmt->close(); // Close the statement
        } else {
            // Handle query preparation error
            die('Query preparation failed: ' . $conn->error);
        }
        return (int)$amount;
    }

    /**
     * This function calculates how many of the transactions on a given date included a wash
     * That is defined as any product that has the "is_wash" field set to true in the products table
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return void
     */
    public function getTransactionsOnDateWashesCount(string $date, int $department_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($date, $department_id, $date_to) {
            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Set the date time to cover the entire day
            $date = date('Y-m-d 00:00:00', strtotime($date));
            $date_to = date('Y-m-d 23:59:59', strtotime($date_to));
            $conn = $db->conn();
            // Get all the product prices, in all the orders (Not counting removed orders), for the given date and department, and sum them
            $stmt = $conn->prepare(
                'SELECT COUNT(DISTINCT o.id) as amount FROM orders o
                        JOIN order_items oi ON o.id = oi.order_id
                        JOIN products p ON oi.product_id = p.id
                        WHERE o.department_id = ? AND DATE(o.created_at) BETWEEN ? AND ? AND o.deleted_at IS NULL AND oi.deleted_at IS NULL AND p.is_wash = 1'
            );
            if ($stmt) {
                $stmt->bind_param('iss', $department_id, $date, $date_to); // Bind parameters (i = integer, s = string)
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array

                // Access the "amount" field
                if (!$data) {
                    // If there are no orders, set the amount to 0
                    $amount = 0;
                } else {
                    $amount = $data['amount'];
                }
                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            return (int)$amount;
        });
    }

    /**
     * This function calculates how much water has been used on the transactions on a given date
     * That is calculated by getting the water usage report on the earliest report before the given date
     * and the water usage report on the latest report on or before the given date
     * The difference between the two is the water usage for the given date
     * @note This does not count products from removed orders, only products from orders that are still active
     * @param string $date The date to get the report for (YYYY-MM-DD)
     * @param int $department_id The id of the department
     * @param string|null $date_to (Optional) The end date to get the report for (YYYY-MM-DD). If provided, the report will be for the date range between $date and $date_to
     * @return int The amount of water used on the given date
     */
    public function getTransactionsOnDateWaterUsage(string $date, int $department_id, string $date_to = null): int
    {
        return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($date, $department_id, $date_to) {
            global /** @var db $db */
            $db;
            // If the date_to is null, set it to the date
            if ($date_to === null) {
                $date_to = $date; // Making the report for an entire day
            }
            // Set the date time to cover the entire day
            $date = date('Y-m-d 00:00:00', strtotime($date));
            $date_to = date('Y-m-d 23:59:59', strtotime($date_to));
            $conn = $db->conn();
            // Get the earliest report before the given date
            $stmt = $conn->prepare(
                'SELECT water_usage FROM department_daily_reports o
                       WHERE o.created_at < ? AND o.department_id = ? AND o.water_usage > 0 ORDER BY o.created_at DESC LIMIT 1'
            );
            if ($stmt) {
                $stmt->bind_param('si', $date, $department_id); // Bind parameters
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array
                $earliest_usage = $data ? (int)$data['water_usage'] : 0;
                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            // Get the report closest to the date_to, but not after
            $stmt = $conn->prepare(
                'SELECT water_usage FROM department_daily_reports o
                       WHERE o.created_at <= ? AND o.department_id = ? AND o.water_usage > 0 ORDER BY o.created_at DESC LIMIT 1'
            );
            if ($stmt) {
                $stmt->bind_param('si', $date_to, $department_id); // Bind parameters
                $stmt->execute();
                $result = $stmt->get_result(); // Get the result set from the statement
                $data = $result->fetch_assoc(); // Fetch the result as an associative array
                $latest_usage = $data ? (int)$data['water_usage'] : 0;
                $stmt->close(); // Close the statement
            } else {
                // Handle query preparation error
                die('Query preparation failed: ' . $conn->error);
            }
            return $latest_usage - $earliest_usage;
        });
    }

}