<?php

declare(strict_types=1);

namespace PpPhp\Core\Realtime;

use PpPhp\Core\DB;

/**
 * Presence for real-time channels — "who is here right now". A tiny heartbeat
 * table (one row per member per channel) that each open tab refreshes every few
 * seconds; a member whose heartbeat has gone stale is treated as gone. Members
 * can be signed-in users (id + name) or anonymous tabs (a random token).
 *
 * Kept deliberately simple + best-effort: presence must never throw up into the
 * page. Snappier than polling — a join/leave broadcasts a `__presence` message
 * on the channel so subscribers update immediately (see api/realtime.php).
 */
final class Presence
{
    private static string $table = 'pp_realtime_presence';
    private const STALE = 45;   // seconds without a heartbeat → considered gone

    public static function setTable(string $name): void
    {
        $clean = preg_replace('/[^A-Za-z0-9_]/', '', $name);
        if ($clean !== null && $clean !== '') {
            self::$table = $clean;
        }
    }

    public static function ensureTable(): void
    {
        $t = self::$table;
        DB::Query(
            "CREATE TABLE IF NOT EXISTS `{$t}` (\n" .
            "  channel VARCHAR(160) NOT NULL,\n" .
            "  member VARCHAR(80) NOT NULL,\n" .
            "  name VARCHAR(160) NULL,\n" .
            "  updated_at DATETIME NOT NULL,\n" .
            "  PRIMARY KEY (channel, member),\n" .
            "  INDEX idx_pr_fresh (channel, updated_at)\n" .
            ")"
        );
    }

    /**
     * Record/refresh a member's presence on a channel.
     * @return bool true when this was a NEW join (so the caller can broadcast).
     */
    public static function heartbeat(string $channel, string $member, ?string $name = null): bool
    {
        $channel = RealtimeBus::cleanChannel($channel);
        $member = self::cleanMember($member);
        if ($channel === '' || $member === '') {
            return false;
        }
        try {
            self::ensureTable();
            $existing = DB::FetchOne(
                "SELECT COUNT(*) FROM `" . self::$table . "` WHERE channel = ? AND member = ? AND updated_at > ?",
                [$channel, $member, self::cutoff()]
            );
            DB::Query(
                "REPLACE INTO `" . self::$table . "` (channel, member, name, updated_at) VALUES (?, ?, ?, ?)",
                [$channel, $member, $name !== null ? mb_substr($name, 0, 160) : null, date('Y-m-d H:i:s')]
            );
            return !((int) $existing > 0);   // fresh row that wasn't already present = a join
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Current members on a channel (fresh heartbeats only).
     * @return array<int, array{member:string,name:?string}>
     */
    public static function members(string $channel): array
    {
        $channel = RealtimeBus::cleanChannel($channel);
        if ($channel === '') {
            return [];
        }
        try {
            $rows = DB::FetchAll(
                "SELECT member, name FROM `" . self::$table . "` WHERE channel = ? AND updated_at > ? ORDER BY name IS NULL, name, member",
                [$channel, self::cutoff()]
            );
            $out = [];
            foreach ((array) $rows as $r) {
                $out[] = ['member' => (string) $r['member'], 'name' => $r['name'] !== null ? (string) $r['name'] : null];
            }
            return $out;
        } catch (\Throwable $e) {
            return [];
        }
    }

    /** Remove a member from a channel (explicit leave). @return bool true if a row was removed. */
    public static function leave(string $channel, string $member): bool
    {
        $channel = RealtimeBus::cleanChannel($channel);
        $member = self::cleanMember($member);
        if ($channel === '' || $member === '') {
            return false;
        }
        try {
            DB::Query("DELETE FROM `" . self::$table . "` WHERE channel = ? AND member = ?", [$channel, $member]);
            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /** Sweep stale rows (best-effort). */
    public static function gc(): void
    {
        try {
            DB::Query("DELETE FROM `" . self::$table . "` WHERE updated_at < ?", [self::cutoff()]);
        } catch (\Throwable $e) {
            // ignore
        }
    }

    private static function cleanMember(string $member): string
    {
        return substr(preg_replace('/[^A-Za-z0-9_.:-]/', '', trim($member)) ?? '', 0, 80);
    }

    private static function cutoff(): string
    {
        return date('Y-m-d H:i:s', time() - self::STALE);
    }
}
