<?php

declare(strict_types=1);

/**
 * Export comptable des commandes / remboursements.
 */
class OrderExporter
{
    /**
     * Comptes produits par pays et par taux de tva.
     */
    public const PR_BY_CTRY
        = [
            8 => ['5.500' => '7071101', '10.000' => '7071102', '20.000' => '70711'], // France
            19 => ['5.500' => '7071301', '10.000' => '7071302', '20.000' => '70713'], // Suisse
        ];

    /**
     * Comptes TVA par taux de TVA.
     */
    public const TVA_BY_TAUX
        = [
            '5.500' => '4457111',
            '10.000' => '4457113',
            '20.000' => '445711',
        ];

    /**
     * @var OrderInvoice
     */
    protected $orderInvoice;

    /**
     * @var Order
     */
    protected $order;

    /**
     * @var Address
     */
    protected $addressInvoice;

    /**
     * @var OrderPayment[]
     */
    protected $orderPayments;

    /**
     * @var OrderSlip[]
     */
    protected $orderSlips;

    /**
     * Export de la TVA détaillée pour une commande.
     *
     * @return array<int|string, float|int>
     */
    protected function getGlobalOrderTVA(): array
    {
        $ret = [
            '5.500' => 0,
            '10.000' => 0,
            '20.000' => 0,
        ];

        // + TVA Sur les frais de port
        $tvaFdp = $this->orderInvoice->total_shipping_tax_incl - $this->orderInvoice->total_shipping_tax_excl;
        if ($tvaFdp) {
            $carrier = new Carrier($this->order->id_carrier);
            $idTaxRuleGroup = $carrier->getIdTaxRulesGroup(Context::getContext());

            // TODO : A patcher
            $tauxTva = 1 == $idTaxRuleGroup ? '20.000' : 0;
            if (!$tauxTva) {
                throw new Exception('Not implemented');
            }

            $ret[$tauxTva] += $tvaFdp;
        }

        // + TVA sur les produits
        /** @var OrderDetail[] $orderDetails */
        $orderDetails = ObjectModel::hydrateCollection(
            OrderDetail::class,
            $this->order->getOrderDetailList()
        );
        foreach ($orderDetails as $orderDetail) {
            $tvaProduit = $orderDetail->total_price_tax_incl - $orderDetail->total_price_tax_excl;
            if (!$tvaProduit) {
                continue;
            }

            $orderDetailTaxes = OrderDetail::getTaxListStatic($orderDetail->id);
            if (count($orderDetailTaxes) > 1) {
                throw new Exception('Not implemented');
            }
            foreach ($orderDetailTaxes as $orderDetailTax) {
                $rate = (string) (new Tax($orderDetailTax['id_tax']))->rate;

                $ret[$rate] += $orderDetailTax['total_amount'];
            }
            /*
                        $taxTvaProduit = self::calculTauxTva(floatval($orderDetail->total_price_tax_incl), floatval($orderDetail->total_price_tax_excl));

                        $ret[strval($taxTvaProduit)] += $tvaProduit;
            */
        }

        // - Correction arrondis de TVA
        $totalTva = round($this->orderInvoice->total_paid_tax_incl - $this->orderInvoice->total_paid_tax_excl, 2);
        $calcTva = round(array_sum($ret), 2);
        if ($totalTva != $calcTva) {
            // Correction à ajouter au plus haut taux de TVA
            $correctionTva = $totalTva - $calcTva;
            $tauxTva = array_keys($ret);
            // Tri par ordre décroissants des taux de TVA
            rsort($tauxTva, \SORT_NUMERIC);

            foreach ($tauxTva as $taux) {
                if ($ret[$taux] > 0) {
                    $ret[$taux] += $correctionTva;
                    $correctionTva = 0;
                }
            }
        }

        // - TVA sur les réductions
        /*
        $tvaDiscount = $this->orderInvoice->total_discount_tax_incl - $this->orderInvoice->total_discount_tax_excl;
        if ($tvaDiscount) {
            //$taxTvaDiscount               = self::calculTauxTva(floatval($this->orderInvoice->total_discount_tax_incl), floatval($this->orderInvoice->total_discount_tax_excl));
            // Par défaut on considère que le taux de TVA sur les réductions est de 20%
            $taxTvaDiscount = '20.000';
            $ret[$taxTvaDiscount] -= $tvaDiscount;
        }*/

        return $ret;
    }

    /**
     * Calcul du montant HT des produits, classés par taux de TVA appliquée.
     *
     * @return array<int|string, float|int>
     */
    protected function getProductInvoiceHtByTVA(): array
    {
        $ret = [
            '5.500' => 0,
            '10.000' => 0,
            '20.000' => 0,
        ];

        // + TVA sur les produits
        /** @var OrderDetail[] $orderDetails */
        $orderDetails = ObjectModel::hydrateCollection(
            OrderDetail::class,
            $this->order->getOrderDetailList()
        );
        foreach ($orderDetails as $orderDetail) {
            $tvaProduit = $orderDetail->total_price_tax_incl - $orderDetail->total_price_tax_excl;
            if (!$tvaProduit) {
                // Cas spécial : les suisses ne payaient pas de TVA (TODO : A voir comment traiter ça correctement)
                $ret['20.000'] += $orderDetail->total_price_tax_excl;
            } else {
                $orderDetailTaxes = OrderDetail::getTaxListStatic($orderDetail->id);
                if (count($orderDetailTaxes) > 1) {
                    throw new Exception('Not implemented');
                }
                foreach ($orderDetailTaxes as $orderDetailTax) {
                    $rate = (string) (new Tax($orderDetailTax['id_tax']))->rate;

                    $ret[$rate] += $orderDetail->total_price_tax_excl;
                }
            }
        }

        // Correction du centime d'erreur de PrestaShop
        $expectedSum = $this->order->total_products;
        $diff = round($expectedSum - round(array_sum($ret), 2), 2);
        if ($diff <= 0.02) {
            if ($ret['20.000'] > 0) {
                $ret['20.000'] += $diff;
            } elseif ($ret['10.000'] > 0) {
                $ret['10.000'] += $diff;
            } elseif ($ret['5.500'] > 0) {
                $ret['5.500'] += $diff;
            }
        }

        return $ret;
    }

    /**
     * Export de la TVA détaillée pour une commande.
     *
     * @return array<int|string, float|int>
     */
    protected function getGlobalOrderSlipTVA(OrderSlip $orderSlip): array
    {
        $ret = [
            '5.500' => 0,
            '10.000' => 0,
            '20.000' => 0,
        ];

        // + TVA Sur les frais de port
        $tvaFdp = $orderSlip->total_shipping_tax_incl - $orderSlip->total_shipping_tax_excl;
        if ($tvaFdp) {
            $carrier = new Carrier($this->order->id_carrier);
            $idTaxRuleGroup = $carrier->getIdTaxRulesGroup(Context::getContext());

            // TODO : A patcher
            $tauxTva = 1 == $idTaxRuleGroup ? '20.000' : 0;
            if (!$tauxTva) {
                throw new Exception('Not implemented');
            }

            $ret[$tauxTva] += $tvaFdp;
        }

        // + TVA sur les produits
        /** @var array<mixed> $orderSlipDetails */
        $orderSlipDetails = OrderSlip::getOrdersSlipDetail($orderSlip->id);
        foreach ($orderSlipDetails as $orderSlipDetail) {
            $tvaProduit = $orderSlipDetail['total_price_tax_incl'] - $orderSlipDetail['total_price_tax_excl'];
            if (!$tvaProduit) {
                continue;
            }

            $orderDetailTaxes = OrderDetail::getTaxListStatic($orderSlipDetail['id_order_detail']);
            foreach ($orderDetailTaxes as $orderDetailTax) {
                $rate = (string) (new Tax($orderDetailTax['id_tax']))->rate;

                $ret[$rate] += $tvaProduit;
            }
        }

        // - TVA sur les réductions (dans le cas d'un remboursement total)
        if (1 == $orderSlip->order_slip_type) {
            // TODO : A améliorer
            $tvaDiscountRate = '20.000';
            $ret[$tvaDiscountRate] -= ($this->order->total_discounts_tax_incl - $this->order->total_discounts_tax_excl);
        }

        return $ret;
    }

    /**
     * Calcul du montant HT des produits, classés par taux de TVA appliquée.
     *
     * @return array<int|string, (float|int)>
     */
    protected function getProductOrderSlipHtByTVA(OrderSlip $orderSlip): array
    {
        $ret = [
            '5.500' => 0,
            '10.000' => 0,
            '20.000' => 0,
        ];

        // Total HT de produit (regroupé par taux de TVA)
        /** @var array<mixed> $orderSlipDetails */
        $orderSlipDetails = OrderSlip::getOrdersSlipDetail($orderSlip->id);
        foreach ($orderSlipDetails as $orderSlipDetail) {
            $orderDetailTaxes = OrderDetail::getTaxListStatic($orderSlipDetail['id_order_detail']);
            if (count($orderDetailTaxes) > 1) {
                throw new Exception('Not implemented');
            }
            foreach ($orderDetailTaxes as $orderDetailTax) {
                $rate = (string) (new Tax($orderDetailTax['id_tax']))->rate;

                $ret[$rate] += $orderSlipDetail['total_price_tax_excl'];
            }
        }

        // Ajout des réductions HT
        if (1 == $orderSlip->order_slip_type) {
            // TODO : A améliorer
            $tvaDiscountRate = '20.000';
            // $ret[$tvaDiscountRate] -= $this->order->total_discounts_tax_excl;
        }

        return $ret;
    }

    /**
     * @return array<int, array<string,float|int|string>>
     */
    protected function getLignesComptablesFacture(): array
    {
        $invoiceDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->order->invoice_date);
        $ret = [];
        $totalPayments = array_reduce(
            $this->orderPayments,
            function (float $carry, OrderPayment $orderPayment) {
                $carry += $orderPayment->amount;

                return $carry;
            },
            0.0
        );

        if (!$invoiceDate instanceof DateTimeImmutable) {
            throw new Exception('La date de la facture n\'a pu être récupérée.');
        }

        // 1 - Montant payé par le client [4110001]
        $ret[] = [
            'date' => $invoiceDate->format('d/m/Y'),
            'journal' => 'VE',
            'compte' => '4110001',
            'intitule' => 'Vente internet',
            'nofacture' => $this->orderInvoice->number,
            'debit' => round($this->orderInvoice->total_paid_tax_incl, 2),
            'credit' => 0.00,
        ];

        // 2 - Montant des remises (HT) [7097]
        if (round($this->orderInvoice->total_discount_tax_excl, 2) > 0) {
            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '7097',
                'intitule' => 'Vente internet',
                'nofacture' => $this->orderInvoice->number,
                'debit' => round($this->orderInvoice->total_discount_tax_excl, 2),
                'credit' => 0.00,
            ];
        }

        // 3 - TVA Totale réglée par le client [445711]
        $tvas = $this->getGlobalOrderTVA();
        foreach ($tvas as $taux => $montant) {
            if (!$montant) {
                continue;
            }

            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => self::TVA_BY_TAUX[$taux],
                'intitule' => 'Vente internet',
                'nofacture' => $this->orderInvoice->number,
                'debit' => 0.00,
                'credit' => round($montant, 2),
            ];
        }

        // 4 - Total produits HT réglé [70711, 70112, 70713]
        $produits = $this->getProductInvoiceHtByTVA();
        foreach ($produits as $taux => $montant) {
            if (!$montant) {
                continue;
            }

            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => self::PR_BY_CTRY[$this->addressInvoice->id_country][$taux] ?? 70112,
                'intitule' => 'Vente internet',
                'nofacture' => $this->orderInvoice->number,
                'debit' => 0.00,
                'credit' => round($montant, 2),
            ];
        }

        // 5 - Total frais de port HT [7080001]
        if ($this->orderInvoice->total_shipping_tax_excl > 0) {
            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '7080001',
                'intitule' => 'Vente internet',
                'nofacture' => $this->orderInvoice->number,
                'debit' => 0.00,
                'credit' => round($this->orderInvoice->total_shipping_tax_excl, 2),
            ];
        }

        // 6 - Commission bancaire (débit) [627]
        $ret[] = [
            'date' => $invoiceDate->format('d/m/Y'),
            'journal' => 'VE',
            'compte' => '627',
            'intitule' => 'Vente internet',
            'nofacture' => $this->orderInvoice->number,
            'debit' => round($totalPayments * 0.29 / 100, 2),
            'credit' => 0.00,
        ];

        // 7 - Commission bancaire (crédit) [4000018]
        $ret[] = [
            'date' => $invoiceDate->format('d/m/Y'),
            'journal' => 'VE',
            'compte' => '4000018',
            'intitule' => 'Vente internet',
            'nofacture' => $this->orderInvoice->number,
            'debit' => 0.00,
            'credit' => round($totalPayments * 0.29 / 100, 2),
        ];

        // 8 - Micro-Don pour JungleVet
        $cart = new Cart($this->order->id_cart);
        if ($cart->donation_amount > 0) {
            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'OD',
                'compte' => '4110001',
                'intitule' => 'Micro-Don',
                'nofacture' => $this->orderInvoice->number,
                'debit' => round($cart->donation_amount, 2),
                'credit' => 0.00,
            ];
            $ret[] = [
                'date' => $invoiceDate->format('d/m/Y'),
                'journal' => 'OD',
                'compte' => '4675',
                'intitule' => 'Micro-Don',
                'nofacture' => $this->orderInvoice->number,
                'debit' => 0.00,
                'credit' => round($cart->donation_amount, 2),
            ];
        }

        return $ret;
    }

    /**
     * @return array<int, array<string, array<int|string,string>|float|int|string>>
     */
    protected function getLignesComptablesAvoir(OrderSlip $orderSlip): array
    {
        $orderSlipDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $orderSlip->date_add);
        if (!$orderSlipDate instanceof DateTimeImmutable) {
            throw new Exception('La date du bon de commande n\'a pu être réccupérée.');
        }
        $ret = [];

        if ($orderSlip->has_voucher) {
            // N'est plus utilisé, gardé pour rétro-compatibilité sur les anciens avoirs

            // Remboursement sous forme d'un bon de réduction
            // 1 - Montant payé par le client [4110001]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '4110001',
                'intitule' => 'Remboursement internet BR',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => 0.00,
                'credit' => round($orderSlip->total_products_tax_incl, 2),
            ];

            // 3 - TVA Totale réglée par le client [445711]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '445711',
                'intitule' => 'Remboursement internet BR',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => round($orderSlip->total_products_tax_incl - $orderSlip->total_products_tax_excl, 2),
                'credit' => 0.00,
            ];
            // TODO : Mettre à jour les comptes :
            /*
             *  70711 :   Ventes internet 20% (celui qu’on utilise actuellement)
                7071101 : Ventes internet 5,5%
                7071102 : Ventes internet 10%

                70713   :  Ventes internet SUISSE autre que UE 20%  (celui qu’on utilise actuellement)
                7071301 : Ventes internet SUISSE autre que UE 5,5%
                7071302 : Ventes internet SUISSE autre que UE 10%
             */

            // 4 - Total produits HT réglé [70711, 70112, 70713]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => self::PR_BY_CTRY[$this->addressInvoice->id_country] ?? 70112,
                'intitule' => 'Remboursement internet BR',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => round($orderSlip->total_products_tax_excl, 2),
                'credit' => 0.00,
            ];
        } else {
            // 1 - Montant payé par le client [4110001]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '4110001',
                'intitule' => 'Remboursement internet CB',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => 0.00,
                'credit' => round($orderSlip->total_products_tax_incl + $orderSlip->total_shipping_tax_incl, 2),
            ];

            // 2 - Montant des remises (HT) [7097]
            if (1 == $orderSlip->order_slip_type && round($this->order->total_discounts_tax_excl, 2) > 0) {
                $ret[] = [
                    'date' => $orderSlipDate->format('d/m/Y'),
                    'journal' => 'VE',
                    'compte' => '7097',
                    'intitule' => 'Remboursement internet CB',
                    'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                    'debit' => 0.00,
                    'credit' => round($this->order->total_discounts_tax_excl, 2),
                ];
            }

            // 3 - TVA Totale réglée par le client [445711]
            $tvas = $this->getGlobalOrderSlipTVA($orderSlip);
            foreach ($tvas as $taux => $montant) {
                if (!$montant) {
                    continue;
                }

                $ret[] = [
                    'date' => $orderSlipDate->format('d/m/Y'),
                    'journal' => 'VE',
                    'compte' => self::TVA_BY_TAUX[$taux],
                    'intitule' => 'Remboursement internet CB',
                    'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                    'debit' => round(
                        $montant,
                        2
                    ),
                    'credit' => 0.00,
                ];
            }

            // 4 - Total produits HT réglé [70711, 70112, 70713]
            $produits = $this->getProductOrderSlipHtByTVA($orderSlip);
            foreach ($produits as $taux => $montant) {
                if (!$montant) {
                    continue;
                }

                $ret[] = [
                    'date' => $orderSlipDate->format('d/m/Y'),
                    'journal' => 'VE',
                    'compte' => self::PR_BY_CTRY[$this->addressInvoice->id_country][$taux] ?? 70112,
                    'intitule' => 'Remboursement internet CB',
                    'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                    'debit' => round($montant, 2),
                    'credit' => 0.00,
                ];
            }

            // 5 - Total frais de port HT [7080001]
            if ($orderSlip->total_shipping_tax_excl > 0) {
                $ret[] = [
                    'date' => $orderSlipDate->format('d/m/Y'),
                    'journal' => 'VE',
                    'compte' => '7080001',
                    'intitule' => 'Remboursement internet CB',
                    'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                    'debit' => round($orderSlip->total_shipping_tax_excl, 2),
                    'credit' => 0.00,
                ];
            }

            // 6 - Commission bancaire (débit) [627]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '627',
                'intitule' => 'Remboursement internet CB',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => 0.00,
                'credit' => round($orderSlip->amount * 0.29 / 100, 2),
            ];

            // 7 - Commission bancaire (crédit) [4000018]
            $ret[] = [
                'date' => $orderSlipDate->format('d/m/Y'),
                'journal' => 'VE',
                'compte' => '4000018',
                'intitule' => 'Remboursement internet CB',
                'nofacture' => $this->orderInvoice->number . '-' . $orderSlip->id,
                'debit' => round($orderSlip->amount * 0.29 / 100, 2),
                'credit' => 0.00,
            ];
        }

        return $ret;
    }

    /**
     * @param OrderPayment[]|null $orderPayments
     * @param OrderSlip[]|null $orderSlips
     *
     * @throws PrestaShopDatabaseException
     * @throws PrestaShopException
     */
    public function __construct(
        OrderInvoice $orderInvoice,
        ?Order $order = null,
        ?array $orderPayments = null,
        ?array $orderSlips = null
    ) {
        $this->orderInvoice = $orderInvoice;
        if (!$order) {
            $this->order = new Order($orderInvoice->id_order);
        } else {
            $this->order = $order;
        }
        if (!$orderPayments) {
            $this->orderPayments = $this->order->getOrderPayments();
        } else {
            $this->orderPayments = $orderPayments;
        }
        $this->addressInvoice = new Address($this->order->id_address_invoice);

        if (!$orderSlips) {
            $result = OrderSlip::getOrdersSlip($this->order->id_customer, $orderInvoice->id_order);

            if (!is_array($result)) {
                $result = [];
            }

            $orderSlips = ObjectModel::hydrateCollection(
                OrderSlip::class, $result
            );

            if (count($orderSlips)) {
                /* @var OrderSlip[] orderSlips */
                $this->orderSlips = $orderSlips;
            }
        } else {
            $this->orderSlips = $orderSlips;
        }
    }

    public function isExportable(): bool
    {
        return !empty($this->orderInvoice->invoice_number);
    }

    public function hasSlip(): bool
    {
        return null !== $this->orderSlips && count($this->orderSlips) > 0;
    }

    /**
     * @return array<int|string,mixed>
     *
     * @throws Exception
     */
    public function getLignesComptables(DateTimeInterface $dateDebut, DateTimeInterface $dateFin): array
    {
        $ret = [];

        // / Export des lignes de la facture
        $invoiceDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->order->invoice_date);

        if ($invoiceDate >= $dateDebut && $invoiceDate <= $dateFin) {
            // Export OK
            $ret = array_merge($ret, $this->getLignesComptablesFacture());
        }

        // / Export des lignes du remboursement
        if ($this->orderSlips) {
            foreach ($this->orderSlips as $orderSlip) {
                $orderSlipDate = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $orderSlip->date_add);

                if ($orderSlipDate >= $dateDebut && $orderSlipDate <= $dateFin) {
                    // Export OK
                    $ret = array_merge($ret, $this->getLignesComptablesAvoir($orderSlip));
                }
            }
        }

        return $ret;
    }

    /**
     * Détermine si une série de lignes comptables sont cohérentes ou non.
     *
     * Vérification effectuée : somme des crédits = sommes des débits (à 1ct près)
     *
     * @param array<string, mixed> $lignesComptables
     */
    public static function ensureCoherent(array $lignesComptables): bool
    {
        $sommes = array_reduce(
            $lignesComptables,
            function ($sommes, $ligne) {
                $sommes['credit'] += $ligne['credit'];
                $sommes['debit'] += $ligne['debit'];

                return $sommes;
            },
            [
                'credit' => 0.0,
                'debit' => 0.0,
            ]
        );

        return 0 == abs(round($sommes['credit'] - $sommes['debit'], 2));
    }

    public function getOrderInvoice(): OrderInvoice
    {
        return $this->orderInvoice;
    }

    public function getOrder(): Order
    {
        return $this->order;
    }

    /**
     * @return OrderSlip[]|null
     */
    public function getOrderSlips(): ?array
    {
        return $this->orderSlips;
    }
}
