<?php

namespace common\logic\Order\Order\Service;

use common\helpers\GlobalParam;
use common\helpers\MeanPayment;
use common\helpers\Price;
use common\logic\AbstractBuilder;
use common\logic\Config\TaxRate\Model\TaxRate;
use common\logic\Distribution\Distribution\Model\Distribution;
use common\logic\Distribution\Distribution\Repository\DistributionRepository;
use common\logic\Distribution\Distribution\Service\DistributionSolver;
use common\logic\Document\DeliveryNote\Model\DeliveryNote;
use common\logic\Document\DeliveryNote\Service\DeliveryNoteBuilder;
use common\logic\Document\Document\Model\Document;
use common\logic\Document\Document\Model\DocumentInterface;
use common\logic\Document\Document\Service\DocumentSolver;
use common\logic\Document\Invoice\Model\Invoice;
use common\logic\Order\Order\Event\OrderDeleteEvent;
use common\logic\Order\Order\Model\Order;
use common\logic\Order\Order\Repository\OrderRepository;
use common\logic\Order\ProductOrder\Model\ProductOrder;
use common\logic\Order\ProductOrder\Service\ProductOrderBuilder;
use common\logic\Order\ProductOrder\Service\ProductOrderSolver;
use common\logic\Payment\Model\Payment;
use common\logic\Payment\Repository\PaymentRepository;
use common\logic\Payment\Service\PaymentBuilder;
use common\logic\PointSale\PointSale\Model\PointSale;
use common\logic\PointSale\PointSale\Service\PointSaleBuilder;
use common\logic\PointSale\PointSale\Repository\PointSaleRepository;
use common\logic\PointSale\UserPointSale\Repository\UserPointSaleRepository;
use common\logic\Producer\Producer\Model\Producer;
use common\logic\Producer\Producer\Repository\ProducerRepository;
use common\logic\Producer\Producer\Service\ProducerSolver;
use common\logic\Product\Product\Service\ProductSolver;
use common\logic\Subscription\Subscription\Model\Subscription;
use common\logic\Subscription\Subscription\Service\SubscriptionBuilder;
use common\logic\Subscription\Subscription\Repository\SubscriptionRepository;
use common\logic\Subscription\Subscription\Service\SubscriptionSolver;
use common\logic\User\User\Repository\UserRepository;
use common\logic\User\User\Service\UserSolver;
use common\logic\User\UserProducer\Repository\UserProducerRepository;
use yii\web\NotFoundHttpException;

class OrderBuilder extends AbstractBuilder
{
    protected UserSolver $userSolver;
    protected OrderSolver $orderSolver;
    protected PaymentRepository $paymentRepository;
    protected ProducerRepository $producerRepository;
    protected PaymentBuilder $paymentBuilder;
    protected ProductOrderBuilder $productOrderBuilder;
    protected OrderRepository $orderRepository;
    protected DistributionRepository $distributionRepository;
    protected SubscriptionBuilder $subscriptionBuilder;
    protected PointSaleRepository $pointSaleRepository;
    protected UserProducerRepository $userProducerRepository;
    protected UserPointSaleRepository $userPointSaleRepository;
    protected PointSaleBuilder $pointSaleBuilder;
    protected ProductOrderSolver $productOrderSolver;
    protected SubscriptionRepository $subscriptionRepository;
    protected SubscriptionSolver $subscriptionSolver;
    protected ProductSolver $productSolver;
    protected DistributionSolver $distributionSolver;
    protected UserRepository $userRepository;
    protected DeliveryNoteBuilder $deliveryNoteBuilder;
    protected DocumentSolver $documentSolver;
    protected ProducerSolver $producerSolver;

    public function loadDependencies(): void
    {
        $this->userSolver = $this->loadService(UserSolver::class);
        $this->orderSolver = $this->loadService(OrderSolver::class);
        $this->paymentRepository = $this->loadService(PaymentRepository::class);
        $this->producerRepository = $this->loadService(ProducerRepository::class);
        $this->paymentBuilder = $this->loadService(PaymentBuilder::class);
        $this->productOrderBuilder = $this->loadService(ProductOrderBuilder::class);
        $this->orderRepository = $this->loadService(OrderRepository::class);
        $this->distributionRepository = $this->loadService(DistributionRepository::class);
        $this->subscriptionBuilder = $this->loadService(SubscriptionBuilder::class);
        $this->pointSaleRepository = $this->loadService(PointSaleRepository::class);
        $this->userProducerRepository = $this->loadService(UserProducerRepository::class);
        $this->userPointSaleRepository = $this->loadService(UserPointSaleRepository::class);
        $this->pointSaleBuilder = $this->loadService(PointSaleBuilder::class);
        $this->productOrderSolver = $this->loadService(ProductOrderSolver::class);
        $this->subscriptionRepository = $this->loadService(SubscriptionRepository::class);
        $this->subscriptionSolver = $this->loadService(SubscriptionSolver::class);
        $this->productSolver = $this->loadService(ProductSolver::class);
        $this->distributionSolver = $this->loadService(DistributionSolver::class);
        $this->userRepository = $this->loadService(UserRepository::class);
        $this->deliveryNoteBuilder = $this->loadService(DeliveryNoteBuilder::class);
        $this->documentSolver = $this->loadService(DocumentSolver::class);
        $this->producerSolver = $this->loadService(ProducerSolver::class);
    }

    public function instanciateOrder(Distribution $distribution): Order
    {
        $order = new Order();
        $order->populateDistribution($distribution);
        $order->date = date('Y-m-d H:i:s');
        $order->status = 'tmp-order';
        $order->id_user = 0;

        return $order;
    }

    public function createOrder(Distribution $distribution): Order
    {
        $order = $this->instanciateOrder($distribution);

        $this->createUserPointSale($order);
        $this->initOrderCommentPointSale($order);
        $this->generateOrderReference($order);

        $order->save();

        return $order;
    }

    public function addProductOrdersFromSubscription(Order $order, Subscription $subscription): bool
    {
        $productsAdd = false;
        $user = $subscription->user ?? null;
        foreach ($subscription->productSubscription as $productSubscription) {

            $this->productOrderBuilder->createProductOrder(
                $order,
                $productSubscription->product,
                $productSubscription->quantity,
                $this->productSolver->getPrice($productSubscription->product, [
                    'user' => $user,
                    'point_sale' => $subscription->pointSale,
                    'quantity' => $productSubscription->quantity
                ])
            );
            $productsAdd = true;
        }

        if (!$productsAdd) {
            $this->deleteOrder($order);
        }

        return $productsAdd;
    }

    public function updateOrderFromSubscription(Order $order, Subscription $subscription): void
    {
        $this->initOrderBaseFromSubscription($order, $subscription);
        $this->initOrderAutoPaymentFromSubscription($order, $subscription);
        $this->addProductOrdersFromSubscription($order, $subscription);

        $this->update($order);
    }

    public function initOrderBaseFromSubscription(Order $order, Subscription $subscription): void
    {
        $order->origin = Order::ORIGIN_AUTO;
        $order->populatePointSale($subscription->pointSale);
        $order->populateSubscription($subscription);

        if (strlen($subscription->comment)) {
            $order->comment = $subscription->comment;
        }

        if (strlen($subscription->username)) {
            $order->username = $subscription->username;
            $order->id_user = 0;
        } else {
            $order->populateUser($subscription->user);
        }
    }

    public function initOrderAutoPaymentFromSubscription(Order $order, Subscription $subscription): void
    {
        $pointSale = $subscription->pointSale;
        if ($pointSale) {
            $creditFunctioning = $this->producerRepository->getPointSaleCreditFunctioning($pointSale);

            $order->auto_payment = 0;
            if ($subscription->auto_payment == Subscription::AUTO_PAYMENT_DEDUCTED) {
                if ($order->id_user && $this->producerSolver->getConfig('credit') && $pointSale->credit) {
                    if ($creditFunctioning == Producer::CREDIT_FUNCTIONING_OPTIONAL) {
                        $order->auto_payment = 0;
                    } elseif ($creditFunctioning == Producer::CREDIT_FUNCTIONING_MANDATORY) {
                        $order->auto_payment = 1;
                    } elseif ($creditFunctioning == Producer::CREDIT_FUNCTIONING_USER) {

                        $userProducer = $this->userProducerRepository->findOneUserProducer($order->user);

                        if ($userProducer) {
                            $order->auto_payment = $userProducer->credit_active;
                        }
                    }
                }
            } elseif ($subscription->auto_payment == Subscription::AUTO_PAYMENT_YES) {
                $order->auto_payment = 1;
            } elseif ($subscription->auto_payment == Subscription::AUTO_PAYMENT_NO) {
                $order->auto_payment = 0;
            }
        }

        $order->tiller_synchronization = $order->auto_payment;
    }

    public function createUserPointSale(Order $order): void
    {
        if ($order->user) {
            $this->pointSaleBuilder->addUser($order->user, $order->pointSale);
        }
    }

    public function initOrderCommentPointSale(Order $order): void
    {
        if ($order->user && $order->pointSale) {
            $userPointSale = $this->userPointSaleRepository->findOneUserPointSale($order->user, $order->pointSale);

            if ($userPointSale && strlen($userPointSale->comment)) {
                $order->comment_point_sale = $userPointSale->comment;
            }
        }
    }

    /**
     * Initialise le montant total, le montant déjà payé et le poids de la commande.
     */
    public function initOrder(Order $order): void
    {
        $taxCalculationMethod = $this->getProducerContext()->option_tax_calculation_method;
        $this->initOrderAmount($order, $taxCalculationMethod);
        $this->initOrderPaidAmount($order);
    }

    /**
     * Initialise le montant de la commande.
     */
    public function initOrderAmount(Order $order, string $taxCalculationMethod = Document::TAX_CALCULATION_METHOD_DEFAULT): void
    {
        $order->amount = 0;
        $order->amount_with_tax = 0;
        $order->amount_vat = [];
        $order->invoice_amount = 0;
        $order->invoice_amount_with_tax = 0;
        $order->invoice_amount_vat = [];
        $order->weight = 0;

        if (isset($order->productOrder)) {
            foreach ($order->productOrder as $productOrder) {
                $this->addProductOrderAmount($order, Order::AMOUNT_TOTAL, $productOrder, $taxCalculationMethod);
                $this->addProductOrderAmount($order, Order::INVOICE_AMOUNT_TOTAL, $productOrder, $taxCalculationMethod);
                $this->addProductOrderWeight($order, $productOrder);
            }
        }
    }

    public function addProductOrderWeight(Order $order, ProductOrder $productOrder): void
    {
        if ($productOrder->unit == 'piece') {
            if (isset($productOrder->product)) {
                $order->weight += ($productOrder->quantity * $productOrder->product->weight) / 1000;
            }
        } else {
            $order->weight += $productOrder->quantity;
        }
    }

    public function addProductOrderAmount(Order $order, string $typeTotal, ProductOrder $productOrder, string $taxCalculationMethod): void
    {
        $fieldNameAmount = $this->orderSolver->getFieldNameAmount($typeTotal);
        $fieldNameAmountWithTax = $this->orderSolver->getFieldNameAmount($typeTotal, 'with_tax');
        $price = $this->productOrderSolver->getPriceByTypeTotal($productOrder, $typeTotal);
        $order->$fieldNameAmount += $price * $productOrder->quantity;
        $order->$fieldNameAmountWithTax += Price::getPriceWithTax(
                $price,
                $productOrder->taxRate->value,
                $taxCalculationMethod
            ) * $productOrder->quantity;
        $this->addProductOrderVat($order, $typeTotal, $price * $productOrder->quantity, $productOrder->taxRate, $taxCalculationMethod);
    }

    public function addProductOrderVat(
        Order   $order,
        string  $typeTotal,
        float   $priceTotalWithoutTax,
        TaxRate $taxRate,
        string  $taxCalculationMethod): void
    {
        $fieldName = $this->orderSolver->getFieldNameAmount($typeTotal, 'vat');

        if (!isset($order->$fieldName[$taxRate->id])) {
            $order->$fieldName[$taxRate->id] = 0;
        }

        $order->$fieldName[$taxRate->id] += Price::getVat($priceTotalWithoutTax, $taxRate->value, $taxCalculationMethod);
    }

    /**
     * Initialise le montant payé de la commande et le retourne.
     */
    public function initOrderPaidAmount(Order $order): void
    {
        $history = $order->payment;
        $order->paid_amount = 0;

        if ($history && count($history)) {
            foreach ($order->payment as $ch) {
                if ($ch->type == Payment::TYPE_PAYMENT) {
                    $order->paid_amount += $ch->amount;
                } elseif ($ch->type == Payment::TYPE_REFUND) {
                    $order->paid_amount -= $ch->amount;
                }
            }
        }
    }

    public function deleteOrder(Order $order, bool $force = false): bool
    {
        $return = false;
        $this->initOrder($order);

        // delete
        if ($this->producerSolver->getConfig('option_behavior_cancel_order') == Producer::BEHAVIOR_DELETE_ORDER_DELETE ||
            ($this->producerSolver->getConfig('option_behavior_cancel_order') == Producer::BEHAVIOR_DELETE_ORDER_STATUS && strlen($order->date_delete))
            || $force) {

            $this->productOrderBuilder->deleteProductOrdersByOrder($order);

            $return = $this->delete($order);
        }
        // status 'delete'
        elseif ($this->producerSolver->getConfig('option_behavior_cancel_order') == Producer::BEHAVIOR_DELETE_ORDER_STATUS) {
            $order->date_delete = date('Y-m-d H:i:s');
            $return = $this->update($order);
        }

        if($return) {
            $orderDeleteEvent = new OrderDeleteEvent();
            $orderDeleteEvent->order = $order;
            $order->trigger(Order::EVENT_DELETE, $orderDeleteEvent);
        }

        return $return;
    }

    /**
     * Ajuste le crédit pour que la commande soit payée.
     */
    public function processCredit(Order $order): void
    {
        if ($order->id_user) {
            $paymentStatus = $this->orderSolver->getPaymentStatus($order);

            if ($paymentStatus == Order::PAYMENT_PAID) {
                return;
            } elseif ($paymentStatus == Order::PAYMENT_SURPLUS) {
                $type = Payment::TYPE_REFUND;
                $amount = $this->orderSolver->getOrderAmount($order, Order::AMOUNT_SURPLUS);
            } elseif ($paymentStatus == Order::PAYMENT_UNPAID) {
                $type = Payment::TYPE_PAYMENT;
                $amount = $this->orderSolver->getOrderAmount($order, Order::AMOUNT_REMAINING);
            }

            $this->paymentBuilder->createPayment(
                $type,
                $amount,
                GlobalParam::getCurrentProducer(),
                $order->user,
                $this->userSolver->getCurrent(),
                MeanPayment::CREDIT,
                null,
                $order
            );
        }
    }

    public function updateOrderTillerSynchronization(Order $order, int $synchroTiller = null): void
    {
        if (!is_null($synchroTiller)) {
            $order->tiller_synchronization = $synchroTiller;
        } else {
            $amountPaid = $this->orderSolver->getOrderAmountPaidByCredit($order);

            if ($amountPaid >= 0.01) {
                $order->tiller_synchronization = 1;
            } else {
                $order->tiller_synchronization = 0;
            }
        }

        $this->saveUpdate($order);
    }

    /**
     * Changement de statut d'une commande.
     */
    public function updateOrderStatus(Order $order, string $newStatus, string $origin): void
    {
        $orderStatusArray = GlobalParam::get('orderStatus');
        $userCurrent = $this->userSolver->getCurrent();

        switch ($newStatus) {
            case 'new-order' :
                $order->status = $newStatus;
                $order->save();
                break;
            case 'waiting-paiement-on-delivery':
            case 'waiting-paiement-by-credit':
            case 'paid-by-credit':
            case 'waiting-delevery' :
            case 'delivered':
            case 'refunded':
            case 'cancel':
                if (in_array($newStatus, $orderStatusArray[$order->status]['nextStatusAllow'])) {
                    $order->status = $newStatus;
                    $order->save();
                }
                break;
            default:
                throw new NotFoundHttpException('Statut de commande inconnu.');
        }
    }

    // initReference
    // generateReference
    public function generateOrderReference(Order $order): void
    {
        $distribution = $this->distributionRepository->findOneDistributionById($order->id_distribution);
        $producer = $this->producerRepository->findOneProducerById($distribution->id_producer);

        if (!$order->reference && $producer->option_order_reference_type == Producer::ORDER_REFERENCE_TYPE_YEARLY) {
            $lastOrder = $this->orderRepository->findOneOrderLastOfYear($producer);

            if ($lastOrder && $lastOrder->reference && strlen($lastOrder->reference) > 0) {
                $pattern = '#A([0-9]+)C([0-9]+)#';
                preg_match($pattern, $lastOrder->reference, $matches, PREG_OFFSET_CAPTURE);
                $sizeNumReference = strlen($matches[2][0]);
                $numReference = ((int)$matches[2][0]) + 1;
                $numReference = str_pad($numReference, $sizeNumReference, '0', STR_PAD_LEFT);

                $order->reference = 'A' . $matches[1][0] . 'C' . $numReference;
            } else {
                $order->reference = 'A' . date('y') . 'C0001';
            }

            $this->saveUpdate($order);
        }
    }

    // initInvoicePrices
    // updateInvoicePrices
    public function updateOrderInvoicePrices(Order $order, array $params = []): void
    {
        foreach ($order->productOrder as $productOrder) {
            if ($productOrder->product) {
                $this->productOrderBuilder->updateProductOrderInvoicePrice($productOrder, $params);
            }
        }
    }

    /**
     * Initialise les commandes liées au point de vente.
     */
    public function initPointSaleOrders(PointSale $pointSale, array $ordersArray): void
    {
        $pointSale->orders = [];
        $pointSale->revenues = 0;
        $pointSale->revenues_with_tax = 0;

        if ($ordersArray) {
            foreach ($ordersArray as $order) {
                $this->initOrder($order);

                if ($pointSale->id == $order->id_point_sale) {
                    $pointSale->orders[] = $order;

                    if (is_null($order->date_delete)) {
                        $pointSale->revenues += (float)$order->amount;
                        $pointSale->revenues_with_tax += (float)$order->amount_with_tax;
                    }
                }
            }
        }
    }

    public function assignAllOrdersDeliveryNote(array $idOrders, DeliveryNote $deliveryNote)
    {
        $this->unassignAllOrdersDeliveryNote($deliveryNote);

        foreach ($idOrders as $idOrder) {
            $order = $this->orderRepository->findOneOrderById((int)$idOrder);
            if ($order) {
                $this->assignOrderDeliveryNote($order, $deliveryNote);
            }
        }
    }

    public function assignOrderDeliveryNote(Order $order, DeliveryNote $deliveryNote)
    {
        if ($this->orderSolver->isOrderFromProducer($order)) {
            $this->updateOrderDeliveryNote($order, $deliveryNote);

            $order = $this->orderRepository->findOneOrderById($order->id);
            $user = $this->userRepository->findOneUserById($deliveryNote->id_user);
            $userProducer = $this->userProducerRepository->findOneUserProducer($user);
            $this->updateOrderInvoicePrices($order,
                [
                    'user' => $user,
                    'user_producer' => $userProducer,
                    'point_sale' => $order->pointSale
                ]);
        }
    }

    public function unassignAllOrdersDeliveryNote(DeliveryNote $deliveryNote)
    {
        Order::updateAll([
            'id_delivery_note' => null
        ], [
            'id_delivery_note' => $deliveryNote->id
        ]);
    }

    public function unassignAllOrdersInvoiceByDeliveryNote(DeliveryNote $deliveryNote)
    {
        Order::updateAll([
            'id_invoice' => null
        ], [
            'id_delivery_note' => $deliveryNote->id
        ]);
    }

    public function assignAllOrdersInvoiceByDeliveryNote(Invoice $invoice, DeliveryNote $deliveryNote)
    {
        Order::updateAll([
            'id_invoice' => $invoice->id
        ], [
            'id_delivery_note' => $deliveryNote->id
        ]);
    }

    public function updateOrderIgnoreWhenInvoicing(Order $order, bool $ignoreWhenInvoicing)
    {
        $order->ignore_when_invoicing = $ignoreWhenInvoicing;
        $this->update($order);
    }

    public function updateOrderDocument(Order $order, DocumentInterface $document = null)
    {
        if($this->documentSolver->isDocumentInvoice($document)) {
            $this->updateOrderInvoice($order, $document);
        }
        elseif($this->documentSolver->isDocumentDeliveryNote($document)) {
            $this->updateOrderDeliveryNote($order, $document);
        }
    }

    public function updateOrderInvoice(Order $order, Invoice $invoice = null)
    {
        if($invoice) {
            $order->populateInvoice($invoice);
        }
        else {
            $order->id_invoice = null;
        }

        $this->update($order);
    }

    public function updateOrderDeliveryNote(Order $order, DeliveryNote $deliveryNote = null)
    {
        if($deliveryNote) {
            $order->populateDeliveryNote($deliveryNote);
        }
        else {
            $order->id_delivery_note = null;
        }

        $this->update($order);
    }
}