<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Export\Helper;

use Chill\MainBundle\Entity\GeographicalUnit;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Repository\AddressRepository;
use Chill\MainBundle\Repository\GeographicalUnitLayerRepositoryInterface;
use Chill\MainBundle\Templating\Entity\AddressRender;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;

/**
 * Helps to load addresses and format them in list.
 */
class ExportAddressHelper
{
    /**
     * Compute all the F_* constants.
     */
    final public const F_ALL =
        self::F_ATTRIBUTES | self::F_BUILDING | self::F_COUNTRY |
        self::F_GEOM | self::F_POSTAL_CODE | self::F_STREET | self::F_GEOGRAPHICAL_UNITS;

    final public const F_AS_STRING = 0b00010000;

    final public const F_ATTRIBUTES = 0b01000000;

    final public const F_BUILDING = 0b00001000;

    final public const F_COUNTRY = 0b00000001;

    final public const F_GEOGRAPHICAL_UNITS = 0b1000000000;

    final public const F_GEOM = 0b00100000;

    final public const F_POSTAL_CODE = 0b00000010;

    final public const F_STREET = 0b00000100;

    private const ALL = [
        'country' => self::F_COUNTRY,
        'postal_code' => self::F_POSTAL_CODE,
        'street' => self::F_STREET,
        'building' => self::F_BUILDING,
        'string' => self::F_AS_STRING,
        'geom' => self::F_GEOM,
        'attributes' => self::F_ATTRIBUTES,
        'geographical_units' => self::F_GEOGRAPHICAL_UNITS,
    ];

    private const COLUMN_MAPPING = [
        'country' => ['country'],
        'postal_code' => ['postcode_code', 'postcode_name'],
        'street' => ['street', 'streetNumber'],
        'building' => ['buildingName', 'corridor', 'distribution', 'extra', 'flat', 'floor', 'steps'],
        'string' => ['_as_string'],
        'attributes' => ['isNoAddress', 'confidential', 'id'],
        'geom' => ['_lat', '_lon'],
        'geographical_units' => ['_unit_names', '_unit_refs'],
    ];

    /**
     * @var array<string, string, GeographicalUnitLayer>>|null
     */
    private ?array $unitNamesKeysCache = [];

    /**
     * @var array<string, array<string, GeographicalUnitLayer>>|null
     */
    private ?array $unitRefsKeysCache = [];

    public function __construct(private readonly AddressRender $addressRender, private readonly AddressRepository $addressRepository, private readonly GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}

    public function addSelectClauses(int $params, QueryBuilder $queryBuilder, $entityName = 'address', $prefix = 'add')
    {
        foreach (self::ALL as $key => $bitmask) {
            if (($params & $bitmask) === $bitmask) {
                foreach (self::COLUMN_MAPPING[$key] as $field) {
                    switch ($field) {
                        case 'id':
                        case '_as_string':
                            $queryBuilder->addSelect(sprintf('%s.id AS %s%s', $entityName, $prefix, $field));

                            break;

                        case 'street':
                        case 'streetNumber':
                        case 'floor':
                        case 'corridor':
                        case 'steps':
                        case 'buildingName':
                        case 'flat':
                        case 'distribution':
                        case 'extra':
                            $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $entityName, $field, $prefix, $field));

                            break;

                        case 'country':
                        case 'postcode_name':
                        case 'postcode_code':
                            $postCodeAlias = sprintf('%spostcode_t', $prefix);

                            if (!\in_array($postCodeAlias, $queryBuilder->getAllAliases(), true)) {
                                $queryBuilder->leftJoin($entityName.'.postcode', $postCodeAlias);
                            }

                            if ('postcode_name' === $field) {
                                $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'name', $prefix, $field));

                                break;
                            }

                            if ('postcode_code' === $field) {
                                $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'code', $prefix, $field));

                                break;
                            }

                            $countryAlias = sprintf('%scountry_t', $prefix);

                            if (!\in_array($countryAlias, $queryBuilder->getAllAliases(), true)) {
                                $queryBuilder->leftJoin(sprintf('%s.country', $postCodeAlias), $countryAlias);
                            }

                            $queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $countryAlias, 'name', $prefix, $field));

                            break;

                        case 'isNoAddress':
                        case 'confidential':
                            $queryBuilder->addSelect(sprintf('CASE WHEN %s.%s = \'TRUE\' THEN 1 ELSE 0 END AS %s%s', $entityName, $field, $prefix, $field));

                            break;

                        case '_lat':
                            $queryBuilder->addSelect(sprintf('ST_Y(%s.point) AS %s%s', $entityName, $prefix, $field));

                            break;

                        case '_lon':
                            $queryBuilder->addSelect(sprintf('ST_X(%s.point) AS %s%s', $entityName, $prefix, $field));

                            break;

                        case '_unit_names':
                            foreach ($this->generateKeysForUnitsNames($prefix) as $alias => $layer) {
                                $queryBuilder
                                    ->addSelect(
                                        sprintf(
                                            '(SELECT AGGREGATE(u_n_%s_%s.unitName) FROM %s u_n_%s_%s WHERE u_n_%s_%s MEMBER OF %s.geographicalUnits AND u_n_%s_%s.layer = :layer_%s_%s) AS %s',
                                            $prefix,
                                            $layer->getId(),
                                            GeographicalUnit::class,
                                            $prefix,
                                            $layer->getId(),
                                            $prefix,
                                            $layer->getId(),
                                            $entityName,
                                            $prefix,
                                            $layer->getId(),
                                            $prefix,
                                            $layer->getId(),
                                            $alias
                                        )
                                    )
                                    ->setParameter(sprintf('layer_%s_%s', $prefix, $layer->getId()), $layer);
                            }

                            break;

                        case '_unit_refs':
                            foreach ($this->generateKeysForUnitsRefs($prefix) as $alias => $layer) {
                                $queryBuilder
                                    ->addSelect(
                                        sprintf(
                                            '(SELECT AGGREGATE(u_r_%s_%s.unitRefId) FROM %s u_r_%s_%s WHERE u_r_%s_%s MEMBER OF %s.geographicalUnits AND u_r_%s_%s.layer = :layer_%s_%s) AS %s',
                                            $prefix,
                                            $layer->getId(),
                                            GeographicalUnit::class,
                                            $prefix,
                                            $layer->getId(),
                                            $prefix,
                                            $layer->getId(),
                                            $entityName,
                                            $prefix,
                                            $layer->getId(),
                                            $prefix,
                                            $layer->getId(),
                                            $alias
                                        )
                                    )
                                    ->setParameter(sprintf('layer_%s_%s', $prefix, $layer->getId()), $layer);
                            }

                            break;

                        default:
                            throw new \LogicException(sprintf('This key is not supported: %s, field %s', $key, $field));
                    }
                }
            }
        }
    }

    /**
     * @param self::F_* $params
     *
     * @return array|string[]
     */
    public function getKeys(int $params, string $prefix = ''): array
    {
        $prefixes = [];

        foreach (self::ALL as $key => $bitmask) {
            if (($params & $bitmask) === $bitmask) {
                if ('geographical_units' === $key) {
                    // geographical unit generate keys dynamically, depending on layers
                    $prefixes = [...$prefixes, ...array_keys($this->generateKeysForUnitsNames($prefix)), ...array_keys($this->generateKeysForUnitsRefs($prefix))];

                    continue;
                }

                $prefixes = array_merge(
                    $prefixes,
                    array_map(
                        static fn ($item) => $prefix.$item,
                        self::COLUMN_MAPPING[$key]
                    )
                );
            }
        }

        return $prefixes;
    }

    public function getLabel($key, array $values, $data, string $prefix = '', string $translationPrefix = 'export.address_helper.'): callable
    {
        $sanitizedKey = substr((string) $key, \strlen($prefix));

        switch ($sanitizedKey) {
            case 'id':
            case 'street':
            case 'streetNumber':
            case 'buildingName':
            case 'corridor':
            case 'distribution':
            case 'extra':
            case 'flat':
            case 'floor':
            case '_lat':
            case '_lon':
            case 'steps':
            case 'postcode_code':
            case 'postcode_name':
                return static function ($value) use ($sanitizedKey, $translationPrefix) {
                    if ('_header' === $value) {
                        return $translationPrefix.$sanitizedKey;
                    }

                    if (null === $value) {
                        return '';
                    }

                    return $value;
                };

            case 'country':
                return function ($value) use ($sanitizedKey, $translationPrefix) {
                    if ('_header' === $value) {
                        return $translationPrefix.$sanitizedKey;
                    }

                    if (null === $value) {
                        return '';
                    }

                    return $this->translatableStringHelper->localize(json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR));
                };

            case 'isNoAddress':
            case 'confidential':
                return static function ($value) use ($sanitizedKey, $translationPrefix) {
                    if ('_header' === $value) {
                        return $translationPrefix.$sanitizedKey;
                    }

                    switch ($value) {
                        case null:
                            return '';

                        case true:
                            return 1;

                        case false:
                            return 0;

                        default:
                            throw new \LogicException('this value is not supported for '.$sanitizedKey.': '.$value);
                    }
                };

            case '_as_string':
                return function ($value) use ($sanitizedKey, $translationPrefix) {
                    if ('_header' === $value) {
                        return $translationPrefix.$sanitizedKey;
                    }

                    if (null === $value) {
                        return '';
                    }

                    $address = $this->addressRepository->find($value);

                    return $this->addressRender->renderString($address, []);
                };

            default:
                $layerNamesKeys = [...$this->generateKeysForUnitsNames($prefix), ...$this->generateKeysForUnitsRefs($prefix)];

                if (\array_key_exists($key, $layerNamesKeys)) {
                    return function ($value) use ($key, $layerNamesKeys) {
                        if ('_header' === $value) {
                            $header = $this->translatableStringHelper->localize($layerNamesKeys[$key]->getName());

                            if (str_contains($key, 'unit_ref')) {
                                $header .= ' (id)';
                            }

                            return $header;
                        }

                        if (null === $value) {
                            return '';
                        }

                        $decodedValues = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);

                        return match (\count($decodedValues)) {
                            0 => '',
                            1 => $decodedValues[0],
                            default => implode('|', $decodedValues),
                        };
                    };
                }

                throw new \LogicException('this key is not supported: '.$sanitizedKey);
        }
    }

    /**
     * @return array<string, GeographicalUnitLayer>
     */
    private function generateKeysForUnitsNames(string $prefix): array
    {
        if (\array_key_exists($prefix, $this->unitNamesKeysCache)) {
            return $this->unitNamesKeysCache[$prefix];
        }

        $keys = [];

        foreach ($this->geographicalUnitLayerRepository->findAllHavingUnits() as $layer) {
            $keys[$prefix.'unit_names_'.$layer->getId()] = $layer;
        }

        return $this->unitNamesKeysCache[$prefix] = $keys;
    }

    /**
     * @return array<string, GeographicalUnitLayer>
     */
    private function generateKeysForUnitsRefs(string $prefix): array
    {
        if (\array_key_exists($prefix, $this->unitRefsKeysCache)) {
            return $this->unitRefsKeysCache[$prefix];
        }

        $keys = [];

        foreach ($this->geographicalUnitLayerRepository->findAllHavingUnits() as $layer) {
            $keys[$prefix.'unit_refs_'.$layer->getId()] = $layer;
        }

        return $this->unitRefsKeysCache[$prefix] = $keys;
    }
}
