<?php

declare(strict_types=1);

final class MyronInstallerEnvWriter
{
    private const ALLOWED_KEYS = [
        'MYRON_APP_ENV',
        'MYRON_APP_DEBUG',
        'MYRON_APP_URL',
        'MYRON_APP_KEY',
        'MYRON_PUBLIC_BASE_PATH',
        'MYRON_ADMIN_BASE_PATH',
        'MYRON_DB_DRIVER',
        'MYRON_DB_HOST',
        'MYRON_DB_PORT',
        'MYRON_DB_SOCKET',
        'MYRON_DB_NAME',
        'MYRON_DB_USER',
        'MYRON_DB_PASS',
        'MYRON_DB_CHARSET',
        'MYRON_DB_SQLITE_PATH',
    ];

    public function __construct(
        private readonly string $envPath,
        private readonly string $examplePath,
    ) {
    }

    /**
     * @param array<string, string> $updates
     */
    public function write(array $updates): void
    {
        $cleanUpdates = $this->normalizeUpdates($updates);
        $lines = $this->sourceLines();
        $seen = [];

        foreach ($lines as $index => $line) {
            $key = $this->lineKey($line);
            if ($key === null || !array_key_exists($key, $cleanUpdates)) {
                continue;
            }

            $lines[$index] = $key . '=' . $cleanUpdates[$key];
            $seen[$key] = true;
        }

        foreach ($cleanUpdates as $key => $value) {
            if (isset($seen[$key])) {
                continue;
            }

            $lines[] = $key . '=' . $value;
        }

        $this->atomicWrite(implode(PHP_EOL, $lines) . PHP_EOL);
    }

    /**
     * @param array<string, string> $updates
     * @return array<string, string>
     */
    private function normalizeUpdates(array $updates): array
    {
        $allowed = array_fill_keys(self::ALLOWED_KEYS, true);
        $normalized = [];

        foreach ($updates as $key => $value) {
            $name = strtoupper(trim((string) $key));
            if (!isset($allowed[$name])) {
                throw new InvalidArgumentException('Installer attempted to write an unsupported environment key.');
            }

            $stringValue = trim((string) $value);
            if (str_contains($stringValue, "\n") || str_contains($stringValue, "\r")) {
                throw new InvalidArgumentException('Environment values cannot contain line breaks.');
            }

            $normalized[$name] = $stringValue;
        }

        return $normalized;
    }

    /**
     * @return list<string>
     */
    private function sourceLines(): array
    {
        $path = is_file($this->envPath) ? $this->envPath : $this->examplePath;
        if (!is_file($path)) {
            return [];
        }

        $lines = file($path, FILE_IGNORE_NEW_LINES);
        return $lines === false ? [] : array_map('strval', $lines);
    }

    private function lineKey(string $line): ?string
    {
        $trimmed = trim($line);
        if ($trimmed === '' || str_starts_with($trimmed, '#') || !str_contains($trimmed, '=')) {
            return null;
        }

        $key = trim(explode('=', $trimmed, 2)[0]);
        return $key !== '' ? strtoupper($key) : null;
    }

    private function atomicWrite(string $contents): void
    {
        $directory = dirname($this->envPath);
        if (!is_dir($directory) || !is_writable($directory)) {
            throw new RuntimeException('The project directory is not writable. Update filesystem permissions before installing.');
        }

        if (is_file($this->envPath) && !is_writable($this->envPath)) {
            throw new RuntimeException('The .env file is not writable. Update file permissions before installing.');
        }

        $temporaryPath = tempnam($directory, '.env.installer.');
        if (!is_string($temporaryPath) || $temporaryPath === '') {
            throw new RuntimeException('Unable to create a temporary environment file.');
        }

        if (file_put_contents($temporaryPath, $contents, LOCK_EX) === false) {
            @unlink($temporaryPath);
            throw new RuntimeException('Unable to write the temporary environment file.');
        }

        @chmod($temporaryPath, 0600);

        if (is_file($this->envPath)) {
            $backupPath = $this->envPath . '.installer-backup';
            if (!@copy($this->envPath, $backupPath)) {
                @unlink($temporaryPath);
                throw new RuntimeException('Unable to create a backup of the existing .env file.');
            }
        }

        if (!@rename($temporaryPath, $this->envPath)) {
            @unlink($temporaryPath);
            throw new RuntimeException('Unable to replace the environment file atomically.');
        }
    }
}
