<?php

declare(strict_types=1);

namespace App\Framework\OAuth;

use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\OAuth\Storage\OAuthTokenRepositoryInterface;
use App\Framework\OAuth\Storage\StoredOAuthToken;

/**
 * OAuth Service
 *
 * Central service for OAuth token management with automatic refresh
 */
final readonly class OAuthService implements OAuthServiceInterface
{
    /**
     * @param array<string, OAuthProvider> $providers
     */
    public function __construct(
        private OAuthTokenRepositoryInterface $tokenRepository,
        private array $providers = [],
    ) {
    }

    /**
     * Get provider by name
     */
    public function getProvider(string $name): OAuthProvider
    {
        if (! isset($this->providers[$name])) {
            throw FrameworkException::create(
                ErrorCode::SYSTEM_CONFIG_MISSING,
                "OAuth provider '{$name}' not configured"
            )->withData([
                'provider' => $name,
                'available_providers' => array_keys($this->providers),
            ]);
        }

        return $this->providers[$name];
    }

    /**
     * Get authorization URL for provider
     *
     * @param array<string, mixed> $options
     */
    public function getAuthorizationUrl(string $provider, array $options = []): string
    {
        return $this->getProvider($provider)->getAuthorizationUrl($options);
    }

    /**
     * Handle OAuth callback and store token
     */
    public function handleCallback(
        string $userId,
        string $provider,
        string $code,
        ?string $state = null
    ): StoredOAuthToken {
        $oauthProvider = $this->getProvider($provider);
        $token = $oauthProvider->getAccessToken($code, $state);

        return $this->tokenRepository->saveForUser($userId, $provider, $token);
    }

    /**
     * Get token for user with automatic refresh if expired
     */
    public function getTokenForUser(string $userId, string $provider): StoredOAuthToken
    {
        $storedToken = $this->tokenRepository->findForUser($userId, $provider);

        if ($storedToken === null) {
            throw FrameworkException::create(
                ErrorCode::ENTITY_NOT_FOUND,
                "No OAuth token found for user"
            )->withData([
                'user_id' => $userId,
                'provider' => $provider,
            ]);
        }

        // Auto-refresh if expired
        if ($storedToken->isExpired() && $storedToken->canRefresh()) {
            return $this->refreshToken($storedToken);
        }

        if ($storedToken->isExpired()) {
            throw FrameworkException::create(
                ErrorCode::AUTH_TOKEN_EXPIRED,
                "OAuth token expired and cannot be refreshed"
            )->withData([
                'user_id' => $userId,
                'provider' => $provider,
                'expires_at' => $storedToken->token->getExpiresAt()->format('Y-m-d H:i:s'),
            ]);
        }

        return $storedToken;
    }

    /**
     * Refresh an expired token
     */
    public function refreshToken(StoredOAuthToken $storedToken): StoredOAuthToken
    {
        $oauthProvider = $this->getProvider($storedToken->provider);
        $newToken = $oauthProvider->refreshToken($storedToken->token);

        $updated = $storedToken->withRefreshedToken($newToken);

        return $this->tokenRepository->save($updated);
    }

    /**
     * Revoke token and remove from storage
     */
    public function revokeToken(string $userId, string $provider): bool
    {
        $storedToken = $this->tokenRepository->findForUser($userId, $provider);

        if ($storedToken === null) {
            return true; // Already revoked
        }

        $oauthProvider = $this->getProvider($provider);
        $oauthProvider->revokeToken($storedToken->token);

        return $this->tokenRepository->deleteForUser($userId, $provider);
    }

    /**
     * Get user profile from provider
     *
     * @return array<string, mixed>
     */
    public function getUserProfile(string $userId, string $provider): array
    {
        $storedToken = $this->getTokenForUser($userId, $provider);
        $oauthProvider = $this->getProvider($provider);

        return $oauthProvider->getUserProfile($storedToken->token);
    }

    /**
     * Check if user has connected a provider
     */
    public function hasProvider(string $userId, string $provider): bool
    {
        return $this->tokenRepository->findForUser($userId, $provider) !== null;
    }

    /**
     * Get all providers for user
     *
     * @return array<StoredOAuthToken>
     */
    public function getUserProviders(string $userId): array
    {
        return $this->tokenRepository->findAllForUser($userId);
    }

    /**
     * Refresh all expiring tokens (background job)
     */
    public function refreshExpiringTokens(int $withinSeconds = 300): int
    {
        $tokens = $this->tokenRepository->findExpiringSoon($withinSeconds);
        $refreshed = 0;

        foreach ($tokens as $storedToken) {
            try {
                $this->refreshToken($storedToken);
                $refreshed++;
            } catch (\Exception $e) {
                // Log but continue with other tokens
                error_log("Failed to refresh token for user {$storedToken->userId}: {$e->getMessage()}");
            }
        }

        return $refreshed;
    }

    /**
     * Clean up expired tokens without refresh token (background job)
     */
    public function cleanupExpiredTokens(): int
    {
        return $this->tokenRepository->deleteExpiredWithoutRefreshToken();
    }
}
