<?php
/**
 * Plugin data export, import, and scheduled backups.
 *
 * @package Maca_Menu_List
 */

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

if (!defined('MACA_MENULIST_BACKUP_FORMAT')) {
    define('MACA_MENULIST_BACKUP_FORMAT', 3);
}
if (!defined('MACA_MENULIST_BACKUP_CRON_HOOK')) {
    define('MACA_MENULIST_BACKUP_CRON_HOOK', 'maca_menulist_daily_backup');
}

/**
 * Raise time/memory limits for backup export/import (best effort).
 *
 * @return void
 */
function maca_menulist_backup_raise_limits() {
    if (function_exists('wp_raise_memory_limit')) {
        wp_raise_memory_limit('admin');
    }

    if (function_exists('set_time_limit')) {
        // phpcs:ignore WordPress.PHP.NoSilencedFunctions.Discouraged
        @set_time_limit(0);
    }

    // phpcs:ignore WordPress.PHP.NoSilencedFunctions.Discouraged
    @ignore_user_abort(true);
}

/**
 * Discard output buffered before headers (notices from other plugins).
 *
 * @return void
 */
function maca_menulist_backup_discard_buffered_output() {
    while (ob_get_level() > 0) {
        // phpcs:ignore WordPress.PHP.NoSilencedFunctions.Discouraged
        @ob_end_clean();
    }
}

/**
 * Table keys omitted from export (analytics/audit — can be very large).
 *
 * @return array<int, string>
 */
function maca_menulist_backup_get_skipped_table_keys() {
    $skipped = array(
        'maca_dish_views',
        'maca_booking_log',
    );

    /**
     * Tables excluded from backup export (not deleted on restore when skipped).
     *
     * @param array<int, string> $skipped Table keys without site prefix.
     */
    return apply_filters('maca_menulist_backup_skipped_tables', $skipped);
}

/**
 * Legacy table keys from backup format v1 mapped to current names.
 *
 * @return array<string, string>
 */
function maca_menulist_backup_get_legacy_table_aliases() {
    return array(
        'menu_categories' => 'maca_menu_categories',
        'menus' => 'maca_menus',
        'dish_categories' => 'maca_dish_categories',
        'allergens' => 'maca_allergens',
        'dishes' => 'maca_dishes',
        'dish_allergens' => 'maca_dish_allergens',
        'dish_ratings' => 'maca_dish_ratings',
        'dish_views' => 'maca_dish_views',
    );
}

/**
 * Option name prefixes included in backups.
 *
 * @return array<int, string>
 */
function maca_menulist_backup_get_option_prefixes() {
    $prefixes = array('maca_menulist_');

    /**
     * Add option prefixes for backup export/import.
     *
     * @param array<int, string> $prefixes Option name prefixes.
     */
    return apply_filters('maca_menulist_backup_option_prefixes', $prefixes);
}

/**
 * Option keys excluded from export/import.
 *
 * @return array<int, string>
 */
function maca_menulist_backup_get_excluded_option_keys() {
    $excluded = array(
        'maca_menulist_backup_last_run',
        'maca_menulist_backup_last_file',
        'maca_menulist_backup_last_reason',
        'maca_menulist_db_version',
        'maca_menulist_https_migrated',
        'maca_menulist_credit_option_removed',
        'maca_menulist_flush_flag_cleared',
    );

    /**
     * Exclude volatile options from backup export/import.
     *
     * @param array<int, string> $excluded Option keys.
     */
    return apply_filters('maca_menulist_backup_excluded_options', $excluded);
}

/**
 * Preferred delete order for known plugin tables (children before parents).
 *
 * @return array<int, string>
 */
function maca_menulist_backup_get_preferred_table_delete_order() {
    $order = array(
        'maca_booking_log',
        'maca_booking_table_day_splits',
        'maca_bookings',
        'maca_booking_exceptions',
        'maca_booking_recurring_exceptions',
        'maca_booking_tables',
        'maca_table_order_payments',
        'maca_table_order_lines',
        'maca_table_orders',
        'maca_waiter_tables',
        'maca_waiters',
        'maca_dish_allergens',
        'maca_dish_campaigns',
        'maca_dish_ratings',
        'maca_dish_views',
        'maca_dishes',
        'maca_allergens',
        'maca_dish_categories',
        'maca_campaigns',
        'maca_menus',
        'maca_menu_categories',
        'maca_info_event_exceptions',
        'maca_info_social_log',
        'maca_info_events',
        'maca_info_news',
    );

    /**
     * Preferred table delete order during restore (children first).
     *
     * @param array<int, string> $order Table keys without site prefix.
     */
    return apply_filters('maca_menulist_backup_table_delete_order', $order);
}

/**
 * Table storage key from full table name.
 *
 * @param string $table_name Full table name.
 * @return string
 */
function maca_menulist_backup_get_table_key($table_name) {
    global $wpdb;

    $prefix = $wpdb->prefix;
    if (strpos($table_name, $prefix) === 0) {
        return substr($table_name, strlen($prefix));
    }

    return $table_name;
}

/**
 * Discover all plugin database tables ({prefix}maca_*).
 *
 * @param bool $ensure_schema When true, run pending DB migrations first (export/import only).
 * @return array<string, string> Table key => full table name.
 */
function maca_menulist_backup_discover_tables($ensure_schema = true) {
    global $wpdb;

    if ($ensure_schema) {
        if (function_exists('maca_menulist_db_migrate_schema_complete')) {
            maca_menulist_db_migrate_schema_complete();
            if (function_exists('maca_menulist_db_migrate_schema_finalize_version')) {
                maca_menulist_db_migrate_schema_finalize_version();
            }
        } elseif (function_exists('maca_menulist_db_migrate_schema')) {
            maca_menulist_db_migrate_schema();
        }
    }

    $pattern = $wpdb->esc_like($wpdb->prefix . 'maca_') . '%';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    $table_names = $wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', $pattern));

    if (!is_array($table_names)) {
        $table_names = array();
    }

    $tables = array();
    foreach ($table_names as $table_name) {
        if (!is_string($table_name) || $table_name === '') {
            continue;
        }
        $tables[maca_menulist_backup_get_table_key($table_name)] = $table_name;
    }

    /**
     * Discovered plugin tables for backup.
     *
     * @param array<string, string> $tables Table key => full table name.
     */
    return apply_filters('maca_menulist_backup_discover_tables', $tables);
}

/**
 * Sort table keys for safe delete/insert order.
 *
 * @param array<int, string> $table_keys Table keys.
 * @return array<int, string>
 */
function maca_menulist_backup_sort_table_keys_for_delete($table_keys) {
    $preferred = maca_menulist_backup_get_preferred_table_delete_order();
    $ordered = array();

    foreach ($preferred as $key) {
        if (in_array($key, $table_keys, true)) {
            $ordered[] = $key;
        }
    }

    $remaining = array_diff($table_keys, $ordered);
    sort($remaining, SORT_STRING);

    return array_merge($ordered, $remaining);
}

/**
 * Discover plugin options from the database.
 *
 * @return array<string, mixed>
 */
function maca_menulist_backup_discover_options() {
    global $wpdb;

    $excluded = array_flip(maca_menulist_backup_get_excluded_option_keys());
    $prefixes = maca_menulist_backup_get_option_prefixes();
    $option_names = array();

    foreach ($prefixes as $prefix) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
                $wpdb->esc_like($prefix) . '%'
            )
        );

        if (!is_array($rows)) {
            continue;
        }

        foreach ($rows as $option_name) {
            if (!is_string($option_name) || isset($excluded[$option_name])) {
                continue;
            }
            $option_names[$option_name] = true;
        }
    }

    $options = array();
    foreach (array_keys($option_names) as $option_name) {
        $value = get_option($option_name, null);
        if ($value !== null) {
            $options[$option_name] = $value;
        }
    }

    /**
     * Plugin options included in backup export.
     *
     * @param array<string, mixed> $options Option key => value.
     */
    return apply_filters('maca_menulist_backup_discover_options', $options);
}

/**
 * Normalize imported table payload keys (supports legacy v1 backups).
 *
 * @param array<string, array<int, array<string, mixed>>> $tables Raw tables from file.
 * @return array<string, array<int, array<string, mixed>>>
 */
function maca_menulist_backup_normalize_tables_payload($tables) {
    $aliases = maca_menulist_backup_get_legacy_table_aliases();
    $normalized = array();

    foreach ($tables as $key => $rows) {
        if (!is_array($rows)) {
            continue;
        }

        $table_key = isset($aliases[$key]) ? $aliases[$key] : (string) $key;

        if (!isset($normalized[$table_key])) {
            $normalized[$table_key] = array();
        }

        foreach ($rows as $row) {
            if (is_array($row)) {
                $normalized[$table_key][] = $row;
            }
        }
    }

    return $normalized;
}

/**
 * Collect media URLs from table rows and options.
 *
 * @param array<string, array<int, array<string, mixed>>> $tables Table data.
 * @param array<string, mixed>                            $options Plugin options.
 * @return array<int, string>
 */
function maca_menulist_backup_collect_media_urls($tables, $options) {
    $url_field_pattern = '/(_url|_logo|_image)$/i';
    $media_urls = array();

    foreach ($tables as $rows) {
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            foreach ($row as $field => $value) {
                if (!is_string($field) || !is_string($value) || $value === '') {
                    continue;
                }
                if (preg_match($url_field_pattern, $field) && preg_match('#^https?://#i', $value)) {
                    $media_urls[] = $value;
                }
            }
        }
    }

    foreach ($options as $value) {
        if (is_string($value) && preg_match('#^https?://#i', $value)) {
            $media_urls[] = $value;
        }
    }

    return array_values(array_unique($media_urls));
}

/**
 * Resolve a media URL to a local uploads file.
 *
 * @param string $url Media URL.
 * @param bool   $lookup_attachment Whether to fall back to attachment_url_to_postid (slow).
 * @return array{relative: string, path: string}|null
 */
function maca_menulist_backup_url_to_upload_file($url, $lookup_attachment = true) {
    if (!is_string($url) || !preg_match('#^https?://#i', $url)) {
        return null;
    }

    $url = preg_replace('#\?.*$#', '', $url);
    $upload = wp_upload_dir();
    $baseurl = trailingslashit($upload['baseurl']);
    $basedir = trailingslashit(wp_normalize_path($upload['basedir']));

    $normalized_url = set_url_scheme($url, wp_parse_url($baseurl, PHP_URL_SCHEME));
    $http_url = set_url_scheme($url, 'http');
    $https_url = set_url_scheme($url, 'https');

    foreach (array_unique(array($url, $normalized_url, $http_url, $https_url)) as $candidate) {
        if (strpos($candidate, $baseurl) !== 0) {
            continue;
        }

        $relative = ltrim(substr($candidate, strlen($baseurl)), '/');
        $path = $basedir . str_replace('/', DIRECTORY_SEPARATOR, $relative);

        if ($relative !== '' && file_exists($path) && is_readable($path) && !is_dir($path)) {
            return array(
                'relative' => str_replace('\\', '/', $relative),
                'path' => $path,
            );
        }
    }

    if ($lookup_attachment) {
        $attachment_id = attachment_url_to_postid($url);
        if (!$attachment_id) {
            $attachment_id = attachment_url_to_postid($normalized_url);
        }

        if ($attachment_id) {
            $attached = get_attached_file($attachment_id);
            if (is_string($attached) && $attached !== '' && file_exists($attached) && is_readable($attached)) {
                $attached = wp_normalize_path($attached);
                if (strpos($attached, $basedir) === 0) {
                    $relative = ltrim(substr($attached, strlen($basedir)), '/');
                    return array(
                        'relative' => str_replace('\\', '/', $relative),
                        'path' => $attached,
                    );
                }
            }
        }
    }

    return null;
}

/**
 * Build media manifest for backup archive.
 *
 * @param array<int, string> $media_urls Media URLs referenced in plugin data.
 * @return array<int, array<string, string>>
 */
function maca_menulist_backup_build_media_manifest($media_urls) {
    $manifest = array();
    $seen = array();

    foreach ($media_urls as $url) {
        $resolved = maca_menulist_backup_url_to_upload_file($url, false);
        if (!$resolved || isset($seen[$resolved['relative']])) {
            continue;
        }

        $seen[$resolved['relative']] = true;
        $manifest[] = array(
            'relative_path' => $resolved['relative'],
            'original_url' => $url,
            'archive_path' => 'media/' . $resolved['relative'],
            'source_path' => $resolved['path'],
        );
    }

    /**
     * Media files included in backup archives.
     *
     * @param array<int, array<string, string>> $manifest Media manifest entries.
     */
    return apply_filters('maca_menulist_backup_media_manifest', $manifest);
}

/**
 * Build URL replacement map when restoring on another site/domain.
 *
 * @param array<string, mixed> $payload Backup payload.
 * @return array<string, string>
 */
function maca_menulist_backup_get_url_replacements($payload) {
    $replacements = array();
    $upload = wp_upload_dir();
    $new_base = untrailingslashit($upload['baseurl']);

    if (!empty($payload['uploads_baseurl']) && is_string($payload['uploads_baseurl'])) {
        $old_base = untrailingslashit($payload['uploads_baseurl']);
        if ($old_base !== '' && $old_base !== $new_base) {
            $replacements[$old_base] = $new_base;
            $replacements[str_replace('https://', 'http://', $old_base)] = str_replace('https://', 'http://', $new_base);
            $replacements[str_replace('http://', 'https://', $old_base)] = str_replace('http://', 'https://', $new_base);
        }
    }

    if (!empty($payload['site_url']) && is_string($payload['site_url'])) {
        $old_site = untrailingslashit($payload['site_url']);
        $new_site = untrailingslashit(home_url('/'));
        if ($old_site !== '' && $old_site !== $new_site) {
            $replacements[$old_site] = $new_site;
            $replacements[str_replace('https://', 'http://', $old_site)] = str_replace('https://', 'http://', $new_site);
            $replacements[str_replace('http://', 'https://', $old_site)] = str_replace('http://', 'https://', $new_site);
        }
    }

    return $replacements;
}

/**
 * Recursively apply URL replacements to exported data.
 *
 * @param mixed                $data         Data to rewrite.
 * @param array<string, string> $replacements Search => replace map.
 * @return mixed
 */
function maca_menulist_backup_apply_url_replacements($data, $replacements) {
    if (empty($replacements)) {
        return $data;
    }

    if (is_string($data)) {
        return str_replace(array_keys($replacements), array_values($replacements), $data);
    }

    if (is_array($data)) {
        foreach ($data as $key => $value) {
            $data[$key] = maca_menulist_backup_apply_url_replacements($value, $replacements);
        }
    }

    return $data;
}

/**
 * Transient key for import progress (scoped to current user).
 *
 * @param string $job_id Client job id.
 * @return string
 */
function maca_menulist_backup_import_progress_key($job_id) {
    $job_id = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $job_id);

    return 'maca_bi_' . get_current_user_id() . '_' . $job_id;
}

/**
 * Store import progress for AJAX polling.
 *
 * @param string $job_id  Client job id.
 * @param int    $percent Progress 0-100.
 * @param string $stage   Human-readable stage label.
 * @return void
 */
function maca_menulist_backup_set_import_progress($job_id, $percent, $stage = '') {
    if ($job_id === '') {
        return;
    }

    $key = maca_menulist_backup_import_progress_key($job_id);
    $safe_percent = max(0, min(100, (int) $percent));
    $existing = get_transient($key);

    if (is_array($existing) && isset($existing['percent']) && $safe_percent < (int) $existing['percent']) {
        $safe_percent = (int) $existing['percent'];
    }

    $data = array(
        'percent'    => $safe_percent,
        'stage'      => (string) $stage,
        'updated_at' => time(),
    );

    set_transient($key, $data, 15 * MINUTE_IN_SECONDS);

    // Ensure parallel progress polls see updates during long imports.
    wp_cache_delete($key, 'transient');
    wp_cache_delete('_transient_' . $key, 'options');
    wp_cache_delete('_transient_timeout_' . $key, 'options');
}

/**
 * Read import progress.
 *
 * @param string $job_id Client job id.
 * @return array{percent: int, stage: string, updated_at: int}|null
 */
function maca_menulist_backup_get_import_progress($job_id) {
    $job_id = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $job_id);
    if ($job_id === '') {
        return null;
    }

    $data = get_transient(maca_menulist_backup_import_progress_key($job_id));

    return is_array($data) ? $data : null;
}

/**
 * Remove import progress transient.
 *
 * @param string $job_id Client job id.
 * @return void
 */
function maca_menulist_backup_clear_import_progress($job_id) {
    if ($job_id === '') {
        return;
    }

    delete_transient(maca_menulist_backup_import_progress_key($job_id));
}

/**
 * Extract media files from a backup ZIP into uploads.
 *
 * @param string $zip_path Absolute path to ZIP backup.
 * @param string $job_id   Optional progress job id.
 * @return int Number of files restored.
 */
function maca_menulist_backup_extract_media_from_zip($zip_path, $job_id = '') {
    if (!class_exists('ZipArchive') || !is_readable($zip_path)) {
        return 0;
    }

    $zip = new ZipArchive();
    if ($zip->open($zip_path) !== true) {
        return 0;
    }

    $upload = wp_upload_dir();
    $basedir = trailingslashit(wp_normalize_path($upload['basedir']));
    $restored = 0;
    $media_total = 0;

    for ($index = 0; $index < $zip->numFiles; $index++) {
        $entry = $zip->getNameIndex($index);
        if (!is_string($entry) || strpos($entry, 'media/') !== 0) {
            continue;
        }

        $relative = substr($entry, 6);
        if ($relative === '' || substr($entry, -1) === '/') {
            continue;
        }

        $media_total++;
    }

    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            8,
            sprintf(
                /* translators: %d: file count */
                __('Återställer mediafiler (%d)…', 'maca-restu-pro'),
                $media_total
            )
        );
    }

    $media_done = 0;

    for ($index = 0; $index < $zip->numFiles; $index++) {
        $entry = $zip->getNameIndex($index);
        if (!is_string($entry) || strpos($entry, 'media/') !== 0) {
            continue;
        }

        $relative = substr($entry, 6);
        if ($relative === '' || substr($entry, -1) === '/') {
            continue;
        }

        $contents = $zip->getFromIndex($index);
        if ($contents === false) {
            continue;
        }

        $relative = str_replace('\\', '/', $relative);
        $destination = $basedir . str_replace('/', DIRECTORY_SEPARATOR, $relative);
        wp_mkdir_p(dirname($destination));

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
        if (file_put_contents($destination, $contents) !== false) {
            $restored++;
        }

        $media_done++;
        if ($job_id !== '' && $media_total > 0) {
            $media_percent = 8 + (int) floor(20 * $media_done / $media_total);
            maca_menulist_backup_set_import_progress(
                $job_id,
                $media_percent,
                sprintf(
                    /* translators: 1: current file number, 2: total files */
                    __('Återställer media %1$d av %2$d…', 'maca-restu-pro'),
                    $media_done,
                    $media_total
                )
            );
        }
    }

    $zip->close();

    return $restored;
}

/**
 * Backup storage directory path.
 *
 * @return string
 */
function maca_menulist_backup_get_directory() {
    $upload = wp_upload_dir();
    $dir = trailingslashit($upload['basedir']) . 'maca-menulist-backups';

    if (!file_exists($dir)) {
        wp_mkdir_p($dir);
    }

    if (file_exists($dir)) {
        $index = trailingslashit($dir) . 'index.html';
        if (!file_exists($index)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
            file_put_contents($index, '');
        }

        $htaccess = trailingslashit($dir) . '.htaccess';
        if (!file_exists($htaccess)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
            file_put_contents($htaccess, "Options -Indexes\n<FilesMatch \"\\.(json|zip)$\">\n  Require all denied\n</FilesMatch>\n");
        }
    }

    return $dir;
}

/**
 * Build export payload.
 *
 * @return array<string, mixed>
 */
function maca_menulist_backup_build_payload() {
    global $wpdb;

    maca_menulist_backup_raise_limits();

    // Export reads existing tables only — do not run migrations here (can timeout the request).
    $discovered_tables = maca_menulist_backup_discover_tables(false);
    $skipped_keys = array_flip(maca_menulist_backup_get_skipped_table_keys());
    $tables = array();
    $skipped_tables = array();

    maca_menulist_debug_log('backup_build_payload: exporting ' . count($discovered_tables) . ' tables');

    foreach ($discovered_tables as $key => $table) {
        if (isset($skipped_keys[ $key ])) {
            $skipped_tables[] = $key;
            continue;
        }

        maca_menulist_debug_log('backup_build_payload: read ' . $key);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $tables[ $key ] = $wpdb->get_results("SELECT * FROM {$table}", ARRAY_A);
        if (!is_array($tables[ $key ])) {
            $tables[ $key ] = array();
        }
    }

    $options = maca_menulist_backup_discover_options();
    $media_urls = maca_menulist_backup_collect_media_urls($tables, $options);
    $media_manifest = maca_menulist_backup_build_media_manifest($media_urls);
    $upload = wp_upload_dir();

    return array(
        'format' => MACA_MENULIST_BACKUP_FORMAT,
        'plugin_version' => MACA_MENULIST_VERSION,
        'exported_at' => wp_date('c'),
        'site_url' => home_url('/'),
        'uploads_baseurl' => $upload['baseurl'],
        'table_prefix' => $wpdb->prefix,
        'content_mode' => function_exists('maca_menulist_get_content_mode') ? maca_menulist_get_content_mode() : '',
        'table_keys' => array_keys($discovered_tables),
        'skipped_tables' => $skipped_tables,
        'tables' => $tables,
        'options' => $options,
        'media_urls' => $media_urls,
        '_media_manifest' => $media_manifest,
        'media_files' => array_map(
            static function ($entry) {
                return array(
                    'relative_path' => $entry['relative_path'],
                    'original_url' => $entry['original_url'],
                    'archive_path' => $entry['archive_path'],
                );
            },
            $media_manifest
        ),
        'media_file_count' => count($media_manifest),
    );
}

/**
 * Write backup archive to disk.
 *
 * @param string $reason Backup reason label.
 * @return array{success: bool, filename?: string, path?: string, message?: string}
 */
function maca_menulist_backup_create_file($reason = 'manual') {
    maca_menulist_backup_raise_limits();
    maca_menulist_debug_log('backup_create_file: start (' . sanitize_key($reason) . ')');

    $payload = maca_menulist_backup_build_payload();
    $media_manifest = !empty($payload['_media_manifest']) && is_array($payload['_media_manifest'])
        ? $payload['_media_manifest']
        : maca_menulist_backup_build_media_manifest($payload['media_urls']);
    unset($payload['_media_manifest']);
    $json = wp_json_encode($payload, JSON_UNESCAPED_UNICODE);

    if ($json === false) {
        return array(
            'success' => false,
            'message' => __('Kunde inte koda säkerhetskopians data.', 'maca-restu-pro'),
        );
    }

    $dir = maca_menulist_backup_get_directory();
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Direct local backup directory check.
    if (!is_writable($dir)) {
        return array(
            'success' => false,
            'message' => __('Säkerhetskopiemappen är inte skrivbar.', 'maca-restu-pro'),
        );
    }

    if (!class_exists('ZipArchive')) {
        return array(
            'success' => false,
            'message' => __('ZIP-stöd krävs för att inkludera mediafiler i säkerhetskopior.', 'maca-restu-pro'),
        );
    }

    $timestamp = wp_date('Y-m-d-His');
    $basename = 'maca-menulist-backup-' . $timestamp;
    $zip_path = trailingslashit($dir) . $basename . '.zip';
    $filename = $basename . '.zip';

    $zip = new ZipArchive();
    if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        return array(
            'success' => false,
            'message' => __('Kunde inte skapa säkerhetskopiearkivet.', 'maca-restu-pro'),
        );
    }

    $zip->addFromString('data.json', $json);

    foreach ($media_manifest as $entry) {
        if (!empty($entry['source_path']) && is_readable($entry['source_path'])) {
            $zip->addFile($entry['source_path'], $entry['archive_path']);
        }
    }

    if (!$zip->close()) {
        if (is_file($zip_path)) {
            wp_delete_file($zip_path);
        }

        return array(
            'success' => false,
            'message' => __('Kunde inte slutföra säkerhetskopiearkivet.', 'maca-restu-pro'),
        );
    }

    $path = trailingslashit($dir) . $filename;

    maca_menulist_debug_log(
        'backup_create_file: done ' . $filename
        . ' tables=' . count($payload['tables'])
        . ' media=' . count($media_manifest)
    );

    update_option('maca_menulist_backup_last_run', time());
    update_option('maca_menulist_backup_last_file', $filename);
    update_option('maca_menulist_backup_last_reason', sanitize_key($reason));

    maca_menulist_backup_rotate_files();

    /**
     * Fires after a backup file has been created.
     *
     * @param string $path Absolute path to backup file.
     * @param string $reason Backup trigger: manual, cron, import.
     */
    do_action('maca_menulist_backup_complete', $path, $reason);

    return array(
        'success' => true,
        'filename' => $filename,
        'path' => $path,
    );
}

/**
 * Find backup files in storage directory.
 *
 * @param string $dir Directory path.
 * @return array<int, string>
 */
function maca_menulist_backup_glob_files($dir) {
    $patterns = array(
        trailingslashit($dir) . 'maca-menulist-backup-*.json',
        trailingslashit($dir) . 'maca-menulist-backup-*.zip',
    );
    $files = array();

    foreach ($patterns as $pattern) {
        $matches = glob($pattern);
        if (is_array($matches)) {
            $files = array_merge($files, $matches);
        }
    }

    return $files;
}

/**
 * Delete old backup files beyond retention.
 */
function maca_menulist_backup_rotate_files() {
    $retention_days = max(1, (int) get_option('maca_menulist_backup_retention_days', 7));
    $dir = maca_menulist_backup_get_directory();
    $cutoff = time() - ($retention_days * DAY_IN_SECONDS);

    foreach (maca_menulist_backup_glob_files($dir) as $file) {
        if (!is_string($file) || !file_exists($file)) {
            continue;
        }
        if (filemtime($file) < $cutoff) {
            wp_delete_file($file);
        }
    }
}

/**
 * List backup files newest first.
 *
 * @return array<int, array<string, mixed>>
 */
function maca_menulist_backup_list_files() {
    $dir = maca_menulist_backup_get_directory();
    $files = maca_menulist_backup_glob_files($dir);

    $items = array();
    foreach ($files as $file) {
        if (!is_string($file) || !file_exists($file)) {
            continue;
        }
        $items[] = array(
            'filename' => basename($file),
            'path' => $file,
            'size' => filesize($file),
            'modified' => filemtime($file),
        );
    }

    usort(
        $items,
        static function ($a, $b) {
            return $b['modified'] <=> $a['modified'];
        }
    );

    return $items;
}

/**
 * Normalize a backup basename from request input.
 *
 * @param string $filename Raw filename.
 * @return string Valid basename or empty string.
 */
function maca_menulist_backup_normalize_filename($filename) {
    $filename = wp_basename(rawurldecode((string) $filename));
    $filename = sanitize_file_name($filename);

    if (maca_menulist_backup_is_valid_filename($filename)) {
        return $filename;
    }

    if (preg_match('/(maca-menulist-backup-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.(?:json|zip))/i', $filename, $matches)) {
        return strtolower($matches[1]);
    }

    return '';
}

/**
 * Whether a filename is a valid plugin backup on disk.
 *
 * @param string $filename Basename only.
 * @return bool
 */
function maca_menulist_backup_is_valid_filename($filename) {
    $filename = sanitize_file_name($filename);

    return $filename !== ''
        && preg_match('/^maca-menulist-backup-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}\.(json|zip)$/', $filename);
}

/**
 * Delete one stored backup file.
 *
 * @param string $filename Basename only.
 * @return array{success: bool, message: string}
 */
function maca_menulist_backup_delete_file($filename) {
    $filename = sanitize_file_name($filename);

    if (!maca_menulist_backup_is_valid_filename($filename)) {
        return array(
            'success' => false,
            'message' => __('Invalid backup file.', 'maca-restu-pro'),
        );
    }

    $path = trailingslashit(maca_menulist_backup_get_directory()) . $filename;

    if (!is_file($path)) {
        return array(
            'success' => false,
            'message' => __('Backup file not found.', 'maca-restu-pro'),
        );
    }

    if (!wp_delete_file($path)) {
        return array(
            'success' => false,
            'message' => __('Could not delete the backup file.', 'maca-restu-pro'),
        );
    }

    if (get_option('maca_menulist_backup_last_file', '') === $filename) {
        delete_option('maca_menulist_backup_last_file');
    }

    /**
     * Fires after a stored backup file is deleted from disk.
     *
     * @param string $filename Deleted backup basename.
     */
    do_action('maca_menulist_backup_deleted', $filename);

    return array(
        'success' => true,
        'message' => __('Backup file deleted.', 'maca-restu-pro'),
    );
}

/**
 * Detect backup archive type from path, original filename, or file header.
 *
 * Uploaded PHP temp files often have no extension (e.g. /tmp/phpXXXX).
 *
 * @param string $path          File path.
 * @param string $filename_hint Original uploaded filename.
 * @return string json|zip|''
 */
function maca_menulist_backup_detect_file_type($path, $filename_hint = '') {
    $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));

    if ($extension === '') {
        $extension = strtolower(pathinfo((string) $filename_hint, PATHINFO_EXTENSION));
    }

    if (in_array($extension, array('json', 'zip'), true)) {
        return $extension;
    }

    if (!is_readable($path)) {
        return '';
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Local magic-byte sniff for backup type.
    $handle = fopen($path, 'rb');
    if ($handle) {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Local magic-byte sniff for backup type.
        $header = fread($handle, 4);
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Local magic-byte sniff for backup type.
        fclose($handle);

        if ($header === "PK\x03\x04") {
            return 'zip';
        }
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    $sample = file_get_contents($path, false, null, 0, 4096);
    if (is_string($sample)) {
        $trimmed = ltrim($sample);
        if ($trimmed !== '' && $trimmed[0] === '{') {
            return 'json';
        }
    }

    return '';
}

/**
 * Parse uploaded or stored backup into payload array.
 *
 * @param string $path          File path.
 * @param string $filename_hint Original uploaded filename (for temp uploads).
 * @return array{payload: array<string, mixed>|null, error: string}
 */
function maca_menulist_backup_read_file($path, $filename_hint = '') {
    if (!is_readable($path)) {
        return array(
            'payload' => null,
            'error'   => __('Backupfilen kunde inte läsas från disk.', 'maca-restu-pro'),
        );
    }

    $extension = maca_menulist_backup_detect_file_type($path, $filename_hint);
    $json = '';

    if ($extension === 'json') {
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        $json = file_get_contents($path);
    } elseif ($extension === 'zip') {
        if (!class_exists('ZipArchive')) {
            return array(
                'payload' => null,
                'error'   => __('ZIP-stöd (PHP ZipArchive) saknas på servern.', 'maca-restu-pro'),
            );
        }

        $zip = new ZipArchive();
        if ($zip->open($path) !== true) {
            return array(
                'payload' => null,
                'error'   => __('Kunde inte öppna ZIP-säkerhetskopian.', 'maca-restu-pro'),
            );
        }

        $json = $zip->getFromName('data.json');
        if ($json === false) {
            for ($index = 0; $index < $zip->numFiles; $index++) {
                $entry_name = (string) $zip->getNameIndex($index);
                if (strtolower(substr($entry_name, -5)) === '.json') {
                    $json = $zip->getFromIndex($index);
                    break;
                }
            }
        }

        if ($json === false) {
            $json = $zip->getFromIndex(0);
        }

        $zip->close();
    } else {
        return array(
            'payload' => null,
            'error'   => __('Okänd filtyp. Ladda upp en .json- eller .zip-säkerhetskopieringsfil.', 'maca-restu-pro'),
        );
    }

    if ($json === '' || $json === false) {
        return array(
            'payload' => null,
            'error'   => __('Säkerhetskopiefilen saknar data (data.json).', 'maca-restu-pro'),
        );
    }

    $payload = json_decode($json, true);

    if (!is_array($payload)) {
        return array(
            'payload' => null,
            'error'   => __('Säkerhetskopiefilen innehåller ogiltig JSON-data.', 'maca-restu-pro'),
        );
    }

    return array(
        'payload' => $payload,
        'error'   => '',
    );
}

/**
 * Rows inserted per chunked import request.
 */
if (!defined('MACA_MENULIST_BACKUP_IMPORT_INSERT_BATCH')) {
    define('MACA_MENULIST_BACKUP_IMPORT_INSERT_BATCH', 75);
}

/**
 * Progress (%) reserved for file upload in the browser (0–24). Server work uses 25–100.
 */
if (!defined('MACA_MENULIST_BACKUP_IMPORT_UPLOAD_PERCENT')) {
    define('MACA_MENULIST_BACKUP_IMPORT_UPLOAD_PERCENT', 25);
}

/**
 * Map legacy 0–100 import progress to display scale (25–100 after upload).
 *
 * @param int $legacy_percent Progress on the old 0–100 server scale.
 * @return int
 */
function maca_menulist_backup_import_display_percent($legacy_percent) {
    $legacy_percent = max(0, min(100, (int) $legacy_percent));
    $base = (int) MACA_MENULIST_BACKUP_IMPORT_UPLOAD_PERCENT;

    return $base + (int) round((100 - $base) * $legacy_percent / 100);
}

/**
 * Store scaled import progress (25–100 % after upload).
 *
 * @param string $job_id         Client job id.
 * @param int    $legacy_percent Progress on the internal 0–100 scale.
 * @param string $stage          Human-readable stage label.
 * @return int Display percent written.
 */
function maca_menulist_backup_set_import_job_progress($job_id, $legacy_percent, $stage = '') {
    $display = maca_menulist_backup_import_display_percent($legacy_percent);
    maca_menulist_backup_set_import_progress($job_id, $display, $stage);

    return $display;
}

/**
 * Media files restored per chunked import request.
 */
if (!defined('MACA_MENULIST_BACKUP_IMPORT_MEDIA_BATCH')) {
    define('MACA_MENULIST_BACKUP_IMPORT_MEDIA_BATCH', 3);
}

/**
 * @param string $job_id Client job id.
 * @return string
 */
function maca_menulist_backup_import_job_dir($job_id) {
    $job_id = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $job_id);
    $upload = wp_upload_dir();
    $dir = trailingslashit(wp_normalize_path($upload['basedir'])) . 'maca-import-jobs/' . $job_id;

    if (!is_dir($dir)) {
        wp_mkdir_p($dir);
    }

    return $dir;
}

/**
 * @param string $job_id Client job id.
 * @return string
 */
function maca_menulist_backup_import_job_state_path($job_id) {
    return trailingslashit(maca_menulist_backup_import_job_dir($job_id)) . 'state.json';
}

/**
 * @param string $job_id Client job id.
 * @return array<string, mixed>|null
 */
function maca_menulist_backup_import_load_job_state($job_id) {
    $path = maca_menulist_backup_import_job_state_path($job_id);

    if (!is_readable($path)) {
        return null;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    $raw = file_get_contents($path);
    if (!is_string($raw) || $raw === '') {
        return null;
    }

    $state = json_decode($raw, true);

    return is_array($state) ? $state : null;
}

/**
 * @param string               $job_id Client job id.
 * @param array<string, mixed> $state  Job state.
 * @return void
 */
function maca_menulist_backup_import_save_job_state($job_id, $state) {
    $path = maca_menulist_backup_import_job_state_path($job_id);
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
    file_put_contents($path, wp_json_encode($state));
}

/**
 * @param string $job_id Client job id.
 * @return void
 */
function maca_menulist_backup_import_cleanup_job($job_id) {
    $dir = maca_menulist_backup_import_job_dir($job_id);

    if (!is_dir($dir)) {
        return;
    }

    $files = glob(trailingslashit($dir) . '*');
    if (is_array($files)) {
        foreach ($files as $file) {
            if (is_file($file)) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
                unlink($file);
            }
        }
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
    @rmdir($dir);
}

/**
 * @param string $job_id        Client job id.
 * @param string $source_path   Uploaded backup path.
 * @param string $filename_hint Original filename.
 * @return bool
 */
function maca_menulist_backup_import_init_job($job_id, $source_path, $filename_hint) {
    $extension = maca_menulist_backup_detect_file_type($source_path, $filename_hint);
    if ($extension === '') {
        $extension = 'json';
    }

    $dir = maca_menulist_backup_import_job_dir($job_id);
    $backup_path = trailingslashit($dir) . 'backup.' . $extension;

    if (!@copy($source_path, $backup_path)) {
        return false;
    }

    $state = array(
        'user_id'            => get_current_user_id(),
        'filename_hint'      => $filename_hint,
        'backup_path'        => $backup_path,
        'extension'          => $extension,
        'phase'              => 'read',
        'schema_index'       => 0,
        'media_index'        => 0,
        'media_total'        => 0,
        'media_restored'     => 0,
        'delete_index'       => 0,
        'insert_table_index' => 0,
        'insert_row_offset'  => 0,
        'message'            => '',
    );

    maca_menulist_backup_import_save_job_state($job_id, $state);

    return true;
}

/**
 * @param string $job_id Client job id.
 * @return array<string, mixed>|null
 */
function maca_menulist_backup_import_get_job_payload($job_id) {
    $path = trailingslashit(maca_menulist_backup_import_job_dir($job_id)) . 'payload.json';

    if (!is_readable($path)) {
        return null;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    $raw = file_get_contents($path);
    if (!is_string($raw) || $raw === '') {
        return null;
    }

    $payload = json_decode($raw, true);

    return is_array($payload) ? $payload : null;
}

/**
 * @param string               $job_id  Client job id.
 * @param array<string, mixed> $payload Backup payload.
 * @return void
 */
function maca_menulist_backup_import_save_job_payload($job_id, $payload) {
    $path = trailingslashit(maca_menulist_backup_import_job_dir($job_id)) . 'payload.json';
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
    file_put_contents($path, wp_json_encode($payload));
}

/**
 * @param string $job_id Client job id.
 * @return array<string, array<int, array<string, mixed>>>|null
 */
function maca_menulist_backup_import_get_job_tables_payload($job_id) {
    $path = trailingslashit(maca_menulist_backup_import_job_dir($job_id)) . 'tables.json';

    if (!is_readable($path)) {
        return null;
    }

    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    $raw = file_get_contents($path);
    if (!is_string($raw) || $raw === '') {
        return null;
    }

    $tables = json_decode($raw, true);

    return is_array($tables) ? $tables : null;
}

/**
 * @param string                                        $job_id Client job id.
 * @param array<string, array<int, array<string, mixed>>> $tables Normalized tables.
 * @return void
 */
function maca_menulist_backup_import_save_job_tables_payload($job_id, $tables) {
    $path = trailingslashit(maca_menulist_backup_import_job_dir($job_id)) . 'tables.json';
    // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
    file_put_contents($path, wp_json_encode($tables));
}

/**
 * Whether this site already has maca Restu database tables (typical import target).
 *
 * @return bool
 */
function maca_menulist_backup_import_target_has_plugin_tables() {
    global $wpdb;

    if (!function_exists('maca_menulist_db_categories_table')) {
        return false;
    }

    $table = maca_menulist_db_categories_table();

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    return $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table;
}

/**
 * Run a table bootstrap callback only when the table is missing (avoids slow dbDelta).
 *
 * @param string   $table_name Table to check.
 * @param callable $create     Callback that creates the table.
 * @return void
 */
function maca_menulist_backup_import_create_table_if_missing($table_name, $create) {
    global $wpdb;

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table_name)) === $table_name) {
        return;
    }

    call_user_func($create);
}

/**
 * Whether schema preparation can be skipped before data import.
 *
 * @return bool
 */
function maca_menulist_backup_import_can_skip_schema_phase() {
    if (!maca_menulist_backup_import_target_has_plugin_tables()) {
        return false;
    }

    if (function_exists('maca_menulist_db_migrate_schema_is_complete')) {
        return maca_menulist_db_migrate_schema_is_complete();
    }

    return true;
}

/**
 * Migration steps skipped during import (already handled by create-table steps).
 *
 * @return array<int, string>
 */
function maca_menulist_backup_get_import_skip_migrate_step_ids() {
    return array(
        'bootstrap_tables',
        'dish_tables',
        'campaign_tables',
        'dish_views',
        'table_orders',
        'table_bookings',
        'attendance_tables',
        'info_hub_tables',
        'info_hub_social_log',
    );
}

/**
 * @param int    $index Step index (0-based).
 * @param int    $total Total steps.
 * @param string $label Step label.
 * @return array{percent: int, stage: string}
 */
function maca_menulist_backup_import_schema_progress($index, $total, $label) {
    $prep_start = 28;
    $prep_end = 45;
    $total = max(1, $total);
    $percent = $prep_start;

    if ($total > 1) {
        $percent = $prep_start + (int) round(($prep_end - $prep_start) * $index / ($total - 1));
    }

    return array(
        'percent' => maca_menulist_backup_import_display_percent($percent),
        'stage'   => sprintf(
            /* translators: 1: step label, 2: current step number, 3: total steps */
            __('%1$s (%2$d/%3$d)', 'maca-restu-pro'),
            $label,
            $index + 1,
            $total
        ),
    );
}

/**
 * Build ordered schema steps for chunked import.
 *
 * @return array<int, array{label: string, run: callable}>
 */
function maca_menulist_backup_get_import_schema_step_items() {
    $step_items = array();

    foreach (maca_menulist_backup_get_import_create_table_steps() as $step) {
        $step_items[] = array(
            'label' => $step[0],
            'run'   => $step[1],
        );
    }

    if (function_exists('maca_menulist_db_get_migrate_steps')) {
        $migrate_labels = maca_menulist_backup_get_import_schema_migrate_labels();
        $skip_ids = array_flip(maca_menulist_backup_get_import_skip_migrate_step_ids());

        foreach (maca_menulist_db_get_migrate_steps() as $step_id => $callback) {
            if (isset($skip_ids[$step_id])) {
                continue;
            }

            $label = isset($migrate_labels[$step_id])
                ? $migrate_labels[$step_id]
                : sprintf(
                    /* translators: %s: migration step id */
                    __('Uppgraderar %s…', 'maca-restu-pro'),
                    $step_id
                );

            $step_items[] = array(
                'label' => $label,
                'run'   => $callback,
            );
        }
    } elseif (function_exists('maca_menulist_db_migrate_schema')) {
        $step_items[] = array(
            'label' => __('Uppgraderar databasschema…', 'maca-restu-pro'),
            'run'   => static function () {
                maca_menulist_db_migrate_schema();
            },
        );
    }

    return $step_items;
}

/**
 * Count media entries in a backup ZIP.
 *
 * @param string $zip_path ZIP path.
 * @return int
 */
function maca_menulist_backup_import_count_zip_media($zip_path) {
    if (!class_exists('ZipArchive') || !is_readable($zip_path)) {
        return 0;
    }

    $zip = new ZipArchive();
    if ($zip->open($zip_path) !== true) {
        return 0;
    }

    $count = 0;

    for ($index = 0; $index < $zip->numFiles; $index++) {
        $entry = $zip->getNameIndex($index);
        if (!is_string($entry) || strpos($entry, 'media/') !== 0) {
            continue;
        }

        $relative = substr($entry, 6);
        if ($relative === '' || substr($entry, -1) === '/') {
            continue;
        }

        $count++;
    }

    $zip->close();

    return $count;
}

/**
 * Restore the next media batch from a backup ZIP.
 *
 * @param string               $zip_path   ZIP path.
 * @param array<string, mixed> $state      Job state (mutated).
 * @param string               $job_id     Progress job id.
 * @return bool True when media phase is complete.
 */
function maca_menulist_backup_import_media_batch($zip_path, &$state, $job_id) {
    if (!class_exists('ZipArchive') || !is_readable($zip_path)) {
        return true;
    }

    $zip = new ZipArchive();
    if ($zip->open($zip_path) !== true) {
        return true;
    }

    $upload = wp_upload_dir();
    $basedir = trailingslashit(wp_normalize_path($upload['basedir']));
    $media_entries = array();

    for ($index = 0; $index < $zip->numFiles; $index++) {
        $entry = $zip->getNameIndex($index);
        if (!is_string($entry) || strpos($entry, 'media/') !== 0) {
            continue;
        }

        $relative = substr($entry, 6);
        if ($relative === '' || substr($entry, -1) === '/') {
            continue;
        }

        $media_entries[] = array(
            'entry'    => $entry,
            'relative' => $relative,
            'index'    => $index,
        );
    }

    $zip->close();

    $media_total = count($media_entries);
    $state['media_total'] = $media_total;
    $start = (int) $state['media_index'];
    $end = min($media_total, $start + MACA_MENULIST_BACKUP_IMPORT_MEDIA_BATCH);

    if ($media_total === 0) {
        return true;
    }

    $zip = new ZipArchive();
    $zip->open($zip_path);

    for ($i = $start; $i < $end; $i++) {
        $item = $media_entries[$i];
        $contents = $zip->getFromIndex($item['index']);
        if ($contents === false) {
            continue;
        }

        $relative = str_replace('\\', '/', $item['relative']);
        $destination = $basedir . str_replace('/', DIRECTORY_SEPARATOR, $relative);
        wp_mkdir_p(dirname($destination));

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
        if (file_put_contents($destination, $contents) !== false) {
            $state['media_restored'] = (int) $state['media_restored'] + 1;
        }
    }

    $zip->close();

    $state['media_index'] = $end;

    if ($job_id !== '') {
        $legacy_percent = 8 + (int) floor(20 * $end / max(1, $media_total));
        maca_menulist_backup_set_import_job_progress(
            $job_id,
            $legacy_percent,
            sprintf(
                /* translators: 1: current file number, 2: total files */
                __('Återställer media %1$d av %2$d…', 'maca-restu-pro'),
                $end,
                $media_total
            )
        );
    }

    return $end >= $media_total;
}

/**
 * Run one chunked import step (short request — progress updates between calls).
 *
 * @param string $job_id Client job id.
 * @return array{done: bool, success: bool, percent: int, stage: string, message: string}
 */
function maca_menulist_backup_import_run_step($job_id) {
    global $wpdb;

    maca_menulist_backup_raise_limits();

    if (function_exists('session_status') && session_status() === PHP_SESSION_ACTIVE) {
        // phpcs:ignore WordPress.PHP.NoSilencedFunctions.Discouraged
        @session_write_close();
    }

    $state = maca_menulist_backup_import_load_job_state($job_id);
    if (!$state || (int) ($state['user_id'] ?? 0) !== get_current_user_id()) {
        return array(
            'done'    => true,
            'success' => false,
            'percent' => 0,
            'stage'   => '',
            'message' => __('Ogiltigt eller utgånget importjobb.', 'maca-restu-pro'),
        );
    }

    $phase = isset($state['phase']) ? (string) $state['phase'] : '';

    if ($phase === 'read') {
        maca_menulist_backup_set_import_job_progress(
            $job_id,
            2,
            __('Läser säkerhetskopiefilen…', 'maca-restu-pro')
        );

        $read = maca_menulist_backup_read_file($state['backup_path'], $state['filename_hint']);
        if (!$read['payload']) {
            maca_menulist_backup_import_cleanup_job($job_id);
            maca_menulist_backup_clear_import_progress($job_id);

            return array(
                'done'    => true,
                'success' => false,
                'percent' => 0,
                'stage'   => '',
                'message' => $read['error'] !== ''
                    ? $read['error']
                    : __('Could not read backup file.', 'maca-restu-pro'),
            );
        }

        maca_menulist_backup_import_save_job_payload($job_id, $read['payload']);
        $read_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            8,
            __('Säkerhetskopiefil inläst.', 'maca-restu-pro')
        );

        $state['phase'] = ($state['extension'] === 'zip') ? 'media' : 'schema';
        if ($state['phase'] === 'media') {
            $state['media_total'] = maca_menulist_backup_import_count_zip_media($state['backup_path']);
        }

        maca_menulist_backup_import_save_job_state($job_id, $state);

        return array(
            'done'    => false,
            'success' => true,
            'percent' => $read_percent,
            'stage'   => __('Säkerhetskopiefil inläst.', 'maca-restu-pro'),
            'message' => '',
        );
    }

    if ($phase === 'media') {
        $done = maca_menulist_backup_import_media_batch($state['backup_path'], $state, $job_id);
        $state['phase'] = $done ? 'schema' : 'media';
        maca_menulist_backup_import_save_job_state($job_id, $state);

        $progress = maca_menulist_backup_get_import_progress($job_id);
        $fallback = maca_menulist_backup_import_display_percent(8);

        return array(
            'done'    => false,
            'success' => true,
            'percent' => isset($progress['percent']) ? (int) $progress['percent'] : $fallback,
            'stage'   => isset($progress['stage']) ? (string) $progress['stage'] : '',
            'message' => '',
        );
    }

    if ($phase === 'schema') {
        delete_transient('maca_menulist_db_migrating');

        if (maca_menulist_backup_import_can_skip_schema_phase()) {
            $state['phase'] = 'prepare';
            maca_menulist_backup_import_save_job_state($job_id, $state);
            $skip_percent = maca_menulist_backup_set_import_job_progress(
                $job_id,
                46,
                __('Databas redan klar — förbereder import…', 'maca-restu-pro')
            );

            return array(
                'done'    => false,
                'success' => true,
                'percent' => $skip_percent,
                'stage'   => __('Databas redan klar — förbereder import…', 'maca-restu-pro'),
                'message' => '',
            );
        }

        $steps = maca_menulist_backup_get_import_schema_step_items();
        $index = (int) $state['schema_index'];
        $total = max(1, count($steps));

        if ($index < count($steps)) {
            $item = $steps[$index];
            $progress = maca_menulist_backup_import_schema_progress($index, $total, $item['label']);
            $working_stage = $progress['stage'] . ' ' . __('(pågår)', 'maca-restu-pro');
            maca_menulist_backup_set_import_progress($job_id, $progress['percent'], $working_stage);

            try {
                call_user_func($item['run']);
            } catch (Throwable $e) {
                if (function_exists('maca_menulist_debug_log')) {
                    maca_menulist_debug_log('import schema step failed: ' . $e->getMessage());
                }
            }

            $state['schema_index'] = $index + 1;
            maca_menulist_backup_import_save_job_state($job_id, $state);
            maca_menulist_backup_set_import_progress($job_id, $progress['percent'], $progress['stage']);

            return array(
                'done'    => false,
                'success' => true,
                'percent' => $progress['percent'],
                'stage'   => $progress['stage'],
                'message' => '',
            );
        }

        if (function_exists('maca_menulist_db_migrate_schema_mark_all_steps_completed')) {
            maca_menulist_db_migrate_schema_mark_all_steps_completed();
        }
        if (function_exists('maca_menulist_db_migrate_schema_finalize_version')) {
            maca_menulist_db_migrate_schema_finalize_version();
        }

        $state['phase'] = 'prepare';
        maca_menulist_backup_import_save_job_state($job_id, $state);
        $schema_done_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            46,
            __('Förbereder import…', 'maca-restu-pro')
        );

        return array(
            'done'    => false,
            'success' => true,
            'percent' => $schema_done_percent,
            'stage'   => __('Förbereder import…', 'maca-restu-pro'),
            'message' => '',
        );
    }

    if ($phase === 'prepare') {
        $payload = maca_menulist_backup_import_get_job_payload($job_id);
        if (!$payload) {
            return array(
                'done'    => true,
                'success' => false,
                'percent' => 0,
                'stage'   => '',
                'message' => __('Backup file contains no table data.', 'maca-restu-pro'),
            );
        }

        $format = isset($payload['format']) ? (int) $payload['format'] : 0;
        if ($format < 1 || $format > MACA_MENULIST_BACKUP_FORMAT) {
            return array(
                'done'    => true,
                'success' => false,
                'percent' => 0,
                'stage'   => '',
                'message' => __('Unsupported backup format.', 'maca-restu-pro'),
            );
        }

        if (empty($payload['tables']) || !is_array($payload['tables'])) {
            return array(
                'done'    => true,
                'success' => false,
                'percent' => 0,
                'stage'   => '',
                'message' => __('Backup file contains no table data.', 'maca-restu-pro'),
            );
        }

        $url_replacements = maca_menulist_backup_get_url_replacements($payload);
        if (!empty($url_replacements) && !empty($payload['options']) && is_array($payload['options'])) {
            maca_menulist_backup_set_import_job_progress(
                $job_id,
                46,
                __('Uppdaterar URL:er i inställningar…', 'maca-restu-pro')
            );
            $payload['options'] = maca_menulist_backup_apply_url_replacements($payload['options'], $url_replacements);
            maca_menulist_backup_import_save_job_payload($job_id, $payload);
        }

        $tables_payload = maca_menulist_backup_normalize_tables_payload($payload['tables']);
        $discovered_tables = maca_menulist_backup_discover_tables(false);
        $delete_keys = maca_menulist_backup_sort_table_keys_for_delete(array_keys($discovered_tables));
        $insert_keys = array_reverse($delete_keys);
        $skipped_tables = array();

        if (!empty($payload['skipped_tables']) && is_array($payload['skipped_tables'])) {
            $skipped_tables = array_map('strval', $payload['skipped_tables']);
        }

        $import_table_keys = array();
        foreach ($delete_keys as $key) {
            if (!in_array($key, $skipped_tables, true) && isset($discovered_tables[$key])) {
                $import_table_keys[] = $key;
            }
        }

        maca_menulist_backup_import_save_job_tables_payload($job_id, $tables_payload);

        $state['url_replacements'] = $url_replacements;
        $state['discovered_tables'] = $discovered_tables;
        $state['delete_keys'] = $delete_keys;
        $state['insert_keys'] = $insert_keys;
        $state['import_table_keys'] = $import_table_keys;
        $state['skipped_tables'] = $skipped_tables;
        $state['table_total'] = max(1, count($import_table_keys));
        $state['table_step'] = 0;
        $state['phase'] = 'delete';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query('SET FOREIGN_KEY_CHECKS=0');

        maca_menulist_backup_import_save_job_state($job_id, $state);
        $prepare_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            46,
            __('Förbereder import…', 'maca-restu-pro')
        );

        return array(
            'done'    => false,
            'success' => true,
            'percent' => $prepare_percent,
            'stage'   => __('Förbereder import…', 'maca-restu-pro'),
            'message' => '',
        );
    }

    if ($phase === 'delete') {
        $delete_keys = isset($state['delete_keys']) && is_array($state['delete_keys']) ? $state['delete_keys'] : array();
        $skipped_tables = isset($state['skipped_tables']) && is_array($state['skipped_tables']) ? $state['skipped_tables'] : array();
        $discovered_tables = isset($state['discovered_tables']) && is_array($state['discovered_tables']) ? $state['discovered_tables'] : array();
        $table_total = max(1, (int) ($state['table_total'] ?? 1));
        $index = (int) $state['delete_index'];

        while ($index < count($delete_keys)) {
            $key = $delete_keys[$index];
            $index++;

            if (in_array($key, $skipped_tables, true) || !isset($discovered_tables[$key])) {
                $state['delete_index'] = $index;
                continue;
            }

            $state['table_step'] = (int) $state['table_step'] + 1;
            $legacy_percent = 47 + (int) floor(8 * (int) $state['table_step'] / $table_total);
            $delete_stage = sprintf(
                /* translators: %s: table key */
                __('Rensar %s…', 'maca-restu-pro'),
                $key
            );
            $display_percent = maca_menulist_backup_set_import_job_progress(
                $job_id,
                $legacy_percent,
                $delete_stage
            );

            $table = $discovered_tables[$key];
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->query("DELETE FROM {$table}");

            $state['delete_index'] = $index;
            $state['phase'] = ($index >= count($delete_keys)) ? 'insert' : 'delete';
            maca_menulist_backup_import_save_job_state($job_id, $state);

            return array(
                'done'    => false,
                'success' => true,
                'percent' => $display_percent,
                'stage'   => $delete_stage,
                'message' => '',
            );
        }

        $state['phase'] = 'insert';
        maca_menulist_backup_import_save_job_state($job_id, $state);

        return maca_menulist_backup_import_run_step($job_id);
    }

    if ($phase === 'insert') {
        $insert_keys = isset($state['insert_keys']) && is_array($state['insert_keys']) ? $state['insert_keys'] : array();
        $tables_payload = maca_menulist_backup_import_get_job_tables_payload($job_id);
        if (!is_array($tables_payload)) {
            $tables_payload = array();
        }
        $discovered_tables = isset($state['discovered_tables']) && is_array($state['discovered_tables']) ? $state['discovered_tables'] : array();
        $url_replacements = isset($state['url_replacements']) && is_array($state['url_replacements']) ? $state['url_replacements'] : array();
        $table_total = max(1, (int) ($state['table_total'] ?? 1));
        $table_index = (int) $state['insert_table_index'];

        while ($table_index < count($insert_keys)) {
            $key = $insert_keys[$table_index];

            if (empty($tables_payload[$key]) || !is_array($tables_payload[$key]) || !isset($discovered_tables[$key])) {
                $table_index++;
                $state['insert_table_index'] = $table_index;
                $state['insert_row_offset'] = 0;
                continue;
            }

            $rows = array_values(array_filter($tables_payload[$key], 'is_array'));
            $row_total = count($rows);
            $offset = (int) $state['insert_row_offset'];
            $batch_end = min($row_total, $offset + MACA_MENULIST_BACKUP_IMPORT_INSERT_BATCH);

            if ($offset === 0) {
                $state['table_step'] = (int) $state['table_step'] + 1;
            }

            $legacy_percent = 55 + (int) floor(33 * (int) $state['table_step'] / $table_total);
            $table = $discovered_tables[$key];

            for ($i = $offset; $i < $batch_end; $i++) {
                $row = $rows[$i];
                if (!empty($url_replacements)) {
                    $row = maca_menulist_backup_apply_url_replacements($row, $url_replacements);
                }
                $wpdb->insert($table, $row);
            }

            $stage = ($row_total > 50)
                ? sprintf(
                    /* translators: 1: table key, 2: current row, 3: total rows */
                    __('Importerar %1$s… %2$d/%3$d rader', 'maca-restu-pro'),
                    $key,
                    $batch_end,
                    $row_total
                )
                : sprintf(
                    /* translators: %s: table key */
                    __('Importerar %s…', 'maca-restu-pro'),
                    $key
                );

            $display_percent = maca_menulist_backup_set_import_job_progress(
                $job_id,
                min(88, $legacy_percent),
                $stage
            );

            if ($batch_end < $row_total) {
                $state['insert_row_offset'] = $batch_end;
                maca_menulist_backup_import_save_job_state($job_id, $state);

                return array(
                    'done'    => false,
                    'success' => true,
                    'percent' => $display_percent,
                    'stage'   => $stage,
                    'message' => '',
                );
            }

            $table_index++;
            $state['insert_table_index'] = $table_index;
            $state['insert_row_offset'] = 0;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query('SET FOREIGN_KEY_CHECKS=1');

        $state['phase'] = 'options';
        maca_menulist_backup_import_save_job_state($job_id, $state);
        $options_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            90,
            __('Importerar inställningar…', 'maca-restu-pro')
        );

        return array(
            'done'    => false,
            'success' => true,
            'percent' => $options_percent,
            'stage'   => __('Importerar inställningar…', 'maca-restu-pro'),
            'message' => '',
        );
    }

    if ($phase === 'options') {
        $payload = maca_menulist_backup_import_get_job_payload($job_id);
        $options_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            90,
            __('Importerar inställningar…', 'maca-restu-pro')
        );

        if (is_array($payload) && !empty($payload['options']) && is_array($payload['options'])) {
            $excluded_options = array_flip(maca_menulist_backup_get_excluded_option_keys());
            $option_prefixes = maca_menulist_backup_get_option_prefixes();

            foreach ($payload['options'] as $option_key => $value) {
                if (!is_string($option_key) || isset($excluded_options[$option_key])) {
                    continue;
                }

                $allowed = false;
                foreach ($option_prefixes as $prefix) {
                    if (strpos($option_key, $prefix) === 0) {
                        $allowed = true;
                        break;
                    }
                }

                if ($allowed) {
                    update_option($option_key, $value);
                }
            }
        }

        $state['phase'] = 'finalize';
        maca_menulist_backup_import_save_job_state($job_id, $state);
        $finalize_percent = maca_menulist_backup_set_import_job_progress(
            $job_id,
            98,
            __('Slutför import…', 'maca-restu-pro')
        );

        return array(
            'done'    => false,
            'success' => true,
            'percent' => $finalize_percent,
            'stage'   => __('Slutför import…', 'maca-restu-pro'),
            'message' => '',
        );
    }

    if ($phase === 'finalize') {
        update_option('maca_menulist_db_version', MACA_MENULIST_VERSION);
        maca_menulist_flush_rewrite_rules();

        $message = __('Backup imported successfully.', 'maca-restu-pro');
        $media_restored = (int) ($state['media_restored'] ?? 0);
        if ($media_restored > 0) {
            $message = sprintf(
                /* translators: %d: number of media files restored */
                __('Backup imported successfully. %d media files restored.', 'maca-restu-pro'),
                $media_restored
            );
        }

        maca_menulist_backup_set_import_progress($job_id, 100, $message);
        maca_menulist_backup_import_cleanup_job($job_id);

        return array(
            'done'    => true,
            'success' => true,
            'percent' => 100,
            'stage'   => $message,
            'message' => $message,
        );
    }

    maca_menulist_backup_import_cleanup_job($job_id);

    return array(
        'done'    => true,
        'success' => false,
        'percent' => 0,
        'stage'   => '',
        'message' => __('Importen avbröts oväntet.', 'maca-restu-pro'),
    );
}

/**
 * Restore plugin data and media from a backup file path.
 *
 * @param string $path          Absolute or temporary backup file path.
 * @param string $filename_hint Original uploaded filename (for temp uploads).
 * @param string $job_id        Optional progress job id for AJAX polling.
 * @return array{success: bool, message: string}
 */
function maca_menulist_backup_restore_from_file($path, $filename_hint = '', $job_id = '') {
    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            2,
            __('Läser säkerhetskopiefilen…', 'maca-restu-pro')
        );
    }

    $read = maca_menulist_backup_read_file($path, $filename_hint);
    $payload = $read['payload'];

    if (!$payload) {
        if ($job_id !== '') {
            maca_menulist_backup_clear_import_progress($job_id);
        }

        return array(
            'success' => false,
            'message' => $read['error'] !== ''
                ? $read['error']
                : __('Could not read backup file.', 'maca-restu-pro'),
        );
    }

    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            8,
            __('Säkerhetskopiefil inläst.', 'maca-restu-pro')
        );
    }

    $media_restored = 0;
    $extension = maca_menulist_backup_detect_file_type($path, $filename_hint);
    if ($extension === 'zip') {
        $media_restored = maca_menulist_backup_extract_media_from_zip($path, $job_id);
    }

    $result = maca_menulist_backup_import_payload($payload, $job_id);

    if ($job_id !== '') {
        if ($result['success']) {
            maca_menulist_backup_set_import_progress(
                $job_id,
                100,
                $result['message']
            );
        } else {
            maca_menulist_backup_clear_import_progress($job_id);
        }
    }

    if ($result['success'] && $media_restored > 0) {
        $result['message'] = sprintf(
            /* translators: %d: number of media files restored */
            __('Backup imported successfully. %d media files restored.', 'maca-restu-pro'),
            $media_restored
        );
    }

    return $result;
}

/**
 * Create core menu/category tables (subset of maca_menulist_db_create_tables).
 *
 * @return void
 */
function maca_menulist_backup_import_create_core_tables() {
    global $wpdb;

    if (!isset($wpdb) || empty($wpdb)) {
        return;
    }

    $charset_collate = $wpdb->get_charset_collate();
    $categories_table = maca_menulist_db_categories_table();
    $menus_table = maca_menulist_db_menus_table();

    $sql_categories = "CREATE TABLE $categories_table (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        name varchar(255) NOT NULL,
        thumbnail_url varchar(500) DEFAULT NULL,
        description text DEFAULT NULL,
        sort_order int(11) DEFAULT 0,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    $sql_menus = "CREATE TABLE $menus_table (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        category_id mediumint(9) NOT NULL,
        title varchar(255) NOT NULL,
        file_url varchar(500) NOT NULL,
        file_type varchar(50) DEFAULT 'image',
        start_date datetime DEFAULT NULL,
        end_date datetime DEFAULT NULL,
        time_start time DEFAULT NULL,
        time_end time DEFAULT NULL,
        days_of_week varchar(32) DEFAULT NULL,
        is_active tinyint(1) DEFAULT 1,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY category_id (category_id)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql_categories);
    dbDelta($sql_menus);
}

/**
 * Human-readable labels for schema migration steps during import.
 *
 * @return array<string, string>
 */
function maca_menulist_backup_get_import_schema_migrate_labels() {
    return array(
        'dish_category_columns' => __('Rättkategorier…', 'maca-restu-pro'),
        'bootstrap_tables'      => __('Kontrollerar grundtabeller…', 'maca-restu-pro'),
        'menu_category_sort'    => __('Menykategorier…', 'maca-restu-pro'),
        'menu_schedule'         => __('Menyschema…', 'maca-restu-pro'),
        'dish_tables'           => __('Rätttabeller…', 'maca-restu-pro'),
        'campaign_tables'       => __('Kampanjtabeller…', 'maca-restu-pro'),
        'dish_ratings'          => __('Betyg…', 'maca-restu-pro'),
        'allergen_columns'      => __('Allergener…', 'maca-restu-pro'),
        'allergen_seed'         => __('Standardallergener…', 'maca-restu-pro'),
        'dish_schedule'         => __('Rättschema…', 'maca-restu-pro'),
        'dish_category_schedule' => __('Rättkategorischema…', 'maca-restu-pro'),
        'dish_vat'              => __('Momskoder…', 'maca-restu-pro'),
        'dish_takeaway'         => __('Takeaway…', 'maca-restu-pro'),
        'dish_views'            => __('Visningsstatistik…', 'maca-restu-pro'),
        'table_orders'          => __('Bordsbeställningar…', 'maca-restu-pro'),
        'table_bookings'        => __('Bordsbokningar…', 'maca-restu-pro'),
        'booking_day_split'     => __('Bokningsdagar…', 'maca-restu-pro'),
        'bilingual_columns'     => __('Flerspråk…', 'maca-restu-pro'),
        'attendance_tables'     => __('Personalliggare…', 'maca-restu-pro'),
        'info_hub_tables'       => __('Informationsdisk…', 'maca-restu-pro'),
        'info_hub_social_log'   => __('Informationsdisk – social logg…', 'maca-restu-pro'),
    );
}

/**
 * Table-creation steps shown during import (mirrors maca_menulist_db_create_tables).
 *
 * @return array<int, array{0: string, 1: callable}>
 */
function maca_menulist_backup_get_import_create_table_steps() {
    if (maca_menulist_backup_import_target_has_plugin_tables()) {
        return array();
    }

    $includes = dirname(__FILE__);

    return array(
        array(
            __('Menyer och kategorier…', 'maca-restu-pro'),
            static function () {
                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_categories_table(),
                    static function () {
                        maca_menulist_backup_import_create_core_tables();
                    }
                );
            },
        ),
        array(
            __('Rätter och menyer…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-dishes.php';

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_dishes_table(),
                    static function () {
                        maca_menulist_db_create_dish_tables();
                    }
                );
            },
        ),
        array(
            __('Kampanjer…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-campaigns.php';

                if (!function_exists('maca_menulist_db_campaigns_table')) {
                    maca_menulist_db_create_campaign_tables();
                    return;
                }

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_campaigns_table(),
                    static function () {
                        maca_menulist_db_create_campaign_tables();
                    }
                );
            },
        ),
        array(
            __('Visningsstatistik…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-dish-stats.php';

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_dish_views_table(),
                    static function () {
                        maca_menulist_db_create_dish_views_table();
                    }
                );
            },
        ),
        array(
            __('Bordsbeställningar…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-table-orders.php';

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_table_orders_table(),
                    static function () {
                        maca_menulist_db_create_table_order_tables();
                    }
                );
            },
        ),
        array(
            __('Bordsbokningar…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-table-bookings.php';

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_bookings_table(),
                    static function () {
                        maca_menulist_db_create_table_booking_tables();
                    }
                );
            },
        ),
        array(
            __('Servitörer…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-waiters.php';

                if (!function_exists('maca_menulist_db_waiters_table')) {
                    maca_menulist_db_create_waiter_tables();
                    return;
                }

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_waiters_table(),
                    static function () {
                        maca_menulist_db_create_waiter_tables();
                    }
                );
            },
        ),
        array(
            __('Personalliggare…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/attendance-log.php';
                require_once $includes . '/db-attendance.php';

                if (!function_exists('maca_menulist_db_employees_table')) {
                    maca_menulist_db_create_attendance_tables();
                    return;
                }

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_employees_table(),
                    static function () {
                        maca_menulist_db_create_attendance_tables();
                    }
                );
            },
        ),
        array(
            __('Informationsdisk…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-info-hub.php';

                if (!function_exists('maca_menulist_db_info_news_table')) {
                    maca_menulist_db_create_info_hub_tables();
                    return;
                }

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_info_news_table(),
                    static function () {
                        maca_menulist_db_create_info_hub_tables();
                    }
                );
            },
        ),
        array(
            __('Informationsdisk – social logg…', 'maca-restu-pro'),
            static function () use ($includes) {
                require_once $includes . '/db-info-hub-social.php';

                if (!function_exists('maca_menulist_db_info_social_log_table')) {
                    maca_menulist_db_create_info_social_log_table();
                    return;
                }

                maca_menulist_backup_import_create_table_if_missing(
                    maca_menulist_db_info_social_log_table(),
                    static function () {
                        maca_menulist_db_create_info_social_log_table();
                    }
                );
            },
        ),
    );
}

/**
 * Ensure database schema during import with per-step progress (28–35 %).
 *
 * @param string $job_id Optional progress job id.
 * @return void
 */
function maca_menulist_backup_ensure_schema_for_import($job_id = '') {
    delete_transient('maca_menulist_db_migrating');

    $step_items = maca_menulist_backup_get_import_schema_step_items();
    $total = max(1, count($step_items));
    $prep_start = 28;
    $prep_end = 35;

    foreach ($step_items as $index => $item) {
        if ($job_id !== '') {
            $percent = $prep_start + (int) floor(($prep_end - $prep_start) * ($index + 1) / $total);
            maca_menulist_backup_set_import_progress($job_id, $percent, $item['label']);
        }

        try {
            call_user_func($item['run']);
        } catch (Throwable $e) {
            if (function_exists('maca_menulist_debug_log')) {
                maca_menulist_debug_log('import schema step failed: ' . $e->getMessage());
            }
        }
    }

    if (function_exists('maca_menulist_db_migrate_schema_mark_all_steps_completed')) {
        maca_menulist_db_migrate_schema_mark_all_steps_completed();
    }

    if (function_exists('maca_menulist_db_migrate_schema_finalize_version')) {
        maca_menulist_db_migrate_schema_finalize_version();
    }
}

/**
 * Import backup payload into database and options.
 *
 * @param array<string, mixed> $payload Backup payload.
 * @param string               $job_id  Optional progress job id.
 * @return array{success: bool, message: string}
 */
function maca_menulist_backup_import_payload($payload, $job_id = '') {
    global $wpdb;

    $format = isset($payload['format']) ? (int) $payload['format'] : 0;
    if ($format < 1 || $format > MACA_MENULIST_BACKUP_FORMAT) {
        return array(
            'success' => false,
            'message' => __('Unsupported backup format.', 'maca-restu-pro'),
        );
    }

    if (empty($payload['tables']) || !is_array($payload['tables'])) {
        return array(
            'success' => false,
            'message' => __('Backup file contains no table data.', 'maca-restu-pro'),
        );
    }

    maca_menulist_backup_ensure_schema_for_import($job_id);

    $url_replacements = maca_menulist_backup_get_url_replacements($payload);
    if (!empty($url_replacements) && !empty($payload['options']) && is_array($payload['options'])) {
        if ($job_id !== '') {
            maca_menulist_backup_set_import_progress(
                $job_id,
                36,
                __('Uppdaterar URL:er i inställningar…', 'maca-restu-pro')
            );
        }

        $payload['options'] = maca_menulist_backup_apply_url_replacements($payload['options'], $url_replacements);
    }

    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            36,
            __('Förbereder import…', 'maca-restu-pro')
        );
    }

    $tables_payload = maca_menulist_backup_normalize_tables_payload($payload['tables']);
    $discovered_tables = maca_menulist_backup_discover_tables(false);
    $delete_keys = maca_menulist_backup_sort_table_keys_for_delete(array_keys($discovered_tables));
    $insert_keys = array_reverse($delete_keys);
    $excluded_options = array_flip(maca_menulist_backup_get_excluded_option_keys());
    $skipped_tables = array();

    if (!empty($payload['skipped_tables']) && is_array($payload['skipped_tables'])) {
        $skipped_tables = array_map('strval', $payload['skipped_tables']);
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query('SET FOREIGN_KEY_CHECKS=0');

    $import_table_keys = array();
    foreach ($delete_keys as $key) {
        if (!in_array($key, $skipped_tables, true) && isset($discovered_tables[$key])) {
            $import_table_keys[] = $key;
        }
    }

    $table_total = max(1, count($import_table_keys));
    $table_step = 0;

    foreach ($delete_keys as $key) {
        if (in_array($key, $skipped_tables, true)) {
            continue;
        }

        if (!isset($discovered_tables[$key])) {
            continue;
        }

        $table_step++;
        if ($job_id !== '') {
            $percent = 36 + (int) floor(19 * $table_step / $table_total);
            maca_menulist_backup_set_import_progress(
                $job_id,
                $percent,
                sprintf(
                    /* translators: %s: table key */
                    __('Rensar %s…', 'maca-restu-pro'),
                    $key
                )
            );
        }

        $table = $discovered_tables[$key];
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $wpdb->query("DELETE FROM {$table}");
    }

    foreach ($insert_keys as $key) {
        if (empty($tables_payload[$key]) || !is_array($tables_payload[$key])) {
            continue;
        }
        if (!isset($discovered_tables[$key])) {
            continue;
        }

        $table_step++;
        if ($job_id !== '') {
            $percent = 55 + (int) floor(33 * $table_step / $table_total);
            maca_menulist_backup_set_import_progress(
                $job_id,
                min(88, $percent),
                sprintf(
                    /* translators: %s: table key */
                    __('Importerar %s…', 'maca-restu-pro'),
                    $key
                )
            );
        }

        $table = $discovered_tables[$key];
        $rows = $tables_payload[$key];
        $row_total = count($rows);
        $row_done = 0;

        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }

            if (!empty($url_replacements)) {
                $row = maca_menulist_backup_apply_url_replacements($row, $url_replacements);
            }

            $wpdb->insert($table, $row);
            $row_done++;

            if ($job_id !== '' && $row_total > 50 && ($row_done % 50 === 0 || $row_done === $row_total)) {
                maca_menulist_backup_set_import_progress(
                    $job_id,
                    min(88, $percent),
                    sprintf(
                        /* translators: 1: table key, 2: current row, 3: total rows */
                        __('Importerar %1$s… %2$d/%3$d rader', 'maca-restu-pro'),
                        $key,
                        $row_done,
                        $row_total
                    )
                );
            }
        }
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query('SET FOREIGN_KEY_CHECKS=1');

    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            90,
            __('Importerar inställningar…', 'maca-restu-pro')
        );
    }

    if (!empty($payload['options']) && is_array($payload['options'])) {
        $option_prefixes = maca_menulist_backup_get_option_prefixes();

        foreach ($payload['options'] as $option_key => $value) {
            if (!is_string($option_key) || isset($excluded_options[$option_key])) {
                continue;
            }

            $allowed = false;
            foreach ($option_prefixes as $prefix) {
                if (strpos($option_key, $prefix) === 0) {
                    $allowed = true;
                    break;
                }
            }

            if ($allowed) {
                update_option($option_key, $value);
            }
        }
    }

    update_option('maca_menulist_db_version', MACA_MENULIST_VERSION);
    maca_menulist_flush_rewrite_rules();

    if ($job_id !== '') {
        maca_menulist_backup_set_import_progress(
            $job_id,
            98,
            __('Slutför import…', 'maca-restu-pro')
        );
    }

    return array(
        'success' => true,
        'message' => __('Backup imported successfully.', 'maca-restu-pro'),
    );
}

/**
 * Cron callback.
 */
function maca_menulist_backup_run_cron() {
    if (get_option('maca_menulist_backup_daily_enabled', '0') !== '1') {
        return;
    }

    maca_menulist_backup_create_file('cron');
}

/**
 * Next daily run timestamp in site timezone.
 *
 * @return int
 */
function maca_menulist_backup_next_run_timestamp() {
    $time = get_option('maca_menulist_backup_daily_time', '03:00');
    if (!is_string($time) || !preg_match('/^\d{2}:\d{2}$/', $time)) {
        $time = '03:00';
    }

    $timezone = wp_timezone();
    $now = new DateTime('now', $timezone);
    $parts = explode(':', $time);
    $scheduled = clone $now;
    $scheduled->setTime((int) $parts[0], (int) $parts[1], 0);

    if ($scheduled <= $now) {
        $scheduled->modify('+1 day');
    }

    return $scheduled->getTimestamp();
}

/**
 * Schedule daily backup cron if enabled.
 */
function maca_menulist_backup_schedule_cron() {
    maca_menulist_backup_unschedule_cron();

    if (get_option('maca_menulist_backup_daily_enabled', '0') !== '1') {
        return;
    }

    wp_schedule_event(
        maca_menulist_backup_next_run_timestamp(),
        'daily',
        MACA_MENULIST_BACKUP_CRON_HOOK
    );
}

/**
 * Clear scheduled backup cron.
 */
function maca_menulist_backup_unschedule_cron() {
    wp_clear_scheduled_hook(MACA_MENULIST_BACKUP_CRON_HOOK);
}

/**
 * Reschedule when settings change.
 */
function maca_menulist_backup_maybe_schedule_cron() {
    if (get_option('maca_menulist_backup_daily_enabled', '0') !== '1') {
        maca_menulist_backup_unschedule_cron();
        return;
    }

    if (!wp_next_scheduled(MACA_MENULIST_BACKUP_CRON_HOOK)) {
        maca_menulist_backup_schedule_cron();
    }
}
