<?php

declare(strict_types=1);

namespace PpPhp\Core\Auth;

use PpPhp\Core\DB;
use PpPhp\Core\Security;

/**
 * Pessimistic record locking for a generated app.
 *
 * Enabled in Misc → Advanced (source of truth) — the build bakes PP_LOCK_ENABLED,
 * PP_LOCK_TABLE and PP_LOCK_TIMEOUT from there. When a user opens a record to edit,
 * the app takes a lock; another user who opens the same record is told it is being
 * edited (and by whom) until the lock is released (save / cancel / leave) or expires
 * after PP_LOCK_TIMEOUT seconds. Rows match the Misc `record_locks` schema:
 * table_name, record_id, user_id, locked_at, expires_at, UNIQUE(table_name, record_id).
 *
 * Best-effort: any storage hiccup fails OPEN (edit allowed) rather than locking a
 * user out of their own data.
 */
final class RecordLock
{
    public static function enabled(): bool
    {
        return defined('PP_LOCK_ENABLED') && PP_LOCK_ENABLED && defined('PP_LOCK_TABLE') && PP_LOCK_TABLE !== '';
    }

    private static function table(): string { return (string) constant('PP_LOCK_TABLE'); }
    private static function ttl(): int { return defined('PP_LOCK_TIMEOUT') ? max(30, (int) PP_LOCK_TIMEOUT) : 300; }
    private static function uid(): int { return (int) (Security::CurrentUserData()['id'] ?? 0); }

    /** Remove locks that have timed out (so a crashed/closed session never blocks forever). */
    private static function sweep(): void
    {
        try { DB::Exec('DELETE FROM `' . self::table() . '` WHERE expires_at IS NOT NULL AND expires_at < NOW()'); }
        catch (\Throwable $e) {}
    }

    /**
     * Try to hold the lock for the current user.
     * @return array{ok:bool,lockedBy?:int,lockedByName?:string}
     */
    public static function acquire(string $table, int|string $recordId): array
    {
        if (!self::enabled()) return ['ok' => true];
        $uid = self::uid();
        if ($uid <= 0) return ['ok' => true];
        $rid = (string) $recordId;
        try {
            self::sweep();
            $row = DB::FetchAll('SELECT user_id FROM `' . self::table() . '` WHERE table_name = ? AND record_id = ? LIMIT 1', [$table, $rid])[0] ?? null;
            if ($row !== null && (int) $row['user_id'] !== $uid) {
                return ['ok' => false, 'lockedBy' => (int) $row['user_id'], 'lockedByName' => self::userName((int) $row['user_id'])];
            }
            // Free, or already ours → (re)take it and extend the expiry.
            DB::Exec(
                'INSERT INTO `' . self::table() . '` (table_name, record_id, user_id, locked_at, expires_at) '
                . 'VALUES (?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? SECOND)) '
                . 'ON DUPLICATE KEY UPDATE user_id = VALUES(user_id), locked_at = NOW(), expires_at = VALUES(expires_at)',
                [$table, $rid, $uid, self::ttl()]
            );
            return ['ok' => true];
        } catch (\Throwable $e) {
            return ['ok' => true]; // fail open
        }
    }

    /** Status without taking the lock. @return array{locked:bool,mine:bool,lockedBy?:int,lockedByName?:string} */
    public static function status(string $table, int|string $recordId): array
    {
        if (!self::enabled()) return ['locked' => false, 'mine' => false];
        try {
            self::sweep();
            $row = DB::FetchAll('SELECT user_id FROM `' . self::table() . '` WHERE table_name = ? AND record_id = ? LIMIT 1', [$table, (string) $recordId])[0] ?? null;
            if ($row === null) return ['locked' => false, 'mine' => false];
            $by = (int) $row['user_id'];
            return ['locked' => true, 'mine' => $by === self::uid(), 'lockedBy' => $by, 'lockedByName' => self::userName($by)];
        } catch (\Throwable $e) {
            return ['locked' => false, 'mine' => false];
        }
    }

    /** True when the current user may write this record (unlocked, or the lock is theirs). */
    public static function heldByOther(string $table, int|string $recordId): bool
    {
        $s = self::status($table, $recordId);
        return !empty($s['locked']) && empty($s['mine']);
    }

    /** Extend our lock (called by the edit page heartbeat). */
    public static function heartbeat(string $table, int|string $recordId): bool
    {
        if (!self::enabled()) return true;
        $uid = self::uid();
        if ($uid <= 0) return false;
        try {
            DB::Exec(
                'UPDATE `' . self::table() . '` SET expires_at = DATE_ADD(NOW(), INTERVAL ? SECOND) WHERE table_name = ? AND record_id = ? AND user_id = ?',
                [self::ttl(), $table, (string) $recordId, $uid]
            );
            return true;
        } catch (\Throwable $e) { return false; }
    }

    /** Release our lock (save / cancel / navigate away). */
    public static function release(string $table, int|string $recordId): void
    {
        if (!self::enabled()) return;
        $uid = self::uid();
        if ($uid <= 0) return;
        try { DB::Delete(self::table(), ['table_name' => $table, 'record_id' => (string) $recordId, 'user_id' => $uid]); }
        catch (\Throwable $e) {}
    }

    private static function userName(int $uid): string
    {
        try {
            $u = DB::FetchAll('SELECT username, fullname FROM `users` WHERE id = ? LIMIT 1', [$uid])[0] ?? null;
            if ($u) return (string) ($u['fullname'] ?: $u['username'] ?: ('user #' . $uid));
        } catch (\Throwable $e) {}
        return 'another user';
    }
}
