<?php
/**
 * Locked-demo helpers: presence, online banner, idle-safe seed restore.
 * No-ops unless demo_is_enabled() (DEMO_MODE, demo host, or demo DB name).
 */
declare(strict_types=1);

require_once __DIR__ . '/db.php';

const DEMO_VISITOR_COOKIE = 'bs_demo_vid';
const DEMO_STATE_FILE = 'demo_state.json';

function demo_is_enabled(): bool
{
    if (defined('DEMO_MODE') && DEMO_MODE) {
        return true;
    }
    $host = strtolower((string) ($_SERVER['HTTP_HOST'] ?? ''));
    $host = preg_replace('/:\d+$/', '', $host) ?? $host;
    if ($host === 'demo.barber-shop.us') {
        return true;
    }
    if (defined('APP_BASE') && str_contains((string) APP_BASE, 'demo')) {
        return true;
    }
    if (defined('DB_NAME') && stripos((string) DB_NAME, 'demo') !== false) {
        return true;
    }
    return false;
}

function demo_idle_seconds(): int
{
    $n = defined('DEMO_PRESENCE_IDLE') ? (int) DEMO_PRESENCE_IDLE : 600;
    return max(120, $n);
}

function demo_state_path(): string
{
    return APP_ROOT . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . DEMO_STATE_FILE;
}

function demo_ensure_schema(): void
{
    static $done = false;
    if ($done) {
        return;
    }
    $pdo = db();
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS demo_presence (
            visitor_id VARCHAR(64) NOT NULL,
            role VARCHAR(20) NOT NULL DEFAULT \'guest\',
            page VARCHAR(255) NULL,
            last_seen DATETIME NOT NULL,
            created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (visitor_id),
            KEY idx_demo_presence_seen (last_seen),
            KEY idx_demo_presence_role (role)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
    );
    $done = true;
}

function demo_visitor_id(): string
{
    $id = (string) ($_COOKIE[DEMO_VISITOR_COOKIE] ?? '');
    if (!preg_match('/^[a-f0-9]{16,64}$/i', $id)) {
        $id = bin2hex(random_bytes(16));
        if (!headers_sent()) {
            $path = (defined('APP_BASE') && APP_BASE !== '') ? APP_BASE . '/' : '/';
            setcookie(DEMO_VISITOR_COOKIE, $id, [
                'expires' => time() + 86400 * 400,
                'path' => $path,
                'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
                'httponly' => false,
                'samesite' => 'Lax',
            ]);
        }
        $_COOKIE[DEMO_VISITOR_COOKIE] = $id;
    }
    return $id;
}

function demo_current_role(): string
{
    if (session_status() === PHP_SESSION_ACTIVE) {
        if (!empty($_SESSION['admin_logged_in'])) {
            return 'admin';
        }
        if (!empty($_SESSION['client_user_id'])) {
            return 'client';
        }
    }
    return 'guest';
}

function demo_presence_touch(string $visitorId, string $role = 'guest', string $page = ''): void
{
    if (!demo_is_enabled() || $visitorId === '') {
        return;
    }
    demo_ensure_schema();
    $role = in_array($role, ['admin', 'client', 'guest'], true) ? $role : 'guest';
    $page = substr($page, 0, 255);
    $stmt = db()->prepare(
        'INSERT INTO demo_presence (visitor_id, role, page, last_seen)
         VALUES (?, ?, ?, NOW())
         ON DUPLICATE KEY UPDATE role = VALUES(role), page = VALUES(page), last_seen = NOW()'
    );
    $stmt->execute([$visitorId, $role, $page]);
    db()->exec(
        'DELETE FROM demo_presence WHERE last_seen < (NOW() - INTERVAL 24 HOUR)'
    );
}

function demo_presence_leave(string $visitorId): void
{
    if (!demo_is_enabled() || $visitorId === '') {
        return;
    }
    try {
        demo_ensure_schema();
        db()->prepare('DELETE FROM demo_presence WHERE visitor_id = ?')->execute([$visitorId]);
    } catch (Throwable $e) {
        // table may not exist yet
    }
}

/**
 * @return array{count:int, logged_in:int, guests:int, roles:array<string,int>, last_reset:?string, last_reset_label:?string}
 */
function demo_presence_snapshot(): array
{
    $empty = [
        'count' => 0,
        'logged_in' => 0,
        'guests' => 0,
        'roles' => ['admin' => 0, 'client' => 0, 'guest' => 0],
        'last_reset' => null,
        'last_reset_label' => null,
    ];
    if (!demo_is_enabled()) {
        return $empty;
    }
    try {
        demo_ensure_schema();
        $idle = demo_idle_seconds();
        $stmt = db()->prepare(
            'SELECT role, COUNT(*) AS n FROM demo_presence
             WHERE last_seen >= (NOW() - INTERVAL ? SECOND)
             GROUP BY role'
        );
        $stmt->execute([$idle]);
        $roles = ['admin' => 0, 'client' => 0, 'guest' => 0];
        $count = 0;
        foreach ($stmt->fetchAll() as $row) {
            $role = (string) $row['role'];
            $n = (int) $row['n'];
            if (!isset($roles[$role])) {
                $roles[$role] = 0;
            }
            $roles[$role] += $n;
            $count += $n;
        }
        $loggedIn = $roles['admin'] + $roles['client'];
        $state = demo_read_state();
        $last = $state['last_reset'] ?? null;
        $label = null;
        if (is_string($last) && $last !== '') {
            $ts = strtotime($last);
            if ($ts) {
                $label = date('g:i A', $ts) . ' on ' . date('M j', $ts);
            }
        }
        return [
            'count' => $count,
            'logged_in' => $loggedIn,
            'guests' => $roles['guest'],
            'roles' => $roles,
            'last_reset' => is_string($last) ? $last : null,
            'last_reset_label' => $label,
        ];
    } catch (Throwable $e) {
        return $empty;
    }
}

function demo_is_busy(): bool
{
    $snap = demo_presence_snapshot();
    return $snap['count'] > 0;
}

/**
 * @return array{last_reset:?string}
 */
function demo_read_state(): array
{
    $path = demo_state_path();
    if (!is_readable($path)) {
        return ['last_reset' => null];
    }
    $raw = json_decode((string) file_get_contents($path), true);
    if (!is_array($raw)) {
        return ['last_reset' => null];
    }
    return [
        'last_reset' => isset($raw['last_reset']) ? (string) $raw['last_reset'] : null,
    ];
}

function demo_write_state(array $state): void
{
    $dir = dirname(demo_state_path());
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    file_put_contents(
        demo_state_path(),
        json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
    );
}

function demo_banner_copy(array $snap): string
{
    $n = (int) $snap['count'];
    $logged = (int) $snap['logged_in'];
    if ($n <= 1) {
        $msg = 'You are exploring this demo alone. Test freely — the shop restores to a clean snapshot after everyone leaves.';
    } else {
        $msg = $n . ' people are exploring this demo right now. Bookings and settings you change can be seen or overwritten by others.';
        if ($logged > 0) {
            $msg .= ' Someone is signed in (admin or client).';
        }
    }
    $reset = $snap['last_reset_label'] ?? null;
    if (is_string($reset) && $reset !== '') {
        $msg .= ' Last reset: ' . $reset . '.';
    }
    return $msg;
}

function demo_render_boot(): void
{
    if (!demo_is_enabled()) {
        return;
    }
    $role = demo_current_role();
    if ($role === 'admin' || $role === 'client') {
        demo_presence_touch(demo_visitor_id(), $role, (string) ($_SERVER['REQUEST_URI'] ?? ''));
    }
    $snap = demo_presence_snapshot();
    $base = defined('APP_BASE') ? APP_BASE : '';
    $copy = demo_banner_copy($snap);
    ?>
<div class="demo-presence-banner" id="demo-presence-banner" role="status" aria-live="polite">
  <span class="demo-presence-kicker">Demo</span>
  <span class="demo-presence-copy" id="demo-presence-copy"><?= htmlspecialchars($copy) ?></span>
</div>
<script src="<?= htmlspecialchars($base) ?>/assets/demo-presence.js?v=20260823a" defer></script>
<?php
}

function demo_db_is_safe_target(): bool
{
    return defined('DB_NAME') && (bool) preg_match('/demo/i', (string) DB_NAME);
}

/**
 * Frozen golden snapshot only.
 * Never fall back to phpMyAdmin / admin/*.sql dumps — those are how cron
 * restored a stale non-golden copy.
 */
function demo_seed_path(): string
{
    return APP_ROOT . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'demobarbershop_seed.sql';
}

function demo_seed_is_golden(string $path): bool
{
    if (!is_readable($path)) {
        return false;
    }
    $fh = fopen($path, 'r');
    if ($fh === false) {
        return false;
    }
    $head = (string) fread($fh, 4096);
    fclose($fh);
    return stripos($head, 'GOLDEN SEED') !== false;
}

function demo_seed_describe(string $path): string
{
    if (!is_readable($path)) {
        return $path . ' (missing)';
    }
    return sprintf(
        '%s (%s bytes, mtime %s, golden=%s)',
        $path,
        number_format((int) filesize($path)),
        date('c', (int) filemtime($path)),
        demo_seed_is_golden($path) ? 'yes' : 'NO — add "-- BarberShop GOLDEN SEED" header'
    );
}

function demo_log(string $line): void
{
    $dir = APP_ROOT . DIRECTORY_SEPARATOR . 'storage';
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    $stamp = date('c');
    file_put_contents($dir . DIRECTORY_SEPARATOR . 'demo_reset.log', "[{$stamp}] {$line}\n", FILE_APPEND);
}

/**
 * Split a dump into executable statements (skips USE / CREATE DATABASE).
 *
 * @return list<string>
 */
function demo_split_sql(string $sql): array
{
    $sql = preg_replace('/^--.*$/m', '', $sql) ?? $sql;
    $out = [];
    $buf = '';
    $inStr = false;
    $quote = '';
    $len = strlen($sql);
    for ($i = 0; $i < $len; $i++) {
        $ch = $sql[$i];
        $buf .= $ch;
        if ($inStr) {
            if ($ch === '\\' && $i + 1 < $len) {
                $buf .= $sql[$i + 1];
                $i++;
                continue;
            }
            if ($ch === $quote) {
                $inStr = false;
            }
            continue;
        }
        if ($ch === "'" || $ch === '"') {
            $inStr = true;
            $quote = $ch;
            continue;
        }
        if ($ch === ';') {
            $stmt = trim($buf);
            $buf = '';
            if ($stmt !== '' && $stmt !== ';') {
                $out[] = $stmt;
            }
        }
    }
    $tail = trim($buf);
    if ($tail !== '') {
        $out[] = $tail;
    }
    return $out;
}

function demo_should_skip_sql(string $stmt): bool
{
    $s = ltrim($stmt);
    if ($s === '' || str_starts_with($s, '--')) {
        return true;
    }
    if (preg_match('/^(CREATE\s+DATABASE|USE\s+)/i', $s)) {
        return true;
    }
    return false;
}

/**
 * Wipe current demo tables so phpMyAdmin dumps (no DROP TABLE) can recreate them.
 */
function demo_drop_all_tables(): int
{
    $pdo = db();
    $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN);
    $n = 0;
    foreach ($tables as $table) {
        $name = (string) $table;
        if (!preg_match('/^[A-Za-z0-9_]+$/', $name)) {
            continue;
        }
        $pdo->exec('DROP TABLE IF EXISTS `' . $name . '`');
        $n++;
    }
    return $n;
}

function demo_exec_sql_dump(string $path): int
{
    $sql = file_get_contents($path);
    if ($sql === false || $sql === '') {
        throw new RuntimeException('Cannot read seed file: ' . $path);
    }
    $pdo = db();
    $pdo->exec('SET FOREIGN_KEY_CHECKS=0');
    $count = 0;
    try {
        demo_drop_all_tables();
        foreach (demo_split_sql($sql) as $stmt) {
            if (demo_should_skip_sql($stmt)) {
                continue;
            }
            $pdo->exec($stmt);
            $count++;
        }
    } finally {
        try {
            $pdo->exec('UNLOCK TABLES');
        } catch (Throwable $e) {
            // ignore
        }
        $pdo->exec('SET FOREIGN_KEY_CHECKS=1');
    }
    return $count;
}

/**
 * Apply migration files not recorded in schema_migrations (e.g. 008 after an older seed).
 *
 * @return list<string>
 */
function demo_apply_pending_migrations(): array
{
    $pdo = db();
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS schema_migrations (
            filename VARCHAR(255) PRIMARY KEY,
            applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
    );
    $files = glob(APP_ROOT . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . '*.sql') ?: [];
    sort($files);
    $applied = [];
    foreach ($files as $file) {
        $name = basename($file);
        if ($name === 'fix_admin_user.sql' || str_starts_with($name, '001_')) {
            continue;
        }
        $chk = $pdo->prepare('SELECT 1 FROM schema_migrations WHERE filename = ?');
        $chk->execute([$name]);
        if ($chk->fetchColumn()) {
            continue;
        }
        $sql = file_get_contents($file);
        if ($sql === false) {
            throw new RuntimeException('Cannot read ' . $name);
        }
        foreach (demo_split_sql($sql) as $stmt) {
            if (demo_should_skip_sql($stmt)) {
                continue;
            }
            $clean = trim(preg_replace('/^\s*--.*$/m', '', $stmt) ?? $stmt);
            if ($clean === '') {
                continue;
            }
            $pdo->exec($clean);
        }
        $pdo->prepare('INSERT INTO schema_migrations (filename) VALUES (?)')->execute([$name]);
        $applied[] = $name;
    }
    return $applied;
}

/**
 * @return array{ok:bool, skipped:bool, reason:string, statements?:int, migrations?:list<string>}
 */
function demo_reset_if_idle(bool $force = false): array
{
    if (!demo_is_enabled()) {
        return ['ok' => false, 'skipped' => true, 'reason' => 'Demo mode is not enabled.'];
    }
    if (!demo_db_is_safe_target()) {
        return [
            'ok' => false,
            'skipped' => true,
            'reason' => 'Refusing reset: DB_NAME must contain "demo" (got ' . (defined('DB_NAME') ? DB_NAME : '?') . ').',
        ];
    }
    $seed = demo_seed_path();
    if (!is_readable($seed)) {
        return ['ok' => false, 'skipped' => true, 'reason' => 'Seed file missing: ' . $seed];
    }
    if (!demo_seed_is_golden($seed)) {
        demo_log('WARNING: seed file has no GOLDEN SEED header: ' . demo_seed_describe($seed));
    }

    $snap = demo_presence_snapshot();
    if (!$force && $snap['count'] > 0) {
        $reason = sprintf(
            'Skipped: %d active explorer(s) (logged in: %d, guests: %d). Idle window %ds.',
            $snap['count'],
            $snap['logged_in'],
            $snap['guests'],
            demo_idle_seconds()
        );
        demo_log($reason);
        return ['ok' => true, 'skipped' => true, 'reason' => $reason];
    }

    $lockPath = APP_ROOT . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'demo_reset.lock';
    $dir = dirname($lockPath);
    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }
    $fh = fopen($lockPath, 'c+');
    if ($fh === false) {
        return ['ok' => false, 'skipped' => true, 'reason' => 'Cannot open reset lock file.'];
    }
    if (!flock($fh, LOCK_EX | LOCK_NB)) {
        fclose($fh);
        $reason = 'Skipped: another reset is already running.';
        demo_log($reason);
        return ['ok' => true, 'skipped' => true, 'reason' => $reason];
    }

    try {
        // Re-check after lock in case someone arrived.
        if (!$force && demo_is_busy()) {
            $reason = 'Skipped: activity appeared while waiting for lock.';
            demo_log($reason);
            return ['ok' => true, 'skipped' => true, 'reason' => $reason];
        }

        $n = demo_exec_sql_dump($seed);
        $migrations = demo_apply_pending_migrations();
        demo_ensure_schema();
        demo_write_state(['last_reset' => date('c')]);
        $reason = sprintf(
            'Reset complete (%d SQL statements) from %s. Migrations applied: %s',
            $n,
            demo_seed_describe($seed),
            $migrations ? implode(', ', $migrations) : 'none'
        );
        demo_log($reason);
        return ['ok' => true, 'skipped' => false, 'reason' => $reason, 'statements' => $n, 'migrations' => $migrations];
    } finally {
        flock($fh, LOCK_UN);
        fclose($fh);
    }
}
