<?php

namespace Datalumo\Sync;

use Datalumo\PhpSdk\Exceptions\DatalumoException;
use WP_Query;

class BulkSync
{
    private const BATCH_SIZE = 50;
    private const BATCH_STAGGER_SECONDS = 10;
    private const ACTION_NAME = 'datalumo_process_sync_batch';
    private const DELETE_ACTION = 'datalumo_delete_sync_entries';

    public function __construct()
    {
        add_action(self::ACTION_NAME, [$this, 'processBatch'], 10, 3);
        add_action(self::DELETE_ACTION, [$this, 'processDeleteBatch'], 10, 3);
    }

    public function start(string $syncId): void
    {
        if (! datalumo_is_configured()) {
            return;
        }

        $sync = datalumo_get_content_sync($syncId);

        if (! $sync || empty($sync['collection_id']) || empty($sync['post_types'])) {
            return;
        }

        $query = new WP_Query([
            'post_type'      => $sync['post_types'],
            'post_status'    => 'publish',
            'posts_per_page' => 1,
            'fields'         => 'ids',
            'no_found_rows'  => false,
        ]);

        $total = $query->found_posts;

        $this->updateSyncState($syncId, [
            'total'     => $total,
            'processed' => 0,
            'started'   => current_time('mysql'),
        ]);

        // Stagger batches so a full sync doesn't saturate PHP workers
        // and the upstream API. Action Scheduler would otherwise burn
        // through the queue with up to 5 concurrent runners.
        $batchSize = $this->batchSize();
        $stagger = (int) apply_filters('datalumo_bulk_sync_stagger_seconds', self::BATCH_STAGGER_SECONDS);
        $start = time();
        $index = 0;

        for ($offset = 0; $offset < $total; $offset += $batchSize) {
            \as_schedule_single_action(
                $start + ($index * $stagger),
                self::ACTION_NAME,
                ['sync_id' => $syncId, 'offset' => $offset, 'batch_size' => $batchSize],
                'datalumo'
            );
            $index++;
        }
    }

    public function processBatch(string $syncId, int $offset, int $batchSize): void
    {
        if (! datalumo_is_configured()) {
            return;
        }

        $sync = datalumo_get_content_sync($syncId);

        if (! $sync || empty($sync['collection_id']) || empty($sync['post_types'])) {
            return;
        }

        $posts = get_posts([
            'post_type'   => $sync['post_types'],
            'post_status' => 'publish',
            'numberposts' => $batchSize,
            'offset'      => $offset,
            'orderby'     => 'ID',
            'order'       => 'ASC',
        ]);

        if (empty($posts)) {
            return;
        }

        // Prime the user cache so EntryPreparer's get_the_author_meta()
        // calls don't hit the DB once per distinct author.
        $authorIds = array_values(array_unique(array_filter(array_map(
            fn ($post) => (int) $post->post_author,
            $posts
        ))));

        if (! empty($authorIds)) {
            cache_users($authorIds);
        }

        $metaMappings = $sync['meta_mappings'] ?? [];
        $preparer = new EntryPreparer();
        $entries = [];

        foreach ($posts as $post) {
            $entry = $preparer->prepare($post, $metaMappings);

            if ($entry !== null) {
                $entries[] = $entry;
            }
        }

        if (! empty($entries)) {
            try {
                $sdk = datalumo_sdk();
                $sdk->entries($sync['collection_id'])->batchUpsert($entries);
            } catch (DatalumoException $e) {
                error_log("Datalumo bulk sync error (sync {$syncId}, offset {$offset}): {$e->getMessage()} — rescheduling 5 min");

                // Re-queue this batch for another attempt. Do NOT advance the
                // processed counter — we want totals to reflect reality so the
                // sync eventually converges after transient outages.
                \as_schedule_single_action(
                    time() + 5 * MINUTE_IN_SECONDS,
                    self::ACTION_NAME,
                    ['sync_id' => $syncId, 'offset' => $offset, 'batch_size' => $batchSize],
                    'datalumo'
                );

                return;
            }
        }

        $processed = $this->incrementProcessed($syncId, count($posts));

        if (! $this->hasPendingBatches($syncId)) {
            $this->updateSyncState($syncId, [
                'processed' => $processed,
                'last_sync' => current_time('mysql'),
            ], true);
        }
    }

    public function deleteEntries(string $syncId): void
    {
        if (! datalumo_is_configured()) {
            return;
        }

        $sync = datalumo_get_content_sync($syncId);

        if (! $sync || empty($sync['collection_id']) || empty($sync['post_types'])) {
            return;
        }

        $postIds = get_posts([
            'post_type'   => $sync['post_types'],
            'post_status' => 'publish',
            'fields'      => 'ids',
            'nopaging'    => true,
        ]);

        $batchSize = $this->batchSize();
        $stagger = (int) apply_filters('datalumo_bulk_sync_stagger_seconds', self::BATCH_STAGGER_SECONDS);
        $start = time();
        $index = 0;

        for ($offset = 0; $offset < count($postIds); $offset += $batchSize) {
            $batch = array_slice($postIds, $offset, $batchSize);

            \as_schedule_single_action(
                $start + ($index * $stagger),
                self::DELETE_ACTION,
                ['sync_id' => $syncId, 'collection_id' => $sync['collection_id'], 'post_ids' => $batch],
                'datalumo'
            );
            $index++;
        }
    }

    public function processDeleteBatch(string $syncId, string $collectionId, array $postIds): void
    {
        if (! datalumo_is_configured() || empty($collectionId)) {
            return;
        }

        try {
            $sdk = datalumo_sdk();
            $entries = $sdk->entries($collectionId);

            foreach ($postIds as $postId) {
                $entries->deleteBySource('wordpress', (string) $postId);
            }
        } catch (DatalumoException $e) {
            error_log("Datalumo delete batch error (sync {$syncId}): " . $e->getMessage());
        }
    }

    public function isInProgress(string $syncId): bool
    {
        return $this->hasPendingBatches($syncId);
    }

    /**
     * Cancel all pending batches (upsert and delete) for a sync.
     *
     * Action Scheduler cannot interrupt an in-flight action, so a
     * batch currently being processed by a runner will finish — only
     * pending/future batches are unscheduled.
     *
     * @return int Number of actions cancelled.
     */
    public function cancel(string $syncId): int
    {
        if (! function_exists('as_get_scheduled_actions')) {
            return 0;
        }

        $cancelled = 0;

        foreach ([self::ACTION_NAME, self::DELETE_ACTION] as $hook) {
            $ids = \as_get_scheduled_actions([
                'hook'     => $hook,
                'status'   => \ActionScheduler_Store::STATUS_PENDING,
                'group'    => 'datalumo',
                'search'   => $syncId,
                'per_page' => -1,
            ], 'ids');

            foreach ($ids as $id) {
                try {
                    \ActionScheduler::store()->cancel_action((int) $id);
                    $cancelled++;
                } catch (\Exception $e) {
                    // Ignore — action may have just transitioned state.
                }
            }
        }

        return $cancelled;
    }

    public function getStatus(string $syncId): array
    {
        $optionKey = 'datalumo_sync_state_' . $syncId;
        wp_cache_delete($optionKey, 'options');
        $state = get_option($optionKey, []);

        return [
            'total'       => $state['total'] ?? 0,
            'processed'   => $state['processed'] ?? 0,
            'in_progress' => $this->hasPendingBatches($syncId),
            'last_sync'   => $state['last_sync'] ?? '',
        ];
    }

    private function updateSyncState(string $syncId, array $data, bool $clearStarted = false): void
    {
        $optionKey = 'datalumo_sync_state_' . $syncId;

        // Bust the object cache so we read fresh from the DB.
        wp_cache_delete($optionKey, 'options');

        $current = get_option($optionKey, []);

        if (! is_array($current)) {
            $current = [];
        }

        $current = array_merge($current, $data);

        if ($clearStarted) {
            unset($current['started']);
        }

        update_option($optionKey, $current, false);
    }

    /**
     * Atomically increment the processed counter for a sync, avoiding
     * race conditions when multiple batches complete concurrently.
     */
    private function incrementProcessed(string $syncId, int $count): int
    {
        $optionKey = 'datalumo_sync_state_' . $syncId;

        // Bust cache before and after the update.
        wp_cache_delete($optionKey, 'options');

        $current = get_option($optionKey, []);

        if (! is_array($current)) {
            $current = [];
        }

        $current['processed'] = ($current['processed'] ?? 0) + $count;

        update_option($optionKey, $current, false);

        wp_cache_delete($optionKey, 'options');

        return $current['processed'];
    }

    private function batchSize(): int
    {
        $size = (int) apply_filters('datalumo_bulk_sync_batch_size', self::BATCH_SIZE);

        return $size > 0 ? $size : self::BATCH_SIZE;
    }

    private function hasPendingBatches(string $syncId): bool
    {
        if (! function_exists('as_get_scheduled_actions')) {
            return false;
        }

        // Use 'search' to filter by sync_id in the serialized args,
        // and 'per_page' to stop as soon as one match is found.
        $actions = \as_get_scheduled_actions([
            'hook'     => self::ACTION_NAME,
            'status'   => \ActionScheduler_Store::STATUS_PENDING,
            'group'    => 'datalumo',
            'search'   => $syncId,
            'per_page' => 1,
        ], 'ids');

        return ! empty($actions);
    }
}
