<?php

namespace MintyPHP\Module\Helpdesk\Service;

use MintyPHP\Module\Helpdesk\Repository\HelpdeskTokenRepository;

/**
 * OAuth2 client_credentials token service for BC OData access.
 *
 * Uses tenant-scoped encrypted cache entries via HelpdeskTokenRepository.
 * Supports both v2 endpoints (`scope`) and legacy v1 endpoints (`resource`)
 * with BC-oriented defaults and audience fallback from the configured OData host.
 */
class HelpdeskOAuthTokenService
{
    private const CONNECT_TIMEOUT = 10;
    private const REQUEST_TIMEOUT = 30;

    public function __construct(
        private readonly EffectiveHelpdeskSettingsService $settingsGateway,
        private readonly HelpdeskTokenRepository $tokenRepository
    ) {
    }

    /**
     * Get a valid access token for BC API calls.
     *
     * @return string|null The bearer token, or null if unavailable.
     */
    public function getAccessToken(int $tenantId, ?string $resourceAudience = null): ?string
    {
        if ($this->settingsGateway->getAuthMode() !== HelpdeskSettingsGateway::AUTH_MODE_OAUTH2) {
            return null;
        }
        if ($tenantId <= 0) {
            return null;
        }

        // Check cache first
        $cached = $this->tokenRepository->findValidToken($tenantId);
        if ($cached !== null) {
            return $cached;
        }

        $clientId = trim((string) ($this->settingsGateway->getOAuthClientId() ?? ''));
        $clientSecret = trim((string) ($this->settingsGateway->getOAuthClientSecret() ?? ''));
        $tokenEndpoint = $this->resolveTokenEndpoint();
        if ($clientId === '' || $clientSecret === '' || $tokenEndpoint === '') {
            return null;
        }

        $tokenResponse = $this->requestToken($tokenEndpoint, $clientId, $clientSecret, $resourceAudience);
        if (!is_array($tokenResponse)) {
            return null;
        }

        $accessToken = trim((string) ($tokenResponse['access_token'] ?? ''));
        if ($accessToken === '') {
            return null;
        }

        $expiresIn = (int) ($tokenResponse['expires_in'] ?? 0);
        if ($expiresIn <= 0) {
            $expiresIn = (int) ($tokenResponse['ext_expires_in'] ?? 0);
        }
        if ($expiresIn <= 0) {
            $expiresIn = 300;
        }

        $expiresAt = new \DateTimeImmutable('+' . max(60, $expiresIn - 60) . ' seconds');
        $this->tokenRepository->storeToken($tenantId, $accessToken, $expiresAt);

        return $accessToken;
    }

    private function resolveTokenEndpoint(): string
    {
        $endpoint = trim((string) ($this->settingsGateway->getOAuthTokenEndpoint() ?? ''));
        if ($endpoint === '') {
            return '';
        }

        $tenantId = trim((string) ($this->settingsGateway->getOAuthTenantId() ?? ''));
        if ($tenantId !== '') {
            $endpoint = str_replace(['{tenant}', '{tenant_id}'], $tenantId, $endpoint);
        }

        return $endpoint;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function requestToken(string $endpoint, string $clientId, string $clientSecret, ?string $resourceAudience): ?array
    {
        $payloads = $this->buildPayloadCandidates($endpoint, $clientId, $clientSecret, $resourceAudience);
        foreach ($payloads as $payload) {
            $result = $this->requestTokenWithPayload($endpoint, $payload);
            if ($result !== null) {
                return $result;
            }
        }

        return null;
    }

    /**
     * @return array<int, array<string, string>>
     */
    private function buildPayloadCandidates(string $endpoint, string $clientId, string $clientSecret, ?string $resourceAudience): array
    {
        $basePayload = [
            'grant_type' => 'client_credentials',
            'client_id' => $clientId,
            'client_secret' => $clientSecret,
        ];

        $payloads = [];
        $isV2Endpoint = str_contains(strtolower($endpoint), '/oauth2/v2.0/token');
        $audience = trim((string) ($resourceAudience ?? ''));

        if ($isV2Endpoint) {
            $scopes = ['https://api.businesscentral.dynamics.com/.default'];
            if ($audience !== '') {
                $scopes[] = rtrim($audience, '/') . '/.default';
            }
            foreach (array_values(array_unique($scopes)) as $scope) {
                $payloads[] = $basePayload + ['scope' => $scope];
            }
        } else {
            $resources = ['https://api.businesscentral.dynamics.com'];
            if ($audience !== '') {
                $resources[] = $audience;
            }
            foreach (array_values(array_unique($resources)) as $resource) {
                $payloads[] = $basePayload + ['resource' => $resource];
            }
        }

        $payloads[] = $basePayload;

        return $payloads;
    }

    /**
     * @param array<string,string> $payload
     * @return array<string,mixed>|null
     */
    private function requestTokenWithPayload(string $endpoint, array $payload): ?array
    {
        $ch = curl_init();
        if ($ch === false) {
            return null;
        }

        curl_setopt_array($ch, [
            CURLOPT_URL => $endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT,
            CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT,
            CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($payload, '', '&', PHP_QUERY_RFC3986),
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        $responseBody = curl_exec($ch);
        $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        unset($ch);

        if (!is_string($responseBody) || $httpCode < 200 || $httpCode >= 300) {
            return null;
        }

        $decoded = json_decode($responseBody, true);
        if (!is_array($decoded)) {
            return null;
        }

        return $decoded;
    }
}
