<?php
/**
 * Shared LUKS lock-state detection — single source of truth for the panel.
 * Mirrors scripts/lib-luks-state.sh. Used by:
 *   - web-files/auto-prepend.php   (intercept all panel access when locked)
 *   - web-files/api/health.php     (?check=luks and ?check=all aggregation)
 *   - web-files/pages/dashboard.php (status badge on Server Information card)
 *   - web-files/pages/locked-unlock.php / web-files/api/luks-unlock.php
 *
 * Reads /etc/whp/luks.conf — kept on the unencrypted boot drive intentionally,
 * so the panel can determine lock-state before /docker is mounted.
 *
 * Mount-state detection parses /proc/self/mountinfo directly — no shell
 * execution, no command-injection surface.
 */

if (!function_exists('luks_conf_path')) {
    function luks_conf_path(): string {
        return getenv('LUKS_CONF_PATH') ?: '/etc/whp/luks.conf';
    }
}

if (!function_exists('luks_load_conf')) {
    /**
     * Parse /etc/whp/luks.conf as KEY=VALUE pairs. Returns null if the file
     * doesn't exist or isn't readable — that's the "LUKS not configured"
     * signal everything else keys off.
     */
    function luks_load_conf(): ?array {
        $path = luks_conf_path();
        if (!is_readable($path)) {
            return null;
        }
        $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) {
            return null;
        }
        $conf = [];
        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '' || $line[0] === '#') {
                continue;
            }
            if (preg_match('/^([A-Z_][A-Z0-9_]*)=(.*)$/', $line, $m)) {
                $val = $m[2];
                if (preg_match('/^"(.*)"$/', $val, $vm) || preg_match("/^'(.*)'$/", $val, $vm)) {
                    $val = $vm[1];
                }
                $conf[$m[1]] = $val;
            }
        }
        return $conf;
    }
}

if (!function_exists('luks_mount_source')) {
    /**
     * Look up the source device a given mount point is backed by, by parsing
     * /proc/self/mountinfo. Returns '' if the path isn't a mount point.
     *
     * Format per `man 5 proc`:
     *   <id> <parent> <maj:min> <root> <mount_point> <opts> [opt-fields...] - <fstype> <source> <super_opts>
     * Optional fields are variable in count, terminated by a literal "-",
     * so we have to scan for the separator rather than index by position.
     */
    function luks_mount_source(string $mount_point): string {
        $lines = file('/proc/self/mountinfo', FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            return '';
        }
        foreach ($lines as $line) {
            $parts = explode(' ', $line);
            if (count($parts) < 10) {
                continue;
            }
            if (($parts[4] ?? '') !== $mount_point) {
                continue;
            }
            $sep_idx = array_search('-', $parts, true);
            if ($sep_idx === false) {
                continue;
            }
            return $parts[$sep_idx + 2] ?? '';
        }
        return '';
    }
}

if (!function_exists('luks_state_summary')) {
    /**
     * Full state in one call. Callers that only need a bool/string can use
     * luks_is_enabled / luks_is_unlocked / luks_locked_reason below — those
     * just unwrap fields from this summary.
     *
     * Shape:
     *   [
     *     'enabled'        => bool,     // LUKS was provisioned on this server
     *     'unlocked'       => bool,     // /docker is on the expected mapper right now
     *     'state'          => string,   // 'disabled' | 'unlocked' | 'locked'
     *     'reason'         => string,   // why it's locked (empty when unlocked/disabled)
     *     'mount_point'    => string,   // /docker (configurable)
     *     'mapper'         => string,   // docker-crypt (configurable)
     *     'backing_device' => string,   // /dev/sdb (informational)
     *     'mounted_on'     => string,   // actual current source, '' if not mounted
     *   ]
     */
    function luks_state_summary(): array {
        $conf = luks_load_conf();
        if ($conf === null || ($conf['LUKS_ENABLED'] ?? '0') !== '1') {
            return [
                'enabled'        => false,
                'unlocked'       => false,
                'state'          => 'disabled',
                'reason'         => '',
                'mount_point'    => '',
                'mapper'         => '',
                'backing_device' => '',
                'mounted_on'     => '',
            ];
        }
        $mount_point    = $conf['LUKS_MOUNT_POINT']    ?? '/docker';
        $mapper         = $conf['LUKS_MAPPER_NAME']    ?? 'docker-crypt';
        $backing_device = $conf['LUKS_BACKING_DEVICE'] ?? '';
        $expected       = '/dev/mapper/' . $mapper;
        $source         = luks_mount_source($mount_point);
        $unlocked       = ($source === $expected);
        $reason         = '';
        if (!$unlocked) {
            $reason = $source === ''
                ? ($mount_point . ' is not mounted')
                : ($mount_point . ' is mounted on ' . $source . ' instead of ' . $expected);
        }
        return [
            'enabled'        => true,
            'unlocked'       => $unlocked,
            'state'          => $unlocked ? 'unlocked' : 'locked',
            'reason'         => $reason,
            'mount_point'    => $mount_point,
            'mapper'         => $mapper,
            'backing_device' => $backing_device,
            'mounted_on'     => $source,
        ];
    }
}

if (!function_exists('luks_is_enabled')) {
    function luks_is_enabled(): bool {
        return luks_state_summary()['enabled'];
    }
}

if (!function_exists('luks_is_unlocked')) {
    function luks_is_unlocked(): bool {
        return luks_state_summary()['unlocked'];
    }
}

if (!function_exists('luks_locked_reason')) {
    function luks_locked_reason(): string {
        return luks_state_summary()['reason'];
    }
}
