<?php

namespace RecastPaywall\Api;

/**
 * Recast API client
 */
class RecastClient {
    /**
     * API base URL
     */
    private const API_BASE_URL = 'https://api.recast.tv/api/v0';

    /**
     * Sandbox API base URL
     */
    private const SANDBOX_API_BASE_URL = 'https://api.recast-sandbox.tv/api/v0';

    /**
     * Request timeout
     */
    private const TIMEOUT = 30;

    /**
     * Maximum retries
     */
    private const MAX_RETRIES = 3;

    /**
     * Settings instance
     */
    private \RecastPaywall\Admin\Settings $settings;

    /**
     * Constructor
     */
    public function __construct() {
        $this->settings = new \RecastPaywall\Admin\Settings();
    }

    /**
     * Get API base URL
     */
    private function getApiBaseUrl(): string {
        $settings = $this->settings->getSettings();
        return $settings['sandbox'] ? self::SANDBOX_API_BASE_URL : self::API_BASE_URL;
    }

    /**
     * Build auth headers using a short-lived JWT generated from API key/secret
     */
    private function getAuthHeaders(): array {
        $jwtOrError = $this->getJwtToken();

        if (is_wp_error($jwtOrError)) {
            return [];
        }

        return [
            'Authorization' => 'Bearer ' . $jwtOrError,
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];
    }

    /**
     * Generate or fetch cached JWT for vendor key
     */
    private function getJwtToken(bool $forceRefresh = false): \WP_Error|string {
        $settings = $this->settings->getSettings();

        $apiKey = trim($settings['api_key'] ?? '');
        $apiSecret = trim($settings['api_secret'] ?? '');

        if ($apiKey === '' || $apiSecret === '') {
            return new \WP_Error('missing_credentials', __('API Key and Secret are required', 'recast-paywall'));
        }

        $transientKey = $this->getJwtTransientKey();

        if (!$forceRefresh) {
            $cached = get_transient($transientKey);
            if (is_string($cached) && $cached !== '') {
                $this->logInfo('Using cached JWT token', [ 'env' => !empty($settings['sandbox']) ? 'sandbox' : 'prod' ]);
                return $cached;
            }
        }

        $jwtResult = $this->requestJwtToken($apiKey, $apiSecret);
        if (is_wp_error($jwtResult)) {
            return $jwtResult;
        }

        $token = $jwtResult['token'];

        // Derive TTL from JWT exp if present; fallback to 50 minutes
        $ttl = $this->getTtlFromJwt($token);
        if ($ttl <= 0) {
            $ttl = 50 * 60;
        }

        set_transient($transientKey, $token, $ttl);

        $this->logInfo('Stored new JWT token', [
            'env' => !empty($settings['sandbox']) ? 'sandbox' : 'prod',
            'ttl_seconds' => $ttl,
        ]);

        return $token;
    }

    /**
     * Unique cache key for JWT depending on environment
     */
    private function getJwtTransientKey(): string {
        $settings = $this->settings->getSettings();
        $envKey = !empty($settings['sandbox']) ? 'sandbox' : 'prod';
        return 'recast_paywall_jwt_' . $envKey;
    }

    /**
     * Call vendor JWT endpoint to obtain a token
     */
    private function requestJwtToken(string $apiKey, string $apiSecret): \WP_Error|array {
        $url = $this->getApiBaseUrl() . '/vendor/keys/jwt';
        $settings = $this->settings->getSettings();
        $brandId = trim($settings['brand_id'] ?? '');

        $payload = [
            'key' => $apiKey,
            'secret' => $apiSecret,
            'brandId' => $brandId,
        ];

        $body = wp_json_encode($payload);

        $args = [
            'method' => 'POST',
            'timeout' => self::TIMEOUT,
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
            'body' => $body,
            'sslverify' => true,
        ];

        $this->logInfo('Requesting JWT token', ['body' => $body, 'has_brand' => $brandId !== '', 'env' => !empty($settings['sandbox']) ? 'sandbox' : 'prod' ]);
        $response = wp_remote_request($url, $args);
        if (is_wp_error($response)) {
            $this->logError('JWT request failed', [ 'error' => $response->get_error_message() ]);
            return $response;
        }

        $statusCode = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        $accessToken = $data['item']['accessToken'];

        if ($statusCode >= 200 && $statusCode < 300 && is_array($data) && !empty($accessToken)) {
            $this->logInfo('JWT request succeeded', [ 'status' => $statusCode ]);
            return [ 'token' => $accessToken ];
        }

        $message = __('Failed to obtain JWT token', 'recast-paywall');
        if (is_array($data) && !empty($data['message'])) {
            $message = (string) $data['message'];
        }
        $this->logError('JWT request error', [ 'status' => $statusCode, 'message' => $message ]);
        return new \WP_Error('jwt_error', $message, ['status' => $statusCode]);
    }

    /**
     * Parse exp claim from JWT to compute TTL (in seconds). Returns 0 on failure.
     */
    private function getTtlFromJwt(string $jwt): int {
        $parts = explode('.', $jwt);
        if (count($parts) < 2) {
            return 0;
        }
        $payloadB64 = $parts[1];
        $payloadJson = base64_decode(strtr($payloadB64, '-_', '+/'));
        if ($payloadJson === false) {
            return 0;
        }
        $payload = json_decode($payloadJson, true);
        if (!is_array($payload) || empty($payload['exp'])) {
            return 0;
        }
        $exp = intval($payload['exp']);
        $now = time();
        $ttl = $exp - $now - 60; // buffer 60s
        return max(0, $ttl);
    }

    /**
     * Make API request with retry logic
     */
    private function makeRequest(string $endpoint, array $args = [], string $method = 'GET'): \WP_Error|array {
        $url = $this->getApiBaseUrl() . $endpoint;
        // Build headers with JWT; on error return it
        $jwtOrError = $this->getJwtToken();
        if (is_wp_error($jwtOrError)) {
            return $jwtOrError;
        }
        $headers = [
            'Authorization' => 'Bearer ' . $jwtOrError,
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
        ];

        $requestArgs = [
            'method' => $method,
            'timeout' => self::TIMEOUT,
            'headers' => $headers,
            'sslverify' => true,
        ];

        if (!empty($args) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            $requestArgs['body'] = wp_json_encode($args);
        }

        $attempt = 0;
        $lastError = null;

        $refreshedAfter401 = false;
        while ($attempt < self::MAX_RETRIES) {
            $response = wp_remote_request($url, $requestArgs);

            if (is_wp_error($response)) {
                $lastError = $response;
                $attempt++;
                
                if ($attempt < self::MAX_RETRIES) {
                    sleep(pow(2, $attempt)); // Exponential backoff
                }
                continue;
            }

            $statusCode = wp_remote_retrieve_response_code($response);

            $body = wp_remote_retrieve_body($response);

            //$this->logInfo('API request response', [ 'status' => $statusCode, 'body' => $body ]);

            if ($statusCode >= 200 && $statusCode < 300) {
                $data = json_decode($body, true);
                return $data ?: [];
            }

            // Handle specific error cases
            if ($statusCode === 401) {
                // Invalidate cached JWT and retry once with a fresh token
                if (!$refreshedAfter401) {
                    $this->logInfo('401 received, refreshing JWT and retrying request', [ 'url' => $url ]);
                    delete_transient($this->getJwtTransientKey());
                    $newJwt = $this->getJwtToken(true);
                    if (!is_wp_error($newJwt)) {
                        $requestArgs['headers']['Authorization'] = 'Bearer ' . $newJwt;
                        $refreshedAfter401 = true;
                        continue;
                    }
                }
                return new \WP_Error('unauthorized', __('Invalid API credentials', 'recast-paywall'));
            }

            if ($statusCode === 429) {
                $attempt++;
                if ($attempt < self::MAX_RETRIES) {
                    $retryAfter = wp_remote_retrieve_header($response, 'Retry-After');
                    sleep($retryAfter ? intval($retryAfter) : pow(2, $attempt));
                    continue;
                }
            }

            $errorMessage = __('API request failed', 'recast-paywall');
            $errorData = json_decode($body, true);
            
            if ($errorData && isset($errorData['message'])) {
                $errorMessage = $errorData['message'];
            }

            return new \WP_Error('api_error', $errorMessage, ['status' => $statusCode, 'body' => $body]);
        }

        return $lastError ?: new \WP_Error('max_retries_exceeded', __('Maximum retries exceeded', 'recast-paywall'));
    }

    /**
     * Test API connection
     */
    public function testConnection(): \WP_Error|array {
        $jwtOrError = $this->getJwtToken(true);
        if (is_wp_error($jwtOrError)) {
            return $jwtOrError;
        }
        return [ 'ok' => true ];
    }

    /**
     * Create or update inventory product
     */
    public function createOrUpdateInventoryItem(array $data): \WP_Error|array {
        $settings = $this->settings->getSettings();

        $product = [
            'title' => (string) ($data['title'] ?? ''),
            'externalId' => (string) ($data['externalId'] ?? $data['externalProductId'] ?? ''),
            'description' => (string) ($data['description'] ?? ''),
        ];

        // Only set type if explicit id empty (create), per request
        if (empty($data['id'])) {
            $product['type'] = (string) ($data['type'] ?? 'SINGLE_PRODUCT');
        }

        // Include brandId from settings if present
        if (!empty($settings['brand_id'])) {
            $product['brandId'] = $settings['brand_id'];
        }

        // Optional fields if provided by caller
        if (!empty($data['geoAvailability']) && is_array($data['geoAvailability'])) {
            $product['geoAvailability'] = array_values(array_filter($data['geoAvailability'], 'is_string'));
        }
        if (!empty($data['statusDetails']) && is_array($data['statusDetails'])) {
            $product['statusDetails'] = $data['statusDetails'];
        }
        if (!empty($data['priceDetails']) && is_array($data['priceDetails'])) {
            $product['priceDetails'] = $data['priceDetails'];
        }
        if (isset($data['affiliateSharePct'])) {
            $product['affiliateSharePct'] = (float) $data['affiliateSharePct'];
        }
        // Vendor splits are not supported yet
        $product['vendorSplits'] = [];

        $payload = [ 'product' => $product ];

        // Allow filtering for customization
        $payload = apply_filters('recast_paywall_inventory_payload', $payload, $data);

        // If we have an explicit product id, update directly
        $existingId = (string) ($data['id'] ?? '');

        // Otherwise, attempt to find by externalId
        if ($existingId === '' && $product['externalId'] !== '') {
            $found = $this->findInventoryItemByExternalId($product['externalId']);
            if (!is_wp_error($found) && is_array($found) && !empty($found['id'])) {
                $existingId = (string) $found['id'];
            }
        }

        if ($existingId !== '') {
            // Update existing product
            $result = $this->updateInventoryItem($existingId, $payload);
            if (!is_wp_error($result)) {
                if (is_array($result) && empty($result['id'])) {
                    $result['id'] = $existingId;
                }
            }
            return $result;
        }

        // Create new product
        $endpoint = '/vendor/inventory/products';
        $result = $this->makeRequest($endpoint, $payload, 'POST');
        if (!is_wp_error($result) && is_array($result)) {
            // Normalize id to top-level if nested
            if (empty($result['id'])) {
                if (!empty($result['item']['id'])) {
                    $result['id'] = $result['item']['id'];
                } elseif (!empty($result['data']['id'])) {
                    $result['id'] = $result['data']['id'];
                } elseif (!empty($result['product']['id'])) {
                    $result['id'] = $result['product']['id'];
                }
            }
        }
        return $result;
    }

    /**
     * Archive inventory item
     */
    public function archiveInventoryItem(string $itemId): \WP_Error|array {
        $endpoint = '/vendor/inventory/products/' . rawurlencode($itemId) . '/archive';
        
        return $this->makeRequest($endpoint, [], 'POST');
    }

    /**
     * Check user entitlement
     */
    public function checkEntitlement(string $externalUserId, string $externalProductId): \WP_Error|array {
        $settings = $this->settings->getSettings();
        $query = [
            'externalUserId' => $externalUserId,
            'externalProductId' => $externalProductId,
        ];
        $endpoint = '/vendor/products/access?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        
        return $this->makeRequest($endpoint);
    }

    /**
     * Create purchase intent using external user/product and optional tariff details
     */
    public function createPurchaseIntent(string $externalUserId, string $externalProductId, array $options = []): \WP_Error|array {
        $payload = [
            'externalUserId' => $externalUserId,
            'externalProductId' => $externalProductId,
        ];

        // Include tariffDetails if provided via options
        if (!empty($options['tariffDetails']) && is_array($options['tariffDetails'])) {
            $payload['tariffDetails'] = [
                'amount' => isset($options['tariffDetails']['amount']) ? (int) $options['tariffDetails']['amount'] : null,
                'currency' => (string) ($options['tariffDetails']['currency'] ?? ''),
            ];
            // Remove nulls
            if ($payload['tariffDetails']['amount'] === null) {
                unset($payload['tariffDetails']['amount']);
            }
            if ($payload['tariffDetails']['currency'] === '') {
                unset($payload['tariffDetails']['currency']);
            }
            if (empty($payload['tariffDetails'])) {
                unset($payload['tariffDetails']);
            }
        } else {
            // Backward-compatible support if amount/currency provided flat
            $amount = isset($options['amount']) ? (int) $options['amount'] : null;
            $currency = isset($options['currency']) ? (string) $options['currency'] : '';
            if ($amount !== null || $currency !== '') {
                $payload['tariffDetails'] = [];
                if ($amount !== null) $payload['tariffDetails']['amount'] = $amount;
                if ($currency !== '') $payload['tariffDetails']['currency'] = $currency;
            }
        }

        $endpoint = '/vendor/purchases/intent';
        return $this->makeRequest($endpoint, $payload, 'POST');
    }

    /**
     * Get inventory item
     */
    public function getInventoryItem(string $itemId): \WP_Error|array {
        $settings = $this->settings->getSettings();
        $endpoint = '/vendor/inventory/products/' . rawurlencode($itemId);
        // Brand ID may not be required when fetching by internal ID; include if API supports
        if (!empty($settings['brand_id'])) {
            $endpoint .= '?brandId=' . rawurlencode($settings['brand_id']);
        }
        return $this->makeRequest($endpoint);
    }

    /**
     * Find inventory product by externalId
     */
    public function findInventoryItemByExternalId(string $externalId): \WP_Error|array|null {
        $settings = $this->settings->getSettings();
        $query = [ 'externalId' => $externalId ];
        if (!empty($settings['brand_id'])) {
            $query['brandId'] = $settings['brand_id'];
        }
        $endpoint = '/vendor/inventory/products?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        $result = $this->makeRequest($endpoint);
        if (is_wp_error($result)) {
            return $result;
        }
        // Support either a single object or a collection response
        if (is_array($result) && isset($result['id'])) {
            return $result;
        }
        if (is_array($result) && isset($result['items']) && is_array($result['items']) && !empty($result['items'])) {
            return $result['items'][0];
        }
        if (is_array($result) && isset($result['data']) && is_array($result['data']) && !empty($result['data'])) {
            return $result['data'][0];
        }
        return null; // Not found
    }

    /**
     * Update inventory product by internal id
     */
    public function updateInventoryItem(string $itemId, array $payload): \WP_Error|array {
        $endpoint = '/vendor/inventory/products/' . rawurlencode($itemId);
        return $this->makeRequest($endpoint, $payload, 'PUT');
    }

    /**
     * Handle test connection AJAX request
     */
    public function handleTestConnection(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('Unauthorized', 'recast-paywall'));
        }

        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'recast_test_connection')) {
            wp_die(esc_html__('Invalid nonce', 'recast-paywall'));
        }

        $result = $this->testConnection();

        if (is_wp_error($result)) {
            wp_send_json_error([
                'message' => $result->get_error_message(),
            ]);
        } else {
            wp_send_json_success([
                'message' => __('Connection successful!', 'recast-paywall'),
            ]);
        }
    }

    /**
     * Log error
     */
    private function logError(string $message, array $context = []): void {
        // Debug logging removed for production
    }

    /**
     * Log info messages when debugging is enabled
     */
    private function logInfo(string $message, array $context = []): void {
        // Debug logging removed for production
    }
}
