<?php
/**
 * Chunked background installer for network activation.
 *
 * @package NimblyMail\SMTP
 *
 * @since 1.0.6
 */

namespace NimblyMail\SMTP\Network;

use NimblyMail\SMTP\Log\Installer;
use NimblyMail\SMTP\Queue\Installer as QueueInstaller;

defined( 'ABSPATH' ) || exit;

/**
 * Class NetworkActivationInstaller
 *
 * Provisions the per-site logs/queue tables for a network activation without
 * running the full switch_to_blog()/dbDelta() loop inside a single HTTP
 * request. Networks at or under FAST_PATH_THRESHOLD sites are provisioned
 * synchronously inline by Plugin::activate() — bounded and cheap enough to
 * stay inside a single request. Larger networks are provisioned in
 * CHUNK_SIZE-site slices via a self-rescheduling Action Scheduler action, with
 * only a cursor persisted between chunks. Mirrors StatsAggregator's
 * fast-path/chunked-background architecture; unlike StatsAggregator this job
 * has no result payload to merge at finalization — install_for_blog() is a
 * side effect, not data to accumulate — so its progress transient only ever
 * needs to hold the cursor.
 *
 * @since 1.0.6
 */
final class NetworkActivationInstaller {

    /**
     * Action Scheduler hook that processes one chunk of sites.
     *
     * @since 1.0.6
     *
     * @var string
     */
    public const HOOK = 'nimblymail_network_activation_chunk';

    /**
     * Action Scheduler group. Shared with StatsAggregator::GROUP — both jobs
     * are NimblyMail background work and there is no reason to split them
     * into separate groups.
     *
     * @since 1.0.6
     *
     * @var string
     */
    public const GROUP = 'nimblymail-smtp';

    /**
     * Number of sites installed per background chunk run. Matches
     * StatsAggregator::CHUNK_SIZE; no reason found to differ, since both jobs
     * do one switch_to_blog()-scoped unit of work per site.
     *
     * @since 1.0.6
     *
     * @var int
     */
    public const CHUNK_SIZE = 30;

    /**
     * Networks at or under this site count are provisioned synchronously
     * inline by Plugin::activate() instead of via the background job.
     * Matches StatsAggregator::FAST_PATH_THRESHOLD.
     *
     * @since 1.0.6
     *
     * @var int
     */
    public const FAST_PATH_THRESHOLD = 50;

    /**
     * Site transient key holding in-progress chunk state.
     *
     * @since 1.0.6
     *
     * @var string
     */
    private const PROGRESS_KEY = 'nimblymail_network_activation_progress';

    /**
     * Progress transient TTL. Generous so a large network's run — many short
     * Action Scheduler ticks — has room to finish before its partial state
     * expires.
     *
     * @since 1.0.6
     *
     * @var int
     */
    private const PROGRESS_TTL = HOUR_IN_SECONDS;

    /**
     * Register the Action Scheduler hook.
     *
     * @since 1.0.6
     *
     * @return void
     */
    public function init(): void {
        add_action( self::HOOK, array( $this, 'process_chunk' ) );
    }

    /**
     * Cancel any scheduled chunk action.
     *
     * Call this from the deactivation hook, alongside StatsAggregator::unschedule()
     * and friends, so no orphan chunk action remains queued after the plugin
     * is disabled.
     *
     * @since 1.0.6
     *
     * @return void
     */
    public static function unschedule(): void {
        if ( function_exists( 'as_unschedule_all_actions' ) ) {
            as_unschedule_all_actions( self::HOOK, array(), self::GROUP );
        }
    }

    /**
     * Whether a chunked installation run is currently scheduled or in flight.
     *
     * @since 1.0.6
     *
     * @return bool
     */
    public static function is_running(): bool {
        if ( ! function_exists( 'as_next_scheduled_action' ) ) {
            return false;
        }

        return false !== as_next_scheduled_action( self::HOOK, array(), self::GROUP );
    }

    /**
     * Kick off a background installation run if one is not already in flight.
     *
     * Idempotent by design, mirroring StatsAggregator::start_or_continue():
     * the enqueue call passes $unique = true so Action Scheduler's own insert
     * is atomic, closing the race window between this method's is_running()
     * check and the enqueue. This is deliberately NOT applied to the
     * chunk-to-chunk reschedule in process_chunk() — that call fires while
     * the current chunk action is still RUNNING, and Action Scheduler's
     * uniqueness check treats RUNNING as blocking too, so a unique insert
     * there would be silently rejected and stall the chain after a single
     * chunk.
     *
     * @since 1.0.6
     *
     * @return bool True when a run is active (already running, or just
     *              started); false when Action Scheduler is unavailable and
     *              no background installation can happen at all.
     */
    public static function start_or_continue(): bool {
        if ( ! function_exists( 'as_enqueue_async_action' ) ) {
            return false;
        }

        if ( self::is_running() ) {
            return true;
        }

        set_site_transient( self::PROGRESS_KEY, self::empty_progress(), self::PROGRESS_TTL );

        as_enqueue_async_action( self::HOOK, array(), self::GROUP, true );

        return true;
    }

    /**
     * Action Scheduler callback. Installs the logs/queue tables for the next
     * CHUNK_SIZE sites, persists the cursor, and either reschedules itself
     * for the next chunk or — once every site has been visited — deletes the
     * progress transient and stops.
     *
     * @since 1.0.6
     *
     * @return void
     */
    public function process_chunk(): void {
        $progress = get_site_transient( self::PROGRESS_KEY );
        if ( ! is_array( $progress ) ) {
            $progress = self::empty_progress();
        }

        $cursor = (int) $progress['cursor'];
        $sites  = get_sites(
            array(
                'number' => self::CHUNK_SIZE,
                'offset' => $cursor,
            )
        );

        if ( empty( $sites ) ) {
            delete_site_transient( self::PROGRESS_KEY );
            return;
        }

        foreach ( $sites as $site ) {
            $blog_id = (int) $site->blog_id;

            Installer::install_for_blog( $blog_id );
            QueueInstaller::install_for_blog( $blog_id );
        }

        set_site_transient(
            self::PROGRESS_KEY,
            array( 'cursor' => $cursor + count( $sites ) ),
            self::PROGRESS_TTL
        );

        if ( function_exists( 'as_enqueue_async_action' ) ) {
            as_enqueue_async_action( self::HOOK, array(), self::GROUP );
        }
    }

    /**
     * Starting accumulator for a chunked run: cursor at the first site.
     *
     * @since 1.0.6
     *
     * @return array{cursor: int}
     */
    private static function empty_progress(): array {
        return array( 'cursor' => 0 );
    }
}
