<?php

declare(strict_types=1);

final class MyronMcpOAuthTokenService
{
    public function __construct(
        private readonly MyronMcpOAuthClientRepository $repository,
        private readonly MyronAiIntegrationRepository $aiIntegrations,
        private readonly MyronMcpOAuthMetadataService $metadata,
    ) {
    }

    /**
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    public function registerClient(array $input): array
    {
        $redirectUris = array_values(array_map('strval', is_array($input['redirect_uris'] ?? null) ? $input['redirect_uris'] : []));
        $scopes = $this->scopeListFromInput((string) ($input['scope'] ?? ''));
        $registered = $this->repository->registerClient(
            (string) ($input['client_name'] ?? 'Claude MCP Connector'),
            $redirectUris,
            $scopes
        );

        return [
            'client_id' => $registered['clientId'],
            'client_secret' => $registered['clientSecret'],
            'client_id_issued_at' => time(),
            'client_secret_expires_at' => 0,
            'redirect_uris' => $redirectUris,
            'grant_types' => ['authorization_code', 'refresh_token'],
            'response_types' => ['code'],
            'token_endpoint_auth_method' => 'client_secret_basic',
        ];
    }

    /**
     * @param array<string, mixed> $request
     * @return array{ok: bool, error?: string, detail?: string, client?: array<string, mixed>, scopes?: list<string>}
     */
    public function validateAuthorizationRequest(array $request): array
    {
        $clientId = trim((string) ($request['client_id'] ?? ''));
        $redirectUri = trim((string) ($request['redirect_uri'] ?? ''));
        $resource = trim((string) ($request['resource'] ?? ''));
        if ((string) ($request['response_type'] ?? '') !== 'code') {
            return ['ok' => false, 'error' => 'unsupported_response_type', 'detail' => 'Only authorization-code flow is supported.'];
        }
        $client = $this->repository->findClient($clientId);
        if ($client === null || ($client['revokedAt'] ?? null) !== null) {
            return ['ok' => false, 'error' => 'invalid_client', 'detail' => 'OAuth client is not registered or is revoked.'];
        }
        if (!$this->repository->clientAcceptsRedirect($clientId, $redirectUri)) {
            return ['ok' => false, 'error' => 'invalid_request', 'detail' => 'Redirect URI is not registered for this client.'];
        }
        if (!$this->repository->resourceMatches($resource, $this->metadata->resource())) {
            return ['ok' => false, 'error' => 'invalid_target', 'detail' => 'Resource must match the canonical MCP server URL.'];
        }
        $scopes = $this->scopeListFromInput((string) ($request['scope'] ?? ''));
        $allowed = array_fill_keys(array_values(array_map('strval', $client['scopes'] ?? [])), true);
        foreach ($scopes as $scope) {
            if (!isset($allowed[$scope])) {
                return ['ok' => false, 'error' => 'invalid_scope', 'detail' => 'Requested scope is not registered for this client.'];
            }
        }

        return ['ok' => true, 'client' => $client, 'scopes' => $scopes];
    }

    /**
     * @param list<string> $scopes
     */
    public function approveAuthorizationCode(
        string $clientId,
        string $redirectUri,
        string $resource,
        array $scopes,
        string $codeChallenge,
        string $codeChallengeMethod,
        string $userId
    ): string {
        $created = $this->repository->createAuthorizationCode(
            $clientId,
            $redirectUri,
            $resource,
            $scopes,
            $codeChallenge,
            $codeChallengeMethod,
            $userId
        );
        return $created['code'];
    }

    /**
     * @param array<string, mixed> $input
     * @param array<string, string|null> $headers
     * @return array<string, mixed>
     */
    public function exchangeToken(array $input, array $headers): array
    {
        $clientId = trim((string) ($input['client_id'] ?? ''));
        $clientSecret = trim((string) ($input['client_secret'] ?? ''));
        $basic = $this->extractBasicClientCredentials($headers);
        if ($basic !== null) {
            $clientId = $basic['clientId'];
            $clientSecret = $basic['clientSecret'];
        }
        $client = $this->repository->findClient($clientId);
        if ($client === null || ($client['revokedAt'] ?? null) !== null) {
            throw new RuntimeException('invalid_client');
        }
        if ($clientSecret === '' || !$this->repository->verifyClientSecret($clientId, $clientSecret)) {
            throw new RuntimeException('invalid_client');
        }

        $grantType = trim((string) ($input['grant_type'] ?? ''));
        if ($grantType === 'authorization_code') {
            return $this->exchangeAuthorizationCode($client, $input);
        }
        if ($grantType === 'refresh_token') {
            return $this->exchangeRefreshToken($client, $input);
        }

        throw new RuntimeException('unsupported_grant_type');
    }

    /**
     * @return array<string, mixed>|null
     */
    public function resolveAccessToken(string $token): ?array
    {
        return $this->repository->findValidAccessToken($token, $this->metadata->resource());
    }

    /**
     * @param array<string, mixed> $client
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    private function exchangeAuthorizationCode(array $client, array $input): array
    {
        $code = trim((string) ($input['code'] ?? ''));
        $redirectUri = trim((string) ($input['redirect_uri'] ?? ''));
        $resource = trim((string) ($input['resource'] ?? ''));
        $record = $this->repository->consumeAuthorizationCode($code);
        if ($record === null) {
            throw new RuntimeException('invalid_grant');
        }
        if ((string) ($record['clientId'] ?? '') !== (string) ($client['id'] ?? '')) {
            throw new RuntimeException('invalid_grant');
        }
        if ((string) ($record['redirectUri'] ?? '') !== $redirectUri) {
            throw new RuntimeException('invalid_grant');
        }
        if (!$this->repository->resourceMatches($resource, (string) ($record['resource'] ?? ''))) {
            throw new RuntimeException('invalid_target');
        }
        if (!$this->pkceVerifierMatches(
            (string) ($input['code_verifier'] ?? ''),
            (string) ($record['codeChallenge'] ?? ''),
            (string) ($record['codeChallengeMethod'] ?? '')
        )) {
            throw new RuntimeException('invalid_grant');
        }
        $scopes = array_values(array_map('strval', is_array($record['scopes'] ?? null) ? $record['scopes'] : []));
        $created = $this->aiIntegrations->create(
            'OAuth Connector: ' . (string) ($client['clientName'] ?? 'MCP Client'),
            $scopes,
            (string) ($record['createdByUserId'] ?? 'oauth'),
            null,
            'oauth'
        );
        $pair = $this->repository->issueTokenPair(
            (string) ($client['id'] ?? ''),
            $created['integration']->id,
            (string) ($record['resource'] ?? ''),
            $scopes
        );

        return $this->tokenResponse($pair, $scopes);
    }

    /**
     * @param array<string, mixed> $client
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    private function exchangeRefreshToken(array $client, array $input): array
    {
        $refresh = trim((string) ($input['refresh_token'] ?? ''));
        $resource = trim((string) ($input['resource'] ?? ''));
        $record = $this->repository->consumeRefreshToken($refresh, (string) ($client['id'] ?? ''), $resource);
        if ($record === null) {
            throw new RuntimeException('invalid_grant');
        }
        $scopes = array_values(array_map('strval', is_array($record['scopes'] ?? null) ? $record['scopes'] : []));
        $pair = $this->repository->issueTokenPair(
            (string) ($client['id'] ?? ''),
            (string) ($record['integrationId'] ?? ''),
            (string) ($record['resource'] ?? ''),
            $scopes
        );
        return $this->tokenResponse($pair, $scopes);
    }

    /**
     * @param array{accessToken: string, refreshToken: string, expiresIn: int, accessTokenId: string} $pair
     * @param list<string> $scopes
     * @return array<string, mixed>
     */
    private function tokenResponse(array $pair, array $scopes): array
    {
        return [
            'access_token' => $pair['accessToken'],
            'token_type' => 'Bearer',
            'expires_in' => $pair['expiresIn'],
            'refresh_token' => $pair['refreshToken'],
            'scope' => implode(' ', $scopes),
        ];
    }

    private function pkceVerifierMatches(string $verifier, string $challenge, string $method): bool
    {
        if ($challenge === '') {
            return true;
        }
        if ($verifier === '') {
            return false;
        }
        $method = strtoupper($method !== '' ? $method : 'S256');
        if ($method === 'S256') {
            return hash_equals($challenge, myronBase64UrlEncode(hash('sha256', $verifier, true)));
        }
        return false;
    }

    /**
     * @return list<string>
     */
    private function scopeListFromInput(string $input): array
    {
        $parts = array_values(array_filter(preg_split('/\s+/', trim($input)) ?: [], static fn (string $v): bool => $v !== ''));
        return $this->repository->normalizeScopes($parts);
    }

    /**
     * @param array<string, string|null> $headers
     * @return array{clientId: string, clientSecret: string}|null
     */
    private function extractBasicClientCredentials(array $headers): ?array
    {
        foreach ($headers as $name => $value) {
            if (strcasecmp((string) $name, 'Authorization') !== 0 && strcasecmp((string) $name, 'HTTP_AUTHORIZATION') !== 0) {
                continue;
            }
            $raw = trim((string) $value);
            if (stripos($raw, 'basic ') !== 0) {
                continue;
            }
            $decoded = base64_decode(trim(substr($raw, 6)), true);
            if (!is_string($decoded) || !str_contains($decoded, ':')) {
                return null;
            }
            [$clientId, $secret] = explode(':', $decoded, 2);
            return ['clientId' => rawurldecode($clientId), 'clientSecret' => rawurldecode($secret)];
        }
        return null;
    }
}
