<?php

namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_mode.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane_command_arguments.php';
require_once WD . '/modules/attachments/helpers/attachment_content.php';
require_once WD . '/objects/selfserve_wash_sessions_o.php';
require_once WD . '/objects/subusers_o.php';

use classes\selfserve;
use classes\economic;
use Exception;
use attachments\helpers\attachment_content;
use modules\selfserve\classes\selfserve_lane_command_arguments;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\helpers\selfserve_lane_mode;
use modules\selfserve\helpers\selfserve_lane_status;
use objects\customer_vehicles_o;
use objects\order_items_o;
use objects\orders_o;
use objects\selfserve_wash_sessions_o;
use objects\subusers_o;

trait selfserve_lane_invoice_t
{
    private const INVOICE_SYSTEM_USER_ID = 2285;
    public ?int $last_invoice_order_id = null;

    /**
     * The product ID for minute-based billing
     * @var int|null $minute_billing_product_id
     */
    public ?int $minute_billing_product_id = null;
    /**
     * Included machine wash minutes before minute billing starts.
     * @var int|null $machine_wash_minutes_included
     */
    public ?int $machine_wash_minutes_included = null;
    /**
     * The billable minutes order item, stored to be corrected / deleted when a machine wash is performed
     * @var order_items_o|null
     */
    public ?order_items_o $billable_minutes_order_item = null;
    /**
     * Get the minute billing product ID
     * @return int|null The product ID for minute-based billing, or null if not set
     */
    public function getMinuteBillingProductId(): ?int
    {
        $minute_billing_product_id = selfserve::getInstance()
            ->config
            ->minute_product
            ->getVariableValue();
        if (!empty($minute_billing_product_id)) {
            $this->minute_billing_product_id = $minute_billing_product_id;
        }
        return $this->minute_billing_product_id;
    }

    /**
     * Get included machine wash minutes before minute billing starts.
     */
    public function getMachineWashMinutesIncluded(): int
    {
        $included_minutes = selfserve::getInstance()
            ->config
            ->machine_wash_minutes_included
            ->getVariableValue();
        $included_minutes = is_numeric($included_minutes) ? (int)$included_minutes : null;

        if ($included_minutes === null || $included_minutes < 0) {
            return 0;
        }

        return $this->machine_wash_minutes_included;
    }

    public function getLastInvoiceOrderId(): ?int
    {
        return $this->last_invoice_order_id;
    }

    /**
     * Ensure there is an invoice order context for optional STOP follow-up lines
     * (for example vehicle-type product) even when minute billing quantity is zero.
     */
    public function ensureInvoiceOrderContextForVehicleProduct(): bool
    {
        if (!empty($this->last_invoice_order_id)) {
            return true;
        }

        if (empty($this->id)) {
            return false;
        }
        if ($this->getLaneStatus() !== selfserve_lane_status::OCCUPIED) {
            return false;
        }
        if (empty($this->getCustomerNumber()) || empty($this->getLicensePlate())) {
            return false;
        }

        $this->createInvoiceOrderContext();
        return !empty($this->last_invoice_order_id);
    }

    /**
     * Invoice for minute-based billing
     * @return bool True on success, false on failure
     * @throws Exception if lane ID is not set, lane is not occupied, customer number or license plate is not set, or product ID is not set
     */
    public function invoice(?selfserve_lane_command_arguments $arguments = null): bool
    {
        $this->last_invoice_order_id = null;

        if (empty($this->id)) throw new \Exception("Lane ID is not set.");
        if ($this->getLaneStatus() !== selfserve_lane_status::OCCUPIED) throw new \Exception("Lane ID {$this->id} is not occupied; cannot invoice.");
        if (empty($this->getCustomerNumber())) throw new \Exception("Customer number is not set for lane ID {$this->id}.");
        if (empty($this->getLicensePlate())) throw new \Exception("License plate is not set for lane ID {$this->id}.");
        if (empty($product_id = $this->getMinuteBillingProductId())) throw new \Exception("Minute billing product ID is not set.");
        $elapsed_minutes = $this->calculateElapsedMinutesForBilling($this->getElapsedWashTime());
        $included_minutes = $this->resolveIncludedMinutesForBilling();
        $billable_minutes = $this->calculateBillableMinutes($elapsed_minutes, $included_minutes);

        if ($billable_minutes > 0) {
            $order = $this->createInvoiceOrderContext($arguments);
            $this->billable_minutes_order_item = $this->addMinuteBillingLine((int)$order->id, (int)$product_id, $billable_minutes);
        }
        return true;
    }

    /**
     * Add the current vehicle type product as a single order line on the latest invoice order.
     * Returns true when the line was added, false when vehicle/product context was unavailable.
     * @throws Exception
     */
    public function addVehicleTypeProductToLastInvoiceOrder(): bool
    {
        if (empty($this->last_invoice_order_id)) {
            return false;
        }

        $vehicle_type_product_id = $this->resolveVehicleTypeProductIdForInvoice();
        if ($vehicle_type_product_id === null || $vehicle_type_product_id <= 0) {
            return false;
        }

        $primary_vehicle_order_item = (new order_items_o())->addItemToOrder(
            (int)$this->last_invoice_order_id,
            $vehicle_type_product_id,
            self::INVOICE_SYSTEM_USER_ID,
            1
        );

        // If the billable minutes order item isn't defined, throw an exception as this method should only be called in the context of an existing invoice with a billable minutes line
        if ($this->billable_minutes_order_item === null) {
            throw new \Exception("Billable minutes order item is not defined; cannot adjust quantity for included minutes.");
        }

        // If the time exceeds the included minutes, reduce the quantity by the included minutes, if not delete the order item
        if ($this->billable_minutes_order_item->quantity->value() > $this->machine_wash_minutes_included) {
            $this->billable_minutes_order_item->quantity->set($this->billable_minutes_order_item->quantity->value() - $this->machine_wash_minutes_included);
        } else {
            $this->billable_minutes_order_item->delete();
        }

        return true;
    }

    protected function resolveVehicleTypeProductIdForInvoice(): ?int
    {
        $license_plate = trim((string)$this->getLicensePlate());
        if ($license_plate === '') {
            return null;
        }

        $vehicle = (new customer_vehicles_o())->selectByPlate(selfserve::standardize_registration($license_plate));
        if (!$vehicle->exists()) {
            return null;
        }

        $product_id = (int)$vehicle->type->value();
        return $product_id > 0 ? $product_id : null;
    }

    protected function calculateElapsedMinutesForBilling(?int $elapsed_wash_time_seconds): int
    {
        if ($elapsed_wash_time_seconds === null || $elapsed_wash_time_seconds <= 0) {
            return 0;
        }

        return (int)ceil($elapsed_wash_time_seconds / 60);
    }

    protected function resolveIncludedMinutesForBilling(): int
    {
        if (!$this->shouldApplyIncludedMinutesReduction()) {
            return 0;
        }

        return $this->getMachineWashMinutesIncluded();
    }

    protected function shouldApplyIncludedMinutesReduction(): bool
    {
        // Only apply included minutes reduction for machine wash mode
        return $this->getLaneMode() === selfserve_lane_mode::AUTOMATIC;
    }

    protected function calculateBillableMinutes(int $elapsed_minutes, int $included_minutes): int
    {
        if ($elapsed_minutes <= 0) {
            return 0;
        }

        $included_minutes = max(0, $included_minutes);
        return max(0, $elapsed_minutes - $included_minutes);
    }

    protected function createInvoiceOrderContext(?selfserve_lane_command_arguments $arguments = null): orders_o
    {
        $billing_customer_number = $this->getCustomerNumber();
        $draft_customer_number = (new economic())->getTransactionDraftCustomerNumber();
        $order = (new orders_o())->add(
            $billing_customer_number,
            self::INVOICE_SYSTEM_USER_ID,
            '',
            '',
            (int)$this->department_lane->department->value(),
            (string)$this->getLicensePlate()
        );
        $order->lane->set($this->id);
        $this->last_invoice_order_id = (int)$order->id;
        $this->attachSelfServeMetadataToOrder($order, $billing_customer_number, $draft_customer_number, $arguments);

        return $order;
    }

    protected function attachSelfServeMetadataToOrder(
        orders_o $order,
        int $billing_customer_number,
        ?int $draft_customer_number,
        ?selfserve_lane_command_arguments $arguments = null
    ): void {
        try {
            $order->addAttachment(
                (new attachment_content())->setOther(
                    $this->buildSelfServeOrderAttachmentPayload($billing_customer_number, $draft_customer_number, $arguments)
                )
            );
        } catch (\Throwable) {
            // Metadata attachments must not block billing; the order itself is the source of record.
        }
    }

    protected function buildSelfServeOrderAttachmentPayload(
        int $billing_customer_number,
        ?int $draft_customer_number,
        ?selfserve_lane_command_arguments $arguments = null
    ): array {
        $session = $this->findOpenSelfServeSessionForAttachment($billing_customer_number, $arguments?->subuser_id);
        $session_subuser_id = $session?->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
        $subuser_id = $arguments?->subuser_id ?? $session_subuser_id;

        return [
            'type' => attachment_content::OTHER_TYPE_SELF_SERVE_WASH,
            'source' => 'selfserve',
            'customer_number' => $billing_customer_number,
            'draft_customer_number' => $draft_customer_number,
            'subuser_id' => $subuser_id,
            'subuser' => $this->formatSelfServeAttachmentSubuser($subuser_id),
            'session_id' => $session?->id,
            'lane_id' => (int)$this->id,
            'department_id' => (int)$this->department_lane->department->value(),
            'license_plate' => (string)$this->getLicensePlate(),
            'lane_status' => $this->getLaneStatus()->name,
            'lane_mode' => $this->getLaneMode()->name,
            'wash_start_time' => (int)$this->getWashStartTime(),
            'elapsed_wash_time_seconds' => (int)$this->getElapsedWashTime(),
            'created_at' => date('Y-m-d H:i:s'),
        ];
    }

    protected function findOpenSelfServeSessionForAttachment(int $billing_customer_number, ?int $subuser_id = null): ?selfserve_wash_sessions_o
    {
        $license_plate = trim((string)$this->getLicensePlate());
        if ($license_plate === '') {
            return null;
        }

        try {
            $session = (new selfserve_wash_sessions_o())->selectLatestOpenByLaneAndReg(
                (int)$this->id,
                selfserve::standardize_registration($license_plate),
                $billing_customer_number > 0 ? $billing_customer_number : null,
                $subuser_id
            );
            if ($session->exists()) {
                return $session;
            }

            if ($subuser_id === null) {
                return null;
            }

            $session = (new selfserve_wash_sessions_o())->selectLatestOpenByLaneAndReg(
                (int)$this->id,
                selfserve::standardize_registration($license_plate),
                $billing_customer_number > 0 ? $billing_customer_number : null
            );
            if (!$session->exists()) {
                return null;
            }

            $sessionSubuserId = $session->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
            return $sessionSubuserId === null || $sessionSubuserId === $subuser_id ? $session : null;
        } catch (\Throwable) {
            return null;
        }
    }

    protected function formatSelfServeAttachmentSubuser(?int $subuser_id): ?array
    {
        if ($subuser_id === null || $subuser_id <= 0) {
            return null;
        }

        try {
            $subuser = (new subusers_o())->select($subuser_id);
            if (!$subuser->exists()) {
                return [
                    'id' => $subuser_id,
                ];
            }

            return [
                'id' => (int)$subuser->id,
                'name' => $subuser->name->value(),
                'username' => $subuser->username->value(),
                'email' => $subuser->email->value(),
            ];
        } catch (\Throwable) {
            return [
                'id' => $subuser_id,
            ];
        }
    }

    protected function addMinuteBillingLine(int $order_id, int $product_id, int $quantity): order_items_o
    {
        return (new order_items_o())->addItemToOrder(
            $order_id,
            $product_id,
            self::INVOICE_SYSTEM_USER_ID,
            $quantity,
        );
    }
}
