<?php
/**
 * Background sync runner — processes all authors via WP-Cron.
 *
 * @package    ScholarSync
 * @subpackage ScholarSync/includes
 */

class ScholarSync_Background_Sync_Runner {

    /**
     * Entry point called by the scholar_sync_background_sync cron action.
     * Also callable manually (e.g. from WP-CLI or the admin UI in future).
     *
     * @param string $triggered_by 'cron' or 'manual'
     */
    public static function run( string $triggered_by = 'cron' ): void {
        global $wpdb;

        // Bail if the plugin is not fully connected.
        require_once SCHOLAR_SYNC_PLUGIN_DIR . '/includes/class-scholar-sync-remote.php';
        if ( ! get_option( 'scholar_sync_service_consent', false ) ) {
            return;
        }
        if ( ! get_option( ScholarSync_Remote::$CUSTOMER_API_KEY ) ) {
            return;
        }

        // --- Stale-run guard -------------------------------------------
        // If a previous run has been stuck in 'running' for over 1 hour,
        // mark it failed before starting a new one.
        $stale_cutoff = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS );
        $table_sync_runs = SCHOLAR_SYNC_DB_SYNC_RUNS;
        $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Update bypasses cache safely
            $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
                "UPDATE %i
                 SET status = 'failed', stop_reason = 'error', completed_at = %s
                 WHERE status = 'running' AND started_at < %s",
                $table_sync_runs,
                gmdate( 'Y-m-d H:i:s' ),
                $stale_cutoff
            )
        );

        // --- Fetch authors with a scholar_id ---------------------------
        $table_authors = SCHOLAR_SYNC_DB_AUTHORS;
        $authors = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $wpdb->prepare( "SELECT author_id, scholar_id FROM %i WHERE scholar_id IS NOT NULL AND scholar_id != '' ORDER BY author_id ASC", $table_authors ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
        );

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

        // --- Open a new sync run row -----------------------------------
        $now = gmdate( 'Y-m-d H:i:s' );
        $wpdb->insert( SCHOLAR_SYNC_DB_SYNC_RUNS, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Insert bypasses cache safely
            'triggered_by' => $triggered_by,
            'started_at'   => $now,
            'status'       => 'running',
        ) );
        $sync_run_id = (int) $wpdb->insert_id;
        if ( ! $sync_run_id ) {
            return; // DB insert failed — abort silently.
        }

        // --- Counters --------------------------------------------------
        $total      = count( $authors );
        $succeeded  = 0;
        $skipped    = 0;
        $errored    = 0;
        $art_created = 0;
        $art_updated = 0;
        $stop_reason = 'none';
        $frequency_bail = false;

        require_once SCHOLAR_SYNC_PLUGIN_DIR . '/admin/class-scholar-sync-author-sync.php';

        // --- Author loop -----------------------------------------------
        foreach ( $authors as $author ) {
            $author_id  = (int) $author->author_id;
            $scholar_id = $author->scholar_id;
            $synced_at  = gmdate( 'Y-m-d H:i:s' );

            if ( $frequency_bail ) {
                // A previous author already hit the frequency limit.
                // Log the rest as skipped_remaining without calling the service.
                self::write_author_log( $sync_run_id, $author_id, $scholar_id, $synced_at,
                    'skipped_remaining', 0, 0, 'skipped_remaining',
                    __( 'Sync skipped — frequency limit reached on a previous author.', 'scholar-sync' )
                );
                $skipped++;
                continue;
            }

            $sync = new ScholarSync_Author_Sync( $scholar_id );
            $sync->set_author_id( $author_id );

            set_error_handler( function() {} ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- intentional suppression of non-fatal errors during remote sync
            $sync->add();
            restore_error_handler();

            if ( $sync->has_error() ) {
                $raw_message = $sync->get_message();
                $error_code  = self::classify_error( $raw_message );

                if ( $error_code === 'error_exceed_sync_frequency' || $error_code === 'sync_in_progress' ) {
                    // Frequency limit — bail out of the whole loop.
                    require_once SCHOLAR_SYNC_PLUGIN_DIR . '/includes/class-scholar-sync-rest-api.php';
                    $log_message = $sync->is_plan_limit_error()
                        ? ScholarSync_REST_API::plan_limit_message( $sync->get_plan_limit_data(), ScholarSync_REST_API::get_upgrade_url() )
                        : self::translate_error( $raw_message );
                    self::write_author_log( $sync_run_id, $author_id, $scholar_id, $synced_at,
                        'skipped_frequency', 0, 0, $error_code,
                        $log_message
                    );
                    $skipped++;
                    $frequency_bail = true;
                    $stop_reason    = 'frequency_limit';
                } else {
                    self::write_author_log( $sync_run_id, $author_id, $scholar_id, $synced_at,
                        'error', 0, 0, $error_code,
                        self::translate_error( $raw_message )
                    );
                    $errored++;
                }
            } else {
                $results   = $sync->get_articles_results();
                $created   = (int) ( $results['articles_created'] ?? 0 );
                $updated   = (int) ( $results['articles_updated'] ?? 0 );

                self::write_author_log( $sync_run_id, $author_id, $scholar_id, $synced_at,
                    'success', $created, $updated, null, null
                );
                $succeeded++;
                $art_created += $created;
                $art_updated += $updated;
            }
        }

        // --- Close the run row ----------------------------------------
        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Update bypasses cache safely
            SCHOLAR_SYNC_DB_SYNC_RUNS,
            array(
                'completed_at'      => gmdate( 'Y-m-d H:i:s' ),
                'status'            => 'completed',
                'stop_reason'       => $stop_reason,
                'authors_total'     => $total,
                'authors_succeeded' => $succeeded,
                'authors_skipped'   => $skipped,
                'authors_errored'   => $errored,
                'articles_created'  => $art_created,
                'articles_updated'  => $art_updated,
            ),
            array( 'sync_run_id' => $sync_run_id )
        );
    }

    /**
     * Returns the last sync log row for a given author, joining the run row
     * so the UI can show triggered_by and stop_reason.
     *
     * @param int $author_id
     * @return object|null
     */
    public static function get_last_log_for_author( int $author_id ): ?object {
        global $wpdb;
        $table_sync_log = SCHOLAR_SYNC_DB_SYNC_LOG;
        $table_sync_runs = SCHOLAR_SYNC_DB_SYNC_RUNS;
        return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read bypasses cache safely
            $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
                "SELECT l.*, r.triggered_by, r.stop_reason, r.started_at AS run_started_at
                 FROM %i l
                 JOIN %i r ON r.sync_run_id = l.sync_run_id
                 WHERE l.author_id = %d
                 ORDER BY l.synced_at DESC
                 LIMIT 1",
                $table_sync_log,
                $table_sync_runs,
                $author_id
            )
        );
    }

    /**
     * Records a completed manual sync (add or sync button) into the log tables.
     *
     * @param int    $author_id
     * @param string $scholar_id
     * @param int    $articles_created
     * @param int    $articles_updated
     */
    public static function log_manual_sync( int $author_id, string $scholar_id, int $articles_created, int $articles_updated ): void {
        global $wpdb;

        $now = gmdate( 'Y-m-d H:i:s' );

        $wpdb->insert( SCHOLAR_SYNC_DB_SYNC_RUNS, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Insert bypasses cache safely
            'triggered_by'      => 'manual',
            'started_at'        => $now,
            'completed_at'      => $now,
            'status'            => 'completed',
            'stop_reason'       => 'none',
            'authors_total'     => 1,
            'authors_succeeded' => 1,
            'authors_skipped'   => 0,
            'authors_errored'   => 0,
            'articles_created'  => $articles_created,
            'articles_updated'  => $articles_updated,
        ) );
        $sync_run_id = (int) $wpdb->insert_id;
        if ( ! $sync_run_id ) {
            return;
        }

        self::write_author_log( $sync_run_id, $author_id, $scholar_id, $now,
            'success', $articles_created, $articles_updated, null, null );
    }

    /**
     * Returns the last $limit sync log rows for an author, newest first.
     *
     * @param int $author_id
     * @param int $limit
     * @return array
     */
    public static function get_logs_for_author( int $author_id, int $limit = 20 ): array {
        global $wpdb;
        $table_sync_log = SCHOLAR_SYNC_DB_SYNC_LOG;
        $table_sync_runs = SCHOLAR_SYNC_DB_SYNC_RUNS;
        // Show only syncs that changed data (added/updated > 0), plus the single
        // most-recent attempt whatever its outcome — so benign "nothing changed"
        // and frequency-limit rows are hidden, but the current status is always visible.
        return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read bypasses cache safely
            $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- %i is the WordPress identifier placeholder (WP 6.2+)
                "SELECT l.synced_at, l.outcome, l.articles_created, l.articles_updated, l.error_message,
                        r.triggered_by
                 FROM %i l
                 JOIN %i r ON r.sync_run_id = l.sync_run_id
                 WHERE l.author_id = %d
                   AND ( l.articles_created > 0
                         OR l.articles_updated > 0
                         OR l.log_id = ( SELECT MAX( l2.log_id ) FROM %i l2 WHERE l2.author_id = %d ) )
                 ORDER BY l.synced_at DESC
                 LIMIT %d",
                $table_sync_log,
                $table_sync_runs,
                $author_id,
                $table_sync_log,
                $author_id,
                $limit
            )
        ) ?: array();
    }

    // ------------------------------------------------------------------
    // Private helpers
    // ------------------------------------------------------------------

    private static function write_author_log(
        int $sync_run_id, int $author_id, string $scholar_id,
        string $synced_at, string $outcome,
        int $articles_created, int $articles_updated,
        ?string $error_code, ?string $error_message
    ): void {
        global $wpdb;
        $wpdb->insert( SCHOLAR_SYNC_DB_SYNC_LOG, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Insert bypasses cache safely
            'sync_run_id'      => $sync_run_id,
            'author_id'        => $author_id,
            'scholar_id'       => $scholar_id,
            'synced_at'        => $synced_at,
            'outcome'          => $outcome,
            'articles_created' => $articles_created,
            'articles_updated' => $articles_updated,
            'error_code'       => $error_code,
            'error_message'    => $error_message,
        ) );
    }

    /**
     * Derive a machine-readable error code from the raw service message.
     */
    private static function classify_error( string $raw ): string {
        if ( str_starts_with( $raw, 'error_exceed_sync_frequency' ) ) return 'error_exceed_sync_frequency';
        // Newer service format for the same "synced too recently" block: sync_frequency max[N].
        if ( str_starts_with( $raw, 'sync_frequency' ) )              return 'error_exceed_sync_frequency';
        if ( str_starts_with( $raw, 'error_invalid_transaction' ) )   return 'error_invalid_transaction';
        if ( str_starts_with( $raw, 'error_transaction_expired' ) )   return 'error_transaction_expired';
        if ( str_starts_with( $raw, 'error_exceed_authors' ) )        return 'error_exceed_authors';
        if ( preg_match( '/^sync_in_progress/', $raw ) )              return 'sync_in_progress';
        return $raw;
    }

    /**
     * Minimal translation of common service error codes for the log.
     * Full translation lives in ScholarSync_REST_API::translate_service_error().
     */
    private static function translate_error( string $raw ): string {
        require_once SCHOLAR_SYNC_PLUGIN_DIR . '/includes/class-scholar-sync-rest-api.php';
        return ScholarSync_REST_API::translate_service_error( $raw );
    }
}
