<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Utils;

use Cocur\Slugify\Slugify;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Filesystem\Filesystem;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\PaymentMethod;
use CpCreation\VitiCore\Contact\Model\Contact;
use CpCreation\VitiCore\Shop\Model\ShopConfig;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Knp\Snappy\Pdf as SnappyPdf;
use Spatie\Browsershot\Browsershot;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;

class Pdf
{
    public function __construct(
        private SnappyPdf $pdf,
        private Environment $twig,
        private Filesystem $filesystem,
        private TranslatorInterface $translator,
        private ShopConfigRepository $shopConfigRepository,
        private string $kernelProjectDir = '',
        private string $includePathNode = '',
        private string $includePathNpm = '',
        private string $includePath = '',
        private string $includePathChromium = '',
    ) {
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfOrder(Contact $contact, Cart $cart, ?string $locale = null, string $documentLabelKey = 'checkout_design.document_order'): string
    {
        return $this->createDocumentPdf($contact, $cart, false, 'pdf/order.html.twig', $locale, $documentLabelKey);
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfOrderAdmin(Contact $contact, Cart $cart, ?string $locale = null, string $documentLabelKey = 'checkout_design.document_order'): string
    {
        return $this->createDocumentPdf($contact, $cart, true, 'pdf/order.html.twig', $locale, $documentLabelKey);
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfInvoice(Contact $contact, Cart $cart, ?string $locale = null, string $documentLabelKey = 'checkout_design.document_invoice'): string
    {
        return $this->createDocumentPdf($contact, $cart, false, 'pdf/invoice.html.twig', $locale, $documentLabelKey);
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfInvoiceAdmin(Contact $contact, Cart $cart, ?string $locale = null, string $documentLabelKey = 'checkout_design.document_invoice'): string
    {
        return $this->createDocumentPdf($contact, $cart, true, 'pdf/invoice.html.twig', $locale, $documentLabelKey);
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfCartDocument(Contact $contact, Cart $cart, ?string $locale = null): string
    {
        return $cart->isInvoiceDocumentRequested()
            ? $this->createPdfInvoice($contact, $cart, $locale, $cart->getDocumentLabelKey())
            : $this->createPdfOrder($contact, $cart, $locale, $cart->getDocumentLabelKey());
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    public function createPdfCartDocumentAdmin(Contact $contact, Cart $cart, ?string $locale = null): string
    {
        return $cart->isInvoiceDocumentRequested()
            ? $this->createPdfInvoiceAdmin($contact, $cart, $locale, $cart->getDocumentLabelKey())
            : $this->createPdfOrderAdmin($contact, $cart, $locale, $cart->getDocumentLabelKey());
    }

    /**
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     */
    private function createDocumentPdf(Contact $contact, Cart $cart, bool $admin, string $template, ?string $locale = null, string $documentLabelKey = 'checkout_design.document_order'): string
    {
        if (null !== $locale && '' !== $locale) {
            $this->translator->setLocale($locale);
        }

        $documentLabel = $this->translator->trans($documentLabelKey, [], 'messages', $locale ?: null);
        $shopConfig = $this->shopConfigRepository->findOne();
        $html = $this->twig->render(
            $template,
            [
                'contact' => $contact,
                'cart' => $cart,
                'admin' => $admin,
                'shop_config' => $shopConfig,
                'document_label' => $documentLabel,
                'invoice_context' => $this->buildInvoiceContext($cart, $shopConfig),
            ]
        );
        $slugify = new Slugify();

        $fileName = $slugify->slugify($documentLabel)
            .'_'
            .$slugify->slugify((string) $cart->getLastname())
            .'_'.date('d-m-Y_H-i')
            .'_'.uniqid('', true)
            .'.pdf';

        $temporaryPath = sys_get_temp_dir().'/'.$fileName;

        $content = $this->renderPdfContent($html, $temporaryPath);

        $this->filesystem->fs('order')->write($fileName, $content);

        @unlink($temporaryPath);

        return $fileName;
    }

    private function renderPdfContent(string $html, string $targetPath): string
    {
        $this->createBrowsershot($html)->savePdf($targetPath);

        $content = file_get_contents($targetPath);

        if (false === $content) {
            throw new \RuntimeException(\sprintf('Impossible de lire le PDF généré: %s', $targetPath));
        }

        return $content;
    }

    /**
     * @return array{
     *     invoice_date: \DateTimeInterface,
     *     order_date: \DateTimeInterface,
     *     payment_due_at: ?\DateTimeInterface,
     *     payment_paid_at: ?\DateTimeInterface,
     *     payment_term_label: ?string,
     *     payment_method_label: ?string,
     *     product_total_ht: int,
     *     product_total_ht_before_promos: int,
     *     product_total_ttc: int,
     *     product_tax: int,
     *     product_tax_rate_label: ?string,
     *     shipping_tax_rate_label: ?string,
     *     total_tax_rate_label: ?string,
     *     invoice_number: ?string,
     *     vat_breakdown: list<array{rate_label: string, base_ht: int, amount_tax: int}>,
     *     shipping_ht_before_promos: int,
     *     shipping_ht: int,
     *     shipping_ttc: int,
     *     shipping_tax: int,
     *     discount_ht: int,
     *     degressive_ht: int,
     *     discount_total_ht: int,
     *     promo_item_discounts: list<array{label: string, amount_ht: int}>,
     *     promo_shipping_discounts: list<array{label: string, amount_ht: int}>
     * }
     */
    private function buildInvoiceContext(Cart $cart, ?ShopConfig $shopConfig): array
    {
        $pricing = $cart->getPricing();
        $invoiceDate = $cart->getCartCheckout()?->getPayedAt()
            ?? $cart->getCartCheckout()?->getOrderedAt()
            ?? $cart->getCreatedAt();
        $orderDate = $cart->getCartCheckout()?->getOrderedAt() ?? $cart->getCreatedAt();
        $shipping = $cart->getCartShipping();
        $shippingTaxRate = max(0.0, (float) ($shipping?->taxRate ?? $pricing?->taxRate ?? 0.0));
        $shippingTTC = max(0, (int) ($shipping?->amountTTC ?? 0));
        $promoShippingTTC = $this->resolvePromoShippingDiscountTTC($cart);
        $shippingNetTTC = max(0, $shippingTTC - $promoShippingTTC);
        $shippingHT = $this->computeHTFromTTC($shippingNetTTC, $shippingTaxRate);
        $shippingTax = $shippingNetTTC - $shippingHT;
        $paymentTerm = $shopConfig?->getInvoicePaymentTerm();
        $paymentTermLabel = $this->resolveInvoicePaymentTermLabel($paymentTerm);
        $paymentMethodLabel = $this->resolvePaymentMethodLabel($cart->getPaymentMethod());
        $productTotalHT = max(0, ($pricing?->totalHT ?? 0) - $shippingHT);
        $productTotalTTC = max(0, ($pricing?->totalTTC ?? 0) - $shippingNetTTC);
        $productTax = max(0, ($pricing?->totalTax ?? 0) - $shippingTax);
        $promoItemDiscounts = $this->buildPromoItemDiscounts($cart, $pricing?->taxRate ?? 0.0);
        $promoShippingDiscounts = $this->buildPromoShippingDiscounts($cart, $shippingTaxRate);
        $promoItemsDiscountHTTotal = array_sum(array_map(static fn (array $discount): int => $discount['amount_ht'], $promoItemDiscounts));
        $promoShippingDiscountHTTotal = array_sum(array_map(static fn (array $discount): int => $discount['amount_ht'], $promoShippingDiscounts));
        $productTaxRateLabel = $this->formatTaxRatePercentLabel($pricing?->taxRate ?? 0.0);
        $shippingTaxRateLabel = $this->formatTaxRatePercentLabel($shippingTaxRate);
        $totalTaxRateLabel = $productTaxRateLabel === $shippingTaxRateLabel
            ? $productTaxRateLabel
            : null;

        return [
            'invoice_date' => $invoiceDate,
            'order_date' => $orderDate,
            'payment_due_at' => $this->resolvePaymentDueDate($invoiceDate, $paymentTerm),
            'payment_paid_at' => $cart->getCartCheckout()?->getPayedAt(),
            'payment_term_label' => $paymentTermLabel,
            'payment_method_label' => $paymentMethodLabel,
            'product_total_ht' => $productTotalHT,
            'product_total_ht_before_promos' => $productTotalHT + $promoItemsDiscountHTTotal,
            'product_total_ttc' => $productTotalTTC,
            'product_tax' => $productTax,
            'product_tax_rate_label' => $productTaxRateLabel,
            'shipping_tax_rate_label' => $shippingTaxRateLabel,
            'total_tax_rate_label' => $totalTaxRateLabel,
            'invoice_number' => $this->formatInvoiceNumber($cart, $shopConfig),
            'vat_breakdown' => $this->buildVatBreakdown($cart, $shippingHT, $shippingTax),
            'shipping_ht_before_promos' => $shippingHT + $promoShippingDiscountHTTotal,
            'shipping_ht' => $shippingHT,
            'shipping_ttc' => $shippingNetTTC,
            'shipping_tax' => $shippingTax,
            'discount_ht' => $pricing?->discountHT ?? 0,
            'degressive_ht' => $pricing?->degressiveHT ?? 0,
            'discount_total_ht' => ($pricing?->discountHT ?? 0) + ($pricing?->degressiveHT ?? 0),
            'promo_item_discounts' => $promoItemDiscounts,
            'promo_shipping_discounts' => $promoShippingDiscounts,
        ];
    }

    /**
     * @return list<array{label: string, amount_ht: int}>
     */
    private function buildPromoItemDiscounts(Cart $cart, float $taxRate): array
    {
        $discounts = [];

        foreach ($cart->getPromos() as $promo) {
            if ($promo->discountItemsTTC <= 0) {
                continue;
            }

            $discounts[] = [
                'label' => $this->buildPromoDiscountLabel($promo),
                'amount_ht' => $this->computeHTFromTTC($promo->discountItemsTTC, $taxRate),
            ];
        }

        return $discounts;
    }

    /**
     * @return list<array{label: string, amount_ht: int}>
     */
    private function buildPromoShippingDiscounts(Cart $cart, float $taxRate): array
    {
        $discounts = [];

        foreach ($cart->getPromos() as $promo) {
            if ($promo->discountShippingTTC <= 0) {
                continue;
            }

            $discounts[] = [
                'label' => $this->buildPromoDiscountLabel($promo),
                'amount_ht' => $this->computeHTFromTTC($promo->discountShippingTTC, $taxRate),
            ];
        }

        return $discounts;
    }

    private function buildPromoDiscountLabel(object $promo): string
    {
        $label = '';

        if (null !== $promo->percent && $promo->percent > 0) {
            $label = number_format($promo->percent * 100, 0, ',', ' ').'%';
        } elseif (null !== $promo->amountTTC && $promo->amountTTC > 0) {
            $label = $this->formatPriceLabel($promo->amountTTC).' TTC';
        }

        if (null !== $promo->code && '' !== mb_trim($promo->code)) {
            $label .= ('' !== $label ? ' ' : '').'('.$promo->code.')';
        }

        return '' !== $label ? $label : 'promo';
    }

    private function formatPriceLabel(int $price): string
    {
        return number_format($price / 100, 2, ',', ' ');
    }

    private function formatInvoiceNumber(Cart $cart, ?ShopConfig $shopConfig = null): ?string
    {
        $invoice = $cart->getInvoice();

        if (null === $invoice || null === $invoice->getNumberDebit()) {
            return null;
        }

        return sprintf(
            '%s-%05d%s',
            (string) $invoice->getYear(),
            $invoice->getNumberDebit(),
            $shopConfig?->getInvoiceSuffix() ?? ''
        );
    }

    private function resolveInvoicePaymentTermLabel(?string $paymentTerm): ?string
    {
        $labels = array_flip(ShopConfig::getInvoicePaymentTermChoices());

        return $labels[$paymentTerm] ?? null;
    }

    private function resolvePaymentMethodLabel(?PaymentMethod $paymentMethod): ?string
    {
        if (null === $paymentMethod) {
            return null;
        }

        $configuration = method_exists($paymentMethod, 'getConfiguration')
            ? $paymentMethod->getConfiguration()
            : [];

        $configuredName = $configuration['name'] ?? null;
        if (\is_string($configuredName) && '' !== $configuredName) {
            return $configuredName;
        }

        $method = $paymentMethod->getMethod();

        return match ($method) {
            'check' => $this->translator->trans('checkout_design.payment_check'),
            'bankTransfert', 'banktransfert' => $this->translator->trans('checkout_design.payment_banktransfert'),
            default => $paymentMethod->isPaymentOnline()
                ? $this->translator->trans('checkout_design.payment_card')
                : $method,
        };
    }

    private function resolvePaymentDueDate(\DateTimeInterface $invoiceDate, ?string $paymentTerm): ?\DateTimeImmutable
    {
        $date = \DateTimeImmutable::createFromInterface($invoiceDate);

        return match ($paymentTerm) {
            ShopConfig::INVOICE_PAYMENT_TERM_IMMEDIATE => $date,
            ShopConfig::INVOICE_PAYMENT_TERM_RECEIPT => $date,
            ShopConfig::INVOICE_PAYMENT_TERM_15_DAYS => $date->modify('+15 days'),
            ShopConfig::INVOICE_PAYMENT_TERM_30_DAYS => $date->modify('+30 days'),
            ShopConfig::INVOICE_PAYMENT_TERM_30_DAYS_END_OF_MONTH => $date->modify('last day of this month')->modify('+30 days'),
            ShopConfig::INVOICE_PAYMENT_TERM_45_DAYS_END_OF_MONTH => $date->modify('last day of this month')->modify('+45 days'),
            ShopConfig::INVOICE_PAYMENT_TERM_60_DAYS => $date->modify('+60 days'),
            default => null,
        };
    }

    /**
     * @return list<array{rate_label: string, base_ht: int, amount_tax: int}>
     */
    private function buildVatBreakdown(Cart $cart, int $shippingHT, int $shippingTax): array
    {
        $pricing = $cart->getPricing();
        $breakdown = [];

        foreach ($pricing?->lines ?? [] as $line) {
            $rateLabel = $this->formatTaxRateLabel($line->netHT, $line->tax);
            if (!isset($breakdown[$rateLabel])) {
                $breakdown[$rateLabel] = [
                    'rate_label' => $rateLabel,
                    'base_ht' => 0,
                    'amount_tax' => 0,
                ];
            }

            $breakdown[$rateLabel]['base_ht'] += $line->netHT;
            $breakdown[$rateLabel]['amount_tax'] += $line->tax;
        }

        if ($shippingHT > 0 || $shippingTax > 0) {
            $rateLabel = $this->formatTaxRateLabel($shippingHT, $shippingTax);
            if (!isset($breakdown[$rateLabel])) {
                $breakdown[$rateLabel] = [
                    'rate_label' => $rateLabel,
                    'base_ht' => 0,
                    'amount_tax' => 0,
                ];
            }

            $breakdown[$rateLabel]['base_ht'] += $shippingHT;
            $breakdown[$rateLabel]['amount_tax'] += $shippingTax;
        }

        return array_values($breakdown);
    }

    private function formatTaxRateLabel(int $baseHt, int $taxAmount): string
    {
        if ($baseHt <= 0 || $taxAmount <= 0) {
            return '0%';
        }

        $rate = ($taxAmount / $baseHt) * 100;

        return number_format($rate, 1, ',', ' ').'%';
    }

    private function formatTaxRatePercentLabel(float $rate): ?string
    {
        if ($rate <= 0.0) {
            return null;
        }

        $percent = $rate * 100;
        $formatted = floor($percent) === $percent
            ? number_format($percent, 0, ',', ' ')
            : number_format($percent, 1, ',', ' ');

        return $formatted.'%';
    }

    private function resolvePromoShippingDiscountTTC(Cart $cart): int
    {
        $promoShippingTTC = 0;

        foreach ($cart->getPromos() as $promo) {
            $promoShippingTTC += max(0, (int) $promo->discountShippingTTC);
        }

        return $promoShippingTTC;
    }

    private function computeHTFromTTC(int $ttc, float $taxRate): int
    {
        if ($taxRate <= 0.0) {
            return max(0, $ttc);
        }

        return (int) round($ttc / (1 + $taxRate));
    }

    private function createBrowsershot(string $html): Browsershot
    {
        $pdf = Browsershot::html($html)
            ->emulateMedia('print')
            ->noSandbox()
            ->format('A4')
            ->ignoreHttpsErrors()
            ->showBackground()
            ->timeout(120)
            ->setOption('args', [
                '--no-sandbox',
                '--disable-dev-shm-usage',
                '--disable-gpu',
                '--disable-web-security',
            ]);

        if ($this->includePathChromium) {
            $pdf->setChromePath($this->includePathChromium);
        }
        if ($this->includePath) {
            $pdf->setIncludePath('$PATH:'.$this->includePath);
        }
        if ($this->includePathNode) {
            $pdf->setNodeBinary($this->includePathNode);
        }
        if ($this->includePathNpm) {
            $pdf->setNpmBinary($this->includePathNpm);
        }

        return $pdf;
    }
}
