<?php

namespace App\Services\License;

/**
 * Verifies an Envato purchase code against the seller's account.
 *
 * Calls api.envato.com/v3/market/author/sale, authenticated with the
 * encrypted PAT from config/license.php (decrypted at runtime by
 * TokenVault). A successful response means the code is genuine AND
 * belongs to the seller — Envato refuses to leak sale data for codes
 * outside the PAT owner's account.
 *
 * We additionally check that the sale's item.id matches the configured
 * product id, so a customer can't buy a totally different Envato item
 * from the same seller and reuse that code here.
 */
class EnvatoVerifier
{
    /**
     * Verify a purchase code. Returns the sale payload on success,
     * throws \RuntimeException with a customer-friendly message on
     * any failure (network error, invalid code, wrong product, etc.).
     *
     * The shape of the returned array mirrors Envato's response, with
     * the fields we care about:
     *
     *   [
     *     'amount'         => '...',
     *     'sold_at'        => 'ISO8601',
     *     'license'        => 'Regular License' | 'Extended License',
     *     'support_amount' => '...',
     *     'supported_until'=> 'ISO8601',
     *     'item' => [
     *       'id'   => '62602989',
     *       'name' => '...',
     *       'url'  => 'https://codecanyon.net/item/...',
     *     ],
     *     'buyer'    => 'envato_username',
     *     'purchase_count' => 1,
     *   ]
     */
    /**
     * `$expectedProductId` defaults to the main-app product id from
     * config('license.product_id'), so the installer can keep calling
     * `verify($code)` with one argument. Addons override it with their
     * own product id (read from each addon's manifest) so a customer
     * can't reuse the main-app purchase code to unlock a paid addon
     * — or vice versa.
     */
    public function verify(string $purchaseCode, ?string $expectedProductId = null): array
    {
        $purchaseCode = trim($purchaseCode);

        // Envato purchase codes are UUID-shaped (8-4-4-4-12 hex with
        // dashes). Reject anything that doesn't match before burning
        // an API round-trip — the customer typed a typo and Envato
        // would just 404 anyway, this way the error is faster + clearer.
        if (!preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $purchaseCode)) {
            throw new \RuntimeException('Purchase code format is invalid. It should look like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.');
        }

        $token = TokenVault::envatoToken();
        $url   = rtrim((string) config('license.envato_api_url'), '/') . '?code=' . urlencode($purchaseCode);

        // Use raw cURL (not Guzzle/Http) so we can deploy on shared
        // hosts without the full Laravel HTTP client extension chain.
        // 20 sec is generous; Envato usually responds in <2s.
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $token,
                'Accept: application/json',
                'User-Agent: MailTrixy-Installer',
            ],
        ]);

        $body  = curl_exec($ch);
        $code  = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        curl_close($ch);

        if ($errno !== 0) {
            // Connection failure — could be the customer's server has
            // no outbound internet, DNS issue, or Envato is down.
            // Surface the cURL text so they can debug.
            throw new \RuntimeException("Could not reach Envato API: {$error}. Check your server has outbound internet access.");
        }

        // Envato returns:
        //   200 + JSON sale on a valid code
        //   404 on an invalid code or one not in this PAT's account
        //   401/403 if the PAT itself is broken (our problem, not the customer's)
        //   5xx if Envato is having issues
        if ($code === 404) {
            throw new \RuntimeException('Purchase code not found. Double-check the code from your Envato downloads page.');
        }
        if ($code === 401 || $code === 403) {
            throw new \RuntimeException('License server authentication failed. Please contact support — this is a platform-side issue, not your purchase code.');
        }
        if ($code >= 500) {
            throw new \RuntimeException("Envato license server is temporarily unavailable (HTTP {$code}). Please try again in a few minutes.");
        }
        if ($code !== 200) {
            throw new \RuntimeException("Unexpected response from Envato (HTTP {$code}). Please try again.");
        }

        $payload = json_decode((string) $body, true);
        if (!is_array($payload)) {
            throw new \RuntimeException('Envato returned an unreadable response. Please try again.');
        }

        // Product id check — what we sell is config('license.product_id').
        // Any other sale (= a different product by the same seller) is
        // not a valid MailTrixy license. Some Envato responses nest
        // item.id under different shapes depending on item type, so
        // probe both common locations.
        $itemId = (string) ($payload['item']['id'] ?? $payload['item_id'] ?? '');
        $expected = (string) ($expectedProductId ?? config('license.product_id'));
        if ($itemId === '' || $itemId !== $expected) {
            throw new \RuntimeException(
                'This purchase code is valid on Envato but for a different product. '
                . 'Use the code from the matching purchase.'
            );
        }

        return $payload;
    }
}
