<?php

declare(strict_types=1);

namespace Gls\GlsPoland\PrestaShop\ObjectModel\Repository;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
use Gls\GlsPoland\Doctrine\DBAL\Query\TablePrefixingQueryBuilder;
use Gls\GlsPoland\PrestaShop\DTO;
use Gls\GlsPoland\PrestaShop\ObjectModel\HydratorInterface;
use Gls\GlsPoland\PrestaShop\ObjectModel\Persistence;
use Gls\GlsPoland\PrestaShop\ObjectModel\PersistenceInterface;

/**
 * @extends AbstractMultiLangObjectModelRepository<\Carrier>
 */
final class CarrierRepository extends AbstractMultiLangObjectModelRepository
{
    private $persistence;

    private $carriersByReferenceId = [];

    public function __construct(Connection $connection, string $dbPrefix, ?HydratorInterface $hydrator = null, ?PersistenceInterface $persistence = null)
    {
        parent::__construct(\Carrier::class, $connection, $dbPrefix, $hydrator);

        $this->persistence = $persistence ?? new Persistence();
    }

    public function findOneByReferenceId(int $referenceId, ?int $languageId = null): ?\Carrier
    {
        if (0 >= $referenceId) {
            return null;
        }

        if (array_key_exists($referenceId, $this->carriersByReferenceId)) {
            return $this->carriersByReferenceId[$referenceId];
        }

        $statement = $this
            ->createLangQueryBuilder('c', $languageId)
            ->andWhere('c.id_reference = :referenceId')
            ->andWhere('deleted = 0')
            ->setParameter('referenceId', $referenceId)
            ->execute();

        $data = $this->fetchAllAssociative($statement);

        return $this->carriersByReferenceId[$referenceId] = $this->hydrate($data, $languageId);
    }

    /**
     * This should probably be executed in a single database transaction.
     * Unfortunately, it would either require rewriting way too much code from \Carrier and \ObjectModel,
     * or have to be done using the \Db class, which, in combination with hooks executed in @see \ObjectModel::add,
     * poses a risk of breaking other modules or possibly letting them commit our unfinished transaction...
     */
    public function add(DTO\Carrier $data): void
    {
        $carrier = $data->getCarrier();
        if (0 < (int) $carrier->id) {
            $this->objectsById[$carrier->id] = $carrier;

            return;
        }

        try {
            $this->persistence->save($carrier);
            $carrier->id_reference = $carrier->id_reference ?? (int) $carrier->id;

            if (null !== $taxRulesGroup = $data->getTaxRulesGroup()) {
                $this->persistence->execute(static function () use ($carrier, $taxRulesGroup) {
                    return $carrier->setTaxRulesGroup((int) $taxRulesGroup->id);
                });
            }

            foreach ($data->getRanges() as $range) {
                $range->id_carrier = (int) $carrier->id;
                $this->persistence->save($range);
            }

            $this->addZones($carrier, ...$data->getZones()->toArray());
            $this->addGroups($carrier, ...$data->getGroups()->toArray());

            if ($disallowedModuleIds = $data->getDisallowedPaymentModuleIds()) {
                $this->disablePaymentModules($carrier, ...$disallowedModuleIds);
            } elseif (null !== $allowedModuleIds = $data->getAllowedPaymentModuleIds()) {
                $this->disablePaymentModulesExcept($carrier, ...$allowedModuleIds);
            }

            $this->objectsById[$carrier->id] = $carrier;
        } catch (\Exception $e) {
            $this->remove($carrier);

            throw $e;
        }
    }

    public function update(\Carrier $carrier): void
    {
        if (0 >= (int) $carrier->id) {
            throw new \InvalidArgumentException('Cannot update carrier: identifier is invalid.');
        }

        $this->persistence->save($carrier);
        $this->objectsById[$carrier->id] = $carrier;
    }

    public function remove(\Carrier $carrier): void
    {
        if (0 >= (int) $carrier->id) {
            return;
        }

        $carrier->id_shop_list = $carrier->getAssociatedShops();
        $this->persistence->delete($carrier);
        $this->objectsById[$carrier->id] = null;
    }

    private function addZones(\Carrier $carrier, \Zone ...$zones): void
    {
        if (0 >= (int) $carrier->id) {
            return;
        }

        $zoneIds = array_unique(array_filter(array_map(static function (\Zone $zone): int {
            return (int) $zone->id;
        }, $zones)));

        if ([] === $zoneIds) {
            return;
        }

        foreach ($zoneIds as $zoneId) {
            $this->persistence->execute(static function () use ($carrier, $zoneId) {
                return $carrier->addZone($zoneId);
            });
        }
    }

    private function addGroups(\Carrier $carrier, \Group ...$groups): void
    {
        if (0 >= $carrierId = (int) $carrier->id) {
            return;
        }

        $groupIds = array_unique(array_filter(array_map(static function (\Group $group): int {
            return (int) $group->id;
        }, $groups)));

        if ([] === $groupIds) {
            return;
        }

        $this->connection->transactional(function () use ($groupIds, $carrierId) {
            foreach ($groupIds as $groupId) {
                (new TablePrefixingQueryBuilder($this->connection, $this->dbPrefix))
                    ->insert('carrier_group')
                    ->values([
                        'id_carrier' => ':carrierId',
                        'id_group' => ':groupId',
                    ])
                    ->setParameter('carrierId', $carrierId)
                    ->setParameter('groupId', $groupId)
                    ->execute();
            }
        });
    }

    private function disablePaymentModules(\Carrier $carrier, int ...$moduleIds): void
    {
        if ([] === $moduleIds || 0 >= (int) $referenceId = $carrier->id_reference) {
            return;
        }

        $this
            ->createDeletePaymentModulesQueryBuilder($referenceId)
            ->andWhere('id_module IN (:moduleIds)')
            ->setParameter('moduleIds', $moduleIds, Connection::PARAM_INT_ARRAY)
            ->execute();
    }

    private function disablePaymentModulesExcept(\Carrier $carrier, int ...$moduleIds): void
    {
        if (0 >= (int) $referenceId = $carrier->id_reference) {
            return;
        }

        $qb = $this->createDeletePaymentModulesQueryBuilder($referenceId);

        if ([] !== $moduleIds) {
            $qb
                ->andWhere('id_module NOT IN (:moduleIds)')
                ->setParameter('moduleIds', $moduleIds, Connection::PARAM_INT_ARRAY);
        }

        $qb->execute();
    }

    private function createDeletePaymentModulesQueryBuilder(int $referenceId): QueryBuilder
    {
        return (new TablePrefixingQueryBuilder($this->connection, $this->dbPrefix))
            ->delete('module_carrier')
            ->andWhere('id_reference = :referenceId')
            ->setParameter('referenceId', $referenceId);
    }
}
