<?php

declare(strict_types=1);

namespace App\Tag;

use App\Entity\Document;
use App\Entity\DocumentTag;
use App\Entity\Tag;
use Doctrine\ORM\EntityManagerInterface;

final readonly class TagNdjsonExporter
{
    public function __construct(
        private EntityManagerInterface $em,
        private string $tagsNdjsonPath,
    ) {
    }

    /**
     * Export all relevant tags into NDJSON (streaming) with atomic switch (.tmp + rename()).
     *
     * Line format:
     * {
     *   "tag_id":"...",
     *   "text":"label\nslug\noptional description",
     *   "type":"catalog_entity|generic|sales_signal",
     *   "document_ids":["...","..."]
     * }
     *
     * Only ACTIVE document assignments are exported. Tags without active document
     * assignments are intentionally skipped so they do not influence retrieval.
     *
     * @return array{tags:int, lines:int, bytes:int, path:string}
     */
    public function export(): array
    {
        $this->ensureTargetDirectoryExists();

        $tmpPath = $this->tagsNdjsonPath . '.tmp';
        $this->cleanupTemporaryFile($tmpPath);

        $fh = @fopen($tmpPath, 'wb');

        if ($fh === false) {
            throw new \RuntimeException('Cannot write tags NDJSON: ' . $tmpPath);
        }

        try {
            /** @var list<Tag> $tags */
            $tags = $this->em->createQueryBuilder()
                ->select('t')
                ->from(Tag::class, 't')
                ->orderBy('t.type', 'ASC')
                ->addOrderBy('t.label', 'ASC')
                ->getQuery()
                ->getResult();

            if ($tags === []) {
                fclose($fh);
                $this->atomicReplace($tmpPath, $this->tagsNdjsonPath);

                return [
                    'tags' => 0,
                    'lines' => 0,
                    'bytes' => (int) @filesize($this->tagsNdjsonPath),
                    'path' => $this->tagsNdjsonPath,
                ];
            }

            $tagToActiveDocs = $this->buildActiveDocumentMap();
            $lines = 0;

            foreach ($tags as $tag) {
                $tagId = $tag->getId()->toRfc4122();
                $docIds = $tagToActiveDocs[$tagId] ?? [];

                if ($docIds === []) {
                    continue;
                }

                $line = [
                    'tag_id' => $tagId,
                    'text' => $this->buildEmbeddingText($tag),
                    'type' => TagTypes::normalize($tag->getType()),
                    'document_ids' => $docIds,
                ];

                $json = json_encode($line, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

                if (!is_string($json)) {
                    continue;
                }

                fwrite($fh, $json . "\n");
                $lines++;
            }

            fclose($fh);
            $this->atomicReplace($tmpPath, $this->tagsNdjsonPath);

            return [
                'tags' => count($tags),
                'lines' => $lines,
                'bytes' => (int) @filesize($this->tagsNdjsonPath),
                'path' => $this->tagsNdjsonPath,
            ];
        } catch (\Throwable $e) {
            fclose($fh);
            $this->cleanupTemporaryFile($tmpPath);

            throw $e;
        }
    }

    /**
     * @return array<string, list<string>>
     */
    private function buildActiveDocumentMap(): array
    {
        /** @var list<DocumentTag> $relations */
        $relations = $this->em->createQueryBuilder()
            ->select('dt')
            ->addSelect('t', 'd')
            ->from(DocumentTag::class, 'dt')
            ->innerJoin('dt.tag', 't')
            ->innerJoin('dt.document', 'd')
            ->where('d.status = :status')
            ->setParameter('status', Document::STATUS_ACTIVE)
            ->getQuery()
            ->getResult();

        $tagToDocs = [];

        foreach ($relations as $relation) {
            $tag = $relation->getTag();
            $document = $relation->getDocument();

            $tagId = $tag->getId()->toRfc4122();
            $docId = $document->getId()->toRfc4122();

            $tagToDocs[$tagId][$docId] = $docId;
        }

        foreach ($tagToDocs as $tagId => $docIds) {
            ksort($docIds);
            $tagToDocs[$tagId] = array_values($docIds);
        }

        return $tagToDocs;
    }

    private function buildEmbeddingText(Tag $tag): string
    {
        $parts = [
            trim($tag->getLabel()),
            trim($tag->getSlug()),
        ];

        $description = trim((string) $tag->getDescription());

        if ($description !== '') {
            $parts[] = preg_replace('/\s+/u', ' ', $description) ?? $description;
        }

        $parts = array_values(array_filter(
            array_unique($parts),
            static fn (string $part): bool => $part !== ''
        ));

        return implode("\n", $parts);
    }

    private function ensureTargetDirectoryExists(): void
    {
        $dir = dirname($this->tagsNdjsonPath);

        if (is_dir($dir)) {
            return;
        }

        if (!@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new \RuntimeException('Cannot create tags NDJSON directory: ' . $dir);
        }
    }

    private function cleanupTemporaryFile(string $tmpPath): void
    {
        if (is_file($tmpPath)) {
            @unlink($tmpPath);
        }
    }

    private function atomicReplace(string $tmpPath, string $finalPath): void
    {
        if (is_file($finalPath)) {
            @chmod($finalPath, 0664);
        }

        if (!@rename($tmpPath, $finalPath)) {
            if (!@copy($tmpPath, $finalPath)) {
                @unlink($tmpPath);
                throw new \RuntimeException('Atomic replace failed for: ' . $finalPath);
            }

            @unlink($tmpPath);
        }

        @chmod($finalPath, 0664);
    }
}