<?php

declare(strict_types=1);

namespace App\Framework\OAuth\Storage;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\OAuth\ValueObjects\OAuthToken;

/**
 * OAuth Token Repository
 *
 * Handles persistence of OAuth tokens with automatic refresh handling
 */
final readonly class OAuthTokenRepository implements OAuthTokenRepositoryInterface
{
    private const TABLE = 'oauth_tokens';

    public function __construct(
        private ConnectionInterface $connection,
    ) {
    }

    /**
     * Find token for user and provider
     */
    public function findForUser(string $userId, string $provider): ?StoredOAuthToken
    {
        $sql = "SELECT * FROM " . self::TABLE . "
                WHERE user_id = ? AND provider = ?
                LIMIT 1";

        $row = $this->connection->fetch($sql, [$userId, $provider]);

        return $row ? StoredOAuthToken::fromArray($row) : null;
    }

    /**
     * Find token by ID
     */
    public function findById(int $id): ?StoredOAuthToken
    {
        $sql = "SELECT * FROM " . self::TABLE . " WHERE id = ? LIMIT 1";
        $row = $this->connection->fetch($sql, [$id]);

        return $row ? StoredOAuthToken::fromArray($row) : null;
    }

    /**
     * Save or update token
     */
    public function save(StoredOAuthToken $storedToken): StoredOAuthToken
    {
        if ($storedToken->id === null) {
            return $this->insert($storedToken);
        }

        return $this->update($storedToken);
    }

    /**
     * Save token for user (upsert by user_id + provider)
     */
    public function saveForUser(
        string $userId,
        string $provider,
        OAuthToken $token
    ): StoredOAuthToken {
        $existing = $this->findForUser($userId, $provider);

        if ($existing !== null) {
            $updated = $existing->withRefreshedToken($token);

            return $this->update($updated);
        }

        $storedToken = StoredOAuthToken::create($userId, $provider, $token);

        return $this->insert($storedToken);
    }

    /**
     * Delete token for user and provider
     */
    public function deleteForUser(string $userId, string $provider): bool
    {
        $sql = "DELETE FROM " . self::TABLE . " WHERE user_id = ? AND provider = ?";
        $this->connection->execute($sql, [$userId, $provider]);

        return true;
    }

    /**
     * Delete expired tokens without refresh token (cleanup job)
     */
    public function deleteExpiredWithoutRefreshToken(): int
    {
        $sql = "DELETE FROM " . self::TABLE . "
                WHERE expires_at < ? AND refresh_token IS NULL";

        $this->connection->execute($sql, [time()]);

        return $this->connection->rowCount();
    }

    /**
     * Get all tokens for a user
     *
     * @return array<StoredOAuthToken>
     */
    public function findAllForUser(string $userId): array
    {
        $sql = "SELECT * FROM " . self::TABLE . " WHERE user_id = ?";
        $rows = $this->connection->fetchAll($sql, [$userId]);

        return array_map(
            fn (array $row) => StoredOAuthToken::fromArray($row),
            $rows
        );
    }

    /**
     * Get all tokens expiring soon (for refresh worker)
     *
     * @return array<StoredOAuthToken>
     */
    public function findExpiringSoon(int $withinSeconds = 300): array
    {
        $expiryThreshold = time() + $withinSeconds;

        $sql = "SELECT * FROM " . self::TABLE . "
                WHERE expires_at < ?
                AND refresh_token IS NOT NULL";

        $rows = $this->connection->fetchAll($sql, [$expiryThreshold]);

        return array_map(
            fn (array $row) => StoredOAuthToken::fromArray($row),
            $rows
        );
    }

    /**
     * Insert new token
     */
    private function insert(StoredOAuthToken $storedToken): StoredOAuthToken
    {
        $data = $storedToken->toArray();
        unset($data['id']); // Don't insert ID

        $sql = "INSERT INTO " . self::TABLE . "
                (user_id, provider, access_token, refresh_token, token_type, scope, expires_at, created_at, updated_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";

        $this->connection->execute($sql, [
            $data['user_id'],
            $data['provider'],
            $data['access_token'],
            $data['refresh_token'],
            $data['token_type'],
            $data['scope'],
            $data['expires_at'],
            $data['created_at'],
            $data['updated_at'],
        ]);

        $id = (int) $this->connection->lastInsertId();

        return new StoredOAuthToken(
            id: $id,
            userId: $storedToken->userId,
            provider: $storedToken->provider,
            token: $storedToken->token,
            createdAt: $storedToken->createdAt,
            updatedAt: $storedToken->updatedAt,
        );
    }

    /**
     * Update existing token
     */
    private function update(StoredOAuthToken $storedToken): StoredOAuthToken
    {
        if ($storedToken->id === null) {
            throw FrameworkException::create(
                ErrorCode::ENTITY_NOT_FOUND,
                'Cannot update token without ID'
            );
        }

        $data = $storedToken->toArray();

        $sql = "UPDATE " . self::TABLE . "
                SET access_token = ?,
                    refresh_token = ?,
                    token_type = ?,
                    scope = ?,
                    expires_at = ?,
                    updated_at = ?
                WHERE id = ?";

        $this->connection->execute($sql, [
            $data['access_token'],
            $data['refresh_token'],
            $data['token_type'],
            $data['scope'],
            $data['expires_at'],
            $data['updated_at'],
            $storedToken->id,
        ]);

        return $storedToken;
    }
}
