<?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;

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Persistence\ObjectRepository;

/**
 * Provides utilities for normalizing and denormalizing data entities and dates.
 */
trait ExportDataNormalizerTrait
{
    /**
     * Normalizes a Doctrine entity or a collection of entities to extract their identifiers.
     *
     * @param object|list<object>|null $entity the entity or collection of entities to normalize
     *
     * @return array|int|string Returns the identifier(s) of the entity or entities. If an array of entities is provided,
     *                          an array of their identifiers is returned. If a single entity is provided, its identifier
     *                          is returned. If null, returns an empty value.
     */
    private function normalizeDoctrineEntity(object|array|null $entity): array|int|string
    {
        if (is_array($entity)) {
            return array_values(array_filter(array_map(static fn (object $entity) => $entity->getId(), $entity), fn ($value) => null !== $value));
        }
        if ($entity instanceof Collection) {
            return $this->normalizeDoctrineEntity($entity->toArray());
        }

        return $entity?->getId();
    }

    /**
     * Denormalizes a Doctrine entity by fetching it from the provided repository based on the given ID(s).
     *
     * @param list<int>|int|string $id         the identifier(s) of the entity to find
     * @param ObjectRepository     $repository the Doctrine repository to query
     *
     * @return object|array<object> the found entity or an array of entities if multiple IDs are provided
     *
     * @throws \UnexpectedValueException when the entity with the given ID does not exist
     */
    private function denormalizeDoctrineEntity(array|int|string $id, ObjectRepository $repository): object|array
    {
        if (is_array($id)) {
            if ([] === $id) {
                return [];
            }

            return $repository->findBy(['id' => $id]);
        }

        if (null === $object = $repository->find($id)) {
            throw new \UnexpectedValueException(sprintf('Object with id "%s" does not exist.', $id));
        }

        return $object;
    }

    /**
     * Normalizer the "user or me" values.
     *
     * @param 'me'|User|iterable<'me'|User> $user
     *
     * @return int|'me'|list<'me'|int>
     */
    private function normalizeUserOrMe(string|User|iterable $user): int|string|array
    {
        if (is_iterable($user)) {
            $users = [];
            foreach ($user as $u) {
                $users[] = $this->normalizeUserOrMe($u);
            }

            return $users;
        }

        if ('me' === $user) {
            return $user;
        }

        return $user->getId();
    }

    /**
     * @param 'me'|int|iterable<'me'|int> $userId
     *
     * @return 'me'|User|array|null
     */
    private function denormalizeUserOrMe(string|int|iterable $userId, UserRepositoryInterface $userRepository): string|User|array|null
    {
        if (is_iterable($userId)) {
            $users = [];
            foreach ($userId as $id) {
                $users[] = $this->denormalizeUserOrMe($id, $userRepository);
            }

            return $users;
        }

        if ('me' === $userId) {
            return 'me';
        }

        return $userRepository->find($userId);
    }

    /**
     * @param 'me'|User|iterable<'me'|User> $user
     *
     * @return User|list<User>
     */
    private function userOrMe(string|User|iterable $user, ExportGenerationContext $context): User|array
    {
        if (is_iterable($user)) {
            $users = [];
            foreach ($user as $u) {
                $users[] = $this->userOrMe($u, $context);
            }

            return array_values(
                array_filter($users, static fn (?User $user) => null !== $user)
            );
        }

        if ('me' === $user) {
            return $context->byUser;
        }

        return $user;
    }

    /**
     * Normalizes a provided date into a specific string format.
     *
     * @param \DateTimeImmutable|\DateTime $date the date instance to normalize
     *
     * @return string a formatted string containing the type and formatted date
     */
    private function normalizeDate(\DateTimeImmutable|\DateTime $date): string
    {
        return sprintf(
            '%s,%s',
            $date instanceof \DateTimeImmutable ? 'imm1' : 'mut1',
            $date->format('d-m-Y-H:i:s.u e'),
        );
    }

    /**
     * Denormalizes a string back into a DateTime instance.
     *
     * The string is expected to contain a kind selector (e.g., 'imm1' or 'mut1')
     * to determine the type of DateTime object (immutable or mutable) followed by a date format.
     *
     * @param string $date the string to be denormalized, containing the kind selector and formatted date
     *
     * @return \DateTimeImmutable|\DateTime a DateTime instance created from the given string
     *
     * @throws \UnexpectedValueException if the kind selector or date format is invalid
     */
    private function denormalizeDate(string $date): \DateTimeImmutable|\DateTime
    {
        $format = 'd-m-Y-H:i:s.u e';

        $denormalized = match (substr($date, 0, 4)) {
            'imm1' => \DateTimeImmutable::createFromFormat($format, substr($date, 5)),
            'mut1' => \DateTime::createFromFormat($format, substr($date, 5)),
            default => throw new \UnexpectedValueException(sprintf('Unexpected format for the kind selector: %s', substr($date, 0, 4))),
        };

        if (false === $denormalized) {
            throw new \UnexpectedValueException(sprintf('Unexpected date format: %s', substr($date, 5)));
        }

        return $denormalized;
    }
}
