<?php
if (!defined('ABSPATH')) exit;

/**
 * Pinery Flow — Amazon Creators API Client
 *
 * Host: creatorsapi.amazon  |  Auth: OAuth 2.0 (v3.x LwA)  |  lowerCamelCase
 */
class Pinery_Creators_API {

    const BASE_URL  = 'https://creatorsapi.amazon';
    const TOKEN_URL = 'https://api.amazon.com/auth/o2/token';
    const TOKEN_TTL = 3000;
    const RATE_DELAY = 1100;

    const PATHS = [
        'SearchItems' => 'catalog/v1/searchItems',
        'GetItems'    => 'catalog/v1/getItems',
    ];

    // ── Config ──

    public static function is_configured() {
        if (!get_option('pinery_flow_creators_enabled', 0)) return false;
        if (empty(get_option('pinery_flow_creators_client_id', ''))) return false;
        if (empty(get_option('pinery_flow_creators_client_secret', ''))) return false;
        return true;
    }

    public static function is_available() {
        if (!self::is_configured()) return false;
        if (get_transient('pinery_creators_unavailable')) return false;
        return true;
    }

    public static function get_marketplace() {
        return get_option('pinery_flow_creators_marketplace', 'www.amazon.com');
    }

    public static function get_partner_tag() {
        return get_option('pinery_flow_amazon_tag', '');
    }

    /** Price cache TTL in seconds (from admin setting, default 60 min). */
    public static function get_price_ttl() {
        return (int) get_option('pinery_flow_creators_price_ttl', 60) * 60;
    }

    // ── OAuth 2.0 ──

    public static function get_token() {
        $cached = get_transient('pinery_creators_token');
        if (!empty($cached)) return $cached;

        $cid = get_option('pinery_flow_creators_client_id', '');
        $csec = get_option('pinery_flow_creators_client_secret', '');
        if (empty($cid) || empty($csec)) return false;

        // v3.x LwA: JSON body.  Fall back to form-encoded (v2.x compat).
        $response = wp_remote_post(self::TOKEN_URL, [
            'headers' => ['Content-Type' => 'application/json'],
            'body'    => json_encode([
                'grant_type'    => 'client_credentials',
                'client_id'     => $cid,
                'client_secret' => $csec,
                'scope'         => 'creatorsapi::default',
            ]),
            'timeout' => 15,
        ]);

        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
            $response = wp_remote_post(self::TOKEN_URL, [
                'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
                'body'    => http_build_query([
                    'grant_type'    => 'client_credentials',
                    'client_id'     => $cid,
                    'client_secret' => $csec,
                    'scope'         => 'creatorsapi::default',
                ]),
                'timeout' => 15,
            ]);
        }

        if (is_wp_error($response)) {
            self::log('error', 'Creators API', 'Token request failed: ' . $response->get_error_message());
            return false;
        }

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

        if ($code !== 200 || empty($body['access_token'])) {
            $err = $body['error_description'] ?? $body['error'] ?? "HTTP {$code}";
            self::log('error', 'Creators API', "Token error: {$err}");
            return false;
        }

        $token   = $body['access_token'];
        $expires = (int) ($body['expires_in'] ?? 3600);
        $ttl     = min(self::TOKEN_TTL, $expires - 120);

        set_transient('pinery_creators_token', $token, $ttl);
        self::log('info', 'Creators API', 'Token cached ' . $ttl . 's');
        return $token;
    }

    public static function clear_token() {
        delete_transient('pinery_creators_token');
        delete_transient('pinery_creators_unavailable');
    }

    // ── Core request ──

    private static function request($operation, $body) {
        $token = self::get_token();
        if (!$token) { self::log('error', 'Creators API', "{$operation}: No token"); return false; }

        $path = self::PATHS[$operation] ?? 'catalog/v1/' . lcfirst($operation);
        $url  = self::BASE_URL . '/' . $path;
        $marketplace = self::get_marketplace();

        self::log('info', 'Creators API', "{$operation} → {$url} | " . json_encode($body));

        $args = [
            'headers' => [
                'Content-Type'  => 'application/json; charset=UTF-8',
                'Authorization' => 'Bearer ' . $token,
                'x-marketplace' => $marketplace,
            ],
            'body'    => json_encode($body),
            'timeout' => 20,
        ];

        $response = wp_remote_post($url, $args);
        if (is_wp_error($response)) {
            self::log('error', 'Creators API', "{$operation} network: " . $response->get_error_message());
            return false;
        }

        $code = wp_remote_retrieve_response_code($response);
        $raw  = wp_remote_retrieve_body($response);
        $resp = json_decode($raw, true);

        self::log('info', 'Creators API', "{$operation} ← HTTP {$code} | " . substr($raw, 0, 500));

        if ($code === 401) {
            self::clear_token();
            $token = self::get_token();
            if (!$token) return false;
            $args['headers']['Authorization'] = 'Bearer ' . $token;
            $response = wp_remote_post($url, $args);
            if (is_wp_error($response)) return false;
            $code = wp_remote_retrieve_response_code($response);
            $resp = json_decode(wp_remote_retrieve_body($response), true);
        }

        if ($code === 429) {
            self::log('warning', 'Creators API', "{$operation} rate limited, 2s…");
            sleep(2);
            $response = wp_remote_post($url, $args);
            if (is_wp_error($response)) return false;
            $code = wp_remote_retrieve_response_code($response);
            $resp = json_decode(wp_remote_retrieve_body($response), true);
        }

        if ($code !== 200) {
            $err = $resp['errors'][0]['message'] ?? $resp['error'] ?? "HTTP {$code}";
            self::log('error', 'Creators API', "{$operation}: " . (is_string($err) ? $err : json_encode($err)));
            if ($code === 403) {
                set_transient('pinery_creators_unavailable', true, HOUR_IN_SECONDS);
                self::log('warning', 'Creators API', 'API unavailable (403) — falling back to HTML scraping.');
            }
            return false;
        }

        return $resp;
    }

    // ── SearchItems ──

    public static function search_items($browse_node_id, $page = 1, $count = 10) {
        $body = [
            'keywords'         => '',
            'browseNodeId'     => $browse_node_id,
            'itemCount'        => min(10, max(1, (int) $count)),
            'itemPage'         => min(10, max(1, (int) $page)),
            'sortBy'           => 'AverageRating',
            'minReviewsRating' => 3,
            'resources'        => [
                'itemInfo.title',
                'itemInfo.features',
                'offersV2.summaries.lowestPrice',
                'images.primary.large',
                'browseNodeInfo.websiteSalesRank',
                'browseNodeInfo.browseNodes',
            ],
            'partnerTag'       => self::get_partner_tag(),
            'marketplace'      => self::get_marketplace(),
        ];

        $result = self::request('SearchItems', $body);
        if (!$result) return false;
        return $result['searchResult']['items'] ?? [];
    }

    // ── GetItems ──

    public static function get_items($asins) {
        if (empty($asins)) return false;
        $asins = array_slice(array_unique(array_filter($asins)), 0, 10);
        if (empty($asins)) return false;

        $body = [
            'itemIds'     => array_values($asins),
            'resources'   => [
                'itemInfo.title',
                'itemInfo.features',
                'itemInfo.byLineInfo',
                'offersV2.listings.price',
                'offersV2.listings.availability',
                'offersV2.listings.condition',
                'offersV2.summaries.lowestPrice',
                'images.primary.large',
                'images.variants.large',
                'browseNodeInfo.websiteSalesRank',
                'parentASIN',
            ],
            'partnerTag'  => self::get_partner_tag(),
            'marketplace' => self::get_marketplace(),
        ];

        $result = self::request('GetItems', $body);
        if (!$result) return false;
        return $result['itemsResult']['items'] ?? [];
    }

    /**
     * Get cached price for a single ASIN.  Uses transient cache with the admin-configured TTL.
     * Called by both process_product (publish-time) and the AJAX handler (frontend).
     */
    public static function get_item_price($asin) {
        $cache_key = 'pinery_price_' . $asin;
        $cached = get_transient($cache_key);
        if ($cached !== false) return $cached;

        $items = self::get_items([$asin]);
        if (empty($items)) return false;

        $price = self::extract_price($items[0]);
        if ($price && $price !== 'N/A') {
            set_transient($cache_key, $price, self::get_price_ttl());
        }
        return $price;
    }

    // ── Response parsing (offersV2 only — V1 is retired) ──

    public static function extract_price($item) {
        // offersV2.listings[0].price.displayAmount (primary)
        foreach ($item['offersV2']['listings'] ?? [] as $l) {
            if (!empty($l['price']['displayAmount'])) return $l['price']['displayAmount'];
        }
        // Fallback: offersV2.summaries.lowestPrice
        foreach ($item['offersV2']['summaries'] ?? [] as $s) {
            if (!empty($s['lowestPrice']['displayAmount'])) return $s['lowestPrice']['displayAmount'];
        }
        return 'N/A';
    }

    public static function extract_price_amount($item) {
        foreach ($item['offersV2']['listings'] ?? [] as $l) {
            if (isset($l['price']['amount'])) return (float) $l['price']['amount'];
        }
        foreach ($item['offersV2']['summaries'] ?? [] as $s) {
            if (isset($s['lowestPrice']['amount'])) return (float) $s['lowestPrice']['amount'];
        }
        return null;
    }

    public static function extract_image_url($item) {
        return $item['images']['primary']['large']['url'] ?? '';
    }

    public static function extract_title($item) {
        return $item['itemInfo']['title']['displayValue'] ?? '';
    }

    public static function map_to_product($item, $slug, $idx = 1) {
        $asin = $item['asin'] ?? '';
        return [
            'rank'     => $idx,
            'category' => $slug,
            'title'    => self::extract_title($item),
            'price'    => self::extract_price($item),
            'rating'   => 'N/A',
            'link'     => $item['detailPageUrl'] ?? "https://www.amazon.com/dp/{$asin}?tag=" . self::get_partner_tag(),
            'image'    => self::extract_image_url($item),
            'asin'     => $asin,
        ];
    }

    // ── Browse node ──

    public static function extract_browse_node_id($url) {
        if (preg_match('#/zgbs/[a-z0-9\-]+/(\d+)/?#i', $url, $m)) {
            self::log('info', 'Creators API', "Browse node {$m[1]}");
            return $m[1];
        }
        self::log('warning', 'Creators API', "No browse node: {$url}");
        return null;
    }

    // ── Category scraper ──

    public static function scrape_category($slug, $url, $max = 30) {
        $node_id = self::extract_browse_node_id($url);
        if (empty($node_id)) return [];

        $all = [];
        $pages = min(3, (int) ceil($max / 10));

        for ($p = 1; $p <= $pages; $p++) {
            $items = self::search_items($node_id, $p, 10);
            if ($items === false || empty($items)) break;
            foreach ($items as $item) {
                $all[] = $item;
                if (count($all) >= $max) break 2;
            }
            if ($p < $pages) usleep(self::RATE_DELAY * 1000);
        }

        $products = [];
        $skipped  = 0;
        foreach ($all as $i => $item) {
            $p = self::map_to_product($item, $slug, $i + 1);
            if (empty($p['title']) || empty($p['asin'])) { $skipped++; continue; }
            $products[] = $p;
        }

        self::log('info', 'Creators API', count($products) . " products ($skipped skipped) for {$slug}");
        return $products;
    }

    // ── Price refresh (cron) ──

    public static function refresh_published_prices($days = 30) {
        if (!self::is_available()) return 0;

        $posts = get_posts([
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => 200,
            'date_query'     => ['after' => date('Y-m-d H:i:s', strtotime("-{$days} days"))],
            'meta_key'       => '_pinery_flow_product',
            'fields'         => 'ids',
        ]);
        if (empty($posts)) return 0;

        $map = [];
        foreach ($posts as $pid) {
            $prod = get_post_meta($pid, '_pinery_flow_product', true);
            $a = $prod['asin'] ?? '';
            if ($a && $a !== 'N/A') $map[$a][] = $pid;
        }

        $updated = 0;
        foreach (array_chunk(array_keys($map), 10) as $chunk) {
            $items = self::get_items($chunk);
            if (empty($items)) { usleep(self::RATE_DELAY * 1000); continue; }

            $prices = [];
            foreach ($items as $it) {
                $a = $it['asin'] ?? '';
                if ($a) $prices[$a] = self::extract_price($it);
            }

            foreach ($chunk as $a) {
                if (!isset($prices[$a])) continue;
                foreach ($map[$a] as $pid) {
                    $prod = get_post_meta($pid, '_pinery_flow_product', true);
                    if (!is_array($prod)) continue;
                    $prod['price']         = $prices[$a];
                    $prod['price_updated'] = time();
                    update_post_meta($pid, '_pinery_flow_product', $prod);
                    $updated++;
                }
            }
            usleep(self::RATE_DELAY * 1000);
        }

        self::log('info', 'Creators API', "Price refresh: {$updated} posts");
        return $updated;
    }

    // ── Gallery images (hotlinked, 1 h transient cache) ──

    public static function get_product_images($asin) {
        $cache_key = 'pinery_gallery_' . $asin;
        $cached = get_transient($cache_key);
        if (is_array($cached)) return $cached;

        $items = self::get_items([$asin]);
        if (empty($items)) return [];

        $item   = $items[0];
        $images = [];

        $p = $item['images']['primary']['large']['url'] ?? '';
        if ($p) $images[] = $p;

        foreach ($item['images']['variants'] ?? [] as $v) {
            $u = $v['large']['url'] ?? '';
            if ($u && !in_array($u, $images)) $images[] = $u;
        }

        if (!empty($images)) set_transient($cache_key, $images, HOUR_IN_SECONDS);
        return $images;
    }

    // ── AJAX handler (secured) ──

    /**
     * Logged-in endpoint (wp_ajax): full API lookup + meta update.
     */
    public static function ajax_get_price() {
        if (!current_user_can('edit_posts')) {
            wp_send_json_error(['message' => 'Unauthorized'], 403);
        }

        check_ajax_referer('pinery_creators_price', 'nonce');

        $asin = sanitize_text_field($_POST['asin'] ?? '');
        if (empty($asin)) wp_send_json_error(['message' => 'No ASIN']);

        $price = self::get_item_price($asin);
        if ($price === false || $price === 'N/A') wp_send_json_error(['message' => 'Could not fetch price']);

        $pid = absint($_POST['post_id'] ?? 0);
        if ($pid && current_user_can('edit_post', $pid)) {
            $prod = get_post_meta($pid, '_pinery_flow_product', true);
            if (is_array($prod)) {
                $prod['price']         = $price;
                $prod['price_updated'] = time();
                update_post_meta($pid, '_pinery_flow_product', $prod);
            }
        }

        wp_send_json_success(['price' => $price, 'updated' => time()]);
    }

    /**
     * Public endpoint (wp_ajax_nopriv): read-only, cached only.
     * Does NOT make a live API call — reads the transient.  No meta write.
     */
    public static function ajax_get_price_public() {
        $asin = sanitize_text_field($_POST['asin'] ?? '');
        if (empty($asin)) wp_send_json_error(['message' => 'No ASIN']);

        // Only serve cached prices to anonymous visitors — never trigger a live API call
        $cache_key = 'pinery_price_' . $asin;
        $cached = get_transient($cache_key);
        if ($cached === false || $cached === 'N/A') {
            wp_send_json_error(['message' => 'Price not cached']);
        }

        wp_send_json_success(['price' => $cached, 'updated' => time()]);
    }

    // ── Logging ──

    private static function log($level, $step, $message, $context = '') {
        if (method_exists('Pinery_Flow_Pipeline', 'debug_log')) {
            Pinery_Flow_Pipeline::debug_log($level, $step, $message, $context);
        }
    }
}
