<?php

declare(strict_types=1);

namespace App\Framework\OAuth\ValueObjects;

use App\Framework\Core\ValueObjects\Timestamp;

/**
 * OAuth Token Value Object
 *
 * Immutable composite representation of OAuth access/refresh tokens
 * Uses Value Objects for maximum type safety
 */
final readonly class OAuthToken
{
    public function __construct(
        public AccessToken $accessToken,
        public ?RefreshToken $refreshToken,
        public TokenType $tokenType,
        public ?TokenScope $scope,
    ) {
    }

    /**
     * Create from provider response
     *
     * @param array<string, mixed> $response
     */
    public static function fromProviderResponse(array $response): self
    {
        if (! isset($response['access_token'])) {
            throw new \InvalidArgumentException('Provider response missing access_token');
        }

        $expiresIn = (int) ($response['expires_in'] ?? 3600);

        return new self(
            accessToken: AccessToken::fromProviderResponse(
                $response['access_token'],
                $expiresIn
            ),
            refreshToken: isset($response['refresh_token'])
                ? RefreshToken::create($response['refresh_token'])
                : null,
            tokenType: TokenType::fromString($response['token_type'] ?? 'Bearer'),
            scope: isset($response['scope']) && ! empty($response['scope'])
                ? TokenScope::fromString($response['scope'])
                : null,
        );
    }

    /**
     * Check if token is expired
     */
    public function isExpired(): bool
    {
        return $this->accessToken->isExpired();
    }

    /**
     * Check if token is still valid
     */
    public function isValid(): bool
    {
        return $this->accessToken->isValid();
    }

    /**
     * Check if token can be refreshed
     */
    public function canRefresh(): bool
    {
        return $this->refreshToken !== null;
    }

    /**
     * Get authorization header value
     */
    public function getAuthorizationHeader(): string
    {
        return $this->tokenType->getHeaderPrefix() . ' ' . $this->accessToken->toString();
    }

    /**
     * Get seconds until expiration
     */
    public function getSecondsUntilExpiration(): int
    {
        return $this->accessToken->getSecondsUntilExpiration();
    }

    /**
     * Get expiration timestamp
     */
    public function getExpiresAt(): Timestamp
    {
        return $this->accessToken->getExpiresAt();
    }

    /**
     * Check if token has specific scope
     */
    public function hasScope(string $scope): bool
    {
        return $this->scope !== null && $this->scope->includes($scope);
    }

    /**
     * Check if token has all required scopes
     *
     * @param array<string> $requiredScopes
     */
    public function hasScopes(array $requiredScopes): bool
    {
        return $this->scope !== null && $this->scope->includesAll($requiredScopes);
    }

    /**
     * Create new token with refreshed access token
     */
    public function withRefreshedAccessToken(
        AccessToken $accessToken,
        ?RefreshToken $refreshToken = null
    ): self {
        return new self(
            accessToken: $accessToken,
            refreshToken: $refreshToken ?? $this->refreshToken,
            tokenType: $this->tokenType,
            scope: $this->scope,
        );
    }

    /**
     * Convert to array for storage
     *
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $array = [
            'access_token' => $this->accessToken->toString(),
            'expires_at' => $this->accessToken->getExpiresAt()->toTimestamp(),
            'token_type' => $this->tokenType->value,
        ];

        if ($this->refreshToken !== null) {
            $array['refresh_token'] = $this->refreshToken->toString();
        }

        if ($this->scope !== null) {
            $array['scope'] = $this->scope->toString();
        }

        return $array;
    }

    /**
     * Convert to array for logging (with masked tokens)
     *
     * @return array<string, mixed>
     */
    public function toArrayMasked(): array
    {
        return [
            'access_token' => $this->accessToken->getMasked(),
            'refresh_token' => $this->refreshToken?->getMasked(),
            'expires_at' => $this->accessToken->getExpiresAt()->format('Y-m-d H:i:s'),
            'expires_in' => $this->getSecondsUntilExpiration() . 's',
            'token_type' => $this->tokenType->value,
            'scope' => $this->scope?->toString(),
            'is_expired' => $this->isExpired(),
            'can_refresh' => $this->canRefresh(),
        ];
    }

    /**
     * Create from stored array
     *
     * @param array<string, mixed> $data
     */
    public static function fromArray(array $data): self
    {
        if (! isset($data['access_token'], $data['expires_at'])) {
            throw new \InvalidArgumentException('Stored token data missing required fields');
        }

        return new self(
            accessToken: AccessToken::create(
                $data['access_token'],
                Timestamp::fromTimestamp((int) $data['expires_at'])
            ),
            refreshToken: isset($data['refresh_token'])
                ? RefreshToken::create($data['refresh_token'])
                : null,
            tokenType: TokenType::fromString($data['token_type'] ?? 'Bearer'),
            scope: isset($data['scope']) && ! empty($data['scope'])
                ? TokenScope::fromString($data['scope'])
                : null,
        );
    }
}
