<?php

declare(strict_types=1);

namespace App\Tag;

use App\Index\IndexMetaManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;

final readonly class TagVectorIndexBuilder
{
    public function __construct(
        private string $pythonBin,
        private string $scriptPath,
        private string $tagsNdjsonPath,
        private string $vectorTagsIndexPath,
        private string $embeddingModel,
        private int $timeoutSeconds,
        private LoggerInterface $agentLogger,
        private IndexMetaManager $metaManager,
    ) {
    }

    public function build(): void
    {
        $this->assertPreconditions();

        $tmpIndex = $this->vectorTagsIndexPath . '.tmp';
        $tmpMeta = $tmpIndex . '.meta.json';
        $finalIndex = $this->vectorTagsIndexPath;
        $finalMeta = $finalIndex . '.meta.json';

        $this->ensureTargetDirectoryExists($finalIndex);
        $this->cleanupTemporaryArtifacts($tmpIndex, $tmpMeta);

        if (!$this->hasEmbeddableTags()) {
            $this->agentLogger->info('[tags] no embeddable tags found, removing stale tag index artifacts.');
            $this->removeFileIfExists($finalIndex);
            $this->removeFileIfExists($finalMeta);
            $this->commitRuntime(false);

            return;
        }

        $cmd = [
            $this->pythonBin,
            $this->scriptPath,
            $this->tagsNdjsonPath,
            $tmpIndex,
        ];

        $this->agentLogger->info('[tags] build tag vector index', [
            'cmd' => $cmd,
            'timeout' => $this->timeoutSeconds,
            'embedding_model' => $this->embeddingModel,
            'model_path_override' => getenv('RETRIEX_EMBEDDING_MODEL_PATH') ?: null,
        ]);

        try {
            $process = new Process($cmd);
            $process->setTimeout($this->timeoutSeconds);
            $process->setIdleTimeout($this->timeoutSeconds);

            $process->run(function (string $type, string $buffer): void {
                $message = trim($buffer);

                if ($message === '') {
                    return;
                }

                $this->agentLogger->info('[tags] vector ingest output', [
                    'type' => $type,
                    'output' => $message,
                ]);
            });

            if (!$process->isSuccessful()) {
                $this->agentLogger->error('[tags] tag vector ingest failed', [
                    'exit' => $process->getExitCode(),
                    'stdout' => trim($process->getOutput()),
                    'stderr' => trim($process->getErrorOutput()),
                ]);

                throw new ProcessFailedException($process);
            }

            if (!$this->isUsableArtifact($tmpIndex) || !$this->isUsableArtifact($tmpMeta)) {
                throw new \RuntimeException('Tag vector ingest produced incomplete artifacts.');
            }

            $this->atomicReplace($tmpIndex, $finalIndex);
            $this->atomicReplace($tmpMeta, $finalMeta);
            $this->commitRuntime(true);

            $this->agentLogger->info('[tags] tag vector index build completed + runtime committed', [
                'index' => $finalIndex,
                'meta' => $finalMeta,
            ]);
        } catch (ProcessTimedOutException $e) {
            $this->cleanupTemporaryArtifacts($tmpIndex, $tmpMeta);

            $this->agentLogger->error('[tags] tag vector ingest timed out', [
                'timeout' => $this->timeoutSeconds,
                'message' => $e->getMessage(),
            ]);

            throw new \RuntimeException(
                'Tag vector ingest timed out after ' . $this->timeoutSeconds . ' seconds. '
                . 'Most likely the embedding model cannot be loaded. '
                . 'Set RETRIEX_EMBEDDING_MODEL_PATH to a local model directory or check the HuggingFace cache.',
                0,
                $e,
            );
        } catch (\Throwable $e) {
            $this->cleanupTemporaryArtifacts($tmpIndex, $tmpMeta);
            throw $e;
        }
    }

    private function assertPreconditions(): void
    {
        if (!is_file($this->tagsNdjsonPath)) {
            throw new \RuntimeException('tags.ndjson missing: ' . $this->tagsNdjsonPath);
        }

        if (!is_file($this->scriptPath)) {
            throw new \RuntimeException('Tag ingest script missing: ' . $this->scriptPath);
        }

        if (trim($this->pythonBin) === '') {
            throw new \RuntimeException('Python binary must not be empty.');
        }

        if ($this->timeoutSeconds < 1) {
            throw new \RuntimeException('Tag vector timeout must be >= 1 second.');
        }
    }

    private function ensureTargetDirectoryExists(string $finalIndexPath): void
    {
        $dir = dirname($finalIndexPath);

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

        if (!@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new \RuntimeException('Unable to create tag vector directory: ' . $dir);
        }
    }

    private function hasEmbeddableTags(): bool
    {
        $fh = @fopen($this->tagsNdjsonPath, 'rb');

        if ($fh === false) {
            throw new \RuntimeException('Unable to read tags NDJSON: ' . $this->tagsNdjsonPath);
        }

        try {
            while (($line = fgets($fh)) !== false) {
                $line = trim($line);

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

                $decoded = json_decode($line, true);

                if (!is_array($decoded)) {
                    continue;
                }

                $tagId = trim((string) ($decoded['tag_id'] ?? ''));
                $text = trim((string) ($decoded['text'] ?? ''));

                if ($tagId !== '' && $text !== '') {
                    return true;
                }
            }
        } finally {
            fclose($fh);
        }

        return false;
    }

    private function isUsableArtifact(string $path): bool
    {
        return is_file($path) && filesize($path) > 0;
    }

    private function cleanupTemporaryArtifacts(string ...$paths): void
    {
        foreach ($paths as $path) {
            $this->removeFileIfExists($path);
        }
    }

    private function removeFileIfExists(string $path): void
    {
        if (is_file($path)) {
            @unlink($path);
        }
    }

    private function commitRuntime(bool $indexPresent): void
    {
        $this->metaManager->touchRuntime([
            'last_tags_rebuild_at' => (new \DateTimeImmutable())->format(DATE_ATOM),
            'tags_index_present' => $indexPresent,
        ]);
    }

    private function atomicReplace(string $tmp, string $final): void
    {
        if (!@rename($tmp, $final)) {
            if (!@copy($tmp, $final)) {
                @unlink($tmp);
                throw new \RuntimeException('Atomic replace failed for: ' . $final);
            }

            @unlink($tmp);
        }

        @chmod($final, 0664);
    }
}
