<?php
/**
 * Native OAuth service for MakePay Invision installations.
 */

namespace IPS\makepay\OAuth;

use IPS\makepay\Api\Client;
use IPS\makepay\Dpop\Dpop;
use IPS\makepay\Storage\Storage;

if (!\defined('\IPS\SUITE_UNIQUE_KEY')) {
    exit;
}

class _OAuth
{
    private const AUTH_BASE = 'https://www.makecrypto.io';
    private const SCOPES = [
        'company:read',
        'makepay:payment-links:read',
        'makepay:payment-links:write',
        'makepay:customers:read',
        'makepay:customers:write',
        'makepay:settings:read',
        'makepay:settings:write',
    ];

    public static function startUrl(): string
    {
        $keys = Dpop::createKeyPair();
        $verifier = self::base64Url(random_bytes(32));
        $challenge = self::base64Url(hash('sha256', $verifier, true));
        $state = self::base64Url(random_bytes(24));
        $siteUrl = rtrim((string) \IPS\Settings::i()->base_url, '/');
        $redirectUri = $siteUrl . '/index.php?/makepay/oauth/callback';

        Storage::setSecret('pkce_verifier', $verifier);
        Storage::setSecret('oauth_state', $state);
        Storage::setSecret('dpop_private_pem', $keys['privatePem']);
        Storage::set('dpop_public_jwk', json_encode($keys['publicJwk'], JSON_UNESCAPED_SLASHES));
        Storage::set('dpop_jkt', $keys['jkt']);

        $registration = self::postJson('/oauth/native/installations', [
            'platform' => 'invision',
            'siteUrl' => $siteUrl,
            'redirectUri' => $redirectUri,
            'dpopJkt' => $keys['jkt'],
            'pluginVersion' => self::applicationVersion(),
            'invisionVersion' => \IPS\Application::load('core')->long_version ?? null,
            'commerceVersion' => \IPS\Application::load('nexus')->long_version ?? null,
            'siteName' => \IPS\Settings::i()->board_name,
        ]);

        Storage::set('client_id', (string) $registration['client_id']);
        Storage::set('redirect_uri', $redirectUri);

        return self::AUTH_BASE . '/oauth/authorize?' . http_build_query([
            'response_type' => 'code',
            'client_id' => $registration['client_id'],
            'redirect_uri' => $redirectUri,
            'scope' => implode(' ', self::SCOPES),
            'state' => $state,
            'code_challenge' => $challenge,
            'code_challenge_method' => 'S256',
            'dpop_jkt' => $keys['jkt'],
        ], '', '&', PHP_QUERY_RFC3986);
    }

    public static function exchangeCode(string $code, string $state): void
    {
        if (!hash_equals(Storage::getSecret('oauth_state') ?: '', $state)) {
            throw new \RuntimeException('Invalid MakePay OAuth state.');
        }

        $clientId = Storage::get('client_id');
        $redirectUri = Storage::get('redirect_uri');
        $verifier = Storage::getSecret('pkce_verifier');
        $privatePem = Storage::getSecret('dpop_private_pem') ?: '';
        $publicJwk = json_decode(Storage::get('dpop_public_jwk') ?: '[]', true) ?: [];
        $tokenUrl = self::AUTH_BASE . '/oauth/token';
        $proof = Dpop::proof($privatePem, $publicJwk, 'POST', $tokenUrl);

        $tokens = self::postForm('/oauth/token', [
            'grant_type' => 'authorization_code',
            'client_id' => $clientId,
            'redirect_uri' => $redirectUri,
            'code' => $code,
            'code_verifier' => $verifier,
        ], ['dpop: ' . $proof]);

        self::storeTokens($tokens);
        self::provisionMakePaySettings();
    }

    public static function refreshToken(): void
    {
        $refreshToken = Storage::getSecret('oauth_refresh_token');
        $clientId = Storage::get('client_id');
        $privatePem = Storage::getSecret('dpop_private_pem') ?: '';
        $publicJwk = json_decode(Storage::get('dpop_public_jwk') ?: '[]', true) ?: [];
        $tokenUrl = self::AUTH_BASE . '/oauth/token';
        $proof = Dpop::proof($privatePem, $publicJwk, 'POST', $tokenUrl);

        $tokens = self::postForm('/oauth/token', [
            'grant_type' => 'refresh_token',
            'client_id' => $clientId,
            'refresh_token' => $refreshToken,
        ], ['dpop: ' . $proof]);

        self::storeTokens($tokens);
    }

    public static function disconnect(): void
    {
        foreach ([
            'oauth_access_token',
            'oauth_refresh_token',
            'pkce_verifier',
            'oauth_state',
            'client_id',
            'redirect_uri',
            'dpop_private_pem',
            'dpop_public_jwk',
            'dpop_jkt',
        ] as $key) {
            Storage::delete($key);
        }
    }

    private static function storeTokens(array $tokens): void
    {
        if (!empty($tokens['access_token'])) {
            Storage::setSecret('oauth_access_token', (string) $tokens['access_token']);
        }
        if (!empty($tokens['refresh_token'])) {
            Storage::setSecret('oauth_refresh_token', (string) $tokens['refresh_token']);
        }
        if (!empty($tokens['company_id'])) {
            Storage::set('company_id', (string) $tokens['company_id']);
        }
    }

    private static function provisionMakePaySettings(): void
    {
        $companyId = Storage::get('company_id');
        if (!$companyId) {
            return;
        }

        $webhookUrl = rtrim((string) \IPS\Settings::i()->base_url, '/') . '/index.php?/makepay/webhook/notify';
        $client = new Client();
        $client->updateSettings($companyId, ['callbackUrl' => $webhookUrl]);
        $secretResponse = $client->rotateWebhookSecret($companyId);
        $secret = (string) ($secretResponse['webhookSecret']['secret'] ?? $secretResponse['secret'] ?? '');
        if ($secret !== '') {
            Storage::setSecret('webhook_secret', $secret);
            Storage::set('webhook_secret_last4', substr($secret, -4));
        }
    }

    private static function postJson(string $path, array $payload): array
    {
        return self::send('POST', self::AUTH_BASE . $path, ['content-type: application/json'], json_encode($payload, JSON_UNESCAPED_SLASHES));
    }

    private static function postForm(string $path, array $payload, array $extraHeaders = []): array
    {
        return self::send('POST', self::AUTH_BASE . $path, array_merge(['content-type: application/x-www-form-urlencoded'], $extraHeaders), http_build_query($payload));
    }

    private static function send(string $method, string $url, array $headers, string $body): array
    {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => array_merge(['accept: application/json', 'user-agent: MakePay-Invision/0.1.0'], $headers),
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
        ]);
        $response = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            throw new \RuntimeException($error ?: 'MakePay OAuth request failed.');
        }

        $decoded = json_decode((string) $response, true);
        if ($status < 200 || $status >= 300) {
            $message = is_array($decoded) && isset($decoded['error'])
                ? (string) $decoded['error']
                : "MakePay OAuth responded with HTTP {$status}.";
            throw new \RuntimeException($message);
        }

        return is_array($decoded) ? $decoded : [];
    }

    private static function applicationVersion(): string
    {
        try {
            return (string) \IPS\Application::load('makepay')->version;
        } catch (\Throwable) {
            return '0.1.0';
        }
    }

    private static function base64Url(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }
}
