<?php
/**
 * Bootstrap – one-time import from eMAG product_offer/read.
 *
 * Reads all offers from eMAG, matches them to WooCommerce products by
 * part_number (= WC SKU), and stores vendor_ext_id + vat_id + price bounds
 * + handling_time per product.
 *
 * Also fetches /vat/read and caches the rate → id mapping in wp_options.
 *
 * Can be re-run at any time via the "Re-import from eMAG" button.
 *
 * @package SkyEmagSync
 */

namespace SkyEmagSync\Sync;

use SkyEmagSync\Api\EmagClient;
use SkyEmagSync\Api\OfferPayload;
use SkyEmagSync\Options;
use SkyEmagSync\PostMeta;

defined( 'ABSPATH' ) || exit;

class Bootstrap {

    // eMAG returns max 100 offers per read page
    const PAGE_SIZE = 100;

    // Progress / lock transients used by the AJAX-backed UI in SettingsPage.
    // Mirrors the Products\CategoryImporter pattern: a long-running AJAX
    // request drives the work while a separate poll endpoint reads these
    // keys to render the progress bar / log.
    const TRANSIENT_PROGRESS = 'skyemag_bs_import_progress';
    const TRANSIENT_LOCK     = 'skyemag_bs_import_lock';

    /**
     * Get current import progress snapshot. Returns the default shape when
     * no import is in flight, so the JS poll can render a stable structure
     * before the first transient write lands.
     *
     * @return array{
     *   running:bool, phase:string, total_offers:int, fetched_offers:int,
     *   processed:int, matched:int, unmatched:int, skipped:int,
     *   captured_margins:int, error:string
     * }
     */
    public static function get_progress(): array {
        $default = [
            'running'          => false,
            'phase'            => '',
            'total_offers'     => 0,
            'fetched_offers'   => 0,
            'processed'        => 0,
            'matched'          => 0,
            'unmatched'        => 0,
            'skipped'          => 0,
            'captured_margins' => 0,
            'error'            => '',
        ];
        $stored = get_transient( self::TRANSIENT_PROGRESS );
        return is_array( $stored ) ? array_merge( $default, $stored ) : $default;
    }

    private static function update_progress( array $patch ): void {
        $current = self::get_progress();
        set_transient( self::TRANSIENT_PROGRESS, array_merge( $current, $patch ), 30 * MINUTE_IN_SECONDS );
    }

    /**
     * Run the full bootstrap.
     *
     * @param string|null $mode  Pricing strategy: 'global_margin' (default,
     *                           leave existing per-product margins alone) or
     *                           'preserve_emag_prices' (capture per-product
     *                           margins from each existing eMAG offer so the
     *                           current marketplace prices stay intact on
     *                           first sync). Null falls back to the stored
     *                           Options::bootstrap_mode().
     *
     * @return array [
     *   'success'          => bool,
     *   'matched'          => int,   products successfully mapped
     *   'unmatched'        => int,   eMAG offers with no WC SKU match
     *   'skipped'          => int,   WC products with no eMAG offer
     *   'captured_margins' => int,   per-product margins back-calculated in preserve mode
     *   'errors'           => string[]
     * ]
     */
    public static function run( ?string $mode = null ): array {
        if ( get_transient( self::TRANSIENT_LOCK ) ) {
            return [
                'success'          => false,
                'matched'          => 0,
                'unmatched'        => 0,
                'skipped'          => 0,
                'captured_margins' => 0,
                'errors'           => [ __( 'Import already running. Wait for the current run to finish.', 'skyservers-product-sync-for-emag' ) ],
            ];
        }
        set_transient( self::TRANSIENT_LOCK, 1, 30 * MINUTE_IN_SECONDS );

        // Resolve mode: explicit arg wins, otherwise fall back to stored Options.
        $effective_mode = ( null === $mode ) ? Options::bootstrap_mode() : $mode;
        if ( ! in_array( $effective_mode, Options::BOOTSTRAP_MODES, true ) ) {
            $effective_mode = Options::BOOTSTRAP_MODE_DEFAULT;
        }

        // Reset progress for this run.
        delete_transient( self::TRANSIENT_PROGRESS );
        self::update_progress( [
            'running' => true,
            'phase'   => 'vat_cache',
            'error'   => '',
        ] );

        $client = new EmagClient();
        $result = [
            'success'          => false,
            'matched'          => 0,
            'unmatched'        => 0,
            'skipped'          => 0,
            'captured_margins' => 0,
            'errors'           => [],
        ];

        // ── Step 1: cache VAT rates ───────────────────────────────────────
        $vat_map = self::fetch_vat_map( $client, $result );
        Options::set( Options::VAT_CACHE, $vat_map );

        // ── Step 2: read all offers from eMAG paginated ──────────────────
        self::update_progress( [ 'phase' => 'fetching' ] );
        $emag_offers = self::fetch_all_offers( $client, $result );

        if ( empty( $emag_offers ) && ! empty( $result['errors'] ) ) {
            self::update_progress( [
                'running' => false,
                'phase'   => 'error',
                'error'   => implode( '; ', $result['errors'] ),
            ] );
            delete_transient( self::TRANSIENT_LOCK );
            return $result; // API unreachable
        }

        // ── Step 3: build SKU → WC product_id index ──────────────────────
        self::update_progress( [
            'phase'          => 'matching',
            'total_offers'   => count( $emag_offers ),
            'fetched_offers' => count( $emag_offers ),
        ] );
        $sku_index = self::build_sku_index();

        // ── Step 4: match and store ───────────────────────────────────────
        $progress_throttle = 0;
        foreach ( $emag_offers as $offer ) {
            $part_number = (string) ( $offer['part_number'] ?? '' );
            if ( $part_number === '' ) {
                $result['unmatched']++;
                continue;
            }

            $post_id = $sku_index[ $part_number ] ?? null;

            if ( null === $post_id ) {
                $result['unmatched']++;
                Logger::log( 0, 0, Logger::ACTION_BOOTSTRAP, Logger::RESULT_SKIP,
                    "eMAG part_number '{$part_number}' has no matching WC SKU" );
                continue;
            }

            // Resolve vat_id from cached rate map
            $vat_rate = isset( $offer['vat_id'] )
                ? null   // API sometimes returns vat_id directly
                : null;

            // product_offer/read returns vat_id directly (integer)
            $vat_id = isset( $offer['vat_id'] ) ? (int) $offer['vat_id'] : null;

            // Fallback: look up by vat_rate if vat_id missing
            if ( null === $vat_id && isset( $offer['vat_rate'] ) ) {
                $vat_id = Options::vat_id_for_rate( (float) $offer['vat_rate'] );
            }

            // handling_time: comes as array [{warehouse_id:1,value:N}], take first value
            $handling = 1;
            if ( ! empty( $offer['handling_time'] ) && is_array( $offer['handling_time'] ) ) {
                $handling = (int) ( $offer['handling_time'][0]['value'] ?? 1 );
            }

            // min/max price
            $min_price = isset( $offer['min_sale_price'] ) ? (float) $offer['min_sale_price'] : null;
            $max_price = isset( $offer['max_sale_price'] ) ? (float) $offer['max_sale_price'] : null;

            PostMeta::store_bootstrap_data( $post_id, [
                'vendor_ext_id'          => (int) $offer['id'],
                'vat_id'                 => $vat_id,
                'min_price'              => $min_price,
                'max_price'              => $max_price,
                'handling_time'          => $handling,
                'status'                 => (int) ( $offer['status'] ?? 1 ),
                'part_number'            => $part_number,
                'buy_button_rank'        => $offer['buy_button_rank'] ?? null,
                'number_of_offers'       => $offer['number_of_offers'] ?? null,
                'best_offer_sale_price'  => $offer['best_offer_sale_price'] ?? null,
            ] );

            Logger::success( $post_id, 0, Logger::ACTION_BOOTSTRAP,
                "Mapped WC SKU '{$part_number}' → eMAG vendor_ext_id {$offer['id']}" );

            // Smart Bootstrap: back-calculate the per-product margin from the
            // current eMAG sale_price so existing marketplace prices survive
            // the first sync intact. Skipped silently in global_margin mode.
            // Both eMAG sale_price and OfferPayload::source_price() are ex-VAT,
            // so the ratio gives a clean margin %. Existing per-product margin
            // overrides are never touched — the seller's manual decisions win.
            if ( 'preserve_emag_prices' === $effective_mode ) {
                self::capture_margin_for_post( $post_id, $offer, $result );
            }

            $result['matched']++;

            // Push progress every 10 offers — frequent enough to feel live,
            // sparse enough that the transient writes don't dominate the loop.
            $progress_throttle++;
            if ( 0 === $progress_throttle % 10 ) {
                self::update_progress( [
                    'processed'        => $result['matched'] + $result['unmatched'],
                    'matched'          => $result['matched'],
                    'unmatched'        => $result['unmatched'],
                    'captured_margins' => $result['captured_margins'],
                ] );
            }
        }

        // ── Step 5: count WC products with no mapping ─────────────────────
        $result['skipped'] = self::count_unmapped_wc_products();
        $result['success'] = true;

        Options::set( Options::BOOTSTRAP_DONE, true );

        $captured_suffix = $result['captured_margins'] > 0
            ? ", captured {$result['captured_margins']} per-product margins from existing eMAG prices"
            : '';
        Logger::log( 0, 0, Logger::ACTION_BOOTSTRAP, Logger::RESULT_SUCCESS,
            "Bootstrap complete: {$result['matched']} matched, {$result['unmatched']} unmatched eMAG offers, {$result['skipped']} unmapped WC products{$captured_suffix}" );

        self::update_progress( [
            'running'          => false,
            'phase'            => 'done',
            'processed'        => $result['matched'] + $result['unmatched'],
            'matched'          => $result['matched'],
            'unmatched'        => $result['unmatched'],
            'skipped'          => $result['skipped'],
            'captured_margins' => $result['captured_margins'],
        ] );
        delete_transient( self::TRANSIENT_LOCK );

        return $result;
    }

    // Safety buffer added to captured margin when seeding per-product max_margin.
    // Prevents eMAG offer/save rejections when the captured sale_price would
    // otherwise exceed a tighter global max_sale_price ceiling. 20 percentage
    // points gives the seller room to raise prices later without manual fixup.
    const CAPTURE_MAX_MARGIN_BUFFER = 20.0;

    /**
     * Back-calculate the implied per-product margin from an existing eMAG
     * offer and store it as a per-product override. Used by the Smart
     * Bootstrap 'preserve_emag_prices' path so the seller's existing
     * marketplace prices stay intact on first sync.
     *
     * Also seeds a per-product max_margin = captured + CAPTURE_MAX_MARGIN_BUFFER
     * when no manual override exists, so OfferPayload's max_sale_price ceiling
     * stays above the captured sale_price and eMAG accepts the next /offer/save.
     *
     * Skips silently if:
     *  - the seller already set a per-product margin override (manual wins)
     *  - the offer payload doesn't include sale_price (defensive)
     *  - the WC product has no resolvable source price (no regular/sale price)
     *
     * Negative implied margins (eMAG price below WC price — seller is losing
     * money on the marketplace) are stored anyway but logged as a warning
     * so the seller sees the situation in the activity log.
     *
     * @param int   $post_id  WC product or variation ID.
     * @param array $offer    Raw row from /product_offer/read.
     * @param array $result   Bootstrap result accumulator (modified by reference).
     */
    private static function capture_margin_for_post( int $post_id, array $offer, array &$result ): void {
        // Respect existing manual overrides — the seller's per-product
        // margin decisions take priority over anything we'd derive.
        $existing_override = PostMeta::get_margin( $post_id );
        if ( null !== $existing_override ) {
            Logger::log( $post_id, 0, Logger::ACTION_BOOTSTRAP, Logger::RESULT_SKIP,
                sprintf( 'Kept existing per-product margin override %.2f%%', $existing_override ) );
            return;
        }

        $emag_sale = isset( $offer['sale_price'] ) ? (float) $offer['sale_price'] : 0.0;
        if ( $emag_sale <= 0 ) {
            return; // offer has no usable sale_price — nothing to derive from
        }

        $product = wc_get_product( $post_id );
        if ( ! $product ) {
            return;
        }

        $wc_source = OfferPayload::source_price( $product );
        if ( null === $wc_source || $wc_source <= 0 ) {
            Logger::log( $post_id, 0, Logger::ACTION_BOOTSTRAP, Logger::RESULT_SKIP,
                'Cannot capture margin — WC product has no resolvable source price' );
            return;
        }

        $implied = round( ( $emag_sale / $wc_source - 1 ) * 100, 2 );
        PostMeta::set_margin( $post_id, $implied );

        // Seed per-product max_margin so OfferPayload's max_sale_price ceiling
        // sits comfortably above the captured sale_price. Only when no manual
        // override exists — the seller's explicit max stays untouched.
        if ( null === PostMeta::get_max_margin( $post_id ) ) {
            PostMeta::set_max_margin( $post_id, $implied + self::CAPTURE_MAX_MARGIN_BUFFER );
        }

        $result['captured_margins']++;

        $msg = sprintf(
            'Captured margin %.2f%% from existing eMAG price %.4f vs WC source price %.4f',
            $implied,
            $emag_sale,
            $wc_source
        );
        Logger::success( $post_id, 0, Logger::ACTION_BOOTSTRAP, $msg );

        if ( $implied < 0 ) {
            Logger::log( $post_id, 0, Logger::ACTION_BOOTSTRAP, Logger::RESULT_FAIL,
                sprintf( 'Warning: implied margin %.2f%% is negative — eMAG price is below WC source price', $implied ) );
        }
    }

    // ── Private helpers ───────────────────────────────────────────────────

    /**
     * Fetch VAT rates and return [ 'rate_string' => vat_id ] map.
     * e.g. [ '0.11' => 3, '0.21' => 1 ]
     */
    private static function fetch_vat_map( EmagClient $client, array &$result ): array {
        $response = $client->read_vat_rates();
        if ( ! $response['success'] ) {
            $result['errors'][] = 'VAT read failed: ' . $response['error'];
            return [];
        }

        $map = [];
        foreach ( (array) $response['data'] as $entry ) {
            // eMAG returns 'vat_id' (not 'id') and 'vat_rate'
            $id  = $entry['vat_id'] ?? $entry['id'] ?? null;
            $rate = $entry['vat_rate'] ?? null;
            if ( null !== $id && null !== $rate ) {
                $map[ (string) $rate ] = (int) $id;
            }
        }
        return $map;
    }

    /**
     * Read all offers from eMAG using paginated product_offer/read.
     */
    private static function fetch_all_offers( EmagClient $client, array &$result ): array {
        $all    = [];
        $page   = 1;

        do {
            $response = $client->read_offers( [
                'currentPage' => $page,
                'itemsPerPage' => self::PAGE_SIZE,
            ] );

            if ( ! $response['success'] ) {
                $result['errors'][] = "product_offer/read page {$page} failed: " . $response['error'];
                break;
            }

            $rows = (array) ( $response['data'] ?? [] );
            $all  = array_merge( $all, $rows );

            // Push fetched count after every page so the JS bar advances
            // through phase=fetching even before the matching loop starts.
            self::update_progress( [
                'fetched_offers' => count( $all ),
            ] );

            // eMAG paginates: stop if fewer rows than page size
            $has_more = count( $rows ) >= self::PAGE_SIZE;
            $page++;

            // Safety cap: max 50 pages = 5000 products
            if ( $page > 50 ) {
                break;
            }
        } while ( $has_more );

        return $all;
    }

    /**
     * Build [ sku => post_id ] index for all WC simple products and variations.
     */
    private static function build_sku_index(): array {
        global $wpdb;

        // Fetch all post IDs + their _sku meta in one query
        $rows = $wpdb->get_results(
            "SELECT p.ID, pm.meta_value AS sku
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = '_sku'
             WHERE p.post_type IN ('product','product_variation')
               AND p.post_status NOT IN ('trash','auto-draft')
               AND pm.meta_value != ''",
            ARRAY_A
        );

        $index = [];
        foreach ( $rows as $row ) {
            $index[ $row['sku'] ] = (int) $row['ID'];
        }
        return $index;
    }

    /**
     * Count WC products/variations that still have no vendor_ext_id.
     */
    private static function count_unmapped_wc_products(): int {
        global $wpdb;

        return (int) $wpdb->get_var(
            "SELECT COUNT(DISTINCT p.ID)
             FROM {$wpdb->posts} p
             WHERE p.post_type IN ('product','product_variation')
               AND p.post_status NOT IN ('trash','auto-draft')
               AND p.ID NOT IN (
                   SELECT post_id FROM {$wpdb->postmeta}
                   WHERE meta_key = '_skyemag_vendor_ext_id'
               )"
        );
    }
}
