<?php

declare(strict_types=1);

namespace App\Tag;

use App\Entity\Document;
use App\Entity\DocumentTag;
use App\Entity\Tag;
use App\Service\TagRebuildJobService;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use RuntimeException;

final readonly class TagService
{
    public function __construct(
        private EntityManagerInterface $em,
        private TagRebuildJobService $jobs,
    ) {
    }

    public function create(
        string $slug,
        string $label,
        ?string $description = null,
        string $type = TagTypes::GENERIC,
    ): Tag {
        $normalizedSlug = $this->normalizeSlug($slug);
        $label = trim($label);

        if ($normalizedSlug === '' || $label === '') {
            throw new InvalidArgumentException('Tag label and slug are required.');
        }

        if ($this->slugExists($normalizedSlug)) {
            throw new RuntimeException(sprintf('Tag slug "%s" already exists.', $normalizedSlug));
        }

        $tag = new Tag(
            $normalizedSlug,
            $label,
            $description,
            TagTypes::normalize($type)
        );

        $this->em->persist($tag);
        $this->em->flush();

        $this->triggerRebuildIfIdle();

        return $tag;
    }

    public function deleteById(string $tagId): void
    {
        $tag = $this->findTagById($tagId);
        $this->delete($tag);
    }

    public function delete(Tag $tag): void
    {
        $this->em->remove($tag);
        $this->em->flush();

        $this->triggerRebuildIfIdle();
    }

    public function syncDocumentTags(Document $document, array $newTagIds): void
    {
        $normalizedTagIds = $this->normalizeIdList($newTagIds);

        /** @var list<DocumentTag> $currentRelations */
        $currentRelations = $this->em
            ->getRepository(DocumentTag::class)
            ->findBy(['document' => $document]);

        $currentTagIds = array_map(
            static fn (DocumentTag $relation): string => (string) $relation->getTag()->getId(),
            $currentRelations
        );

        $toAdd = array_values(array_diff($normalizedTagIds, $currentTagIds));
        $toRemove = array_values(array_diff($currentTagIds, $normalizedTagIds));

        foreach ($toAdd as $tagId) {
            $tag = $this->em->getRepository(Tag::class)->find($tagId);

            if ($tag instanceof Tag) {
                $this->em->persist(new DocumentTag($document, $tag));
            }
        }

        foreach ($currentRelations as $relation) {
            $relationTagId = (string) $relation->getTag()->getId();

            if (in_array($relationTagId, $toRemove, true)) {
                $this->em->remove($relation);
            }
        }

        if ($toAdd !== [] || $toRemove !== []) {
            $this->em->flush();
            $this->triggerRebuildIfIdle();
        }
    }

    public function syncTagDocuments(Tag $tag, array $newDocumentIds): void
    {
        $normalizedDocumentIds = $this->normalizeIdList($newDocumentIds);

        /** @var list<DocumentTag> $currentRelations */
        $currentRelations = $this->em
            ->getRepository(DocumentTag::class)
            ->findBy(['tag' => $tag]);

        $currentDocumentIds = array_map(
            static fn (DocumentTag $relation): string => (string) $relation->getDocument()->getId(),
            $currentRelations
        );

        $toAdd = array_values(array_diff($normalizedDocumentIds, $currentDocumentIds));
        $toRemove = array_values(array_diff($currentDocumentIds, $normalizedDocumentIds));

        foreach ($toAdd as $documentId) {
            $document = $this->em->getRepository(Document::class)->find($documentId);

            if (
                $document instanceof Document
                && $document->getStatus() === Document::STATUS_ACTIVE
            ) {
                $this->em->persist(new DocumentTag($document, $tag));
            }
        }

        foreach ($currentRelations as $relation) {
            $relationDocumentId = (string) $relation->getDocument()->getId();

            if (in_array($relationDocumentId, $toRemove, true)) {
                $this->em->remove($relation);
            }
        }

        if ($toAdd !== [] || $toRemove !== []) {
            $this->em->flush();
            $this->triggerRebuildIfIdle();
        }
    }

    private function findTagById(string $tagId): Tag
    {
        $tagId = trim($tagId);

        if ($tagId === '') {
            throw new InvalidArgumentException('Tag id must not be empty.');
        }

        $tag = $this->em->getRepository(Tag::class)->find($tagId);

        if (!$tag instanceof Tag) {
            throw new RuntimeException('Tag not found.');
        }

        return $tag;
    }

    private function slugExists(string $slug): bool
    {
        return (int) $this->em->createQueryBuilder()
                ->select('COUNT(t.id)')
                ->from(Tag::class, 't')
                ->where('t.slug = :slug')
                ->setParameter('slug', $slug)
                ->getQuery()
                ->getSingleScalarResult() > 0;
    }

    /**
     * @param array<mixed> $ids
     * @return list<string>
     */
    private function normalizeIdList(array $ids): array
    {
        $normalized = [];

        foreach ($ids as $id) {
            $id = trim((string) $id);

            if ($id === '') {
                continue;
            }

            $normalized[] = $id;
        }

        return array_values(array_unique($normalized));
    }

    private function normalizeSlug(string $slug): string
    {
        $slug = mb_strtolower(trim($slug));
        $slug = preg_replace('/\s+/u', '-', $slug) ?? $slug;
        $slug = preg_replace('/-+/u', '-', $slug) ?? $slug;

        return trim($slug, '-');
    }

    private function triggerRebuildIfIdle(): void
    {
        if (!$this->jobs->hasActiveJob()) {
            $this->jobs->enqueueAndStartAsync();
        }
    }
}