<?php
// =============================================================
//  include/ApiTokenManager.php
//
//  Bearer-token issuing/verification for the mobile REST API (api/v1/*).
//  Tokens are opaque random strings; only their sha256 hash is ever
//  stored (see scripts/add_mobile_api_tables.sql) — same principle as
//  password hashing, so a stolen DB dump doesn't hand out usable tokens.
//
//  Polymorphic over the same three account tables AccountRepository
//  already unifies (users / rotation_nurses / clinical_students).
// =============================================================

final class ApiTokenManager
{
    private mysqli $conn;
    private const DEFAULT_TTL_DAYS = 90;

    public function __construct(mysqli $conn)
    {
        $this->conn = $conn;
    }

    /**
     * Issues a fresh token for the given account. Returns the raw token —
     * shown to the caller exactly once; only its hash is persisted.
     */
    public function issue(string $accountTable, int $accountId, ?string $deviceLabel = null, string $platform = 'other'): string
    {
        $raw     = bin2hex(random_bytes(40));
        $hash    = hash('sha256', $raw);
        $expires = date('Y-m-d H:i:s', time() + self::DEFAULT_TTL_DAYS * 86400);

        $stmt = $this->conn->prepare("
            INSERT INTO api_access_tokens (account_table, account_id, token_hash, device_label, platform, expires_at)
            VALUES (?, ?, ?, ?, ?, ?)
        ");
        $stmt->bind_param('sissss', $accountTable, $accountId, $hash, $deviceLabel, $platform, $expires);
        $stmt->execute();
        $stmt->close();

        return $raw;
    }

    /**
     * Resolves a raw bearer token to its owning account, or null if missing,
     * expired, or revoked. Touches last_used_at on success.
     *
     * @return array{account_table: string, account_id: int}|null
     */
    public function resolve(string $rawToken): ?array
    {
        $hash = hash('sha256', $rawToken);
        $stmt = $this->conn->prepare("
            SELECT id, account_table, account_id
            FROM api_access_tokens
            WHERE token_hash = ?
              AND revoked_at IS NULL
              AND (expires_at IS NULL OR expires_at > NOW())
            LIMIT 1
        ");
        $stmt->bind_param('s', $hash);
        $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        if (!$row) return null;

        $upd = $this->conn->prepare("UPDATE api_access_tokens SET last_used_at = NOW() WHERE id = ?");
        $upd->bind_param('i', $row['id']);
        $upd->execute();
        $upd->close();

        return ['account_table' => $row['account_table'], 'account_id' => (int)$row['account_id']];
    }

    /** Revokes the presented token — used by logout. Silently no-ops if not found. */
    public function revoke(string $rawToken): void
    {
        $hash = hash('sha256', $rawToken);
        $stmt = $this->conn->prepare("UPDATE api_access_tokens SET revoked_at = NOW() WHERE token_hash = ? AND revoked_at IS NULL");
        $stmt->bind_param('s', $hash);
        $stmt->execute();
        $stmt->close();
    }

    /** Revokes every token for an account — used on password change/reset. */
    public function revokeAllForAccount(string $accountTable, int $accountId): void
    {
        $stmt = $this->conn->prepare("
            UPDATE api_access_tokens SET revoked_at = NOW()
            WHERE account_table = ? AND account_id = ? AND revoked_at IS NULL
        ");
        $stmt->bind_param('si', $accountTable, $accountId);
        $stmt->execute();
        $stmt->close();
    }
}
