<?php

namespace App\Services\Env;

use Illuminate\Support\Facades\File;

/**
 * A raw editor for the file that holds every credential this installation
 * has — .env itself.
 *
 * Exists because "everything the console has no field for" is still real:
 * MAIL_*, DB_*, APP_KEY, whichever third-party key nobody has built a form
 * for yet. Without this, an operator needing one of those needs a shell
 * anyway, and the rest of the console page (App\Livewire\Admin\Integrations)
 * would not have removed that need — only moved most of it.
 *
 * The net, not just the warning — three rules, because the obvious
 * alternative is a page that can brick the installation with no way back
 * through that same page:
 *
 * 1. VALIDATE BEFORE WRITING. write() never touches the file before checking
 *    the new content first. A line that is neither blank, nor a comment, nor
 *    KEY=value is rejected outright — see isValidLine().
 * 2. An EMPTY file is rejected too, on top of the syntax check. Zero lines is
 *    syntactically valid by that rule (there is nothing to object to) and
 *    would still erase every setting in the file — worth refusing even though
 *    nothing asked for this check by name.
 * 3. BACK UP BEFORE EVERY WRITE that actually happens: a timestamped copy of
 *    the file about to be REPLACED, written beside it, before the new content
 *    lands. Nothing here prunes old backups — App\Livewire\Admin\Integrations
 *    says so, in the same sentence that tells an operator where they land.
 *
 * Deliberately NOT built on vlucas/phpdotenv's own parser (already a Laravel
 * dependency): that parser is more permissive than the rule this class
 * documents — multiline values, `export KEY=`, variable interpolation — and
 * borrowing it here would validate against a different, looser grammar than
 * the one an operator is actually told about. This class enforces exactly the
 * rule in its own docblock and nothing more.
 */
final class EnvFileEditor
{
    /**
     * @param  string  $path  Empty string means base_path('.env') — the
     *                        installation's real file. Overridden in tests so
     *                        nothing here ever has to write the one on this
     *                        dev machine to be exercised.
     */
    public function __construct(private readonly string $path = '') {}

    public function path(): string
    {
        return $this->path !== '' ? $this->path : base_path('.env');
    }

    public function read(): string
    {
        return File::exists($this->path()) ? File::get($this->path()) : '';
    }

    /**
     * The first line that is neither blank, nor a comment, nor KEY=value —
     * or null when every line is one of those three things. 1-indexed, so it
     * can be shown to an operator directly ("Zeile 42").
     */
    public function firstInvalidLine(string $content): ?int
    {
        foreach (preg_split('/\r\n|\r|\n/', $content) as $number => $line) {
            if (! self::isValidLine($line)) {
                return $number + 1;
            }
        }

        return null;
    }

    public function isValid(string $content): bool
    {
        return $this->firstInvalidLine($content) === null;
    }

    /**
     * Validate, back up, then write — in that order, and never out of it.
     * Content that fails validation is never backed up (nothing is about to
     * change) and never written.
     *
     * @return string the backup path, so the caller can tell the operator
     *                exactly where the previous version landed
     *
     * @throws InvalidEnvContentException the content is empty, or does not parse
     */
    public function write(string $content): string
    {
        if (trim($content) === '') {
            throw InvalidEnvContentException::empty();
        }

        $line = $this->firstInvalidLine($content);
        if ($line !== null) {
            throw InvalidEnvContentException::atLine($line);
        }

        $backup = $this->backup();
        File::put($this->path(), $content);

        return $backup;
    }

    /**
     * A timestamped copy of the CURRENT file, beside it — before whatever
     * write() is about to do to the original. Microseconds in the name so two
     * saves a second apart (a real thing to hit while testing this class, not
     * only a hypothetical operator habit) do not collide and silently
     * overwrite one another.
     *
     * Returns '' — never a path — when there is nothing to back up, i.e. the
     * file does not exist yet. read() already treats a missing file as ''
     * rather than an error (Codex review, P2); write() has to agree, or the
     * very first save on a fresh installation would fail trying to copy a
     * file that was never there instead of simply creating one.
     */
    public function backup(): string
    {
        if (! File::exists($this->path())) {
            return '';
        }

        $target = $this->path().'.bak-'.now()->format('Ymd-His-u');

        if (! File::copy($this->path(), $target)) {
            throw new \RuntimeException("Could not back up [{$this->path()}] before writing it.");
        }

        return $target;
    }

    private static function isValidLine(string $line): bool
    {
        $line = ltrim(rtrim($line, "\r\n"));

        if ($line === '' || str_starts_with($line, '#')) {
            return true;
        }

        return (bool) preg_match('/^[A-Za-z_][A-Za-z0-9_]*=.*$/', $line);
    }
}
