<?php

declare(strict_types=1);

const ENV_EDITOR_VERSION = '2.2-srp-hardened';
const MAX_ENV_KEY_BYTES = 80;
const MAX_ENV_VALUE_BYTES = 4096;
const MAX_ENV_FILE_BYTES = 524288;
const MAX_LOGIN_FAILS = 8;
const MAX_BACKUPS = 50;

require_once __DIR__ . '/login_throttle.php';

$isHttps = isHttpsRequest();

session_name('srp_env_clean_editor');
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => $isHttps,
    'httponly' => true,
    'samesite' => 'Strict',
]);
session_start();

$nonce = base64_encode(random_bytes(16));
sendSecurityHeaders($nonce, $isHttps);

if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$flash = takeFlash();
$baseInfo = detectBaseInfo();
$candidates = detectEnvCandidates($baseInfo);
$selectedId = getStringInput($_GET, 'env_id', '');
$selectedEnv = selectEnvCandidate($candidates, $selectedId);
$envFile = $selectedEnv['path'] ?? '';
$backupDir = $envFile !== '' ? dirname($envFile) . DIRECTORY_SEPARATOR . '.env-backups' : __DIR__ . DIRECTORY_SEPARATOR . '.env-backups';
$lockFile = $envFile !== '' ? $envFile . '.lock' : __DIR__ . DIRECTORY_SEPARATOR . '.env.lock';

try {
    $envData = $envFile !== '' ? readEnvData($envFile) : emptyEnvData('No .env file detected.');
} catch (Throwable $e) {
    $envData = emptyEnvData('ENV file cannot be read safely.');
}

$isAuthed = isset($_SESSION['env_editor_auth']) && $_SESSION['env_editor_auth'] === true;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    handlePost($candidates, $baseInfo);
}

$stats = buildStats($envData, $selectedEnv, $backupDir);
$groups = groupEnvItems($envData['items']);
$logoPath = detectPublicLogo();

function isHttpsRequest(): bool
{
    if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
        return true;
    }

    if (isset($_SERVER['SERVER_PORT']) && (string) $_SERVER['SERVER_PORT'] === '443') {
        return true;
    }

    if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
        return true;
    }

    return false;
}

function sendSecurityHeaders(string $nonce, bool $isHttps): void
{
    header('Content-Type: text/html; charset=UTF-8');
    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: DENY');
    header('Referrer-Policy: no-referrer');
    header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()');
    header("Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-{$nonce}'; script-src 'self' 'nonce-{$nonce}'; img-src 'self' data:; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests");

    if ($isHttps) {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
    }
}

/**
 * @return array{type: string, message: string}|null
 */
function takeFlash(): ?array
{
    if (!isset($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
        return null;
    }

    $flash = $_SESSION['flash'];
    unset($_SESSION['flash']);

    return [
        'type' => isset($flash['type']) && is_string($flash['type']) ? $flash['type'] : 'info',
        'message' => isset($flash['message']) && is_string($flash['message']) ? $flash['message'] : '',
    ];
}

function setFlash(string $type, string $message): void
{
    $_SESSION['flash'] = [
        'type' => $type,
        'message' => safeMessage($message),
    ];
}

function redirectSelf(?string $envId = null): never
{
    $uri = strtok((string) ($_SERVER['REQUEST_URI'] ?? 'env-editor.php'), '?');
    $target = $uri;

    if ($envId !== null && $envId !== '') {
        $target .= '?env_id=' . rawurlencode($envId);
    }

    header('Location: ' . $target, true, 303);
    exit;
}

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function safeMessage(string $message): string
{
    $message = preg_replace('/[^\p{L}\p{N}\s\.\,\:\_\-\/]/u', '', $message);
    if (!is_string($message) || trim($message) === '') {
        return 'internal error';
    }

    return mb_substr(trim($message), 0, 220, 'UTF-8');
}

function getStringInput(array $source, string $key, string $default): string
{
    if (!isset($source[$key]) || !is_string($source[$key])) {
        return $default;
    }

    return trim($source[$key]);
}

/**
 * @return array{script_dir: string, document_root: string, cwd: string, account_root: string, host: string}
 */
function detectBaseInfo(): array
{
    $documentRoot = isset($_SERVER['DOCUMENT_ROOT']) && is_string($_SERVER['DOCUMENT_ROOT'])
        ? realpath($_SERVER['DOCUMENT_ROOT'])
        : false;

    $scriptDir = realpath(__DIR__);
    $cwd = realpath((string) getcwd());
    $accountRoot = $scriptDir !== false ? realpath(dirname($scriptDir)) : false;

    return [
        'script_dir' => $scriptDir !== false ? $scriptDir : __DIR__,
        'document_root' => $documentRoot !== false ? $documentRoot : '',
        'cwd' => $cwd !== false ? $cwd : '',
        'account_root' => $accountRoot !== false ? $accountRoot : ($scriptDir !== false ? $scriptDir : __DIR__),
        'host' => isset($_SERVER['HTTP_HOST']) && is_string($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost',
    ];
}

/**
 * @param array<string, mixed> $baseInfo
 * @return array<string, array{id: string, path: string, label: string, readable: bool, writable: bool, bytes: int, mtime: int}>
 */
function detectEnvCandidates(array $baseInfo): array
{
    $seedDirs = [];
    foreach (['script_dir', 'document_root', 'cwd', 'account_root'] as $key) {
        if (isset($baseInfo[$key]) && is_string($baseInfo[$key]) && $baseInfo[$key] !== '') {
            $seedDirs[] = $baseInfo[$key];
        }
    }

    $paths = [];
    foreach ($seedDirs as $dir) {
        $paths[] = $dir . DIRECTORY_SEPARATOR . '.env';
        $paths[] = $dir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . '.env';
        $paths[] = dirname($dir) . DIRECTORY_SEPARATOR . '.env';
    }

    $paths = array_values(array_unique($paths));
    $candidates = [];

    foreach ($paths as $path) {
        $real = realpath($path);
        if ($real === false || !is_file($real)) {
            continue;
        }

        if (!isSafeEnvPath($real, $baseInfo)) {
            continue;
        }

        $id = shortId($real);
        $candidates[$id] = [
            'id' => $id,
            'path' => $real,
            'label' => labelPath($real, $baseInfo),
            'readable' => is_readable($real),
            'writable' => is_writable($real),
            'bytes' => filesize($real) !== false ? (int) filesize($real) : 0,
            'mtime' => filemtime($real) !== false ? (int) filemtime($real) : 0,
        ];
    }

    uasort($candidates, function (array $a, array $b): int {
        $aScore = scoreCandidate($a);
        $bScore = scoreCandidate($b);

        if ($aScore === $bScore) {
            return strcmp((string) $a['path'], (string) $b['path']);
        }

        return $bScore <=> $aScore;
    });

    return $candidates;
}

function isSafeEnvPath(string $path, array $baseInfo): bool
{
    if (basename($path) !== '.env') {
        return false;
    }

    if (str_contains($path, "\0")) {
        return false;
    }

    $accountRoot = isset($baseInfo['account_root']) && is_string($baseInfo['account_root'])
        ? $baseInfo['account_root']
        : '';

    $scriptDir = isset($baseInfo['script_dir']) && is_string($baseInfo['script_dir'])
        ? $baseInfo['script_dir']
        : '';

    $allowedRoots = array_values(array_filter(array_unique([$accountRoot, $scriptDir])));

    foreach ($allowedRoots as $root) {
        if ($root !== '' && isPathInside($path, $root)) {
            return true;
        }
    }

    return false;
}

function isPathInside(string $path, string $root): bool
{
    $path = rtrim(str_replace('\\', '/', $path), '/');
    $root = rtrim(str_replace('\\', '/', $root), '/');

    return $path === $root || str_starts_with($path . '/', $root . '/');
}

function shortId(string $value): string
{
    return substr(hash('sha256', $value), 0, 12);
}

function labelPath(string $path, array $baseInfo): string
{
    foreach (['script_dir', 'document_root', 'account_root'] as $key) {
        if (!isset($baseInfo[$key]) || !is_string($baseInfo[$key]) || $baseInfo[$key] === '') {
            continue;
        }

        $root = rtrim(str_replace('\\', '/', $baseInfo[$key]), '/');
        $normalized = str_replace('\\', '/', $path);
        if (str_starts_with($normalized, $root . '/')) {
            return $key . ':' . substr($normalized, strlen($root));
        }
    }

    return $path;
}

function scoreCandidate(array $candidate): int
{
    $score = 0;

    if (!empty($candidate['readable'])) {
        $score += 30;
    }

    if (!empty($candidate['writable'])) {
        $score += 20;
    }

    if (str_ends_with((string) $candidate['path'], DIRECTORY_SEPARATOR . '.env')) {
        $score += 10;
    }

    $bytes = isset($candidate['bytes']) ? (int) $candidate['bytes'] : 0;
    if ($bytes > 0 && $bytes <= MAX_ENV_FILE_BYTES) {
        $score += 10;
    }

    return $score;
}

function selectEnvCandidate(array $candidates, string $selectedId): array
{
    if ($selectedId !== '' && isset($candidates[$selectedId])) {
        return $candidates[$selectedId];
    }

    $first = reset($candidates);
    if (is_array($first)) {
        return $first;
    }

    return [];
}

/**
 * @return array{items: list<array<string, mixed>>, values: array<string, string>, duplicates: list<string>, error: string|null}
 */
function emptyEnvData(string $error): array
{
    return [
        'items' => [],
        'values' => [],
        'duplicates' => [],
        'error' => $error,
    ];
}

/**
 * @return array{items: list<array<string, mixed>>, values: array<string, string>, duplicates: list<string>, error: string|null}
 */
function readEnvData(string $path): array
{
    if (!is_file($path)) {
        throw new RuntimeException('.env not found.');
    }

    if (!is_readable($path)) {
        throw new RuntimeException('.env is not readable.');
    }

    $size = filesize($path);
    if ($size !== false && $size > MAX_ENV_FILE_BYTES) {
        throw new RuntimeException('.env file is too large.');
    }

    $raw = file_get_contents($path);
    if (!is_string($raw)) {
        throw new RuntimeException('.env read failed.');
    }

    $lines = preg_split('/\r\n|\n|\r/', $raw);
    if (!is_array($lines)) {
        $lines = [];
    }

    $items = [];
    $values = [];
    $seen = [];
    $duplicates = [];

    foreach ($lines as $line) {
        if (trim($line) === '') {
            $items[] = ['type' => 'blank', 'raw' => $line];
            continue;
        }

        if (preg_match('/^\s*#/', $line) === 1) {
            $items[] = ['type' => 'comment', 'raw' => $line, 'group' => normalizeGroupName($line)];
            continue;
        }

        if (preg_match('/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/', $line, $matches) === 1) {
            $key = strtoupper($matches[1]);
            $rawValue = $matches[2];
            $value = decodeEnvValue($rawValue);

            if (isset($seen[$key])) {
                $duplicates[$key] = $key;
            }

            $seen[$key] = true;
            $values[$key] = $value;

            $items[] = [
                'type' => 'pair',
                'key' => $key,
                'value' => $value,
                'kind' => detectValueKind($key, $value),
                'raw_value' => $rawValue,
                'raw' => $line,
            ];
            continue;
        }

        $items[] = ['type' => 'raw', 'raw' => $line];
    }

    return [
        'items' => $items,
        'values' => $values,
        'duplicates' => array_values($duplicates),
        'error' => null,
    ];
}

function normalizeGroupName(string $comment): string
{
    $group = trim(preg_replace('/^\s*#+\s*/', '', $comment) ?? '');
    $group = preg_replace('/[─━=\-]+/u', '', $group) ?? $group;
    $group = trim($group);

    if ($group === '') {
        return 'Notes';
    }

    return mb_substr($group, 0, 42, 'UTF-8');
}

function decodeEnvValue(string $rawValue): string
{
    $value = trim($rawValue);

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

    $first = $value[0];

    if ($first === '"' && substr($value, -1) === '"') {
        $inner = substr($value, 1, -1);

        return str_replace(['\\"', '\\\\', '\\n', '\\r', '\\t'], ['"', '\\', "\n", "\r", "\t"], $inner);
    }

    if ($first === "'" && substr($value, -1) === "'") {
        return substr($value, 1, -1);
    }

    if (preg_match('/\s+#/', $value) === 1) {
        $parts = preg_split('/\s+#/', $value, 2);
        if (is_array($parts) && isset($parts[0])) {
            return rtrim($parts[0]);
        }
    }

    return $value;
}

function encodeEnvValue(string $value): string
{
    if ($value === '') {
        return '';
    }

    if (preg_match('/^[A-Za-z0-9._@:+\/=-]*$/', $value) === 1) {
        return $value;
    }

    $encoded = str_replace(
        ['\\', '"', "\n", "\r", "\t"],
        ['\\\\', '\\"', '\\n', '\\r', '\\t'],
        $value,
    );

    return '"' . $encoded . '"';
}

function isSensitiveKey(string $key): bool
{
    return preg_match('/(PASSWORD|PASS|TOKEN|SECRET|API_KEY|KEY|PRIVATE|SALT|CREDENTIAL|LICENSE|ACCOUNT_ID)/i', $key) === 1;
}

function detectValueKind(string $key, string $value): string
{
    if (str_ends_with($key, '_PASSWORD_HASH') || preg_match('/^\$2[ayb]\$\d{2}\$/', $value) === 1) {
        return 'password_hash';
    }

    if (isSensitiveKey($key)) {
        return 'secret';
    }

    if (preg_match('/(^|_)(PORT)$/i', $key) === 1) {
        return 'port';
    }

    if (preg_match('/(^|_)(HOST|DOMAIN|ENDPOINT)$/i', $key) === 1) {
        return str_starts_with($value, 'http://') || str_starts_with($value, 'https://') ? 'url' : 'host';
    }

    if (preg_match('/(URL|URI)$/i', $key) === 1 || str_starts_with($value, 'http://') || str_starts_with($value, 'https://')) {
        return 'url';
    }

    if (preg_match('/(DIR|PATH|SOCKET)$/i', $key) === 1) {
        return 'path';
    }

    if (preg_match('/(TIMEZONE|TZ)$/i', $key) === 1) {
        return 'timezone';
    }

    if (preg_match('/(COUNTRY|COUNTRIES)$/i', $key) === 1) {
        return 'country';
    }

    if ($value === '0' || $value === '1' || strcasecmp($value, 'true') === 0 || strcasecmp($value, 'false') === 0) {
        return 'boolean';
    }

    if (preg_match('/^-?\d+$/', $value) === 1) {
        return 'integer';
    }

    return $value === '' ? 'empty' : 'text';
}

/**
 * @param list<array<string, mixed>> $items
 * @return array<string, list<array<string, mixed>>>
 */
function groupEnvItems(array $items): array
{
    $groups = [];
    $currentGroup = 'General';

    foreach ($items as $item) {
        if (($item['type'] ?? '') === 'comment') {
            $candidate = isset($item['group']) && is_string($item['group']) ? $item['group'] : '';
            if ($candidate !== '' && !str_contains($candidate, 'Generated by')) {
                $currentGroup = $candidate;
            }
            continue;
        }

        if (($item['type'] ?? '') !== 'pair') {
            continue;
        }

        if (!isset($groups[$currentGroup])) {
            $groups[$currentGroup] = [];
        }

        $groups[$currentGroup][] = $item;
    }

    return $groups;
}

/**
 * Passwords that may unlock the .env editor.
 *
 * Preferred keys:
 * - ENV_EDITOR_PASSWORD_HASH
 * - A2ROOT_PASSWORD_HASH
 * - ADMIN_PASSWORD_HASH only when ENV_EDITOR_PASSWORD_HASH is absent
 *
 * Plaintext fallback is kept for migration compatibility only.
 *
 * @param array<string,mixed> $values
 * @return array{hashes:list<string>, plain:list<string>}
 */
function getEditorPasswordCredentials(array $values): array
{
    $read = static function (string $key) use ($values): string {
        return isset($values[$key]) && is_string($values[$key]) ? trim($values[$key]) : '';
    };

    $editorHash = $read('ENV_EDITOR_PASSWORD_HASH');
    $a2rootHash = $read('A2ROOT_PASSWORD_HASH');
    $adminHash = $read('ADMIN_PASSWORD_HASH');

    $editorPlain = $read('ENV_EDITOR_PASSWORD');
    $a2rootPlain = $read('A2ROOT_PASSWORD');
    $adminPlain = $read('ADMIN_PASSWORD');

    $hashes = [];
    $plain = [];

    if ($editorHash !== '') {
        $hashes[] = $editorHash;
    }

    if ($a2rootHash !== '') {
        $hashes[] = $a2rootHash;
    }

    if ($editorHash === '' && $adminHash !== '') {
        $hashes[] = $adminHash;
    }

    if ($editorHash === '' && $editorPlain !== '') {
        $plain[] = $editorPlain;
    }

    if ($a2rootHash === '' && $a2rootPlain !== '') {
        $plain[] = $a2rootPlain;
    }

    if ($editorHash === '' && $adminHash === '' && $adminPlain !== '') {
        $plain[] = $adminPlain;
    }

    return [
        'hashes' => array_values(array_unique($hashes)),
        'plain' => array_values(array_unique($plain)),
    ];
}

/**
 * @param array{hashes: list<string>, plain: list<string>} $credentials
 */
function passwordMatchesCredentials(string $password, array $credentials): bool
{
    if ($password === '') {
        return false;
    }

    $valid = false;

    foreach ($credentials['hashes'] ?? [] as $hash) {
        if (is_string($hash) && password_verify($password, $hash)) {
            $valid = true;
        }
    }

    foreach ($credentials['plain'] ?? [] as $plain) {
        if (is_string($plain) && hash_equals($plain, $password)) {
            $valid = true;
        }
    }

    return $valid;
}

function handlePost(array $candidates, array $baseInfo): void
{
    $postedToken = getStringInput($_POST, 'csrf_token', '');
    if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $postedToken)) {
        setFlash('bad', 'CSRF token invalid.');
        redirectSelf(getStringInput($_POST, 'env_id', ''));
    }

    $action = getStringInput($_POST, 'action', '');
    $envId = getStringInput($_POST, 'env_id', '');
    $selected = selectEnvCandidate($candidates, $envId);

    if ($action === 'login') {
        handleLogin($selected, $envId);
    }

    if ($action === 'logout') {
        $_SESSION = [];
        session_destroy();
        redirectSelf($envId);
    }

    if ($action === 'save') {
        if (!isset($_SESSION['env_editor_auth']) || $_SESSION['env_editor_auth'] !== true) {
            setFlash('bad', 'Unauthorized.');
            redirectSelf($envId);
        }

        if (empty($selected['path']) || !is_string($selected['path'])) {
            setFlash('bad', 'No .env file selected.');
            redirectSelf($envId);
        }

        $envFile = $selected['path'];

        if (!isSafeEnvPath($envFile, $baseInfo)) {
            setFlash('bad', 'Unsafe .env path blocked.');
            redirectSelf($envId);
        }

        try {
            $envData = readEnvData($envFile);
            if (!empty($envData['duplicates'])) {
                throw new RuntimeException('Duplicate ENV key detected: ' . implode(', ', $envData['duplicates']));
            }

            $newContent = buildUpdatedEnvContent($envData, $_POST);
            saveEnvAtomically(
                $envFile,
                $envFile . '.lock',
                dirname($envFile) . DIRECTORY_SEPARATOR . '.env-backups',
                $newContent,
            );

            setFlash('ok', 'Saved. Backup created.');
            redirectSelf($envId);
        } catch (Throwable $e) {
            setFlash('bad', 'Save failed: ' . $e->getMessage());
            redirectSelf($envId);
        }
    }

    setFlash('bad', 'Invalid action.');
    redirectSelf($envId);
}

function handleLogin(array $selected, string $envId): void
{
    $sessionFails = isset($_SESSION['login_fails']) && is_int($_SESSION['login_fails']) ? $_SESSION['login_fails'] : 0;
    $fails = max($sessionFails, srp_login_throttle_state('env_editor')['fails']);
    if ($fails >= MAX_LOGIN_FAILS) {
        setFlash('bad', 'Login locked. Try again later.');
        redirectSelf($envId);
    }

    $password = getStringInput($_POST, 'password', '');

    if (empty($selected['path']) || !is_string($selected['path'])) {
        setFlash('bad', 'No .env file detected for login.');
        redirectSelf($envId);
    }

    try {
        $envData = readEnvData($selected['path']);
        $acceptedCredentials = getEditorPasswordCredentials($envData['values']);
    } catch (Throwable $e) {
        $acceptedCredentials = ['hashes' => [], 'plain' => []];
    }

    if (($acceptedCredentials['hashes'] ?? []) === [] && ($acceptedCredentials['plain'] ?? []) === []) {
        setFlash('bad', 'ENV_EDITOR_PASSWORD_HASH / A2ROOT_PASSWORD_HASH / ADMIN_PASSWORD_HASH not found.');
        redirectSelf($envId);
    }

    $passwordValid = passwordMatchesCredentials($password, $acceptedCredentials);

    if ($passwordValid) {
        session_regenerate_id(true);
        $_SESSION['env_editor_auth'] = true;
        $_SESSION['login_fails'] = 0;
        srp_login_throttle_reset('env_editor');
        setFlash('ok', 'Login OK.');
        redirectSelf($envId);
    }

    $_SESSION['login_fails'] = $sessionFails + 1;
    srp_login_throttle_register_fail('env_editor');
    setFlash('bad', 'Invalid password.');
    redirectSelf($envId);
}

function validateEnvKey(string $key): string
{
    $key = strtoupper(trim($key));

    if ($key === '') {
        throw new RuntimeException('ENV key is empty.');
    }

    if (strlen($key) > MAX_ENV_KEY_BYTES) {
        throw new RuntimeException('ENV key too long.');
    }

    if (preg_match('/^[A-Z][A-Z0-9_]{1,79}$/', $key) !== 1) {
        throw new RuntimeException('Invalid ENV key: ' . $key);
    }

    return $key;
}

function validateEnvValue(string $value): string
{
    if (strlen($value) > MAX_ENV_VALUE_BYTES) {
        throw new RuntimeException('ENV value too long.');
    }

    if (str_contains($value, "\n") || str_contains($value, "\r")) {
        throw new RuntimeException('Multiline ENV value is blocked.');
    }

    return $value;
}

function buildUpdatedEnvContent(array $envData, array $post): string
{
    $postedValues = [];
    if (isset($post['env']) && is_array($post['env'])) {
        foreach ($post['env'] as $key => $value) {
            if (!is_string($key) || !is_string($value)) {
                continue;
            }

            $postedValues[validateEnvKey($key)] = validateEnvValue($value);
        }
    }

    $clearValues = [];
    if (isset($post['clear']) && is_array($post['clear'])) {
        foreach ($post['clear'] as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            $clearValues[validateEnvKey($key)] = true;
        }
    }

    $existingKeys = [];
    $output = [];

    foreach ($envData['items'] as $item) {
        if (($item['type'] ?? '') !== 'pair') {
            $output[] = (string) ($item['raw'] ?? '');
            continue;
        }

        $key = validateEnvKey((string) $item['key']);
        $existingKeys[$key] = true;
        $currentValue = (string) ($item['value'] ?? '');

        if (isset($clearValues[$key])) {
            $newValue = '';
        } elseif (isSensitiveKey($key)) {
            $candidate = $postedValues[$key] ?? '';
            $newValue = $candidate === '' ? $currentValue : $candidate;
        } else {
            $newValue = $postedValues[$key] ?? $currentValue;
        }

        $output[] = $key . '=' . encodeEnvValue(validateEnvValue($newValue));
    }

    $addKey = getStringInput($post, 'add_key', '');
    $addValue = isset($post['add_value']) && is_string($post['add_value']) ? $post['add_value'] : '';

    if ($addKey !== '') {
        $newKey = validateEnvKey($addKey);
        if (isset($existingKeys[$newKey])) {
            throw new RuntimeException('ENV key already exists: ' . $newKey);
        }

        $output[] = '';
        $output[] = '# Added from Clean ENV Editor - ' . gmdate('Y-m-d H:i:s') . ' UTC';
        $output[] = $newKey . '=' . encodeEnvValue(validateEnvValue($addValue));
    }

    return rtrim(implode(PHP_EOL, $output), PHP_EOL) . PHP_EOL;
}

function saveEnvAtomically(string $envFile, string $lockFile, string $backupDir, string $content): void
{
    $dir = dirname($envFile);

    if (!is_dir($dir) || !is_writable($dir)) {
        throw new RuntimeException('ENV directory is not writable.');
    }

    if (is_file($envFile) && !is_writable($envFile)) {
        throw new RuntimeException('.env is not writable.');
    }

    if (!is_dir($backupDir)) {
        if (!mkdir($backupDir, 0700, true) && !is_dir($backupDir)) {
            throw new RuntimeException('Backup directory cannot be created.');
        }
    }

    $lockHandle = fopen($lockFile, 'c');
    if ($lockHandle === false) {
        throw new RuntimeException('Lock file cannot be opened.');
    }

    try {
        if (!flock($lockHandle, LOCK_EX)) {
            throw new RuntimeException('Lock failed.');
        }

        if (is_file($envFile)) {
            $backupFile = $backupDir . DIRECTORY_SEPARATOR . '.env.' . gmdate('Ymd_His') . '.bak';
            if (!copy($envFile, $backupFile)) {
                throw new RuntimeException('Backup failed.');
            }
            @chmod($backupFile, 0600);
            rotateBackups($backupDir);
        }

        $tmpFile = $envFile . '.tmp.' . bin2hex(random_bytes(8));
        if (file_put_contents($tmpFile, $content, LOCK_EX) === false) {
            throw new RuntimeException('Temporary write failed.');
        }

        @chmod($tmpFile, 0600);

        if (!rename($tmpFile, $envFile)) {
            @unlink($tmpFile);

            throw new RuntimeException('Atomic rename failed.');
        }

        @chmod($envFile, 0600);
        flock($lockHandle, LOCK_UN);
    } finally {
        fclose($lockHandle);
        @chmod($lockFile, 0600);
    }
}


function rotateBackups(string $backupDir): void
{
    $files = glob($backupDir . DIRECTORY_SEPARATOR . '.env.*.bak');
    if (!is_array($files) || count($files) <= MAX_BACKUPS) {
        return;
    }

    usort($files, static function (string $a, string $b): int {
        return (filemtime($a) ?: 0) <=> (filemtime($b) ?: 0);
    });

    $remove = array_slice($files, 0, max(0, count($files) - MAX_BACKUPS));
    foreach ($remove as $file) {
        if (is_file($file)) {
            @unlink($file);
        }
    }
}

function buildStats(array $envData, array $selectedEnv, string $backupDir): array
{
    $values = isset($envData['values']) && is_array($envData['values']) ? $envData['values'] : [];
    $secretCount = 0;
    $emptyCount = 0;

    foreach ($values as $key => $value) {
        if (is_string($key) && isSensitiveKey($key)) {
            $secretCount++;
        }

        if ($value === '') {
            $emptyCount++;
        }
    }

    return [
        'keys' => count($values),
        'secrets' => $secretCount,
        'empty' => $emptyCount,
        'duplicates' => isset($envData['duplicates']) && is_array($envData['duplicates']) ? count($envData['duplicates']) : 0,
        'writable' => !empty($selectedEnv['writable']),
        'readable' => !empty($selectedEnv['readable']),
        'backup_ready' => is_dir($backupDir) ? is_writable($backupDir) : is_writable(dirname($backupDir)),
    ];
}

function detectPublicLogo(): string
{
    $docRoot = isset($_SERVER['DOCUMENT_ROOT']) && is_string($_SERVER['DOCUMENT_ROOT'])
        ? realpath($_SERVER['DOCUMENT_ROOT'])
        : false;

    if ($docRoot !== false && is_file($docRoot . DIRECTORY_SEPARATOR . 'logo.svg')) {
        return '/logo.svg';
    }

    return '';
}

function maskValue(string $value): string
{
    if ($value === '') {
        return 'empty';
    }

    return 'hidden · len ' . strlen($value);
}

function inputTypeForKind(string $kind): string
{
    if ($kind === 'url') {
        return 'url';
    }

    if ($kind === 'integer' || $kind === 'port') {
        return 'number';
    }

    return 'text';
}

function activeSelected(string $candidateId, string $selectedId): string
{
    return hash_equals($candidateId, $selectedId) ? ' active' : '';
}

function statusClass(bool $ok): string
{
    return $ok ? 'ok' : 'bad';
}

?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Clean ENV Editor</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style nonce="<?= e($nonce) ?>">
        :root{
            --bg:#f7f7f7;
            --fg:#292b2c;
            --surface:rgba(255,255,255,.84);
            --surface-solid:#ffffff;
            --surface-soft:rgba(255,255,255,.64);
            --border:#e3e5e6;
            --line:rgba(227,229,230,.92);
            --muted:#6b6f72;
            --grey:#6c757d !important;
            --grey-soft:rgba(227, 229, 230,.12);
            --green:#5c8a6b;
            --blue:#6d86a8;
            --red:#a86b6b;
            --shadow:0 1px 0 rgba(41,43,44,.04),0 10px 30px rgba(41,43,44,.045);
            --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;
            --mono:"SFMono-Regular",Consolas,"Liberation Mono",monospace;
            --radius:.3rem;
            --side:304px;
            --top-offset:14px;
            --sticky-main:10px;
        }

        *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
        html{min-height:100%;color-scheme:light;scroll-behavior:smooth}
        ::selection{background:var(--border);color:var(--fg)}
        ::-moz-selection{background:var(--border);color:var(--fg)}
        body{
            min-height:100vh;
            background:
                radial-gradient(circle at 50% 0,rgba(227, 229, 230,.055),transparent 34rem),
                linear-gradient(180deg,#ffffff 0,#f7f7f7 44%,#f3f4f4 100%);
            color:var(--fg);
            font:13px/1.5 var(--font);
            overflow-x:hidden;
            position:relative;
            text-rendering:optimizeLegibility;
            -webkit-font-smoothing:antialiased;
            -moz-osx-font-smoothing:grayscale;
        }
        body::before{
            content:"";
            position:fixed;
            inset:0;
            background:<?= $logoPath !== '' ? 'url("' . e($logoPath) . '")' : 'linear-gradient(135deg,rgba(227, 229, 230,.12),rgba(41,43,44,.035))' ?> center 48%/min(68vw,620px) min(68vw,620px) no-repeat;
            opacity:.055;
            filter:grayscale(.7) contrast(1.05) saturate(.8);
            pointer-events:none;
            z-index:0;
        }
        body::after{
            content:"";
            position:fixed;
            inset:0;
            background:
                linear-gradient(90deg,rgba(255,255,255,.84),rgba(255,255,255,.56),rgba(255,255,255,.84)),
                radial-gradient(circle at center,rgba(227, 229, 230,.028) 0,rgba(247,247,247,.35) 35%,rgba(247,247,247,.94) 100%);
            pointer-events:none;
            z-index:0;
        }

        a{color:inherit}
        button,input,select{font:inherit}
        button{appearance:none}
        svg{display:block}

        .shell{
            position:relative;
            z-index:1;
            width:min(100%,1420px);
            min-height:100vh;
            margin:0 auto;
            padding:var(--top-offset);
            display:grid;
            grid-template-columns:var(--side) minmax(0,1fr);
            align-items:start;
            gap:12px;
        }
        .panel,.card{
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:var(--surface);
            box-shadow:var(--shadow);
            backdrop-filter:blur(12px);
            -webkit-backdrop-filter:blur(12px);
        }
        .side{
            position:sticky;
            top:var(--top-offset);
            height:calc(100vh - (var(--top-offset) * 2));
            min-height:460px;
            overflow:auto;
            padding:10px;
            scrollbar-width:thin;
        }
        .main{
            min-width:0;
            padding-bottom:92px;
        }
        .topbar{
            position:sticky;
            top:var(--sticky-main);
            z-index:12;
            display:flex;
            align-items:center;
            justify-content:space-between;
            gap:10px;
            min-height:58px;
            margin-bottom:12px;
            padding:10px 11px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(247,247,247,.92);
            box-shadow:var(--shadow);
            backdrop-filter:blur(14px);
            -webkit-backdrop-filter:blur(14px);
        }

        .brand{
            display:flex;
            align-items:center;
            gap:10px;
            min-width:0;
        }
        .logo-box{
            width:38px;
            height:38px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(247,247,247,.72);
            display:grid;
            place-items:center;
            flex:0 0 auto;
        }
        .title{min-width:0}
        .title h1{
            font-size:15px;
            line-height:1.2;
            font-weight:800;
            letter-spacing:.02em;
        }
        .title p{
            max-width:100%;
            margin-top:3px;
            color:var(--muted);
            font-size:10px;
            overflow:hidden;
            text-overflow:ellipsis;
            white-space:nowrap;
        }
        .mark{color:var(--grey)}

        .section-title{
            font-size:10px;
            text-transform:uppercase;
            letter-spacing:.16em;
            color:var(--muted);
            margin:12px 2px 7px;
        }
        .detected{display:grid;gap:7px}
        .env-card{
            display:block;
            text-decoration:none;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:var(--surface-soft);
            padding:9px;
            color:var(--fg);
            transition:border-color .15s ease,background .15s ease,transform .15s ease;
        }
        .env-card:hover,.env-card.active{
            border-color:rgba(227, 229, 230,.45);
            background:rgba(255,255,255,.94);
            transform:translateY(-1px);
        }
        .env-card strong{
            display:block;
            font-size:11px;
            margin-bottom:4px;
            overflow:hidden;
            text-overflow:ellipsis;
            white-space:nowrap;
        }
        .env-card small{
            display:block;
            color:var(--muted);
            font-size:10px;
            overflow-wrap:anywhere;
            line-height:1.45;
        }

        .chip-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
        .chip{
            display:inline-flex;
            align-items:center;
            gap:6px;
            min-height:24px;
            padding:5px 8px;
            border:1px solid rgba(227,229,230,.88);
            border-radius:var(--radius);
            background:rgba(247,247,247,.72);
            color:rgba(41,43,44,.72);
            font-size:9px;
            font-weight:800;
            letter-spacing:.06em;
            text-transform:uppercase;
            white-space:nowrap;
        }
        .chip-dot{
            width:6px;
            height:6px;
            border-radius:999px;
            background:var(--muted);
            opacity:.78;
        }
        .chip.ok .chip-dot{background:var(--green)}
        .chip.bad .chip-dot{background:var(--red)}
        .chip.grey .chip-dot{background:var(--grey)}
        .chip.blue .chip-dot{background:var(--blue)}

        .card{margin-bottom:12px}
        .card-head{
            display:flex;
            align-items:center;
            justify-content:space-between;
            gap:8px;
            min-height:50px;
            padding:10px 11px;
            border-bottom:1px solid var(--line);
        }
        .card-head h2{
            font-size:12px;
            letter-spacing:.04em;
            text-transform:uppercase;
        }
        .card-body{padding:10px 11px}
        .notice{
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(255,255,255,.84);
            padding:9px 10px;
            margin-bottom:10px;
            color:var(--muted);
            overflow-wrap:anywhere;
        }
        .notice.ok{border-color:rgba(92,138,107,.38);color:var(--green)}
        .notice.bad{border-color:rgba(168,107,107,.42);color:var(--red)}
        .notice.warn{border-color:rgba(227, 229, 230,.42);color:var(--grey)}

        .login{
            position:relative;
            z-index:1;
            width:min(94vw,440px);
            margin:clamp(46px,10vh,120px) auto;
            overflow:hidden;
        }
        .login .card-body{padding:14px}
        .login-help{margin:0;color:var(--muted);font-size:12px;line-height:1.5;overflow-wrap:anywhere}

        .field{display:grid;gap:6px;margin-bottom:10px}
        label{
            font-size:10px;
            text-transform:uppercase;
            letter-spacing:.12em;
            color:var(--muted);
            font-weight:800;
        }
        .input,
        input[type=text],
        input[type=password],
        input[type=url],
        input[type=number],
        input[type=search],
        select{
            width:100%;
            min-height:34px;
            border:1px solid rgba(227,229,230,.98);
            border-radius:var(--radius);
            background:rgba(255,255,255,.86);
            color:var(--fg);
            font:12px/1.2 var(--font);
            font-weight:700;
            letter-spacing:0;
            outline:0;
            padding:8px 9px;
        }
        input:focus,select:focus{
            border-color:rgba(227, 229, 230,.42);
            box-shadow:0 0 0 2px rgba(227, 229, 230,.075);
            background:#fff;
        }
        input::placeholder{color:rgba(107,111,114,.62);font-weight:400;}

        .btn{
            display:inline-flex;
            align-items:center;
            justify-content:center;
            gap:7px;
            min-height:34px;
            border:1px solid var(--fg);
            border-radius:var(--radius);
            background:var(--fg);
            color:var(--bg);
            padding:8px 11px;
            font:11px/1 var(--font);
            font-weight:900;
            text-transform:uppercase;
            letter-spacing:.04em;
            cursor:pointer;
            text-decoration:none;
            user-select:none;
            white-space:nowrap;
        }
        .btn.secondary{
            background:rgba(255,255,255,.78);
            color:var(--fg);
            border-color:rgba(227,229,230,.98);
        }
        .btn.danger{
            background:rgba(255,255,255,.78);
            color:var(--red);
            border-color:rgba(168,107,107,.45);
        }
        .btn:hover{filter:contrast(1.05)}
        .btn:focus-visible{
            outline:2px solid rgba(227, 229, 230,.32);
            outline-offset:2px;
        }
        .actions{
            display:flex;
            gap:7px;
            flex-wrap:wrap;
            align-items:center;
        }

        .toolbar{
            position:sticky;
            top:80px;
            z-index:8;
            display:grid;
            grid-template-columns:minmax(220px,1fr) 170px auto;
            gap:8px;
            align-items:center;
            margin:-1px 0 10px;
            padding:8px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(247,247,247,.94);
            backdrop-filter:blur(12px);
            -webkit-backdrop-filter:blur(12px);
        }
        .stats{
            display:grid;
            grid-template-columns:repeat(4,minmax(0,1fr));
            gap:7px;
        }
        .stat{
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(255,255,255,.68);
            padding:9px;
        }
        .stat b{
            display:block;
            font-size:16px;
            line-height:1.1;
        }
        .stat span{
            display:block;
            margin-top:3px;
            color:var(--muted);
            font-size:9px;
            text-transform:uppercase;
            letter-spacing:.08em;
        }

        .group{margin-top:10px}
        .group-title{
            position:sticky;
            top:136px;
            z-index:6;
            display:flex;
            justify-content:space-between;
            align-items:center;
            gap:8px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(247,247,247,.97);
            padding:7px 8px;
            margin-bottom:7px;
            backdrop-filter:blur(10px);
            -webkit-backdrop-filter:blur(10px);
        }
        .group-title strong{font-size:11px}
        .rows{display:grid;gap:7px}
        .env-row{
            display:grid;
            grid-template-columns:minmax(190px,250px) minmax(0,1fr) 136px;
            gap:8px;
            align-items:center;
            border:1px solid rgba(227,229,230,.82);
            border-radius:var(--radius);
            background:rgba(255,255,255,.76);
            padding:8px;
            transition:border-color .15s ease,background .15s ease;
        }
        .env-row:focus-within{
            border-color:rgba(227, 229, 230,.32);
            background:rgba(255,255,255,.94);
        }
        .env-row.is-hidden{display:none}
        .env-key{
            font-size:11px;
            font-weight:900;
            overflow-wrap:anywhere;
        }
        .env-meta{
            margin-top:4px;
            color:var(--muted);
            font-size:9px;
            text-transform:uppercase;
            letter-spacing:.06em;
            overflow-wrap:anywhere;
        }
        .env-kind{color:var(--grey)}
        .secret-hint{
            margin-top:5px;
            color:var(--muted);
            font-size:10px;
        }
        .mini-check{
            display:flex;
            align-items:center;
            gap:6px;
            color:var(--muted);
            font-size:10px;
            text-transform:uppercase;
            letter-spacing:.05em;
        }
        .mini-check input{width:auto}
        .row-actions{
            display:grid;
            gap:6px;
            justify-items:stretch;
        }

        .add-grid{
            display:grid;
            grid-template-columns:220px minmax(0,1fr) auto;
            gap:8px;
            align-items:start;
        }
        .savebar{
            position:sticky;
            bottom:10px;
            z-index:14;
            justify-content:flex-end;
            margin-top:12px;
            padding:10px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(247,247,247,.94);
            box-shadow:var(--shadow);
            backdrop-filter:blur(14px);
            -webkit-backdrop-filter:blur(14px);
        }
        .footer{
            color:var(--muted);
            font-size:10px;
            text-align:center;
            margin:14px 0 0;
            overflow-wrap:anywhere;
        }
        .toast{
            position:fixed;
            right:14px;
            bottom:14px;
            z-index:30;
            max-width:min(440px,calc(100vw - 28px));
            border:1px solid var(--line);
            border-radius:var(--radius);
            background:rgba(255,255,255,.96);
            padding:10px 11px;
            color:var(--fg);
            display:none;
            box-shadow:var(--shadow);
        }
        .toast.show{display:block}
        .toast.bad{border-color:rgba(168,107,107,.42);color:var(--red)}
        .toast.ok{border-color:rgba(92,138,107,.38);color:var(--green)}

        .dd-wrap{display:block;min-width:0}
        .dd-wrap select.form-control.input-sm{
            appearance:none;
            -webkit-appearance:none;
            background-image:
                linear-gradient(45deg,transparent 50%,var(--muted) 50%),
                linear-gradient(135deg,var(--muted) 50%,transparent 50%);
            background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;
            background-size:5px 5px,5px 5px;
            background-repeat:no-repeat;
            padding-right:30px;
        }
        .kbd{
            display:inline-flex;
            align-items:center;
            justify-content:center;
            min-height:28px;
            border:1px solid var(--line);
            border-radius:var(--radius);
            padding:5px 8px;
            background:rgba(255,255,255,.68);
            font-size:9px;
            color:var(--muted);
            white-space:nowrap;
        }

        @media(max-width:1080px){
            :root{--side:280px}
            .env-row{grid-template-columns:minmax(160px,220px) minmax(0,1fr)}
            .row-actions{grid-column:1/-1;grid-template-columns:repeat(2,minmax(0,1fr))}
        }
        @media(max-width:920px){
            .shell{grid-template-columns:1fr}
            .side{
                position:relative;
                top:auto;
                height:auto;
                min-height:0;
                max-height:none;
            }
            .topbar{top:8px}
            .toolbar{top:76px;grid-template-columns:1fr}
            .group-title{top:190px}
            .stats{grid-template-columns:repeat(2,minmax(0,1fr))}
            .env-row{grid-template-columns:1fr}
            .row-actions{grid-template-columns:1fr}
            .add-grid{grid-template-columns:1fr}
        }
        @media(max-width:560px){
            .shell{padding:9px}
            .main{padding-bottom:112px}
            .topbar{
                position:relative;
                top:auto;
                align-items:flex-start;
                flex-direction:column;
            }
            .toolbar{
                position:relative;
                top:auto;
            }
            .group-title{
                top:8px;
            }
            .stats{grid-template-columns:1fr}
            .logo-box{width:34px;height:34px}
            .title h1{font-size:13px}
            .card-head{
                display:grid;
                grid-template-columns:1fr;
            }
            .card-head .actions{justify-content:flex-start}
            .env-row{padding:7px}
            .btn{width:100%}
            .actions{width:100%}
            .savebar{bottom:8px;justify-content:stretch}
            .savebar .btn{flex:1 1 100%}
        }
    </style>
</head>
<body>
<?php if (!$isAuthed) : ?>
    <main class="login panel">
        <div class="card-head">
            <div class="brand">
                <div class="logo-box" aria-hidden="true">
                    <svg width="19" height="19" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h10" stroke="#292b2c" stroke-width="2" stroke-linecap="round"/></svg>
                </div>
                <div class="title">
                    <h1>Clean <span class="mark">ENV</span> Editor</h1>
                    <p>auto-detect locked mode</p>
                </div>
            </div>
        </div>
        <div class="card-body">
            <?php if (empty($selectedEnv)) : ?>
                <p class="login-help">No .env file detected near this script. Put this file beside .env or inside public_html.</p>
            <?php else : ?>
                <form method="post" autocomplete="off">
                    <input type="hidden" name="csrf_token" value="<?= e((string) $_SESSION['csrf_token']) ?>">
                    <input type="hidden" name="action" value="login">
                    <input type="hidden" name="env_id" value="<?= e((string) $selectedEnv['id']) ?>">
                    <div class="field">
                        <label for="password">Editor Password</label>
                        <input id="password" type="password" name="password" placeholder="ENV_EDITOR_PASSWORD_HASH password / A2ROOT fallback" required autofocus autocomplete="current-password">
                    </div>
                    <button class="btn" type="submit">Login</button>
                </form>
            <?php endif; ?>
        </div>
    </main>
<?php else : ?>
    <div class="shell">
        <aside class="side panel">
            <div class="brand">
                <div class="logo-box" aria-hidden="true">
                    <svg width="19" height="19" viewBox="0 0 24 24" fill="none"><path d="M5 6h14M5 12h14M5 18h9" stroke="#292b2c" stroke-width="2" stroke-linecap="round"/></svg>
                </div>
                <div class="title">
                    <h1>Clean <span class="mark">ENV</span></h1>
                    <p>v<?= e(ENV_EDITOR_VERSION) ?></p>
                </div>
            </div>

            <div class="section-title">Auto detected</div>
            <div class="detected">
                <?php if (empty($candidates)) : ?>
                    <div class="notice bad">No .env candidates.</div>
                <?php endif; ?>

                <?php foreach ($candidates as $candidate) : ?>
                    <a class="env-card<?= activeSelected((string) $candidate['id'], (string) ($selectedEnv['id'] ?? '')) ?>" href="?env_id=<?= e((string) $candidate['id']) ?>">
                        <strong><?= e((string) $candidate['label']) ?></strong>
                        <small><?= e((string) $candidate['label']) ?></small>
                        <span class="chip-row">
                            <span class="chip <?= statusClass((bool) $candidate['readable']) ?>"><i class="chip-dot"></i>read</span>
                            <span class="chip <?= statusClass((bool) $candidate['writable']) ?>"><i class="chip-dot"></i>write</span>
                            <span class="chip blue"><i class="chip-dot"></i><?= e((string) $candidate['bytes']) ?>b</span>
                        </span>
                    </a>
                <?php endforeach; ?>
            </div>

            <div class="section-title">Runtime</div>
            <div class="chip-row">
                <span class="chip blue"><i class="chip-dot"></i><?= e(PHP_VERSION) ?></span>
                <span class="chip grey"><i class="chip-dot"></i><?= e($baseInfo['host']) ?></span>
                <span class="chip <?= statusClass($stats['backup_ready']) ?>"><i class="chip-dot"></i>backup</span>
            </div>
        </aside>

        <main class="main">
            <div class="topbar">
                <div class="title">
                    <h1>Clean Editor</h1>
                    <p><?= e((string) ($selectedEnv['label'] ?? 'No .env selected')) ?></p>
                </div>
                <form method="post" class="actions">
                    <input type="hidden" name="csrf_token" value="<?= e((string) $_SESSION['csrf_token']) ?>">
                    <input type="hidden" name="action" value="logout">
                    <input type="hidden" name="env_id" value="<?= e((string) ($selectedEnv['id'] ?? '')) ?>">
                    <button class="btn secondary" type="submit">Logout</button>
                </form>
            </div>

            <?php if ($flash !== null) : ?>
                <div class="notice <?= e($flash['type']) ?>"><?= e($flash['message']) ?></div>
            <?php endif; ?>

            <?php if (!empty($envData['error'])) : ?>
                <div class="notice bad"><?= e((string) $envData['error']) ?></div>
            <?php endif; ?>

            <?php if (!empty($envData['duplicates'])) : ?>
                <div class="notice bad">Duplicate keys blocked: <?= e(implode(', ', $envData['duplicates'])) ?></div>
            <?php endif; ?>

            <div class="notice warn">Secret values are never revealed. Empty secret input keeps current value. Use Generate or Clear intentionally.</div>

            <section class="card">
                <div class="card-body stats">
                    <div class="stat"><b><?= e((string) $stats['keys']) ?></b><span>keys</span></div>
                    <div class="stat"><b><?= e((string) $stats['secrets']) ?></b><span>secrets</span></div>
                    <div class="stat"><b><?= e((string) $stats['empty']) ?></b><span>empty</span></div>
                    <div class="stat"><b><?= $stats['writable'] ? 'OK' : 'NO' ?></b><span>writable</span></div>
                </div>
            </section>

            <form method="post" id="envForm" autocomplete="off">
                <input type="hidden" name="csrf_token" value="<?= e((string) $_SESSION['csrf_token']) ?>">
                <input type="hidden" name="action" value="save">
                <input type="hidden" name="env_id" value="<?= e((string) ($selectedEnv['id'] ?? '')) ?>">

                <section class="card">
                    <div class="card-head">
                        <h2>Variables</h2>
                        <div class="actions">
                            <button class="btn secondary" type="button" id="resetFilter">Reset</button>
                            <button class="btn" type="submit">Save ENV</button>
                        </div>
                    </div>
                    <div class="card-body">
                        <div class="toolbar">
                            <input type="search" id="filter" placeholder="Filter key / group / kind...">
                            <span class="dd-wrap is-fluid"><select class="form-control input-sm" id="kindFilter">
                                <option value="">All kinds</option>
                                <option value="secret">Secrets</option>
                                <option value="password_hash">Password Hash</option>
                                <option value="url">URL</option>
                                <option value="host">Host</option>
                                <option value="port">Port</option>
                                <option value="boolean">Boolean</option>
                                <option value="empty">Empty</option>
                            </select></span>
                            <span class="kbd">Ctrl+S Save</span>
                        </div>

                        <?php foreach ($groups as $groupName => $items) : ?>
                            <div class="group env-group" data-group="<?= e(strtolower((string) $groupName)) ?>">
                                <div class="group-title">
                                    <strong><?= e((string) $groupName) ?></strong>
                                    <span class="chip"><i class="chip-dot"></i><?= count($items) ?> keys</span>
                                </div>
                                <div class="rows">
                                    <?php foreach ($items as $item) : ?>
                                        <?php
                                        $key = (string) $item['key'];
                                        $value = (string) $item['value'];
                                        $kind = (string) $item['kind'];
                                        $sensitive = isSensitiveKey($key);
                                        $inputType = inputTypeForKind($kind);
                                        ?>
                                        <div class="env-row" data-key="<?= e(strtolower($key)) ?>" data-kind="<?= e($kind) ?>" data-group="<?= e(strtolower((string) $groupName)) ?>">
                                            <div>
                                                <div class="env-key"><?= e($key) ?></div>
                                                <div class="env-meta"><span class="env-kind"><?= e($kind) ?></span> · <?= $sensitive ? e(maskValue($value)) : e(strlen($value) . ' bytes') ?></div>
                                            </div>
                                            <div>
                                                <?php if ($sensitive) : ?>
                                                    <input type="password" name="env[<?= e($key) ?>]" value="" placeholder="[hidden] leave blank to keep current" autocomplete="new-password" data-secret-input="1">
                                                    <div class="secret-hint">Current value is masked. New value only.</div>
                                                <?php else : ?>
                                                    <input type="<?= e($inputType) ?>" name="env[<?= e($key) ?>]" value="<?= e($value) ?>" autocomplete="off" <?= $kind === 'port' ? 'min="1" max="65535"' : '' ?>>
                                                <?php endif; ?>
                                            </div>
                                            <div class="row-actions">
                                                <?php if ($sensitive) : ?>
                                                    <button class="btn secondary gen-secret" type="button">Generate</button>
                                                    <label class="mini-check"><input type="checkbox" name="clear[<?= e($key) ?>]" value="1"> clear</label>
                                                <?php else : ?>
                                                    <span class="chip"><i class="chip-dot"></i>editable</span>
                                                <?php endif; ?>
                                            </div>
                                        </div>
                                    <?php endforeach; ?>
                                </div>
                            </div>
                        <?php endforeach; ?>
                    </div>
                </section>

                <section class="card">
                    <div class="card-head"><h2>Add variable</h2></div>
                    <div class="card-body add-grid">
                        <input type="text" name="add_key" placeholder="NEW_KEY" autocomplete="off">
                        <input type="text" name="add_value" placeholder="value" autocomplete="off">
                        <button class="btn secondary" type="submit">Add + Save</button>
                    </div>
                </section>

                <div class="actions savebar">
                    <button class="btn" type="submit">Save ENV</button>
                    <button class="btn secondary" type="button" id="markSecrets">Generate Empty Secrets</button>
                </div>
            </form>

            <div class="footer">Backup enabled · max <?= e((string) MAX_BACKUPS) ?> files · remove this editor after editing.</div>
        </main>
    </div>

    <div class="toast" id="toast" role="status" aria-live="polite"></div>
<?php endif; ?>
<script nonce="<?= e($nonce) ?>">
(function () {
    var filter = document.getElementById('filter');
    var kindFilter = document.getElementById('kindFilter');
    var resetFilter = document.getElementById('resetFilter');
    var form = document.getElementById('envForm');
    var toast = document.getElementById('toast');
    var dirty = false;

    function showToast(message, type) {
        if (!toast) {
            return;
        }
        toast.textContent = message;
        toast.className = 'toast show ' + (type || 'ok');
        window.setTimeout(function () {
            toast.className = 'toast';
        }, 2200);
    }

    function randomHex(bytes) {
        var data = new Uint8Array(bytes);
        window.crypto.getRandomValues(data);
        var out = '';
        for (var i = 0; i < data.length; i++) {
            out += data[i].toString(16).padStart(2, '0');
        }
        return out;
    }

    function applyFilter() {
        var q = filter ? filter.value.toLowerCase().trim() : '';
        var kind = kindFilter ? kindFilter.value : '';
        var rows = document.querySelectorAll('.env-row');
        var groups = document.querySelectorAll('.env-group');

        for (var i = 0; i < rows.length; i++) {
            var key = rows[i].getAttribute('data-key') || '';
            var rowKind = rows[i].getAttribute('data-kind') || '';
            var group = rows[i].getAttribute('data-group') || '';
            var matchText = q === '' || key.indexOf(q) !== -1 || group.indexOf(q) !== -1 || rowKind.indexOf(q) !== -1;
            var matchKind = kind === '' || rowKind === kind;
            rows[i].classList.toggle('is-hidden', !(matchText && matchKind));
        }

        for (var g = 0; g < groups.length; g++) {
            var visible = groups[g].querySelector('.env-row:not(.is-hidden)');
            groups[g].style.display = visible ? '' : 'none';
        }
    }

    if (filter) {
        filter.addEventListener('input', applyFilter);
    }

    if (kindFilter) {
        kindFilter.addEventListener('change', applyFilter);
    }

    if (resetFilter) {
        resetFilter.addEventListener('click', function () {
            if (filter) {
                filter.value = '';
            }
            if (kindFilter) {
                kindFilter.value = '';
            }
            applyFilter();
        });
    }

    document.addEventListener('click', function (event) {
        var button = event.target.closest('.gen-secret');
        if (!button) {
            return;
        }
        var row = button.closest('.env-row');
        var input = row ? row.querySelector('input[data-secret-input="1"]') : null;
        if (!input) {
            return;
        }
        input.value = randomHex(32);
        dirty = true;
        showToast('Secret generated locally.', 'ok');
    });

    var markSecrets = document.getElementById('markSecrets');
    if (markSecrets) {
        markSecrets.addEventListener('click', function () {
            var inputs = document.querySelectorAll('input[data-secret-input="1"]');
            var count = 0;
            for (var i = 0; i < inputs.length; i++) {
                if (inputs[i].value === '') {
                    inputs[i].value = randomHex(32);
                    count++;
                }
            }
            dirty = count > 0 || dirty;
            showToast(count + ' empty secret fields generated.', 'ok');
        });
    }

    document.addEventListener('input', function () {
        dirty = true;
    });

    if (form) {
        form.addEventListener('submit', function () {
            dirty = false;
        });
    }

    window.addEventListener('beforeunload', function (event) {
        if (!dirty) {
            return;
        }
        event.preventDefault();
        event.returnValue = '';
    });

    document.addEventListener('keydown', function (event) {
        if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
            if (!form) {
                return;
            }
            event.preventDefault();
            form.requestSubmit();
        }
    });
}());
</script>
</body>
</html>
