<?php
/***********************************************************************************
 * The contents of this file are subject to the Extension License Agreement
 * ("Agreement") which can be viewed at
 * https://www.espocrm.com/extension-license-agreement/.
 * By copying, installing downloading, or using this file, You have unconditionally
 * agreed to the terms and conditions of the Agreement, and You may not use this
 * file except in compliance with the Agreement. Under the terms of the Agreement,
 * You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
 * redistribute, market, publish, commercialize, or otherwise transfer rights or
 * usage to the software or any modified version or derivative work of the software
 * created by or for you.
 *
 * Copyright (C) 2015-2025 Letrium Ltd.
 *
 * License ID: a67be8f75bc1974a987315ddc1024a78
 ************************************************************************************/

namespace Espo\Modules\Sales\Tools\Invoice\EInvoice;

use DateTime;
use Einvoicing\Delivery;
use Einvoicing\Identifier;
use Einvoicing\Invoice as EInvoice;
use Einvoicing\InvoiceLine;
use Einvoicing\Party;
use Einvoicing\Presets\AbstractPreset;
use Einvoicing\Presets\CiusAtGov;
use Einvoicing\Presets\CiusAtNat;
use Einvoicing\Presets\CiusEsFace;
use Einvoicing\Presets\CiusIt;
use Einvoicing\Presets\CiusRo;
use Einvoicing\Presets\Nlcius;
use Einvoicing\Presets\Peppol;
use Espo\Core\Field\Address;
use Espo\Core\Field\Date;
use Espo\Entities\AddressCountry;
use Espo\Modules\Crm\Entities\Account;
use Espo\Modules\Sales\Entities\Invoice;
use Espo\Modules\Sales\Tools\Invoice\EInvoice\Exceptions\UnknownFormat;
use Espo\Modules\Sales\Tools\Invoice\EInvoice\Presets\XRechnung;
use Espo\ORM\EntityManager;
use Exception;
use RuntimeException;

class DefaultPreparator implements Preparator
{
    /**
     * @var array<string, class-string<AbstractPreset>>
     * @noinspection SpellCheckingInspection
     */
    private array $formatPresetMap = [
        'Peppol' => Peppol::class,
        'CiusAtGov' => CiusAtGov::class,
        'CiusAtNat' => CiusAtNat::class,
        'CiusEsFace' => CiusEsFace::class,
        'CiusIt' => CiusIt::class,
        'CiusRo' => CiusRo::class,
        'Nlcius' => Nlcius::class,
        'XRechnung' => XRechnung::class,
    ];

    public function __construct(
        private EntityManager $entityManager,
        private ConfigProvider $configProvider,
    ) {}


    /**
     * @throws UnknownFormat
     */
    public function prepare(Invoice $invoice, string $format): EInvoice
    {
        $preset = $this->formatPresetMap[$format] ?? null;

        if ($preset === null) {
            throw new UnknownFormat();
        }

        $eInvoice = new EInvoice($preset);

        if ($invoice->getNumber()) {
            $eInvoice->setNumber($invoice->getNumber());
        }

        if ($invoice->getDateInvoiced()) {
            $eInvoice->setIssueDate($this->convertDate($invoice->getDateInvoiced()));
        }

        if ($invoice->getDateDue()) {
            $eInvoice->setDueDate($this->convertDate($invoice->getDateDue()));
        }

        if ($invoice->getDateInvoiced()) {
            $eInvoice->setIssueDate($this->convertDate($invoice->getDateInvoiced()));
        }

        if ($invoice->getAmount()) {
            $eInvoice->setCurrency($invoice->getAmount()->getCode());
        }

        if ($invoice->getBuyerReference()) {
            $eInvoice->setBuyerReference($invoice->getBuyerReference());
        }

        if ($invoice->getPurchaseOrderReference()) {
            $eInvoice->setPurchaseOrderReference($invoice->getPurchaseOrderReference());
        }

        if ($invoice->getNote()) {
            $eInvoice->addNote($invoice->getNote());
        }

        $eInvoice->setSeller($this->prepareSeller());

        $account = $this->getAccount($invoice);

        if ($account) {
            $eInvoice->setBuyer($this->prepareBuyer($account, $invoice));
        }

        if (!$invoice->hasItemList()) {
            $invoice->loadItemListField();
        }

        $delivery = $this->prepareDelivery($invoice);

        $eInvoice->setDelivery($delivery);

        foreach ($invoice->getItems() as $item) {
            $line = new InvoiceLine();

            $vatRate = $item->get('taxRate') ?? 0.0;

            $line
                ->setName($item->getName())
                ->setPrice($item->get('unitPrice') ?? 0.0)
                ->setVatRate($vatRate)
                ->setQuantity($item->getQuantity() ?? 0.0);

            if ($vatRate === 0.0) {
                $line->setVatCategory('Z');
            }

            $eInvoice->addLine($line);
        }

        return $eInvoice;
    }

    private function convertDate(Date $date): DateTime
    {
        try {
            return new DateTime($date->toString());
        } catch (Exception $e) {
            throw new RuntimeException($e->getMessage(), 0, $e);
        }
    }

    private function prepareSeller(): Party
    {
        $seller = new Party();

        $seller
            ->setName($this->configProvider->getSellerCompanyName())
            ->setVatNumber($this->configProvider->getSellerVatNumber());

        if ($this->configProvider->getSellerElectronicAddressIdentifier()) {
            $seller->setElectronicAddress(
                new Identifier(
                    $this->configProvider->getSellerElectronicAddressIdentifier(),
                    $this->configProvider->getSellerElectronicAddressScheme()
                )
            );
        }

        if ($this->configProvider->getSellerTaxRegistrationIdentifier()) {
            $seller->setTaxRegistrationId(
                new Identifier(
                    $this->configProvider->getSellerTaxRegistrationIdentifier(),
                    $this->configProvider->getSellerTaxRegistrationScheme()
                )
            );
        }

        $address = $this->configProvider->getSellerAddress();

        $seller
            ->setCity($address->getCity())
            ->setPostalCode($address->getPostalCode())
            ->setSubdivision($address->getState());

        if ($address->getStreet()) {
            $lines = explode("\n", $address->getStreet());
            $lines = array_map(fn ($it) => trim($it), $lines);

            $seller->setAddress($lines);
        }

        if ($address->getCountry()) {
            $seller->setCountry($this->getCountryCode($address->getCountry()));
        }

        $seller
            ->setContactEmail($this->configProvider->getSellerContactEmailAddress())
            ->setContactPhone($this->configProvider->getSellerContactPhoneNumber())
            ->setContactName($this->configProvider->getSellerContactName());

        return $seller;
    }

    private function getAccount(Invoice $invoice): ?Account
    {
        $account = null;

        if ($invoice->getAccount()) {
            $account = $this->entityManager
                ->getRDBRepositoryByClass(Account::class)
                ->getById($invoice->getAccount()->getId());
        }

        return $account;
    }

    private function addAddress(Party|Delivery $party, Address $address): void
    {
        if ($address->getStreet()) {
            $lines = explode("\n", $address->getStreet());
            $lines = array_map(fn($it) => trim($it), $lines);

            $party->setAddress($lines);
        }

        if ($address->getCity()) {
            $party->setCity($address->getCity());
        }

        if ($address->getPostalCode()) {
            $party->setPostalCode($address->getPostalCode());
        }

        if ($address->getState()) {
            $party->setSubdivision($address->getState());
        }

        if ($address->getCountry()) {
            $countryCode = $this->getCountryCode($address->getCountry());

            if ($countryCode) {
                $party->setCountry($countryCode);
            }
        }
    }

    private function prepareBuyer(Account $account, Invoice $invoice): Party
    {
        $buyer = new Party();

        $buyer->setName($account->getName());

        $this->addAddress($buyer, $invoice->getBillingAddress());

        if ($account->get('electronicAddressIdentifier')) {
            $identifier = new Identifier(
                $account->get('electronicAddressIdentifier'),
                $account->get('electronicAddressScheme')
            );

            $buyer->setElectronicAddress($identifier);
        }

        return $buyer;
    }

    private function prepareDelivery(Invoice $invoice): Delivery
    {
        $delivery = new Delivery();

        $this->addAddress($delivery, $invoice->getShippingAddress());

        return $delivery;
    }

    private function getCountryCode(string $country): ?string
    {
        if (!class_exists("Espo\\Entities\\AddressCountry")) {
            return null;
        }

        $entity = $this->entityManager
            ->getRDBRepositoryByClass(AddressCountry::class)
            ->where(['name' => $country])
            ->findOne();

        if (!$entity) {
            return null;
        }

        return $entity->getCode();
    }
}
