<?php

declare(strict_types=1);

namespace BadamSoft\WooProductExporter\Api;

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

use BadamSoft\WooProductExporter\Access\AccessManager;
use BadamSoft\WooProductExporter\Core\Plugin;
use BadamSoft\WooProductExporter\Exporter\Exporter;
use BadamSoft\WooProductExporter\Scheduler\ScheduledExportManager;
use BadamSoft\WooProductExporter\Settings\SettingsRepository;
use BadamSoft\WooProductExporter\Templates\TemplateManager;
use DateTimeImmutable;
use Throwable;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

use function sanitize_text_field;
use function wp_unslash;

class RestApi {
    private const RATE_LIMIT_REQUESTS = 60;
    private const RATE_LIMIT_WINDOW   = 60;

    private TemplateManager $template_manager;
    private Exporter $exporter;
    private AccessManager $access_manager;
    private ScheduledExportManager $scheduled_manager;
    private SettingsRepository $settings_repository;

    /**
     * RestApi constructor.
     *
     * @param TemplateManager        $template_manager Templates manager.
     * @param Exporter               $exporter         Exporter service.
     * @param AccessManager          $access_manager   Access control manager.
     * @param ScheduledExportManager $scheduled_manager Scheduler for exports.
     * @param SettingsRepository     $settings_repository Settings storage.
     */
    public function __construct( TemplateManager $template_manager, Exporter $exporter, AccessManager $access_manager, ScheduledExportManager $scheduled_manager, SettingsRepository $settings_repository ) {
        $this->template_manager   = $template_manager;
        $this->exporter           = $exporter;
        $this->access_manager     = $access_manager;
        $this->scheduled_manager  = $scheduled_manager;
        $this->settings_repository = $settings_repository;

        add_action( 'rest_api_init', [ $this, 'register_routes' ] );
        add_filter( 'rest_pre_dispatch', [ $this, 'check_rate_limit' ], 10, 3 );
    }

    /**
     * Convert a run created_at value (stored in WP timezone) into a UTC timestamp.
     */
    private function get_run_created_timestamp( string $created_at ): ?int {
        $created_at = trim( $created_at );

        if ( '' === $created_at ) {
            return null;
        }

        try {
            $date = new DateTimeImmutable( $created_at, wp_timezone() );
        } catch ( Throwable $e ) {
            return null;
        }

        return (int) $date->format( 'U' );
    }

    /**
     * Get the current UTC timestamp using WordPress time helpers.
     */
    private function current_utc_timestamp(): int {
        return (int) current_time( 'timestamp', true );
    }

    /**
     * Return a single export run by ID.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_history_run( WP_REST_Request $request ) {
        $id = (int) $request->get_param( 'id' );

        if ( $id <= 0 ) {
            return new WP_Error( 'prodexfo_history_invalid_id', __( 'Invalid run ID.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 400 ] );
        }

        $run = $this->scheduled_manager->get_run( $id );

        if ( $run && isset( $run['status'] ) && is_string( $run['status'] ) ) {
            $status = sanitize_key( $run['status'] );

            if ( in_array( $status, [ 'pending', 'running' ], true ) ) {
                // Try to kick background processing while the UI is polling.
                if ( class_exists( '\\ActionScheduler_QueueRunner' ) ) {
                    try {
                        \ActionScheduler_QueueRunner::instance()->maybe_dispatch_async_request();
                    } catch ( \Throwable $e ) {
                        // Intentionally ignored.
                    }
                } elseif ( function_exists( 'spawn_cron' ) ) {
                    spawn_cron();
                }
            }

            if ( 'pending' === $status ) {
                $this->maybe_reschedule_pending_manual_export( $run, $id );
            }
        }

        if ( ! $run ) {
            return new WP_Error( 'prodexfo_history_not_found', __( 'Export run not found.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 404 ] );
        }

        return rest_ensure_response( [ 'run' => $run ] );
    }

    /**
     * Execute pending manual exports synchronously as a last-resort fallback.
     *
     * This is used when WP-Cron / Action Scheduler runners are blocked and scheduled actions never start.
     *
     * @param array<string, mixed> $run
     * @param int                  $run_id
     */
    private function maybe_work_pending_manual_export( array $run, int $run_id ): void {
        if ( isset( $run['run_type'] ) && 'manual' !== (string) $run['run_type'] ) {
            return;
        }

        if ( isset( $run['status'] ) && 'pending' !== (string) $run['status'] ) {
            return;
        }

        $created_at = isset( $run['created_at'] ) ? (string) $run['created_at'] : '';

        if ( '' === $created_at ) {
            return;
        }

        $created_ts = $this->get_run_created_timestamp( $created_at );

        if ( ! $created_ts || ( $this->current_utc_timestamp() - $created_ts ) < 20 ) {
            return;
        }

        $throttle_key = 'prodexfo_manual_export_worker_throttle_' . $run_id;

        if ( false !== get_transient( $throttle_key ) ) {
            return;
        }

        $lock_key = 'prodexfo_manual_export_worker_lock_' . $run_id;

        if ( false !== get_transient( $lock_key ) ) {
            return;
        }

        set_transient( $lock_key, time(), 600 );
        set_transient( $throttle_key, time(), 60 );

        try {
            do_action( 'prodexfo_manual_export_async_run', $run_id );
        } catch ( Throwable $e ) {
            // Intentionally ignored.
        } finally {
            delete_transient( $lock_key );
        }
    }

    /**
     * Requeue pending manual exports as cron-based single actions when async actions stall.
     *
     * @param array<string, mixed> $run
     * @param int                  $run_id
     */
    private function maybe_reschedule_pending_manual_export( array $run, int $run_id ): void {
        if ( isset( $run['run_type'] ) && 'manual' !== (string) $run['run_type'] ) {
            return;
        }

        $created_at = isset( $run['created_at'] ) ? (string) $run['created_at'] : '';

        if ( '' === $created_at ) {
            return;
        }

        $created_ts = $this->get_run_created_timestamp( $created_at );

        if ( ! $created_ts || ( $this->current_utc_timestamp() - $created_ts ) < 20 ) {
            return;
        }

        $kick_key = 'prodexfo_manual_export_kick_' . $run_id;

        if ( false !== get_transient( $kick_key ) ) {
            return;
        }

        set_transient( $kick_key, time(), 120 );

        if ( function_exists( 'as_unschedule_all_actions' ) ) {
            try {
                as_unschedule_all_actions( 'prodexfo_manual_export_async_run', [ $run_id ], 'prodexfo' );
            } catch ( \Throwable $e ) {
                // Intentionally ignored.
            }
        }

        $scheduled = false;

        if ( function_exists( 'as_schedule_single_action' ) ) {
            $action_id = as_schedule_single_action( time() + 1, 'prodexfo_manual_export_async_run', [ $run_id ], 'prodexfo' );
            $scheduled = is_numeric( $action_id ) && (int) $action_id > 0;
        } elseif ( function_exists( 'as_enqueue_async_action' ) ) {
            $action_id = as_enqueue_async_action( 'prodexfo_manual_export_async_run', [ $run_id ], 'prodexfo' );
            $scheduled = is_numeric( $action_id ) && (int) $action_id > 0;
        } elseif ( function_exists( 'wp_schedule_single_event' ) ) {
            $scheduled = wp_schedule_single_event( time() + 1, 'prodexfo_manual_export_async_run', [ $run_id ] );
        }

        if ( ! $scheduled ) {
            delete_transient( $kick_key );
        }
    }

    /**
     * Check rate limit for REST API requests.
     *
     * @param mixed           $result  Response to replace the requested version with.
     * @param WP_REST_Server  $server  Server instance.
     * @param WP_REST_Request $request Request used to generate the response.
     * @return mixed|WP_Error
     */
    public function check_rate_limit( $result, $server, $request ) {
        $route = $request->get_route();

        if ( 0 !== strpos( $route, '/wc-pce/v1' ) ) {
            return $result;
        }

        $user_id     = get_current_user_id();
        $remote_addr = $this->get_remote_addr();
        $key         = 'prodexfo_rate_limit_' . ( $user_id > 0 ? $user_id : md5( $remote_addr ) );

        $data = get_transient( $key );

        if ( false === $data ) {
            $data = [ 'count' => 1, 'start' => time() ];
            set_transient( $key, $data, self::RATE_LIMIT_WINDOW );

            return $result;
        }

        if ( time() - $data['start'] > self::RATE_LIMIT_WINDOW ) {
            $data = [ 'count' => 1, 'start' => time() ];
            set_transient( $key, $data, self::RATE_LIMIT_WINDOW );

            return $result;
        }

        $data['count']++;
        set_transient( $key, $data, self::RATE_LIMIT_WINDOW - ( time() - $data['start'] ) );

        if ( $data['count'] > self::RATE_LIMIT_REQUESTS ) {
            $this->log_suspicious_activity( 'rate_limit_exceeded', $user_id, $route );

            return new WP_Error(
                'rate_limit_exceeded',
                __( 'Too many requests. Please try again later.', 'badamsoft-product-exporter-for-woocommerce' ),
                [ 'status' => 429 ]
            );
        }

        return $result;
    }

    /**
     * Log suspicious activity for security monitoring.
     *
     * @param string $type    Type of suspicious activity.
     * @param int    $user_id User ID (0 for anonymous).
     * @param string $context Additional context.
     */
    private function log_suspicious_activity( string $type, int $user_id, string $context = '' ): void {
        $remote_addr = $this->get_remote_addr();
        $message = sprintf(
            '[WPE Security] %s | User: %d | IP: %s | Context: %s',
            $type,
            $user_id,
            $remote_addr,
            $context
        );

        do_action( 'prodexfo_suspicious_activity', $type, $user_id, $context );
    }

    private function get_remote_addr(): string {
        return isset( $_SERVER['REMOTE_ADDR'] )
            ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) )
            : 'unknown';
    }

    /**
     * Register REST API routes for templates, history and scheduler.
     */
    public function register_routes(): void {
        register_rest_route(
            'wc-pce/v1',
            '/templates',
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_templates' ],
                'permission_callback' => [ $this, 'can_manage_exports' ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/history/(?P<id>\d+)',
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_history_run' ],
                'permission_callback' => [ $this, 'can_view_history' ],
                'args'                => [
                    'id' => [
                        'type'     => 'integer',
                        'required' => true,
                        'minimum'  => 1,
                    ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/export/(?P<id>[a-zA-Z0-9_-]+)',
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'export_template' ],
                'permission_callback' => [ $this, 'can_export' ],
                'args'                => [
                    'id' => [
                        'description' => __( 'Template identifier.', 'badamsoft-product-exporter-for-woocommerce' ),
                        'type'        => 'string',
                        'required'    => true,
                    ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/history',
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_history' ],
                'permission_callback' => [ $this, 'can_view_history' ],
                'args'                => [
                    'status'    => [ 'type' => 'string', 'required' => false ],
                    'template_id' => [ 'type' => 'string', 'required' => false ],
                    'run_type'  => [ 'type' => 'string', 'required' => false ],
                    'date_from' => [ 'type' => 'string', 'required' => false ],
                    'date_to'   => [ 'type' => 'string', 'required' => false ],
                    'template_name' => [ 'type' => 'string', 'required' => false ],
                    'task_name' => [ 'type' => 'string', 'required' => false ],
                    'page'      => [ 'type' => 'integer', 'required' => false, 'minimum' => 1 ],
                    'per_page'  => [ 'type' => 'integer', 'required' => false, 'minimum' => 1 ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/history/(?P<id>\d+)',
            [
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => [ $this, 'delete_history_run' ],
                'permission_callback' => [ $this, 'can_manage_history' ],
                'args'                => [
                    'id' => [
                        'type'     => 'integer',
                        'required' => true,
                        'minimum'  => 1,
                    ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/diagnostics',
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_diagnostics' ],
                'permission_callback' => [ $this, 'can_manage_exports' ],
                'args'                => [
                    'run_id' => [
                        'type'     => 'integer',
                        'required' => false,
                        'minimum'  => 1,
                    ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/history/bulk-delete',
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'delete_history_runs_bulk' ],
                'permission_callback' => [ $this, 'can_manage_history' ],
                'args'                => [
                    'ids' => [
                        'type'     => 'array',
                        'required' => true,
                        'items'    => [ 'type' => 'integer' ],
                    ],
                ],
            ]
        );

        register_rest_route(
            'wc-pce/v1',
            '/history/(?P<id>\d+)/retry',
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'retry_history_run' ],
                'permission_callback' => [ $this, 'can_manage_history' ],
                'args'                => [
                    'id' => [
                        'type'     => 'integer',
                        'required' => true,
                        'minimum'  => 1,
                    ],
                ],
            ]
        );

        do_action( 'prodexfo_register_rest_routes', $this );
    }

    /**
     * Return server-side diagnostics to help troubleshoot polling / background processing.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_diagnostics( WP_REST_Request $request ): WP_REST_Response {
        $run_id = (int) $request->get_param( 'run_id' );

        $data = [
            'time' => [
                'utc' => gmdate( 'c' ),
                'wp'  => current_time( 'mysql' ),
            ],
            'env'  => [
                'wp_version'  => get_bloginfo( 'version' ),
                'php_version' => PHP_VERSION,
                'site_url'    => get_site_url(),
                'home_url'    => home_url(),
            ],
            'cron' => [
                'disable_wp_cron'    => defined( 'DISABLE_WP_CRON' ) ? (bool) DISABLE_WP_CRON : false,
                'alternate_wp_cron'  => defined( 'ALTERNATE_WP_CRON' ) ? (bool) ALTERNATE_WP_CRON : false,
                'wp_doing_cron'      => function_exists( 'wp_doing_cron' ) ? (bool) wp_doing_cron() : false,
            ],
            'scheduler' => [
                'action_scheduler_available' => function_exists( 'as_schedule_single_action' ) || function_exists( 'as_enqueue_async_action' ),
                'queue_runner_available'     => class_exists( '\\ActionScheduler_QueueRunner' ),
            ],
        ];

        if ( $run_id > 0 ) {
            $run = $this->scheduled_manager->get_run( $run_id );
            $data['run'] = $run ? $run : null;

            $scheduled_actions = null;
            if ( function_exists( 'as_get_scheduled_actions' ) ) {
                try {
                    $scheduled_actions = as_get_scheduled_actions(
                        [
                            'hook'   => Plugin::MANUAL_EXPORT_ASYNC_HOOK,
                            'args'   => [ $run_id ],
                            'status' => [ 'pending', 'in-progress' ],
                            'group'  => 'prodexfo',
                            'per_page' => 5,
                        ],
                        'ids'
                    );
                } catch ( Throwable $e ) {
                    $scheduled_actions = null;
                }
            }

            $data['scheduler']['manual_export_actions'] = is_array( $scheduled_actions ) ? array_values( $scheduled_actions ) : null;
        }

        return rest_ensure_response( [ 'diagnostics' => $data ] );
    }

    /**
     * Return the list of available export templates.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response
     */
    public function get_templates( WP_REST_Request $request ): WP_REST_Response {
        $templates = array_map( [ $this, 'shapeTemplateForResponse' ], $this->template_manager->get_templates() );

        return rest_ensure_response(
            [
                'templates' => array_values( $templates ),
            ]
        );
    }

    /**
     * Run an export based on a saved template and return metadata.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function export_template( WP_REST_Request $request ) {
        $template_id = $request->get_param( 'id' );
        $template    = is_string( $template_id ) ? $this->template_manager->get_template( $template_id ) : null;

        if ( ! $template ) {
            return new WP_Error( 'prodexfo_template_not_found', __( 'Template not found.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 404 ] );
        }

        $fields = isset( $template['fields'] ) && is_array( $template['fields'] ) && ! empty( $template['fields'] )
            ? array_values( array_filter( array_map( [ $this->exporter, 'sanitize_field_key' ], $template['fields'] ) ) )
            : $this->exporter->get_default_fields();

        if ( empty( $fields ) ) {
            $fields = $this->exporter->get_default_fields();
        }

        $filters  = isset( $template['filters'] ) && is_array( $template['filters'] ) ? $template['filters'] : [];
        $format   = isset( $template['format'] ) ? (string) $template['format'] : '';
        $settings = isset( $template['settings'] ) && is_array( $template['settings'] ) ? $template['settings'] : [];

        if ( '' === $format ) {
            $format = $this->exporter->get_default_format_key();
        }

        $upload_dir = wp_upload_dir();

        if ( ! empty( $upload_dir['error'] ) || empty( $upload_dir['basedir'] ) ) {
            return new WP_Error( 'prodexfo_upload_unavailable', __( 'Uploads directory is not writable.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 500 ] );
        }

        $root_dir = $this->settings_repository->resolve_export_root_directory( (string) ( $upload_dir['basedir'] ?? '' ) );
        $base_dir = trailingslashit( $root_dir . 'api-exports' );
        wp_mkdir_p( $base_dir );

        $resolved_name = $this->exporter->resolve_filename_for_format( $settings, $format );
        $filename      = wp_unique_filename( $base_dir, $resolved_name );
        $file_path     = trailingslashit( $base_dir ) . $filename;

        try {
            $result = $this->exporter->export_to_file( $fields, $filters, $format, $settings, $file_path );
        } catch ( Throwable $exception ) {
            return new WP_Error( 'prodexfo_export_failed', $exception->getMessage(), [ 'status' => 500 ] );
        }

        $run_meta = $this->exporter->record_run( $fields, $filters, $format, $result, $template_id, 'api', 0, [] );

        $response = [
            'run_id'        => $run_meta['run_id'] ?? 0,
            'template_id'   => $template_id,
            'rows_exported' => $run_meta['rows'] ?? ( $run_meta['rows_exported'] ?? 0 ),
            'format'        => $run_meta['format'] ?? $format,
            'file_url'      => $run_meta['file_path'] ?? '',
            'images_zip'    => $run_meta['images_zip'] ?? null,
        ];

        return rest_ensure_response( $response );
    }

    /**
     * Check whether the current user can manage exports via API.
     */
    public function can_manage_exports(): bool {
        return current_user_can( 'manage_options' );
    }

    /**
     * Check whether the current user can trigger exports via API.
     */
    public function can_export(): bool {
        return current_user_can( 'manage_options' );
    }

    /**
     * Check whether the current user can view export history.
     */
    public function can_view_history(): bool {
        return current_user_can( 'manage_options' );
    }

    /**
     * Check whether the current user can manage export history.
     */
    public function can_manage_history(): bool {
        return current_user_can( 'manage_options' );
    }

    /**
     * Return paginated export history with optional filters.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response
     */
    public function get_history( WP_REST_Request $request ): WP_REST_Response {
        $this->maybe_work_one_pending_manual_export();

        $status      = sanitize_key( (string) $request->get_param( 'status' ) );
        $template_id = sanitize_text_field( (string) $request->get_param( 'template_id' ) );
        $run_type    = sanitize_key( (string) $request->get_param( 'run_type' ) );
        $date_from   = sanitize_text_field( (string) $request->get_param( 'date_from' ) );
        $date_to     = sanitize_text_field( (string) $request->get_param( 'date_to' ) );
        $raw_template_name = $request->get_param( 'template_name' );
        $raw_task_name     = $request->get_param( 'task_name' );

        $template_name = is_string( $raw_template_name ) ? sanitize_text_field( $raw_template_name ) : '';
        $task_name     = is_string( $raw_task_name ) ? sanitize_text_field( $raw_task_name ) : '';

        $per_page = (int) $request->get_param( 'per_page' );
        $allowed_per_page = [ 20, 50 ];
        if ( ! in_array( $per_page, $allowed_per_page, true ) ) {
            $per_page = 20;
        }

        $page = (int) $request->get_param( 'page' );
        $page = max( 1, $page );
        $offset = ( $page - 1 ) * $per_page;

        $template_name = trim( $template_name );
        $task_name     = trim( $task_name );

        $matched_template_ids = [];

        if ( '' !== $template_name ) {
            $templates = $this->template_manager->get_templates();

            foreach ( $templates as $id => $template ) {
                $name = isset( $template['name'] ) ? (string) $template['name'] : '';

                if ( '' !== $name && false !== stripos( $name, $template_name ) ) {
                    $matched_template_ids[] = $id;
                }
            }
        }

        $query = [
            'status'      => $status,
            'template_id' => $template_id,
            'run_type'    => $run_type,
            'date_from'   => $date_from,
            'date_to'     => $date_to,
            'limit'       => $per_page,
            'offset'      => $offset,
        ];

        if ( ! empty( $matched_template_ids ) ) {
            $query['template_ids'] = $matched_template_ids;
        } elseif ( '' !== $template_name ) {
            $query['template_ids'] = [];
        }

        if ( '' !== $task_name ) {
            $query['task_name'] = $task_name;
        }

        $runs = [];
        $total_runs = 0;

        $count_args = [
            'status'      => $status,
            'template_id' => $template_id,
            'run_type'    => $run_type,
            'date_from'   => $date_from,
            'date_to'     => $date_to,
        ];

        if ( '' !== $task_name ) {
            $count_args['task_name'] = $task_name;
        }

        if ( ! empty( $matched_template_ids ) ) {
            $count_args['template_ids'] = $matched_template_ids;
        } elseif ( '' !== $template_name ) {
            $count_args['template_ids'] = [];
        }

        if ( '' === $template_name || ! empty( $matched_template_ids ) ) {
            $runs = $this->scheduled_manager->get_runs( $query );
            $total_runs = $this->scheduled_manager->count_runs( $count_args );
        }

        $pages      = max( 1, (int) ceil( $total_runs / $per_page ) );

        $filters = [
            'status'      => $status,
            'template_id' => $template_id,
            'run_type'    => $run_type,
            'date_from'   => $date_from,
            'date_to'     => $date_to,
            'template_name' => $template_name,
            'task_name'     => $task_name,
            'limit'       => $per_page,
            'offset'      => $offset,
            'page'        => $page,
            'per_page'    => $per_page,
        ];

        $pagination = [
            'total'    => $total_runs,
            'current'  => $page,
            'per_page' => $per_page,
            'pages'    => $pages,
        ];

        return rest_ensure_response(
            [
                'filters'    => $filters,
                'runs'       => $runs,
                'pagination' => $pagination,
            ]
        );
    }

    /**
     * Execute a single pending manual export when fetching history list.
     *
     * @return void
     */
    private function maybe_work_one_pending_manual_export(): void {
        $throttle_key = 'prodexfo_manual_export_list_worker_throttle';

        if ( false !== get_transient( $throttle_key ) ) {
            return;
        }

        set_transient( $throttle_key, time(), 60 );

        $runs = $this->scheduled_manager->get_runs(
            [
                'status'   => 'pending',
                'run_type' => 'manual',
                'limit'    => 1,
                'offset'   => 0,
            ]
        );

        if ( empty( $runs ) || ! is_array( $runs[0] ) ) {
            return;
        }

        $run = $runs[0];
        $run_id = isset( $run['id'] ) ? (int) $run['id'] : 0;

        if ( $run_id <= 0 ) {
            return;
        }

        $created_at = isset( $run['created_at'] ) ? (string) $run['created_at'] : '';

        if ( '' === $created_at ) {
            return;
        }

        $created_ts = $this->get_run_created_timestamp( $created_at );

        if ( ! $created_ts || ( $this->current_utc_timestamp() - $created_ts ) < 20 ) {
            return;
        }

        $lock_key = 'prodexfo_manual_export_worker_lock_' . $run_id;

        if ( false !== get_transient( $lock_key ) ) {
            return;
        }

        set_transient( $lock_key, time(), 600 );

        try {
            do_action( 'prodexfo_manual_export_async_run', $run_id );
        } catch ( Throwable $e ) {
            // Intentionally ignored.
        } finally {
            delete_transient( $lock_key );
        }
    }

    /**
     * Delete a single history run.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_history_run( WP_REST_Request $request ) {
        $id = (int) $request->get_param( 'id' );

        if ( $id <= 0 ) {
            return new WP_Error( 'prodexfo_history_invalid_id', __( 'Invalid run ID.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 400 ] );
        }

        $deleted = $this->scheduled_manager->delete_run( $id );

        if ( ! $deleted ) {
            return new WP_Error( 'prodexfo_history_delete_failed', __( 'Export run not found or could not be deleted.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 404 ] );
        }

        return rest_ensure_response( [ 'deleted' => true ] );
    }

    /**
     * Delete multiple history runs at once.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_history_runs_bulk( WP_REST_Request $request ) {
        $raw_ids = $request->get_param( 'ids' );

        if ( ! is_array( $raw_ids ) ) {
            return new WP_Error( 'prodexfo_history_bulk_invalid', __( 'Provide a list of export IDs to delete.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 400 ] );
        }

        $ids = array_values( array_filter( array_map( 'intval', $raw_ids ), static function ( int $value ): bool {
            return $value > 0;
        } ) );

        if ( empty( $ids ) ) {
            return new WP_Error( 'prodexfo_history_bulk_empty', __( 'No valid export IDs supplied.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 400 ] );
        }

        $deleted = 0;
        $failed  = [];

        foreach ( $ids as $export_id ) {
            $result = $this->scheduled_manager->delete_run( $export_id );

            if ( $result ) {
                $deleted++;
                continue;
            }

            $failed[] = $export_id;
        }

        if ( 0 === $deleted ) {
            return new WP_Error( 'prodexfo_history_bulk_failed', __( 'None of the selected exports could be deleted.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 500 ] );
        }

        return rest_ensure_response(
            [
                'deleted' => $deleted,
                'failed'  => $failed,
            ]
        );
    }

    /**
     * Trigger a retry for a specific history run.
     *
     * @param WP_REST_Request $request REST request.
     * @return WP_REST_Response|WP_Error
     */
    public function retry_history_run( WP_REST_Request $request ) {
        $id = (int) $request->get_param( 'id' );

        if ( $id <= 0 ) {
            return new WP_Error( 'prodexfo_history_invalid_id', __( 'Invalid run ID.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 400 ] );
        }

        $run = $this->scheduled_manager->get_run( $id );

        if ( ! $run ) {
            return new WP_Error( 'prodexfo_history_missing', __( 'Export run not found.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 404 ] );
        }

        $template = ! empty( $run['template_id'] ) ? $this->template_manager->get_template( (string) $run['template_id'] ) : null;

        $fields   = $this->resolveFieldsForRetry( $run, $template );
        $filters  = $this->resolveFiltersForRetry( $run, $template );
        $settings = $this->resolveSettingsForRetry( $run, $template );
        $format   = $this->resolveFormatForRetry( $run, $template );

        $upload_dir = wp_upload_dir();

        if ( ! empty( $upload_dir['error'] ) || empty( $upload_dir['basedir'] ) ) {
            return new WP_Error( 'prodexfo_history_retry_uploads', __( 'Uploads directory is not writable.', 'badamsoft-product-exporter-for-woocommerce' ), [ 'status' => 500 ] );
        }

        $root_dir = $this->settings_repository->resolve_export_root_directory( (string) ( $upload_dir['basedir'] ?? '' ) );
        $base_dir = trailingslashit( $root_dir . 'history-retry' );
        wp_mkdir_p( $base_dir );

        $resolved_name = $this->exporter->resolve_filename_for_format( $settings, $format );
        $filename      = wp_unique_filename( $base_dir, $resolved_name );
        $file_path     = trailingslashit( $base_dir ) . $filename;

        try {
            $result = $this->exporter->export_to_file( $fields, $filters, $format, $settings, $file_path );
        } catch ( Throwable $exception ) {
            return new WP_Error( 'prodexfo_history_retry_failed', $exception->getMessage(), [ 'status' => 500 ] );
        }

        $absolute_path = isset( $result['file_path'] ) ? (string) $result['file_path'] : $file_path;

        $file_meta = [
            'file_path'       => $absolute_path,
            'file_size'       => file_exists( $absolute_path ) ? filesize( $absolute_path ) : 0,
            'file_format'     => strtoupper( sanitize_key( $format ) ),
            'rows_exported'   => (int) ( $result['rows'] ?? 0 ),
            'images_zip_path' => $result['images_zip_path'] ?? null,
            'images_zip_size' => isset( $result['images_zip_size'] ) ? (int) $result['images_zip_size'] : 0,
            'status'          => 'success',
            'finished_at'     => current_time( 'mysql' ),
        ];

        $this->scheduled_manager->update_run_file_meta( $id, $file_meta );

        $updated_run = $this->scheduled_manager->get_run( $id );

        return rest_ensure_response( [ 'run' => $updated_run ] );
    }

    /**
     * @param array<string, mixed>|null $template
     * @return array<int, string>
     */
    private function resolveFieldsForRetry( array $run, ?array $template ): array {
        $fields = [];

        if ( isset( $run['fields'] ) && is_array( $run['fields'] ) ) {
            $fields = array_filter( array_map( [ $this->exporter, 'sanitize_field_key' ], $run['fields'] ) );
        }

        if ( empty( $fields ) && $template && ! empty( $template['fields'] ) && is_array( $template['fields'] ) ) {
            $fields = array_filter( array_map( [ $this->exporter, 'sanitize_field_key' ], $template['fields'] ) );
        }

        if ( empty( $fields ) ) {
            $fields = $this->exporter->get_default_fields();
        }

        return array_values( $fields );
    }

    /**
     * @param array<string, mixed>|null $template
     * @return array<string, mixed>
     */
    private function resolveFiltersForRetry( array $run, ?array $template ): array {
        if ( isset( $run['filters'] ) && is_array( $run['filters'] ) ) {
            return $run['filters'];
        }

        if ( $template && isset( $template['filters'] ) && is_array( $template['filters'] ) ) {
            return $template['filters'];
        }

        return [];
    }

    /**
     * @param array<string, mixed>|null $template
     * @return array<string, mixed>
     */
    private function resolveSettingsForRetry( array $run, ?array $template ): array {
        if ( isset( $run['settings'] ) && is_array( $run['settings'] ) && ! empty( $run['settings'] ) ) {
            return $run['settings'];
        }

        if ( $template && isset( $template['settings'] ) && is_array( $template['settings'] ) && ! empty( $template['settings'] ) ) {
            return $template['settings'];
        }

        return $this->exporter->get_saved_settings();
    }

    /**
     * @param array<string, mixed>|null $template
     */
    private function resolveFormatForRetry( array $run, ?array $template ): string {
        $format = isset( $run['file_format'] ) ? strtolower( (string) $run['file_format'] ) : '';

        if ( '' === $format && $template && ! empty( $template['format'] ) ) {
            $format = strtolower( (string) $template['format'] );
        }

        $format = sanitize_key( $format );

        if ( '' === $format ) {
            $format = $this->exporter->get_default_format_key();
        }

        return $format;
    }

    /**
     * @param array<string, mixed> $template
     * @return array<string, mixed>
     */
    private function shapeTemplateForResponse( array $template ): array {
        return [
            'id'          => isset( $template['id'] ) ? (string) $template['id'] : '',
            'name'        => isset( $template['name'] ) ? (string) $template['name'] : '',
            'description' => isset( $template['description'] ) ? (string) $template['description'] : '',
            'fields'      => isset( $template['fields'] ) && is_array( $template['fields'] ) ? array_values( $template['fields'] ) : [],
            'filters'     => isset( $template['filters'] ) && is_array( $template['filters'] ) ? $template['filters'] : [],
            'format'      => isset( $template['format'] ) ? (string) $template['format'] : '',
            'settings'    => isset( $template['settings'] ) && is_array( $template['settings'] ) ? $template['settings'] : [],
            'created_at'  => isset( $template['created_at'] ) ? (string) $template['created_at'] : '',
            'updated_at'  => isset( $template['updated_at'] ) ? (string) $template['updated_at'] : '',
        ];
    }
}
