<?php
if (!defined('ABSPATH')) exit;

// Ensure request helper functions are available when this file is loaded directly
$newssync_request_helpers_file = __DIR__ . '/request-helpers.php';
if ( file_exists( $newssync_request_helpers_file ) ) {
    require_once $newssync_request_helpers_file;
}

/**
 * AUTO IMPORT – Robust cron scheduling + repair
 *
 * This version ensures custom cron schedules are registered as early as possible
 * and attempts to repair/reschedule any existing invalid events (invalid_schedule).
 *
 * Replace the existing includes/core/auto-import.php with this file.
 * After deploying:
 *  - Visit Tools » Cron Events (WP Crontrol) or check wp_next_scheduled for newssync_import_every_* hooks
 *  - If you had previous "invalid_schedule" warnings, they should stop after this file runs.
 */

/* ---------------------------
 * Helpers required early
 * Try new names first (`newssync-*.php`) then fallback to legacy names.
 * ---------------------------
 */
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local variable, not global
$helpers_dir = __DIR__ . '/helpers/';

// items helper (prefer new name)
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local variable, not global
$items_new = $helpers_dir . 'newssync-items.php';
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local variable, not global
$items_old = $helpers_dir . 'items.php';
if ( file_exists( $items_new ) ) {
    require_once $items_new;
} elseif ( file_exists( $items_old ) ) {
    require_once $items_old;
} else {
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Critical file missing error, debug only
        error_log( 'newssync: missing items helper (' . $items_new . ' or ' . $items_old . ')' );
    }
}

// save-posts helper (prefer new name)
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local variable, not global
$save_new = $helpers_dir . 'newssync-save-posts.php';
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Local variable, not global
$save_old = $helpers_dir . 'save-posts.php';
if ( file_exists( $save_new ) ) {
    require_once $save_new;
} elseif ( file_exists( $save_old ) ) {
    require_once $save_old;
} else {
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Critical file missing error, debug only
        error_log( 'newssync: missing save-posts helper (' . $save_new . ' or ' . $save_old . ')' );
    }
}

/* ---------------------------
 * Register custom cron schedules immediately (important)
 * - We add the filter now so WP always knows about the schedules when rescheduling events.
 * ---------------------------
 */
add_filter( 'cron_schedules', 'newssync_add_custom_cron_schedules' );
function newssync_add_custom_cron_schedules( $schedules ) {
    // Avoid clobbering if already present
    if ( empty( $schedules['newssync_custom_1h'] ) ) {
        $schedules['newssync_custom_1h'] = [
            'interval' => HOUR_IN_SECONDS,
            'display'  => __('Every 1 Hour', 'daazsync'),
        ];
    }
    if ( empty( $schedules['newssync_custom_6h'] ) ) {
        $schedules['newssync_custom_6h'] = [
            'interval' => 6 * HOUR_IN_SECONDS,
            'display'  => __('Every 6 Hours', 'daazsync'),
        ];
    }
    if ( empty( $schedules['newssync_custom_24h'] ) ) {
        $schedules['newssync_custom_24h'] = [
            'interval' => 24 * HOUR_IN_SECONDS,
            'display'  => __('Every 24 Hours', 'daazsync'),
        ];
    }
    return $schedules;
}

/* ---------------------------
 * Repair existing scheduled events if they reference a missing schedule
 * ---------------------------
 */
add_action( 'init', 'newssync_repair_import_schedules', 5 );
function newssync_repair_import_schedules() {
    // Ensure schedules are available
    $schedules = wp_get_schedules();

    // Map hooks -> expected recurrence slug
    $map = [
        'newssync_import_every_1h'  => 'newssync_custom_1h',
        'newssync_import_every_6h'  => 'newssync_custom_6h',
        'newssync_import_every_24h' => 'newssync_custom_24h',
    ];

    $first_run_delay = defined( 'NEWSSYNC_FIRST_IMPORT_DELAY' ) ? (int) NEWSSYNC_FIRST_IMPORT_DELAY : 300;

    foreach ( $map as $hook => $recurrence_slug ) {
        $next = wp_next_scheduled( $hook );
        if ( ! $next ) {
            continue;
        }

        // If the recurrence slug is missing, clear the event so it gets rescheduled cleanly.
        if ( ! isset( $schedules[ $recurrence_slug ] ) ) {
            wp_clear_scheduled_hook( $hook );
            continue;
        }

        // If the event is overdue by more than 60 seconds, reschedule it with the
        // first-run delay. This prevents a previously-scheduled "time()" event from
        // immediately spawning wp-cron.php on the very next page load, which would
        // block a PHP-FPM worker and cause 503 errors on shared hosting.
        if ( $next < ( time() - 60 ) ) {
            wp_unschedule_event( $next, $hook );
            wp_schedule_event( time() + $first_run_delay, $recurrence_slug, $hook );
        }
    }
}

/* ---------------------------
 * Schedule import cron based on cache duration
 * - This runs on init (priority 10) after repair (priority 5)
 * ---------------------------
 */
add_action( 'init', 'newssync_schedule_import_cron', 10 );
function newssync_schedule_import_cron() {
    // Only schedule if saving/importing is enabled
    $newssync_save_as_posts_opt = get_option( 'newssync_save_as_posts', '' );
    $save_enabled = ( (string)$newssync_save_as_posts_opt === '1' || (int)$newssync_save_as_posts_opt === 1 );
    if ( ! $save_enabled ) {
        return;
    }

    // Ensure our custom schedules are present (defensive)
    add_filter( 'cron_schedules', 'newssync_add_custom_cron_schedules' );

    // Derive interval and normalize to 1, 6 or 24 hours
    $interval = (int) newssync_get_cache_duration();
    if ( $interval <= 0 ) $interval = 6 * HOUR_IN_SECONDS; // fallback

    $hours = max(1, (int) floor( $interval / HOUR_IN_SECONDS ));
    if ( $hours >= 24 ) $hours = 24;
    elseif ( $hours >= 6 ) $hours = 6;
    elseif ( $hours >= 1 ) $hours = 1;
    else $hours = 6;

    $cron_hook = 'newssync_import_every_' . $hours . 'h';
    $recurrence = 'newssync_custom_' . $hours . 'h';

    // If recurrence not registered for some reason, fallback to built-in schedules
    $schedules = wp_get_schedules();
    if ( ! isset( $schedules[ $recurrence ] ) ) {
        // Defensive attempt: register custom schedules and re-fetch
        add_filter( 'cron_schedules', 'newssync_add_custom_cron_schedules' );
        $schedules = wp_get_schedules();
    }

    // Final fallback mapping if still missing
    if ( ! isset( $schedules[ $recurrence ] ) ) {
        if ( $hours === 1 ) {
            $recurrence = 'hourly';
        } elseif ( $hours === 24 ) {
            $recurrence = 'daily';
        } else {
            // No built-in 6h; best fallback is 'twicedaily' (12h) or 'hourly' — choose twicedaily if available
            $recurrence = isset( $schedules['twicedaily'] ) ? 'twicedaily' : 'hourly';
        }
    }

    // Schedule if not already scheduled.
    // Use a 5-minute delay for the very first run so that enabling "Save as Posts"
    // does not immediately fire a cron import that competes with the admin page
    // load and occupies all PHP-FPM workers (causing 503 on shared/LiteSpeed hosting).
    // Subsequent runs use the normal recurrence interval; only the first one is delayed.
    if ( ! wp_next_scheduled( $cron_hook ) ) {
        $first_run_delay = defined( 'NEWSSYNC_FIRST_IMPORT_DELAY' ) ? (int) NEWSSYNC_FIRST_IMPORT_DELAY : 300;
        wp_schedule_event( time() + $first_run_delay, $recurrence, $cron_hook );
    }
}

/* ---------------------------
 * Register cron callbacks for all three possible import hooks.
 * Without these registrations the scheduled events fire but do nothing.
 * ---------------------------
 */
add_action( 'newssync_import_every_1h',  'newssync_do_full_import' );
add_action( 'newssync_import_every_6h',  'newssync_do_full_import' );
add_action( 'newssync_import_every_24h', 'newssync_do_full_import' );

/* ---------------------------
 * Import runner
 *
 * @param bool $fetch_fresh When true, fetches feeds via HTTP (for AJAX force-import).
 *                          When false (default, cron), reads only from existing cache —
 *                          no remote HTTP, no worker blocking, no 503 risk.
 *                          Cache is filled naturally by the frontend shortcode; cron
 *                          just converts cached items into posts.
 * ---------------------------
 */
function newssync_do_full_import( $fetch_fresh = false ) {
    $lock_key = 'newssync_import_running_lock';
    if ( get_transient( $lock_key ) ) {
        return 0;
    }

    // Protect low-resource servers: skip import when system load is very high.
    // sys_getloadavg() is available on Linux/Unix; on Windows it returns false — safe to skip.
    if ( function_exists( 'sys_getloadavg' ) ) {
        $load = sys_getloadavg();
        $max_load = defined( 'NEWSSYNC_MAX_SERVER_LOAD' ) ? (float) NEWSSYNC_MAX_SERVER_LOAD : 10.0;
        if ( is_array( $load ) && isset( $load[0] ) && (float) $load[0] > $max_load ) {
            return 0;
        }
    }

    // Use a longer lock (10 min) so overlapping cron invocations cannot pile up.
    set_transient( $lock_key, true, 10 * MINUTE_IN_SECONDS );

    $newssync_save_as_posts_opt = get_option( 'newssync_save_as_posts', '' );
    $newssync_save_as_posts = ( (string)$newssync_save_as_posts_opt === '1' || (string)$newssync_save_as_posts_opt === 'yes' || (int)$newssync_save_as_posts_opt === 1 );

    if ( ! $newssync_save_as_posts ) {
        delete_transient( $lock_key );
        return 0;
    }

    if ( $fetch_fresh ) {
        // Full remote fetch — only for AJAX force-import (user-initiated, can wait).
        // Request more resources for this potentially longer operation.
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 120 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged -- soft request, may fail on restricted hosts
        }
        @ini_set( 'memory_limit', '256M' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged,Squiz.PHP.DiscouragedFunctions.Discouraged -- soft request, may fail on restricted hosts
        $fresh_items = function_exists( 'newssync_get_feed_items_raw' ) ? newssync_get_feed_items_raw( [] ) : [];
    } else {
        // CRON MODE: read only from existing transient cache — zero HTTP requests,
        // returns in milliseconds, never blocks PHP-FPM workers.
        // If a feed's cache is empty (e.g. first install), that feed is skipped;
        // cache fills naturally when visitors trigger the frontend shortcode.
        $fresh_items = newssync_get_items_from_transient_cache();
    }

    if ( ! is_array( $fresh_items ) || empty( $fresh_items ) ) {
        delete_transient( $lock_key );
        return 0;
    }

    $per_feed = [];
    $total_imported = 0;

    // Respect configured max items per import; default to 15 for this distribution.
    $max_items = (int) get_option( 'newssync_max_import_items', 15 );
    $max_items = max( 0, $max_items );

    /**
     * Filter: Maximum items per import run
     * Free version caps at 15, extensions can increase/remove limit
     *
     * @param int $max_items Current limit
     */
    $max_items = apply_filters( 'newssync/max_import_items', $max_items );

    foreach ( $fresh_items as $item ) {
        if ( $max_items > 0 && $total_imported >= $max_items ) break;

        $link = $item['link'] ?? '';
        if ( empty( $link ) ) continue;

        $guid = sanitize_text_field( (string) ( $item['guid'] ?? $item['id'] ?? $link ) );
        $found_post_id = null;
        if ( function_exists( 'newssync_find_post_id_by_guid' ) && function_exists( 'newssync_table_exists' ) && newssync_table_exists() ) {
            $found_post_id = newssync_find_post_id_by_guid( $guid );
        }
        if ( $found_post_id ) {
            continue;
        }

        if ( function_exists( 'newssync_find_post_id_by_original_url' ) ) {
            $existing_post_id = newssync_find_post_id_by_original_url( $link );
            if ( $existing_post_id ) {
                continue;
            }
        }

        if ( function_exists( 'newssync_check_duplicate_similarity' ) && newssync_check_duplicate_similarity( $item ) ) {
            continue;
        }

        $fid = $item['feed_id'] ?? '';
        $per_feed[$fid] = ($per_feed[$fid] ?? 0) + 1;

        if ( $per_feed[$fid] > newssync_get_max_per_feed( ['category' => $item['category'] ?? '', 'limit' => 3] ) ) {
            continue;
        }

        if ( function_exists( 'newssync_create_post_from_item' ) ) {
            $post_id = newssync_create_post_from_item( $item );
        } else {
            $post_id = false;
        }

        if ( $post_id ) {
            $total_imported++;
        }
    }

    delete_transient( $lock_key );
    return $total_imported;
}

/**
 * Read all feed items from existing transient cache — no HTTP requests.
 * Returns combined items from all feeds that have a cached entry, sorted by date desc.
 * Feeds with no cache are silently skipped (cache fills on next frontend shortcode render).
 */
function newssync_get_items_from_transient_cache() {
    $feeds = (array) get_option( 'newssync_feeds', [] );
    if ( empty( $feeds ) ) {
        return [];
    }
    $all_items = [];
    foreach ( $feeds as $feed ) {
        $url = trim( $feed['url'] ?? '' );
        if ( empty( $url ) ) {
            continue;
        }
        $transient_key = 'newssync_feed_raw_' . md5( $url );
        $transient_key = apply_filters( 'newssync/cache_key', $transient_key, $url );
        $cached = get_transient( $transient_key );
        if ( is_array( $cached ) ) {
            foreach ( $cached as $item ) {
                $all_items[] = $item;
            }
        }
    }
    // Sort newest first
    usort( $all_items, function ( $a, $b ) {
        $ta = (int) ( $a['date_timestamp'] ?? $a['timestamp'] ?? 0 );
        $tb = (int) ( $b['date_timestamp'] ?? $b['timestamp'] ?? 0 );
        return $tb <=> $ta;
    } );
    return $all_items;
}

/* ---------------------------
 * AJAX endpoint for manual force import
 * ---------------------------
 */
add_action( 'wp_ajax_newssync_force_import', 'newssync_ajax_force_import' );
function newssync_ajax_force_import() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( [ 'message' => esc_html__( 'Forbidden', 'daazsync' ) ], 403 );
    }

    // Nonce check: will terminate with -1 on failure; keep as-is for security.
    check_ajax_referer( 'newssync_force_import' );

    // Force-import fetches fresh data from all feeds (user-initiated, so this is acceptable).
    try {
        $imported = newssync_do_full_import( true );
    } catch ( Throwable $e ) {
        /* translators: %s: exception message (for debugging purposes). */
        wp_send_json_error( [ 'message' => sprintf( esc_html__( 'Import failed with exception: %s', 'daazsync' ), $e->getMessage() ) ], 500 );
    }

    // Provide clearer failure reasons for troubleshooting
    if ( $imported === null ) {
        wp_send_json_error( [ 'message' => esc_html__( 'Import returned no result (null).', 'daazsync' ) ], 500 );
    }
    if ( $imported === false ) {
        wp_send_json_error( [ 'message' => esc_html__( 'Import reported failure (false).', 'daazsync' ) ], 500 );
    }
    if ( ! is_int( $imported ) && ! is_numeric( $imported ) ) {
        wp_send_json_error( [ 'message' => esc_html__( 'Import returned unexpected type.', 'daazsync' ) ], 500 );
    }

    /* translators: %d: number of posts successfully imported by the force import action. */
    $format = esc_html__( 'Import complete. Posts imported: %d', 'daazsync' );
    $message = sprintf( $format, (int) $imported );

    wp_send_json_success( [ 'message' => $message ] );
}
