<?php
/**
 * Migration coordinator + per-plugin adapters.
 *
 * Adapters expose:
 *   ::source_label()           — human-readable source name
 *   ::is_available()           — competitor plugin tables/options exist
 *   ::count_posts()            — int, how many posts have meta to migrate
 *   ::count_redirects()        — int, how many redirects to migrate
 *   ::map_post_meta($post_id)  — array compatible with Seoforge_Meta::save_post_meta
 *   ::list_redirects()         — array of [source_path, target_url, status, match]
 *
 * The coordinator runs the actual import in batches via the
 * `seoforge_run_migration` cron hook so big sites don't time out.
 *
 * @package Brainwerk SEO Suite
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Seoforge_Migrate {

    const STATE_KEY = 'seoforge_migration_state';
    const BATCH     = 100;

    /** @var array<string,string> source-id => adapter class */
    private static $adapters = array(
        'yoast'    => 'Seoforge_Migrate_Yoast',
        'rankmath' => 'Seoforge_Migrate_RankMath',
        'aioseo'   => 'Seoforge_Migrate_AIOSEO',
    );

    public static function register( Seoforge_Loader $loader ) {
        $loader->add_action( 'seoforge_run_migration', __CLASS__, 'process_batch' );
    }

    public static function adapters_available() {
        $out = array();
        foreach ( self::$adapters as $id => $class ) {
            if ( class_exists( $class ) && call_user_func( array( $class, 'is_available' ) ) ) {
                $out[ $id ] = array(
                    'id'        => $id,
                    'label'     => call_user_func( array( $class, 'source_label' ) ),
                    'posts'     => (int) call_user_func( array( $class, 'count_posts' ) ),
                    'redirects' => (int) call_user_func( array( $class, 'count_redirects' ) ),
                );
            }
        }
        return $out;
    }

    public static function adapter_class( $id ) {
        return isset( self::$adapters[ $id ] ) ? self::$adapters[ $id ] : null;
    }

    public static function get_state() {
        $defaults = array(
            'source'        => '',
            'status'        => 'idle',  // idle|running|done|error
            'total_posts'   => 0,
            'done_posts'    => 0,
            'imported_meta' => 0,
            'imported_red'  => 0,
            'errors'        => 0,
            'started_at'    => '',
            'updated_at'    => '',
            'last_error'    => '',
        );
        $existing = get_site_option( self::STATE_KEY, array() );
        return array_replace( $defaults, is_array( $existing ) ? $existing : array() );
    }

    private static function set_state( array $patch ) {
        $next = array_replace( self::get_state(), $patch );
        $next['updated_at'] = current_time( 'mysql', 1 );
        update_site_option( self::STATE_KEY, $next );
        return $next;
    }

    /**
     * Kick off a new migration. Schedules an immediate cron run that processes
     * the first batch and re-schedules itself until done.
     */
    public static function start( $source_id ) {
        $class = self::adapter_class( $source_id );
        if ( ! $class || ! call_user_func( array( $class, 'is_available' ) ) ) {
            return new WP_Error( 'unknown_source', __( 'Source plugin not detected.', 'brainwerk-seo-suite' ) );
        }
        // total_posts must reflect the loop the worker actually walks.
        $count = (int) self::count_iterable_posts();
        self::set_state( array(
            'source'        => $source_id,
            'status'        => 'running',
            'total_posts'   => $count,
            'done_posts'    => 0,
            'imported_meta' => 0,
            'imported_red'  => 0,
            'errors'        => 0,
            'started_at'    => current_time( 'mysql', 1 ),
            'last_error'    => '',
        ) );
        // First batch right away, then chain via cron.
        wp_schedule_single_event( time() + 2, 'seoforge_run_migration' );
        return true;
    }

    /**
     * Process one batch. Re-schedules itself if work remains.
     */
    public static function process_batch() {
        $state = self::get_state();
        if ( 'running' !== $state['status'] ) {
            return;
        }
        $class = self::adapter_class( $state['source'] );
        if ( ! $class ) {
            self::set_state( array( 'status' => 'error', 'last_error' => 'unknown source' ) );
            return;
        }

        $offset = (int) $state['done_posts'];
        $batch  = self::BATCH;
        $ids    = self::iterable_post_ids( $offset, $batch );

        $imported_meta = (int) $state['imported_meta'];
        $errors        = (int) $state['errors'];

        foreach ( (array) $ids as $post_id ) {
            $post_id = (int) $post_id;
            try {
                $mapped = call_user_func( array( $class, 'map_post_meta' ), $post_id );
                if ( ! empty( $mapped ) ) {
                    $existing = Seoforge_Meta::get_post_meta( $post_id );
                    Seoforge_Meta::save_post_meta( $post_id, array_replace( $existing, $mapped ) );
                    $imported_meta++;
                }
            } catch ( \Throwable $e ) {
                $errors++;
            }
        }

        // On the first batch also import redirects (one-shot, not chunked).
        $imported_red = (int) $state['imported_red'];
        if ( 0 === $offset ) {
            $rows = (array) call_user_func( array( $class, 'list_redirects' ) );
            foreach ( $rows as $r ) {
                if ( empty( $r['source_path'] ) || empty( $r['target_url'] ) ) {
                    continue;
                }
                $id = Seoforge_Redirects::add(
                    $r['source_path'],
                    $r['target_url'],
                    isset( $r['status_code'] ) ? (int) $r['status_code'] : 301,
                    isset( $r['match_type'] ) ? $r['match_type'] : 'exact',
                    isset( $r['note'] ) ? $r['note'] : ''
                );
                if ( $id ) { $imported_red++; }
            }
        }

        $done_posts = $offset + count( (array) $ids );

        // If we got fewer ids than the batch size, we're at the end.
        $finished = count( (array) $ids ) < $batch;

        self::set_state( array(
            'done_posts'    => $done_posts,
            'imported_meta' => $imported_meta,
            'imported_red'  => $imported_red,
            'errors'        => $errors,
            'status'        => $finished ? 'done' : 'running',
        ) );

        if ( ! $finished ) {
            wp_schedule_single_event( time() + 5, 'seoforge_run_migration' );
        }
    }

    /**
     * Helpers — both `start()` (for total_posts) and `process_batch()` walk
     * the same post universe so progress percentages stay honest.
     */
    private static function iterable_post_types() {
        return array_values( (array) get_post_types( array( 'public' => true ) ) );
    }
    private static function iterable_post_ids( $offset, $batch ) {
        global $wpdb;
        $types = self::iterable_post_types();
        if ( empty( $types ) ) {
            return array();
        }
        $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
        // {$placeholders} is a generated list of %s placeholders; every post_type is bound via prepare(). The dynamic IN-list cannot use %i and is the documented prepare() exception.
        $ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} WHERE post_status IN ('publish','draft','private')
             AND post_type IN ({$placeholders}) ORDER BY ID ASC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            array_merge( $types, array( (int) $batch, (int) $offset ) )
        ) );
        return $ids;
    }
    private static function count_iterable_posts() {
        global $wpdb;
        $types = self::iterable_post_types();
        if ( empty( $types ) ) {
            return 0;
        }
        $placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
        // {$placeholders} is a generated list of %s placeholders; every post_type is bound via prepare(). The dynamic IN-list cannot use %i and is the documented prepare() exception.
        $count = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status IN ('publish','draft','private')
             AND post_type IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $types
        ) );
        return $count;
    }
}

require_once SEOFORGE_PATH . 'includes/migrations/class-seoforge-migrate-yoast.php';
require_once SEOFORGE_PATH . 'includes/migrations/class-seoforge-migrate-rankmath.php';
require_once SEOFORGE_PATH . 'includes/migrations/class-seoforge-migrate-aioseo.php';
