<?php
/**
 * Per-batch handler invoked by Action Scheduler for a single chunk of
 * product IDs. Runs the eligibility + payload build flow on the chunk,
 * sends one offer/save call to eMAG, records per-product Logger entries,
 * and updates ProgressStore. When the chunk is the last one in a sync,
 * triggers post-sync notifications, releases the lock, and marks progress
 * complete.
 *
 * @package SkyEmagSync
 */

namespace SkyEmagSync\Sync;

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

defined( 'ABSPATH' ) || exit;

class BatchProcessor {

    /**
     * Action Scheduler callback. Hooked to `skyemag_sync_batch` in Plugin.php.
     *
     * @param int[]  $chunk_ids Product/variation post IDs to process in this batch.
     * @param string $sync_id   Identifier from ProgressStore for the parent sync.
     */
    public static function handle( array $chunk_ids, string $sync_id ): void {
        if ( ProgressStore::is_cancelled( $sync_id ) ) {
            return;
        }

        $state = ProgressStore::get();
        if ( null === $state || $state['sync_id'] !== $sync_id ) {
            return;
        }

        $products = [];
        foreach ( $chunk_ids as $id ) {
            $product = wc_get_product( (int) $id );
            if ( $product && ! $product->is_type( 'variable' ) ) {
                $products[] = $product;
            }
        }

        $synced  = 0;
        $failed  = 0;
        $skipped = 0;
        $batch_payloads = []; // [ wc_post_id => payload ]
        $blocked        = [];

        foreach ( $products as $product ) {
            $post_id = $product->get_id();

            $excluded = Runner::matches_exclusion_filters( $product );
            if ( null !== $excluded ) {
                Logger::skip( $post_id, 0, $excluded );
                $skipped++;
                continue;
            }

            $check = Runner::eligibility_check( $product );
            if ( ! $check['eligible'] ) {
                Logger::skip( $post_id, 0, $check['reason'] );
                $skipped++;
                continue;
            }

            $payload = OfferPayload::from_product( $product );
            if ( null === $payload ) {
                Logger::skip( $post_id, 0, 'Stock is 0 – skipped' );
                $skipped++;
                continue;
            }

            $price_check = Runner::price_guard( $post_id, $payload['sale_price'] ?? null );
            if ( ! $price_check['ok'] ) {
                PostMeta::mark_error( $post_id, $price_check['reason'] );
                Logger::fail( $post_id, 0, Logger::ACTION_PRICE, $price_check['reason'] );
                $failed++;
                $blocked[] = [
                    'id'     => $post_id,
                    'name'   => $product->get_name(),
                    'reason' => $price_check['reason'],
                ];
                continue;
            }

            $product_fresh = wc_get_product( $post_id );
            if ( $product_fresh && OfferPayload::is_below_min_margin( $product_fresh ) ) {
                $min_margin = OfferPayload::effective_min_margin( $product_fresh );
                $reason     = sprintf(
                    'Calculated price (%.2f) below minimum margin (%.2f%%) — sync blocked',
                    $payload['sale_price'] ?? 0,
                    $min_margin
                );
                PostMeta::mark_error( $post_id, $reason );
                Logger::fail( $post_id, 0, Logger::ACTION_PRICE, $reason );
                $failed++;
                $blocked[] = [
                    'id'     => $post_id,
                    'name'   => $product_fresh->get_name(),
                    'reason' => $reason,
                ];
                continue;
            }

            $batch_payloads[ $post_id ] = $payload;
        }

        if ( ! empty( $batch_payloads ) ) {
            $client = new EmagClient();
            $result = self::flush_batch( $client, $batch_payloads );
            $synced += $result['synced'];
            $failed += $result['failed'];
        }

        ProgressStore::increment(
            $synced,
            $failed,
            $skipped,
            sprintf(
                'Batch processed: %d synced, %d failed, %d skipped',
                $synced,
                $failed,
                $skipped
            )
        );

        // Detect last-batch completion: when processed reaches total.
        $latest = ProgressStore::get();
        if ( null !== $latest
            && $latest['sync_id'] === $sync_id
            && $latest['processed'] >= $latest['total']
            && $latest['state'] === 'running'
        ) {
            self::finalize( $latest, $blocked );
        }
    }

    /**
     * Send the payloads to eMAG and mark per-product success/error.
     * Mirrors Runner::flush_batch but operates on a single AS-scheduled chunk.
     * The preflight min_sale_price two-call dance is preserved.
     *
     * @return array{synced:int,failed:int}
     */
    private static function flush_batch( EmagClient $client, array $batch ): array {
        $synced = 0;
        $failed = 0;

        // Same duplicate-mapping guard as Runner::flush_batch — see
        // Runner::extract_mapping_conflicts for the rationale.
        $split = Runner::extract_mapping_conflicts( $batch );
        foreach ( $split['conflicts'] as $post_id => $conflict ) {
            PostMeta::mark_error( $post_id, $conflict['reason'] );
            Logger::fail( $post_id, 0, Logger::ACTION_BOTH, $conflict['reason'] );
            $failed++;
        }
        $batch = $split['clean'];
        if ( empty( $batch ) ) {
            return [ 'synced' => $synced, 'failed' => $failed ];
        }

        $id_map          = [];
        $payloads        = [];
        $min_preflight   = [];
        $needs_preflight = [];

        foreach ( $batch as $post_id => $payload ) {
            $vendor_id            = (int) $payload['id'];
            $id_map[ $vendor_id ] = $post_id;
            $payloads[]           = $payload;

            $stored_min = PostMeta::get_min_price( $post_id );
            $new_sale   = isset( $payload['sale_price'] )     ? (float) $payload['sale_price']     : null;
            $new_min    = isset( $payload['min_sale_price'] ) ? (float) $payload['min_sale_price'] : null;

            if (
                null !== $stored_min && $stored_min > 0
                && null !== $new_sale && null !== $new_min
                && $new_sale < $stored_min
            ) {
                $min_preflight[] = [
                    'id'             => $vendor_id,
                    'min_sale_price' => $new_min,
                ];
                $needs_preflight[ $post_id ] = $new_min;
            }
        }

        if ( ! empty( $min_preflight ) ) {
            $pre_response = $client->save_offers( $min_preflight );
            if ( $pre_response['success'] ) {
                foreach ( $needs_preflight as $post_id => $new_min ) {
                    update_post_meta( $post_id, PostMeta::MIN_PRICE, $new_min );
                    Logger::success( $post_id, 0, Logger::ACTION_PRICE,
                        sprintf( 'min_sale_price lowered to %.2f (preflight)', $new_min )
                    );
                }
            } else {
                Logger::log( 0, 0, Logger::ACTION_PRICE, Logger::RESULT_FAIL,
                    'min_sale_price preflight failed: ' . $pre_response['error']
                );
            }
        }

        $response = $client->save_offers( $payloads );

        if ( ! $response['success'] ) {
            foreach ( $batch as $post_id => $payload ) {
                PostMeta::mark_error( $post_id, $response['error'] );
                Logger::fail( $post_id, 0, Logger::ACTION_BOTH, $response['error'] );
                $failed++;
            }
            return [ 'synced' => $synced, 'failed' => $failed ];
        }

        $api_results = (array) ( $response['data'] ?? [] );
        $item_errors = [];
        foreach ( $api_results as $item ) {
            if ( ! empty( $item['id'] ) && ! empty( $item['isError'] ) ) {
                $msg = '';
                if ( ! empty( $item['messages'] ) && is_array( $item['messages'] ) ) {
                    $texts = [];
                    foreach ( $item['messages'] as $m ) {
                        if ( is_string( $m ) ) {
                            $texts[] = $m;
                        } elseif ( is_array( $m ) && isset( $m['text'] ) ) {
                            $texts[] = $m['text'];
                        }
                    }
                    $msg = implode( '; ', array_filter( $texts ) );
                }
                $item_errors[ (int) $item['id'] ] = $msg ?: 'eMAG rejected this offer';
            }
        }

        foreach ( $batch as $post_id => $payload ) {
            $vendor_id = (int) $payload['id'];

            if ( isset( $item_errors[ $vendor_id ] ) ) {
                PostMeta::mark_error( $post_id, $item_errors[ $vendor_id ] );
                Logger::fail( $post_id, 0, Logger::ACTION_BOTH, $item_errors[ $vendor_id ] );
                $failed++;
            } else {
                PostMeta::mark_success( $post_id );
                if ( isset( $payload['min_sale_price'] ) ) {
                    update_post_meta( $post_id, PostMeta::MIN_PRICE, (float) $payload['min_sale_price'] );
                }
                if ( isset( $payload['max_sale_price'] ) ) {
                    update_post_meta( $post_id, PostMeta::MAX_PRICE, (float) $payload['max_sale_price'] );
                }
                Logger::success( $post_id, 0, Logger::ACTION_BOTH,
                    "vendor_ext_id={$vendor_id} price={$payload['sale_price']} stock={$payload['stock'][0]['value']}" );
                $synced++;
            }
        }

        return [ 'synced' => $synced, 'failed' => $failed ];
    }

    /**
     * Sync is fully processed: write the summary log row, fire post-sync
     * notifications, mark progress complete, release the global lock.
     */
    private static function finalize( array $state, array $blocked ): void {
        Logger::log( 0, 0, Logger::ACTION_BOTH, Logger::RESULT_SUCCESS,
            "Sync ({$state['trigger']}): {$state['synced']} synced, {$state['skipped']} skipped, {$state['failed']} failed" );

        $result = [
            'success'          => $state['failed'] === 0,
            'synced'           => (int) $state['synced'],
            'skipped'          => (int) $state['skipped'],
            'failed'           => (int) $state['failed'],
            'errors'           => [],
            'blocked_products' => $blocked,
        ];

        Runner::dispatch_post_sync_notifications( $state['trigger'], $result );

        ProgressStore::complete();
        ProgressStore::clear_cancel( $state['sync_id'] );
        Runner::release_lock();
    }
}
