<?php

declare(strict_types=1);

namespace OCA\Talk\Vendor\CuyZ\Valinor\Cache;

use OCA\Talk\Vendor\CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable;
use OCA\Talk\Vendor\CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten;
use OCA\Talk\Vendor\CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile;
use OCA\Talk\Vendor\CuyZ\Valinor\Definition\ClassDefinition;
use OCA\Talk\Vendor\CuyZ\Valinor\Definition\FunctionDefinition;
use OCA\Talk\Vendor\CuyZ\Valinor\Definition\Repository\Cache\Compiler\ClassDefinitionCompiler;
use OCA\Talk\Vendor\CuyZ\Valinor\Definition\Repository\Cache\Compiler\FunctionDefinitionCompiler;
use OCA\Talk\Vendor\CuyZ\Valinor\Normalizer\Transformer\EvaluatedTransformer;
use Error;
use FilesystemIterator;
use Traversable;

use function bin2hex;
use function file_exists;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function random_bytes;
use function rename;
use function rmdir;
use function str_contains;
use function unlink;
use function var_export;

/**
 * @api
 *
 * @template EntryType
 * @implements WarmupCache<EntryType>
 */
final class FileSystemCache implements WarmupCache
{
    private const TEMPORARY_DIR_PERMISSION = 510;

    private const GENERATED_MESSAGE = 'Generated by ' . self::class;

    private string $cacheDir;

    private ClassDefinitionCompiler $classDefinitionCompiler;

    private FunctionDefinitionCompiler $functionDefinitionCompiler;

    public function __construct(string $cacheDir)
    {
        $this->cacheDir = $cacheDir;
        $this->classDefinitionCompiler = new ClassDefinitionCompiler();
        $this->functionDefinitionCompiler = new FunctionDefinitionCompiler();
    }

    public function warmup(): void
    {
        $this->createTemporaryDir();
    }

    public function has($key): bool
    {
        $filename = $this->path($key);

        return file_exists($filename);
    }

    public function get($key, $default = null): mixed
    {
        $filename = $this->path($key);

        if (! file_exists($filename)) {
            return $default;
        }

        try {
            return include $filename;
        } catch (Error) {
            throw new CorruptedCompiledPhpCacheFile($filename);
        }
    }

    public function set($key, $value, $ttl = null): bool
    {
        $filename = $this->path($key);

        $code = $this->compile($value);

        $tmpDir = $this->createTemporaryDir();

        /** @infection-ignore-all */
        $tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));

        try {
            if (! @file_put_contents($tmpFilename, $code)) {
                throw new CompiledPhpCacheFileNotWritten($tmpFilename);
            }

            if (! file_exists($filename) && ! @rename($tmpFilename, $filename)) {
                throw new CompiledPhpCacheFileNotWritten($filename);
            }
        } finally {
            if (file_exists($tmpFilename)) {
                unlink($tmpFilename);
            }
        }

        return true;
    }

    public function delete($key): bool
    {
        $filename = $this->path($key);

        if (file_exists($filename)) {
            return @unlink($filename);
        }

        return true;
    }

    public function clear(): bool
    {
        if (! is_dir($this->cacheDir)) {
            return true;
        }

        $success = true;
        $shouldDeleteRootDir = true;

        /** @var FilesystemIterator $file */
        foreach (new FilesystemIterator($this->cacheDir) as $file) {
            if ($file->getFilename() === '.valinor.tmp') {
                $success = @rmdir($this->cacheDir . DIRECTORY_SEPARATOR . $file->getFilename()) && $success;
                continue;
            }

            if (! $file->isFile()) {
                $shouldDeleteRootDir = false;
                continue;
            }

            $line = $file->openFile()->getCurrentLine();

            if (! $line || ! str_contains($line, self::GENERATED_MESSAGE)) {
                $shouldDeleteRootDir = false;
                continue;
            }

            $success = @unlink($this->cacheDir . DIRECTORY_SEPARATOR . $file->getFilename()) && $success;
        }

        if ($shouldDeleteRootDir) {
            $success = @rmdir($this->cacheDir) && $success;
        }

        return $success;
    }

    /**
     * @return Traversable<string, EntryType|null>
     */
    public function getMultiple($keys, $default = null): Traversable
    {
        foreach ($keys as $key) {
            yield $key => $this->get($key, $default);
        }
    }

    public function setMultiple($values, $ttl = null): bool
    {
        foreach ($values as $key => $value) {
            $this->set($key, $value, $ttl);
        }

        return true;
    }

    public function deleteMultiple($keys): bool
    {
        $deleted = true;

        foreach ($keys as $key) {
            $deleted = $this->delete($key) && $deleted;
        }

        return $deleted;
    }

    private function compile(mixed $value): string
    {
        $generatedMessage = self::GENERATED_MESSAGE;

        $code = match (true) {
            $value instanceof ClassDefinition => $this->classDefinitionCompiler->compile($value),
            $value instanceof FunctionDefinition => $this->functionDefinitionCompiler->compile($value),
            $value instanceof EvaluatedTransformer => $value->code,
            default => var_export($value, true),
        };

        return <<<PHP
        <?php // $generatedMessage
        return $code;
        PHP;
    }

    private function createTemporaryDir(): string
    {
        $tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp';

        if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) {
            throw new CacheDirectoryNotWritable($this->cacheDir);
        }

        return $tmpDir;
    }

    private function path(string $key): string
    {
        /** @infection-ignore-all */
        return $this->cacheDir . DIRECTORY_SEPARATOR . $key . '.php';
    }
}
