<?php
/**
 * BulkMatchProductsJob (free)
 *
 * Background job that scans every unlinked simple WooCommerce product and
 * tries to link it to a Square catalog ITEM by exact SKU match.
 *
 * Free version restrictions vs. Pro:
 *   - Only simple products are queued; variable products are surfaced
 *     separately as a "skipped_variable" count, with a Pro upsell.
 *   - Variation-level matching is not performed.
 *
 * Auto-link rules:
 *   - Exactly one Square ITEM matches the product SKU and that ITEM is not
 *     already linked → link.
 *   - Multiple matching Square ITEMs → "conflict_multiple_squares".
 *   - Square match already linked elsewhere → "conflict_already_linked".
 *   - Woo product has no SKU → "needs_review" with title-based suggestions.
 */

namespace Pixeldev\SquareWooSync\Admin\Helpers;

use Pixeldev\SquareWooSync\Logger\Logger;
use Pixeldev\SquareWooSync\Square\SquareInventory;

defined('ABSPATH') || exit;

class BulkMatchProductsJob
{
    public const HOOK          = 'sws_bulk_match_products_batch';
    public const GROUP         = 'squarewoosync_bulk_match';
    public const OPTION        = 'squarewoosync_bulk_match_progress';
    public const REPORT_OPTION = 'squarewoosync_bulk_match_report';
    public const BATCH_SIZE    = 10;

    public static function register(): void
    {
        add_action(self::HOOK, [self::class, 'handle_batch'], 10, 2);
    }

    public static function start(): array
    {
        $progress = self::get_progress();
        if ('running' === $progress['status']) {
            return ['status' => 'already_running', 'progress' => self::strip_internals($progress)];
        }

        // Free: only simple products. Variable products are skipped and
        // counted so the UI can prompt the user to upgrade.
        $query_all = new \WP_Query([
            'post_type'      => 'product',
            'post_status'    => ['publish', 'draft', 'pending', 'private'],
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'meta_query'     => [
                [
                    'key'     => 'square_product_id',
                    'compare' => 'NOT EXISTS',
                ],
            ],
        ]);

        $all_ids        = array_values(array_map('intval', (array) $query_all->posts));
        $simple_ids     = [];
        $skipped_variable = 0;

        foreach ($all_ids as $pid) {
            $product = wc_get_product($pid);
            if (!$product) {
                continue;
            }
            if ($product->is_type('variable')) {
                $skipped_variable++;
                continue;
            }
            // Treat anything that isn't a variation as "simple" for our purposes
            // (simple, external, grouped). Variations themselves are never queried
            // because we restricted post_type to 'product'.
            $simple_ids[] = $pid;
        }

        global $wpdb;
        // phpcs:ignore WordPress.DB.PreparedSQL
        $linked_ids = (array) $wpdb->get_col("SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key = 'square_product_id' AND meta_value <> ''");
        $linked_map = array_fill_keys($linked_ids, true);

        $now    = time();
        $job_id = wp_generate_uuid4();

        if (empty($simple_ids)) {
            $progress = self::default_progress();
            $progress = array_merge($progress, [
                'job_id'           => $job_id,
                'status'           => 'completed',
                'started_at'       => $now,
                'updated_at'       => $now,
                'finished_at'      => $now,
                'skipped_variable' => $skipped_variable,
            ]);
            update_option(self::OPTION, $progress, false);
            update_option(self::REPORT_OPTION, self::default_report(), false);
            return ['status' => 'no_products', 'progress' => self::strip_internals($progress)];
        }

        $progress = self::default_progress();
        $progress = array_merge($progress, [
            'job_id'           => $job_id,
            'status'           => 'running',
            'product_ids'      => $simple_ids,
            'linked_map'       => $linked_map,
            'total'            => count($simple_ids),
            'started_at'       => $now,
            'updated_at'       => $now,
            'skipped_variable' => $skipped_variable,
        ]);
        update_option(self::OPTION, $progress, false);
        update_option(self::REPORT_OPTION, self::default_report(), false);

        if (!function_exists('as_enqueue_async_action')) {
            $progress['status']      = 'failed';
            $progress['last_error']  = 'Action Scheduler is not available.';
            $progress['finished_at'] = $now;
            $progress['product_ids'] = [];
            $progress['linked_map']  = [];
            update_option(self::OPTION, $progress, false);
            return ['status' => 'no_scheduler', 'progress' => self::strip_internals($progress)];
        }

        as_enqueue_async_action(self::HOOK, [$job_id, 0], self::GROUP);

        return ['status' => 'started', 'progress' => self::strip_internals($progress)];
    }

    public static function cancel(): array
    {
        $progress = self::get_progress();
        if ('running' !== $progress['status']) {
            return ['status' => 'not_running', 'progress' => self::strip_internals($progress)];
        }

        if (function_exists('as_unschedule_all_actions')) {
            as_unschedule_all_actions(self::HOOK, [], self::GROUP);
        }

        $now                     = time();
        $progress['status']      = 'cancelled';
        $progress['finished_at'] = $now;
        $progress['updated_at']  = $now;
        $progress['product_ids'] = [];
        $progress['linked_map']  = [];
        update_option(self::OPTION, $progress, false);

        return ['status' => 'cancelled', 'progress' => self::strip_internals($progress)];
    }

    public static function handle_batch(string $job_id, int $offset): void
    {
        $progress = self::get_progress();
        if ('running' !== $progress['status'] || $progress['job_id'] !== $job_id) {
            return;
        }

        $ids   = isset($progress['product_ids']) && is_array($progress['product_ids']) ? $progress['product_ids'] : [];
        $total = (int) ($progress['total'] ?? count($ids));
        $batch = array_slice($ids, $offset, self::BATCH_SIZE);

        if (empty($batch)) {
            self::mark_completed($job_id);
            return;
        }

        $square     = new SquareInventory();
        $linked_map = isset($progress['linked_map']) && is_array($progress['linked_map']) ? $progress['linked_map'] : [];
        $report     = self::get_report();

        foreach ($batch as $woo_id) {
            $woo_id  = (int) $woo_id;
            $product = wc_get_product($woo_id);
            if (!$product) {
                continue;
            }
            // Defensive: variable products should already have been filtered, but
            // never run them through here even if they slipped past start().
            if ($product->is_type('variable')) {
                continue;
            }
            if (!empty(get_post_meta($woo_id, 'square_product_id', true))) {
                continue;
            }

            $result = self::classify_product($product, $square, $linked_map);
            self::record_result($report, $product, $result);

            if ('matched' === $result['classification']) {
                $linked_map[$result['square_id']] = true;
            }
        }

        $latest = self::get_progress();
        if ($latest['job_id'] !== $job_id || 'running' !== $latest['status']) {
            return;
        }

        $latest['processed']  = max((int) ($latest['processed'] ?? 0), $offset + count($batch));
        $latest['updated_at'] = time();
        $latest['linked_map'] = $linked_map;

        update_option(self::OPTION, $latest, false);
        update_option(self::REPORT_OPTION, $report, false);

        $next_offset = $offset + self::BATCH_SIZE;

        if ($next_offset < $total) {
            if (function_exists('as_enqueue_async_action')) {
                as_enqueue_async_action(self::HOOK, [$job_id, $next_offset], self::GROUP);
            }
            return;
        }

        $latest['status']      = 'completed';
        $latest['finished_at'] = time();
        $latest['product_ids'] = [];
        $latest['linked_map']  = [];
        update_option(self::OPTION, $latest, false);
    }

    private static function classify_product(\WC_Product $product, SquareInventory $square, array $linked_map): array
    {
        $sku = trim((string) $product->get_sku());

        if ('' === $sku) {
            $suggestions = self::title_suggestions($product, $square, $linked_map);
            return [
                'classification' => 'needs_review',
                'reason'         => 'no_sku',
                'suggestions'    => $suggestions,
            ];
        }

        $candidates = []; // item_id => ['name', 'matched_skus', 'variation_ids']
        $hits = self::sku_lookup($square, $sku);
        foreach ($hits as $hit) {
            $id = $hit['item_id'];
            if (!isset($candidates[$id])) {
                $candidates[$id] = [
                    'name'          => $hit['item_name'],
                    'matched_skus'  => [],
                    'variation_ids' => [],
                ];
            }
            $candidates[$id]['matched_skus'][]  = $sku;
            $candidates[$id]['variation_ids'][] = $hit['variation_id'];
        }
        foreach ($candidates as &$c) {
            $c['matched_skus']  = array_values(array_unique($c['matched_skus']));
            $c['variation_ids'] = array_values(array_unique($c['variation_ids']));
        }
        unset($c);

        if (empty($candidates)) {
            return [
                'classification' => 'no_match',
                'reason'         => 'no_sku_match',
                'skus'           => [$sku],
            ];
        }

        if (count($candidates) > 1) {
            return [
                'classification' => 'conflict',
                'reason'         => 'multiple_squares',
                'skus'           => [$sku],
                'candidates'     => self::shape_candidates($candidates),
            ];
        }

        $square_id   = (string) array_key_first($candidates);
        $candidate   = $candidates[$square_id];
        $square_name = (string) ($candidate['name'] ?? '');

        if (isset($linked_map[$square_id])) {
            $conflict_woo_id = self::find_woo_for_square($square_id);
            return [
                'classification'  => 'conflict',
                'reason'          => 'square_already_linked',
                'skus'            => [$sku],
                'square_id'       => $square_id,
                'square_name'     => $square_name,
                'conflict_woo_id' => $conflict_woo_id,
            ];
        }

        $link = self::link_product($product, $square_id, $square);
        if (!$link['ok']) {
            return [
                'classification' => 'no_match',
                'reason'         => $link['reason'] ?? 'link_failed',
                'skus'           => [$sku],
            ];
        }

        return [
            'classification' => 'matched',
            'square_id'      => $square_id,
            'square_name'    => $square_name,
        ];
    }

    /**
     * @return array<int, array{item_id:string, item_name:string, variation_id:string}>
     */
    private static function sku_lookup(SquareInventory $square, string $sku): array
    {
        $resp = $square->square_api_request(
            '/catalog/search-catalog-objects',
            'POST',
            [
                'object_types'            => ['ITEM_VARIATION'],
                'include_related_objects' => true,
                'query'                   => [
                    'exact_query' => [
                        'attribute_name'  => 'sku',
                        'attribute_value' => $sku,
                    ],
                ],
                'limit'                   => 25,
            ]
        );

        if (empty($resp['success']) || empty($resp['data']['objects'])) {
            return [];
        }

        $item_names = [];
        if (!empty($resp['data']['related_objects'])) {
            foreach ($resp['data']['related_objects'] as $related) {
                if (($related['type'] ?? '') === 'ITEM' && !empty($related['id'])) {
                    $item_names[$related['id']] = $related['item_data']['name'] ?? '(unnamed)';
                }
            }
        }

        $hits = [];
        foreach ($resp['data']['objects'] as $obj) {
            if (($obj['type'] ?? '') !== 'ITEM_VARIATION') {
                continue;
            }
            $item_id      = $obj['item_variation_data']['item_id'] ?? '';
            $variation_id = $obj['id'] ?? '';
            if ('' === $item_id || '' === $variation_id) {
                continue;
            }
            $hits[] = [
                'item_id'      => $item_id,
                'item_name'    => $item_names[$item_id] ?? '(unnamed)',
                'variation_id' => $variation_id,
            ];
        }
        return $hits;
    }

    /**
     * @return array<int, array{id:string, name:string, variation_count:int}>
     */
    private static function title_suggestions(\WC_Product $product, SquareInventory $square, array $linked_map): array
    {
        $title = trim((string) $product->get_name());
        if ('' === $title) {
            return [];
        }
        $resp = $square->square_api_request(
            '/catalog/search-catalog-items',
            'POST',
            [
                'text_filter'   => $title,
                'limit'         => 10,
                'product_types' => ['REGULAR'],
            ]
        );
        if (empty($resp['success']) || empty($resp['data']['items'])) {
            return [];
        }

        $out = [];
        foreach ($resp['data']['items'] as $item) {
            if (($item['type'] ?? '') !== 'ITEM' || empty($item['id'])) {
                continue;
            }
            if (isset($linked_map[$item['id']])) {
                continue;
            }
            $out[] = [
                'id'              => $item['id'],
                'name'            => $item['item_data']['name'] ?? '(unnamed)',
                'variation_count' => isset($item['item_data']['variations']) && is_array($item['item_data']['variations']) ? count($item['item_data']['variations']) : 0,
            ];
            if (count($out) >= 5) {
                break;
            }
        }
        return $out;
    }

    /**
     * Apply the link to a simple Woo product. Variable products are not
     * supported in the free version.
     */
    private static function link_product(\WC_Product $product, string $square_item_id, SquareInventory $square): array
    {
        $resp = $square->square_api_request('/catalog/object/' . rawurlencode($square_item_id), 'GET');
        if (empty($resp['success']) || empty($resp['data']['object'])) {
            return ['ok' => false, 'reason' => 'square_fetch_failed'];
        }
        $square_object = $resp['data']['object'];
        if (($square_object['type'] ?? '') !== 'ITEM') {
            return ['ok' => false, 'reason' => 'not_an_item'];
        }
        $square_variations = $square_object['item_data']['variations'] ?? [];
        if (empty($square_variations)) {
            return ['ok' => false, 'reason' => 'no_variations'];
        }

        $woo_id     = $product->get_id();
        $simple_sku = (string) $product->get_sku();

        $variation_id = '';
        if ('' !== $simple_sku) {
            foreach ($square_variations as $sv) {
                if (($sv['item_variation_data']['sku'] ?? '') === $simple_sku) {
                    $variation_id = $sv['id'] ?? '';
                    break;
                }
            }
        }
        if ('' === $variation_id) {
            $variation_id = $square_variations[0]['id'] ?? '';
        }
        update_post_meta($woo_id, 'square_product_id', $square_item_id);
        if ('' !== $variation_id) {
            update_post_meta($woo_id, 'square_variation_id', $variation_id);
        }

        Logger::log('success', 'Bulk match linked simple product: ' . $product->get_title(), [
            'product_id'          => $woo_id,
            'square_item_id'      => $square_item_id,
            'square_variation_id' => $variation_id,
        ]);

        return ['ok' => true];
    }

    public static function get_progress(): array
    {
        $stored = get_option(self::OPTION, []);
        return array_merge(self::default_progress(), is_array($stored) ? $stored : []);
    }

    public static function get_report(): array
    {
        $stored = get_option(self::REPORT_OPTION, []);
        return array_merge(self::default_report(), is_array($stored) ? $stored : []);
    }

    private static function strip_internals(array $progress): array
    {
        unset($progress['product_ids'], $progress['linked_map']);
        return $progress;
    }

    private static function record_result(array &$report, \WC_Product $product, array $result): void
    {
        $base = [
            'woo_id'    => $product->get_id(),
            'name'      => $product->get_name(),
            'edit_link' => get_edit_post_link($product->get_id(), 'raw'),
            'sku'       => $product->get_sku(),
        ];

        switch ($result['classification']) {
            case 'matched':
                $report['matched'][] = $base + [
                    'square_id'   => $result['square_id'],
                    'square_name' => $result['square_name'] ?? '',
                ];
                break;

            case 'conflict':
                if (($result['reason'] ?? '') === 'square_already_linked') {
                    $conflict_link = '';
                    if (!empty($result['conflict_woo_id'])) {
                        $conflict_link = get_edit_post_link((int) $result['conflict_woo_id'], 'raw') ?: '';
                    }
                    $report['conflicts_already_linked'][] = $base + [
                        'square_id'         => $result['square_id'] ?? '',
                        'square_name'       => $result['square_name'] ?? '',
                        'conflict_woo_id'   => (int) ($result['conflict_woo_id'] ?? 0),
                        'conflict_woo_link' => $conflict_link,
                    ];
                } else {
                    $report['conflicts_multiple_squares'][] = $base + [
                        'candidates' => $result['candidates'] ?? [],
                        'skus'       => $result['skus'] ?? [],
                    ];
                }
                break;

            case 'no_match':
                $report['no_match'][] = $base + [
                    'skus'   => $result['skus'] ?? [],
                    'reason' => $result['reason'] ?? 'no_sku_match',
                ];
                break;

            case 'needs_review':
                $report['needs_review'][] = $base + [
                    'reason'      => $result['reason'] ?? 'no_sku',
                    'suggestions' => $result['suggestions'] ?? [],
                ];
                break;
        }
    }

    private static function shape_candidates(array $candidates): array
    {
        $out = [];
        foreach ($candidates as $id => $c) {
            $out[] = [
                'square_id'    => $id,
                'name'         => $c['name'] ?? '',
                'matched_skus' => $c['matched_skus'] ?? [],
            ];
        }
        return $out;
    }

    private static function find_woo_for_square(string $square_id): int
    {
        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT pm.post_id, p.post_type FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = 'square_product_id' AND pm.meta_value = %s LIMIT 1",
                $square_id
            )
        );
        if (!$row) {
            return 0;
        }
        if ('product_variation' === $row->post_type) {
            $parent = (int) wp_get_post_parent_id((int) $row->post_id);
            return $parent ?: (int) $row->post_id;
        }
        return (int) $row->post_id;
    }

    private static function default_progress(): array
    {
        return [
            'job_id'           => '',
            'status'           => 'idle',
            'product_ids'      => [],
            'linked_map'       => [],
            'processed'        => 0,
            'total'            => 0,
            'skipped_variable' => 0,
            'started_at'       => 0,
            'updated_at'       => 0,
            'finished_at'      => null,
            'last_error'       => null,
        ];
    }

    private static function default_report(): array
    {
        return [
            'matched'                    => [],
            'conflicts_already_linked'   => [],
            'conflicts_multiple_squares' => [],
            'no_match'                   => [],
            'needs_review'               => [],
        ];
    }

    private static function mark_completed(string $job_id): void
    {
        $latest = self::get_progress();
        if ($latest['job_id'] !== $job_id || 'running' !== $latest['status']) {
            return;
        }
        $now                   = time();
        $latest['status']      = 'completed';
        $latest['finished_at'] = $now;
        $latest['updated_at']  = $now;
        $latest['product_ids'] = [];
        $latest['linked_map']  = [];
        update_option(self::OPTION, $latest, false);
    }
}
