<?php

/**
 * Lightweight OAuth 2.0 data/access service for DSP -> JupyterHub integration.
 */
class OAuthService
{
    private const AUTH_CODE_TTL = 600; // 10 minutes
    private const ACCESS_TOKEN_TTL = 3600; // 1 hour
    private const REFRESH_TOKEN_TTL = 2592000; // 30 days

    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getClient(string $clientId): ?array
    {
        $sql = "SELECT client_id, client_name, client_secret_hash, redirect_uris, allowed_scopes, is_confidential
                FROM dsp_oauth_clients
                WHERE client_id = :client_id AND is_revoked = 0";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':client_id' => $clientId]);
        $client = $stmt->fetch(PDO::FETCH_ASSOC);

        return $client ?: null;
    }

    public function verifyClientSecret(array $client, string $candidate): bool
    {
        if (empty($client['client_secret_hash'])) {
            return $candidate === '';
        }

        return password_verify($candidate, $client['client_secret_hash']);
    }

    public function isRedirectUriAllowed(array $client, string $redirectUri): bool
    {
        $allowed = array_filter(array_map('trim', preg_split('/[\s,]+/', (string) ($client['redirect_uris'] ?? ''))));
        if (empty($allowed)) {
            return false;
        }

        foreach ($allowed as $prefix) {
            if (stripos($redirectUri, $prefix) === 0) {
                return true;
            }
        }

        return false;
    }

    public function isScopeAllowed(array $client, ?string $requestedScope): bool
    {
        $requestedScope = trim((string) $requestedScope);
        if ($requestedScope === '') {
            return true;
        }

        $allowedScopes = array_filter(array_map('trim', explode(' ', (string) ($client['allowed_scopes'] ?? ''))));

        if (empty($allowedScopes)) {
            return true;
        }

        foreach (explode(' ', $requestedScope) as $scope) {
            if (!in_array($scope, $allowedScopes, true)) {
                return false;
            }
        }

        return true;
    }

    public function issueAuthorizationCode(string $clientId, int $personId, string $redirectUri, ?string $scope = null): array
    {
        $code = $this->generateToken(32);
        $codeHash = $this->hashToken($code);
        $expiresAt = time() + self::AUTH_CODE_TTL;

        $sql = "INSERT INTO dsp_oauth_auth_codes
                (code_hash, client_id, person_id, scope, redirect_uri, expires_at, created_at)
                VALUES (:code_hash, :client_id, :person_id, :scope, :redirect_uri, FROM_UNIXTIME(:expires_at), NOW())";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':code_hash' => $codeHash,
            ':client_id' => $clientId,
            ':person_id' => $personId,
            ':scope' => $scope,
            ':redirect_uri' => $redirectUri,
            ':expires_at' => $expiresAt,
        ]);

        return [
            'code' => $code,
            'expires_at' => $expiresAt,
        ];
    }

    public function consumeAuthorizationCode(string $code, string $clientId): ?array
    {
        $codeHash = $this->hashToken($code);

        $sql = "SELECT code_hash, client_id, person_id, scope, redirect_uri, UNIX_TIMESTAMP(expires_at) AS expires_at
                FROM dsp_oauth_auth_codes
                WHERE code_hash = :code_hash";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':code_hash' => $codeHash]);
        $record = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$record) {
            return null;
        }

        // Delete regardless of outcome
        $deleteStmt = $this->pdo->prepare("DELETE FROM dsp_oauth_auth_codes WHERE code_hash = :code_hash");
        $deleteStmt->execute([':code_hash' => $codeHash]);

        if ((int) $record['expires_at'] < time()) {
            return null;
        }

        if ($record['client_id'] !== $clientId) {
            return null;
        }

        return $record;
    }

    public function issueTokens(string $clientId, int $personId, ?string $scope = null, bool $includeRefresh = true): array
    {
        $accessToken = $this->generateToken(43);
        $accessHash = $this->hashToken($accessToken);
        $accessExpiresAt = time() + self::ACCESS_TOKEN_TTL;

        $refreshToken = null;
        $refreshHash = null;
        $refreshExpiresAt = null;

        if ($includeRefresh) {
            $refreshToken = $this->generateToken(43);
            $refreshHash = $this->hashToken($refreshToken);
            $refreshExpiresAt = time() + self::REFRESH_TOKEN_TTL;
        }

        $sql = "INSERT INTO dsp_oauth_access_tokens
                (token_hash, client_id, person_id, scope, expires_at, refresh_token_hash, refresh_expires_at, created_at)
                VALUES (:token_hash, :client_id, :person_id, :scope, FROM_UNIXTIME(:expires_at),
                        :refresh_hash, " . ($refreshExpiresAt ? "FROM_UNIXTIME(:refresh_expires_at)" : "NULL") . ", NOW())";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([
            ':token_hash' => $accessHash,
            ':client_id' => $clientId,
            ':person_id' => $personId,
            ':scope' => $scope,
            ':expires_at' => $accessExpiresAt,
            ':refresh_hash' => $refreshHash,
            ':refresh_expires_at' => $refreshExpiresAt,
        ]);

        return [
            'access_token' => $accessToken,
            'access_expires_at' => $accessExpiresAt,
            'refresh_token' => $refreshToken,
            'refresh_expires_at' => $refreshExpiresAt,
            'token_type' => 'Bearer',
            'scope' => $scope,
        ];
    }

    public function exchangeRefreshToken(string $clientId, string $refreshToken): ?array
    {
        $refreshHash = $this->hashToken($refreshToken);

        $sql = "SELECT token_hash, client_id, person_id, scope, UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
                FROM dsp_oauth_access_tokens
                WHERE refresh_token_hash = :refresh_hash AND is_revoked = 0";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':refresh_hash' => $refreshHash]);
        $record = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$record) {
            return null;
        }

        if ($record['client_id'] !== $clientId) {
            return null;
        }

        if (!empty($record['refresh_expires_at']) && (int) $record['refresh_expires_at'] < time()) {
            $this->revokeTokenByHash($record['token_hash']);
            return null;
        }

        // Revoke old access token
        $this->revokeTokenByHash($record['token_hash']);

        // Issue new pair
        return $this->issueTokens($clientId, (int) $record['person_id'], $record['scope'], true);
    }

    public function getAccessToken(string $token): ?array
    {
        $hash = $this->hashToken($token);
        $sql = "SELECT token_hash, client_id, person_id, scope,
                       UNIX_TIMESTAMP(expires_at) AS expires_at,
                       refresh_token_hash,
                       UNIX_TIMESTAMP(refresh_expires_at) AS refresh_expires_at
                FROM dsp_oauth_access_tokens
                WHERE token_hash = :hash AND is_revoked = 0";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':hash' => $hash]);
        $record = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$record) {
            return null;
        }

        if ((int) $record['expires_at'] < time()) {
            $this->revokeTokenByHash($record['token_hash']);
            return null;
        }

        return $record;
    }

    public function revokeTokenByHash(string $hash): void
    {
        $sql = "UPDATE dsp_oauth_access_tokens SET is_revoked = 1, revoked_at = NOW()
                WHERE token_hash = :hash";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':hash' => $hash]);
    }

    public function recordTokenUsage(string $hash): void
    {
        $sql = "UPDATE dsp_oauth_access_tokens SET last_used_at = NOW() WHERE token_hash = :hash";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([':hash' => $hash]);
    }

    private function generateToken(int $length): string
    {
        return rtrim(strtr(base64_encode(random_bytes($length)), '+/', '-_'), '=');
    }

    private function hashToken(string $token): string
    {
        return hash('sha256', $token);
    }
}
