<?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\TicketBundle\Service\Import;

use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\TicketBundle\Entity\EmergencyStatusEnum;
use Chill\TicketBundle\Entity\Motive;
use Chill\TicketBundle\Repository\MotiveRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Yaml\Yaml;

final readonly class ImportMotivesFromDirectory
{
    /**
     * Cache en mémoire des Motive créés/trouvés pendant l'import.
     * Clé: "$lang|lower(trim(label))" → valeur: Motive.
     */
    private \ArrayObject $parentsAndMotives;

    public function __construct(
        private EntityManagerInterface $entityManager,
        private StoredObjectManagerInterface $storedObjectManager,
        private MotiveRepository $motiveRepository,
    ) {
        // Objet mutable pour rester compatible avec la classe readonly
        $this->parentsAndMotives = new \ArrayObject();
    }

    private function buildCacheKey(string $lang, string $label): string
    {
        return $lang.'|'.mb_strtolower(trim($label));
    }

    private function getFromCache(string $lang, string $label): ?Motive
    {
        $key = $this->buildCacheKey($lang, $label);

        if ($this->parentsAndMotives->offsetExists($key)) {
            return $this->parentsAndMotives->offsetGet($key);
        }

        return null;
    }

    private function putInCache(string $lang, string $label, Motive $motive): void
    {
        $this->parentsAndMotives->offsetSet($this->buildCacheKey($lang, $label), $motive);
    }

    /**
     * Trouve un Motive par label/lang en utilisant d'abord le cache, puis le repository.
     * Si inexistant, le crée, le persiste, l’ajoute au cache et le retourne.
     *
     * @param array $baseLabels tableau de labels (sera copié/ajusté pour la langue courante)
     */
    private function findOrCreateMotiveByLabel(string $name, string $lang, array $baseLabels): Motive
    {
        $name = trim($name);

        if ('' === $name) {
            throw new \InvalidArgumentException('Empty motive name provided to findOrCreateMotiveByLabel');
        }

        if (null !== ($cached = $this->getFromCache($lang, $name))) {
            return $cached;
        }

        $existing = $this->motiveRepository->findByLabel($name, $lang);
        if (\count($existing) > 1) {
            throw new \RuntimeException(sprintf('Multiple motives found with label "%s" for lang "%s".', $name, $lang));
        }
        $motive = $existing[0] ?? new Motive();

        // Préparer les labels: copie, trim, puis forcer la valeur de la langue courante
        $labels = $baseLabels;
        foreach ($labels as $k => $v) {
            if (\is_string($v)) {
                $labels[$k] = trim($v);
            }
        }
        $labels[$lang] = $name; // forcer le label courant
        $motive->setLabel($labels);

        if (!isset($existing[0])) {
            $this->entityManager->persist($motive);
        }

        $this->putInCache($lang, $name, $motive);

        return $motive;
    }

    public function import(string $directory, string $lang): void
    {
        $directory = rtrim($directory, DIRECTORY_SEPARATOR);
        if (!is_dir($directory)) {
            throw new \InvalidArgumentException(sprintf('The given path "%s" is not a directory.', $directory));
        }

        $configPath = $directory.DIRECTORY_SEPARATOR.'motives.yaml';
        if (!is_file($configPath)) {
            throw new \RuntimeException(sprintf('Missing motives.yaml in directory: %s', $directory));
        }

        $items = Yaml::parseFile($configPath);
        if (!\is_array($items)) {
            throw new \RuntimeException('The motives.yaml must contain a YAML array of items.');
        }

        foreach ($items as $index => $item) {
            if (!\is_array($item)) {
                throw new \RuntimeException(sprintf('Item at index %d is not an object/array.', $index));
            }

            // label: set as-is (expects an array)
            if (!array_key_exists('label', $item) || !\is_array($item['label'])) {
                throw new \RuntimeException(sprintf('Item %d: missing or invalid "label" (expected array).', $index));
            }
            $labelArray = $item['label'];
            // Trim all labels when they are strings
            foreach ($labelArray as $k => $v) {
                if (\is_string($v)) {
                    $labelArray[$k] = trim($v);
                }
            }

            $labelForSearch = $labelArray[$lang] ?? null;
            if (!\is_string($labelForSearch) || '' === trim($labelForSearch)) {
                throw new \RuntimeException(sprintf('Item %d: missing label for language "%s".', $index, $lang));
            }

            // Support parent > child notation in the current language label
            $parentName = null;
            $childName = trim($labelForSearch);
            if (false !== strpos($childName, '>')) {
                [$parentName, $childName] = array_map('trim', explode('>', $childName, 2));
            }

            // Find or create the current motive (child or standalone) en utilisant le cache
            $motive = $this->getFromCache($lang, $childName)
                ?? $this->findOrCreateMotiveByLabel($childName, $lang, $labelArray);
            // S’assurer que le label courant reflète bien le nom de l’enfant/standalone
            // (utile si l’entité existait déjà mais avec un label non trim)
            $labelsForChild = $motive->getLabel();
            $labelsForChild[$lang] = $childName;
            $motive->setLabel($labelsForChild);

            // If a parent is defined, ensure it exists and link it
            if (null !== $parentName && '' !== $parentName) {
                // Trouver/créer le parent via cache pour éviter les doublons
                $parentLabel = $labelArray;
                $parent = $this->getFromCache($lang, $parentName)
                    ?? $this->findOrCreateMotiveByLabel($parentName, $lang, $parentLabel);

                $motive->setParent($parent);
            }

            // ordering: applies only to the current (child/standalone) motive when provided
            if (array_key_exists('ordering', $item)) {
                $ordering = $item['ordering'];
                if (!\is_int($ordering) && !\is_float($ordering) && !(\is_string($ordering) && is_numeric($ordering))) {
                    throw new \RuntimeException(sprintf('Item %d: invalid "ordering" value (must be numeric).', $index));
                }
                $motive->setOrdering((float) $ordering);
            }

            // urgent: if true => YES, if explicitly false => NO, if absent => leave null
            if (array_key_exists('urgent', $item)) {
                $urgent = (bool) $item['urgent'];
                $motive->setMakeTicketEmergency($urgent ? EmergencyStatusEnum::YES : EmergencyStatusEnum::NO);
            }

            // supplementary informations (support both keys with/without underscore)
            $suppKey = array_key_exists('supplementary_informations', $item) ? 'supplementary_informations' : (array_key_exists('supplementary informations', $item) ? 'supplementary informations' : null);
            if (null !== $suppKey && \is_array($item[$suppKey])) {
                $motive->setSupplementaryComment(array_map(fn (array $supplementaryDefinition) => ['label' => $supplementaryDefinition['label'][$lang]], $item[$suppKey]));
            }

            // stored objects
            $storedKey = array_key_exists('stored_objects', $item) ? 'stored_objects' : (array_key_exists('stored object', $item) ? 'stored object' : null);
            if (null !== $storedKey && \is_array($item[$storedKey])) {
                foreach ($item[$storedKey] as $docIndex => $doc) {
                    if (!\is_array($doc)) {
                        throw new \RuntimeException(sprintf('Item %d, stored object %d: invalid entry.', $index, $docIndex));
                    }
                    $label = $doc['label'][$lang] ?? ($doc['label'] ?? null);
                    $filename = $doc['filename'] ?? null;
                    if (null === $filename || !\is_string($filename)) {
                        throw new \RuntimeException(sprintf('Item %d, stored object %d: missing filename.', $index, $docIndex));
                    }

                    $fullPath = $directory.DIRECTORY_SEPARATOR.$filename;
                    if (!is_file($fullPath)) {
                        throw new \RuntimeException(sprintf('Referenced file not found: %s', $fullPath));
                    }

                    $storedObject = new StoredObject();
                    if (\is_string($label)) {
                        $storedObject->setTitle($label);
                    } elseif (\is_array($label) && isset($label['fr']) && \is_string($label['fr'])) {
                        $storedObject->setTitle($label['fr']);
                    } else {
                        $storedObject->setTitle(pathinfo($filename, PATHINFO_FILENAME));
                    }

                    $content = file_get_contents($fullPath);
                    if (false === $content) {
                        throw new \RuntimeException(sprintf('Unable to read file: %s', $fullPath));
                    }

                    $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
                    $contentType = match ($ext) {
                        'pdf' => 'application/pdf',
                        'png' => 'image/png',
                        'jpg', 'jpeg' => 'image/jpeg',
                        'webp' => 'image/webp',
                        'txt' => 'text/plain',
                        default => 'application/octet-stream',
                    };

                    $this->storedObjectManager->write($storedObject, $content, $contentType);

                    $motive->addStoredObject($storedObject);
                    $this->entityManager->persist($storedObject);
                }
            }

            $this->entityManager->persist($motive);
        }

        $this->entityManager->flush();
    }
}
