#!/usr/local/cpanel/3rdparty/bin/perl
# gniza4cp WHM Plugin — Restore
# Step-by-step restore workflow with dynamic dropdowns
use strict;
use warnings;

use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza4cp-whm/lib';

use Whostmgr::HTMLInterface ();
use Cpanel::Form            ();
use Gniza4cpWHM::Config;
use Gniza4cpWHM::Runner;
use Gniza4cpWHM::UI;

my $form   = Cpanel::Form::parseform();
my $method = $ENV{'REQUEST_METHOD'} // 'GET';
my $step   = $form->{'restore_step'} // '1';

my %TYPE_LABELS = (
    account  => 'Full Account',
    files    => 'Files',
    database => 'Database',
    mailbox  => 'Mailbox',
    cron     => 'Cron Jobs',
    dbusers  => 'Database Users & Grants',
    cpconfig => 'Panel Config',
    domains  => 'Domains',
    ssl      => 'SSL Certificates',
);

my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron cpconfig);

if    ($step eq 'fetch_options') { handle_fetch_options() }
elsif ($step eq 'fetch_accounts') { handle_fetch_accounts() }
elsif ($step eq '2') { handle_step2() }
elsif ($step eq '3') { handle_step3() }
elsif ($step eq '4') { handle_step4() }
else                 { handle_step1() }

exit;

# ── Helpers ───────────────────────────────────────────────────

sub _uri_escape {
    my $str = shift // '';
    $str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge;
    return $str;
}

# ── JSON endpoint: fetch database/mailbox options ─────────────

sub handle_fetch_options {
    my $remote    = $form->{'remote'}    // '';
    my $account   = $form->{'account'}   // '';
    my $timestamp = $form->{'timestamp'} // '';
    my $type      = $form->{'type'}      // '';

    print "Content-Type: application/json\r\n\r\n";

    if ($remote eq '' || $account eq '' || $timestamp eq '' || $type eq '') {
        print qq({"error":"Missing required parameters"});
        return;
    }

    my $subcmd;
    my %extra_opts;
    if    ($type eq 'database') { $subcmd = 'list-databases' }
    elsif ($type eq 'mailbox')  { $subcmd = 'list-mailboxes' }
    elsif ($type eq 'dbusers')  { $subcmd = 'list-dbusers' }
    elsif ($type eq 'cron')     { $subcmd = 'list-cron' }
    elsif ($type eq 'domains')  { $subcmd = 'list-dns' }
    elsif ($type eq 'ssl')      { $subcmd = 'list-ssl' }
    elsif ($type eq 'files')    {
        $subcmd = 'list-files';
        my $path = $form->{'path'} // '';
        $extra_opts{path} = $path if $path ne '';
    }
    else {
        print qq({"error":"Invalid type"});
        return;
    }

    my ($ok, $stdout, $stderr) = Gniza4cpWHM::Runner::run(
        'restore', $subcmd, [$account],
        { remote => $remote, timestamp => $timestamp, %extra_opts }
    );

    unless ($ok) {
        my $msg = $stderr // 'Failed to list options';
        $msg =~ s/\\/\\\\/g;
        $msg =~ s/"/\\"/g;
        $msg =~ s/\n/\\n/g;
        $msg =~ s/\r/\\r/g;
        print qq({"error":"$msg"});
        return;
    }

    my @options;
    for my $line (split /\n/, $stdout) {
        $line =~ s/^\s+|\s+$//g;
        next unless $line ne '';
        push @options, $line;
    }

    # Build JSON array manually (no JSON module dependency)
    my $json_arr = join(',', map { my $v = $_; $v =~ s/\\/\\\\/g; $v =~ s/"/\\"/g; qq("$v") } @options);
    print qq({"options":[$json_arr]});
}

# ── JSON endpoint: fetch accounts from remote ──────────────────

sub handle_fetch_accounts {
    my $remote = $form->{'remote'} // '';

    print "Content-Type: application/json\r\n\r\n";

    if ($remote eq '') {
        print qq({"error":"Remote is required"});
        return;
    }

    my ($ok, $stdout, $stderr) = Gniza4cpWHM::Runner::run(
        'list', 'accounts', [], { remote => $remote }
    );

    unless ($ok) {
        my $msg = $stderr // 'Failed to list accounts';
        $msg =~ s/\\/\\\\/g;
        $msg =~ s/"/\\"/g;
        $msg =~ s/\n/\\n/g;
        $msg =~ s/\r/\\r/g;
        print qq({"error":"$msg"});
        return;
    }

    # Get local cPanel accounts for terminated detection
    my %local_accounts = map { $_ => 1 } Gniza4cpWHM::UI::get_cpanel_accounts();

    my @accounts;
    for my $line (split /\n/, $stdout) {
        $line =~ s/^\s+|\s+$//g;
        next unless $line =~ /^[a-z][a-z0-9_-]*$/;
        my $terminated = $local_accounts{$line} ? 0 : 1;
        $line =~ s/\\/\\\\/g;
        $line =~ s/"/\\"/g;
        push @accounts, qq({"name":"$line","terminated":$terminated});
    }

    print '{"accounts":[' . join(',', @accounts) . ']}';
}

# ── Step 1: Select Account + Remote ─────────────────────────

sub handle_step1 {
    print "Content-Type: text/html\r\n\r\n";
    Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza4cp-whm/restore.cgi');

    print Gniza4cpWHM::UI::page_header('Restore from Backup');
    print Gniza4cpWHM::UI::render_nav('restore.cgi');
    print Gniza4cpWHM::UI::render_flash();

    my @remotes = Gniza4cpWHM::UI::list_remotes();

    unless (@remotes) {
        print qq{<div class="alert alert-info mb-4">No remotes configured. <a href="remotes.cgi?action=add" class="link">Add a remote</a> first.</div>\n};
        print Gniza4cpWHM::UI::page_footer();
        Whostmgr::HTMLInterface::footer();
        return;
    }

    print qq{<form method="GET" action="restore.cgi" id="gniza4cpStep1Form">\n};
    print qq{<input type="hidden" name="restore_step" value="2">\n};

    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
    print qq{<h2 class="card-title text-sm">Step 1: Select Source</h2>\n};

    # Remote dropdown
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="remote">Remote</label>\n};
    print qq{  <select class="select select-bordered select-sm w-full max-w-xs" id="remote" name="remote" required onchange="gniza4cpLoadAccounts()">\n};
    if (@remotes > 1) {
        print qq{    <option value="">-- Select remote --</option>\n};
    }
    for my $r (@remotes) {
        my $esc = Gniza4cpWHM::UI::esc($r);
        print qq{    <option value="$esc">$esc</option>\n};
    }
    print qq{  </select>\n};
    print qq{</div>\n};

    # Account dropdown (populated via AJAX)
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="account">Account</label>\n};
    print qq{  <select class="select select-bordered select-sm w-full max-w-xs" id="account" name="account" required disabled>\n};
    print qq{    <option value="">-- Select remote first --</option>\n};
    print qq{  </select>\n};
    print qq{  <span class="loading loading-spinner loading-sm hidden" id="gniza4cpAccountSpinner"></span>\n};
    print qq{</div>\n};

    print qq{</div>\n</div>\n};

    print qq{<div class="flex items-center gap-2">\n};
    print qq{  <button type="submit" class="btn btn-primary btn-sm" id="gniza4cpSubmitBtn" disabled>Load Snapshots</button>\n};
    print qq{</div>\n};

    print qq{</form>\n};

    # JavaScript for dynamic account loading
    my $single_remote = (@remotes == 1) ? 'true' : 'false';
    print <<'JSEOF';
<script>
function gniza4cpLoadAccounts() {
    var sel = document.getElementById('remote');
    var remote = sel.value;
    var acctSel = document.getElementById('account');
    var spinner = document.getElementById('gniza4cpAccountSpinner');
    var submitBtn = document.getElementById('gniza4cpSubmitBtn');

    if (!remote) {
        acctSel.innerHTML = '<option value="">-- Select remote first --</option>';
        acctSel.disabled = true;
        submitBtn.disabled = true;
        return;
    }

    acctSel.innerHTML = '<option value="">Loading...</option>';
    acctSel.disabled = true;
    submitBtn.disabled = true;
    spinner.classList.remove('hidden');

    function gniza4cpSetAcctMsg(msg) {
        acctSel.innerHTML = '';
        var opt = document.createElement('option');
        opt.value = '';
        opt.textContent = msg;
        acctSel.appendChild(opt);
    }

    function gniza4cpDone() { spinner.classList.add('hidden'); }

    var xhr = new XMLHttpRequest();
    xhr.timeout = 30000;
    xhr.open('GET', 'restore.cgi?restore_step=fetch_accounts&remote=' + encodeURIComponent(remote));
    xhr.onload = function() {
        gniza4cpDone();
        if (xhr.status === 200) {
            try {
                var data = JSON.parse(xhr.responseText);
                if (data.error) {
                    gniza4cpSetAcctMsg('Error: ' + data.error);
                    return;
                }
                acctSel.innerHTML = '';
                var placeholder = document.createElement('option');
                placeholder.value = '';
                placeholder.textContent = '-- Select account --';
                acctSel.appendChild(placeholder);
                for (var i = 0; i < data.accounts.length; i++) {
                    var a = data.accounts[i];
                    var opt = document.createElement('option');
                    opt.value = a.name;
                    opt.textContent = a.name + (a.terminated ? ' (terminated)' : '');
                    acctSel.appendChild(opt);
                }
                if (data.accounts.length === 0) {
                    gniza4cpSetAcctMsg('No accounts found');
                } else {
                    acctSel.disabled = false;
                    submitBtn.disabled = false;
                }
            } catch(e) {
                gniza4cpSetAcctMsg('Failed to parse response');
            }
        } else {
            gniza4cpSetAcctMsg('Request failed');
        }
    };
    xhr.onerror = function() { gniza4cpDone(); gniza4cpSetAcctMsg('Connection error'); };
    xhr.ontimeout = function() { gniza4cpDone(); gniza4cpSetAcctMsg('Request timed out'); };
    xhr.send();
}
JSEOF

    print "if ($single_remote) { gniza4cpLoadAccounts(); }\n";
    print "</script>\n";

    print Gniza4cpWHM::UI::page_footer();
    Whostmgr::HTMLInterface::footer();
}

# ── Step 2: Select Snapshot + Restore Type ───────────────────

sub handle_step2 {
    my $remote  = $form->{'remote'}  // '';
    my $account = $form->{'account'} // '';

    if ($remote eq '' || $account eq '') {
        Gniza4cpWHM::UI::set_flash('error', 'Remote and account are required.');
        print "Status: 302 Found\r\n";
        print "Location: restore.cgi\r\n\r\n";
        exit;
    }

    # Fetch snapshots via Runner
    my ($ok, $stdout, $stderr) = Gniza4cpWHM::Runner::run('list', undef, [], { remote => $remote, account => $account });

    print "Content-Type: text/html\r\n\r\n";
    Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza4cp-whm/restore.cgi');

    print Gniza4cpWHM::UI::page_header('Restore from Backup');
    print Gniza4cpWHM::UI::render_nav('restore.cgi');
    print Gniza4cpWHM::UI::render_flash();

    my $esc_remote  = Gniza4cpWHM::UI::esc($remote);
    my $esc_account = Gniza4cpWHM::UI::esc($account);

    unless ($ok) {
        my $msg = Gniza4cpWHM::UI::esc($stderr || 'Failed to list snapshots');
        print qq{<div class="alert alert-error mb-4">$msg</div>\n};
        print qq{<button type="button" class="btn btn-info btn-sm" onclick="location.href='restore.cgi'">Back</button>\n};
        print Gniza4cpWHM::UI::page_footer();
        Whostmgr::HTMLInterface::footer();
        return;
    }

    # Parse snapshot timestamps from output
    my @snapshots;
    for my $line (split /\n/, $stdout) {
        if ($line =~ /^\s+(\d{4}-\d{2}-\d{2}T\d{6})/) {
            push @snapshots, $1;
        }
    }

    print qq{<form method="GET" action="restore.cgi">\n};
    print qq{<input type="hidden" name="restore_step" value="3">\n};
    print qq{<input type="hidden" name="remote" value="$esc_remote">\n};
    print qq{<input type="hidden" name="account" value="$esc_account">\n};

    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
    print qq{<h2 class="card-title text-sm">Step 2: Choose Restore Options</h2>\n};
    print qq{<p class="text-sm mb-3">Account: <strong>$esc_account</strong> on remote <strong>$esc_remote</strong></p>\n};

    # Snapshot dropdown
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="timestamp">Snapshot</label>\n};
    if (@snapshots) {
        print qq{  <select class="select select-bordered select-sm w-full max-w-xs" id="timestamp" name="timestamp" required onchange="gniza4cpSnapshotChange()">\n};
        for my $snap (sort { $b cmp $a } @snapshots) {
            my $esc = Gniza4cpWHM::UI::esc($snap);
            print qq{    <option value="$esc">$esc</option>\n};
        }
        print qq{  </select>\n};
    } else {
        print qq{  <span class="text-sm text-base-content/60">No snapshots found</span>\n};
    }
    print qq{</div>\n};

    # Restore mode toggle: Full Account vs Selective
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm whitespace-nowrap">Restore Mode <span class="tooltip tooltip-top" data-tip="Full Account restores everything; Selective lets you pick specific items like files, databases, or mailboxes">&#9432;</span></label>\n};
    print qq{  <div class="join inline-flex items-stretch">\n};
    print qq{    <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Full Account" value="full" checked onchange="gniza4cpModeChanged()">\n};
    print qq{    <input type="radio" name="restore_mode" class="join-item btn btn-sm m-0" aria-label="Selective" value="selective" onchange="gniza4cpModeChanged()">\n};
    print qq{  </div>\n};
    print qq{</div>\n};

    # Terminate toggle (only visible in Full Account mode)
    print qq{<div id="terminate-panel" class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-52 font-medium text-sm whitespace-nowrap">Terminate First <span class="tooltip tooltip-top" data-tip="Remove the existing cPanel account before restoring. Results in a clean restore but causes brief downtime.">&#9432;</span></label>\n};
    print qq{  <input type="checkbox" class="toggle toggle-sm toggle-error" id="terminate" name="terminate" value="1">\n};
    print qq{</div>\n};

    # Exclude paths (visible in both Full Account and Selective modes)
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3 mt-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Directories and Files to Exclude</h3>\n};
    print qq{  <div class="flex items-center gap-2">\n};
    print qq{    <input type="text" class="input input-bordered input-sm flex-1 max-w-xs" id="exclude-input" placeholder="e.g. public_html/cache">\n};
    print qq{    <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpAddExclude()">Add Path</button>\n};
    print qq{    <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpOpenExcludeModal()">Insert Multiple</button>\n};
    print qq{  </div>\n};
    print qq{  <p class="text-xs text-base-content/60">Exclude files and directories from restoration</p>\n};
    print qq{  <div id="exclude-tags" class="flex flex-wrap gap-1 mt-1"></div>\n};
    print qq{  <input type="hidden" id="exclude_paths" name="exclude_paths" value="">\n};
    print qq{</div>\n};
    print qq{</div>\n};

    # Exclude modal
    print qq{<dialog id="exclude-modal" class="modal">\n};
    print qq{<div class="modal-box">\n};
    print qq{  <h3 class="text-lg font-bold">Directories and Files to Exclude</h3>\n};
    print qq{  <textarea id="exclude-textarea" class="textarea textarea-bordered w-full h-40 mt-3" placeholder="One path per line"></textarea>\n};
    print qq{  <p class="text-xs text-base-content/60 mt-1">* Separated by new line</p>\n};
    print qq{  <div class="modal-action">\n};
    print qq{    <button type="button" class="btn btn-sm" onclick="document.getElementById('exclude-modal').close()">Cancel</button>\n};
    print qq{    <button type="button" class="btn btn-warning btn-sm" onclick="gniza4cpExcludeModalOk()">OK</button>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{<div class="modal-backdrop" onclick="this.closest('dialog').close()"><button type="button">close</button></div>\n};
    print qq{</dialog>\n};

    # Hidden field that always carries account type when Full is selected
    print qq{<input type="hidden" id="type_account_hidden" name="type_account" value="1">\n};

    # Selective type buttons (hidden by default)
    my @selective_types = (
        ['files',    'Files'],
        ['database', 'Database'],
        ['dbusers',  'Database Users'],
        ['mailbox',  'Mailbox'],
        ['cron',     'Cron'],
        ['cpconfig', 'Config'],
        ['domains',  'Domains'],
        ['ssl',      'SSL'],
    );

    print qq{<div id="selective-panel" hidden>\n};

    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm">Restore Types</label>\n};
    print qq{  <div class="flex flex-wrap gap-1">\n};
    for my $t (@selective_types) {
        print qq{    <input type="checkbox" class="btn btn-sm" aria-label="$t->[1]" name="type_$t->[0]" value="1" onchange="gniza4cpTypesChanged()">\n};
    }
    print qq{  </div>\n};
    print qq{</div>\n};

    # Sub-field cards per type
    print qq{<div id="field-path" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Files</h3>\n};
    print qq{  <div class="flex items-center gap-3">\n};
    print qq{    <label class="font-medium text-sm" for="path">Path</label>\n};
    print qq{    <div class="flex items-center gap-2 w-full max-w-xs">\n};
    print qq{      <input type="text" class="input input-bordered input-sm flex-1" id="path" name="path" placeholder="e.g. public_html/index.html">\n};
    print qq{      <button type="button" class="btn btn-secondary btn-sm" onclick="gniza4cpOpenFileBrowser()">Browse</button>\n};
    print qq{    </div>\n};
    print qq{  </div>\n};
    print qq{  <p class="text-xs text-base-content/60">Leave empty to restore all files.</p>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-dbname" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Databases</h3>\n};
    print qq{  <input type="hidden" id="dbnames" name="dbnames">\n};
    print qq{  <div id="dbname-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-email" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Mailboxes</h3>\n};
    print qq{  <input type="hidden" id="emails" name="emails">\n};
    print qq{  <div id="email-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-dbusers" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Database Users</h3>\n};
    print qq{  <input type="hidden" id="dbuser_names" name="dbuser_names">\n};
    print qq{  <div id="dbusers-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-cron" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Cron Jobs</h3>\n};
    print qq{  <div id="cron-list" class="max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-domains" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">Domains</h3>\n};
    print qq{  <input type="hidden" id="domain_names" name="domain_names">\n};
    print qq{  <div id="domains-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{<div id="field-ssl" hidden>\n};
    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-3">\n};
    print qq{<div class="card-body py-3 px-4">\n};
    print qq{  <h3 class="card-title text-sm">SSL Certificates</h3>\n};
    print qq{  <input type="hidden" id="ssl_names" name="ssl_names">\n};
    print qq{  <div id="ssl-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto">\n};
    print qq{    <span class="text-sm text-base-content/60">Loading...</span>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{</div>\n};
    print qq{</div>\n};

    print qq{</div>\n};

    # File browser modal
    print qq{<dialog id="fb-modal" class="modal">\n};
    print qq{<div class="modal-box w-11/12 max-w-2xl">\n};
    print qq{  <h3 class="text-lg font-bold mb-3">Browse Files</h3>\n};
    print qq{  <div id="fb-breadcrumbs" class="breadcrumbs text-sm mb-3"><ul><li>homedir</li></ul></div>\n};
    print qq{  <div id="fb-loading" class="text-center py-4" hidden><span class="loading loading-spinner"></span> Loading...</div>\n};
    print qq{  <div id="fb-error" class="alert alert-error mb-3" hidden></div>\n};
    print qq{  <div id="fb-list" class="overflow-y-auto" class="max-h-[360px]">\n};
    print qq{    <table class="table table-zebra w-full"><tbody id="fb-tbody"></tbody></table>\n};
    print qq{  </div>\n};
    print qq{  <div class="modal-action">\n};
    print qq{    <button type="button" id="fb-select-btn" class="btn btn-primary btn-sm" disabled onclick="gniza4cpSelectPath()">Select</button>\n};
    print qq{    <button type="button" class="btn btn-info btn-sm" onclick="document.getElementById('fb-modal').close()">Cancel</button>\n};
    print qq{  </div>\n};
    print qq{</div>\n};
    print qq{<div class="modal-backdrop" onclick="document.getElementById('fb-modal').close()"><button type="button">close</button></div>\n};
    print qq{</dialog>\n};

    print qq{</div>\n</div>\n};

    if (@snapshots) {
        print qq{<div class="flex items-center gap-2">\n};
        print qq{  <button type="submit" class="btn btn-primary btn-sm">Review &amp; Confirm</button>\n};
        print qq{  <button type="button" class="btn btn-info btn-sm" onclick="location.href='restore.cgi'">Back</button>\n};
        print qq{</div>\n};
    } else {
        print qq{<button type="button" class="btn btn-info btn-sm" onclick="location.href='restore.cgi'">Back</button>\n};
    }

    print qq{</form>\n};

    # JavaScript for dynamic dropdowns
    print <<JS;
<script>
var gniza4cpCache = {};
var gniza4cpRemote = '$esc_remote';
var gniza4cpAccount = '$esc_account';

var fbCache = {};
var fbSelected = '';

function gniza4cpSnapshotChange() {
    gniza4cpCache = {};
    fbCache = {};
    gniza4cpModeChanged();
}

function gniza4cpModeChanged() {
    var mode = document.querySelector('input[name="restore_mode"]:checked').value;
    var selective = mode === 'selective';
    document.getElementById('selective-panel').hidden = !selective;
    document.getElementById('terminate-panel').hidden = selective;
    var termCb = document.getElementById('terminate');
    if (selective) { termCb.checked = false; termCb.disabled = true; }
    else { termCb.disabled = false; }
    document.getElementById('type_account_hidden').disabled = selective;
    if (selective) {
        gniza4cpTypesChanged();
    } else {
        var panels = ['field-path','field-dbname','field-email','field-dbusers','field-cron','field-domains','field-ssl'];
        for (var i = 0; i < panels.length; i++) {
            document.getElementById(panels[i]).hidden = true;
        }
    }
}

function gniza4cpTypesChanged() {
    var types = {
        files:    'field-path',
        database: 'field-dbname',
        mailbox:  'field-email',
        dbusers:  'field-dbusers',
        cron:     'field-cron',
        domains:  'field-domains',
        ssl:      'field-ssl'
    };
    for (var t in types) {
        var el = document.querySelector('input[name="type_' + t + '"]');
        document.getElementById(types[t]).hidden = !(el && el.checked);
    }

    if (document.querySelector('input[name="type_database"]').checked) { gniza4cpLoadOptions('database', 'dbname-list', 'dbnames'); }
    if (document.querySelector('input[name="type_mailbox"]').checked)  { gniza4cpLoadOptions('mailbox', 'email-list', 'emails'); }
    if (document.querySelector('input[name="type_dbusers"]').checked)  { gniza4cpLoadOptions('dbusers', 'dbusers-list', 'dbuser_names'); }
    if (document.querySelector('input[name="type_cron"]').checked)     { gniza4cpLoadPreview('cron', 'cron-list'); }
    if (document.querySelector('input[name="type_domains"]').checked)  { gniza4cpLoadOptions('domains', 'domains-list', 'domain_names'); }
    if (document.querySelector('input[name="type_ssl"]').checked)      { gniza4cpLoadOptions('ssl', 'ssl-list', 'ssl_names'); }
}

function gniza4cpLoadOptions(type, containerId, hiddenId) {
    var ts = document.getElementById('timestamp').value;
    var cacheKey = type + ':' + ts;

    if (gniza4cpCache[cacheKey]) {
        gniza4cpPopulateChecklist(containerId, hiddenId, gniza4cpCache[cacheKey]);
        return;
    }

    var container = document.getElementById(containerId);
    container.innerHTML = '<span class="text-sm text-base-content/60"><span class="loading loading-spinner loading-xs"></span> Loading...</span>';
    document.getElementById(hiddenId).value = '';

    var url = 'restore.cgi?restore_step=fetch_options'
        + '&remote=' + encodeURIComponent(gniza4cpRemote)
        + '&account=' + encodeURIComponent(gniza4cpAccount)
        + '&timestamp=' + encodeURIComponent(ts)
        + '&type=' + encodeURIComponent(type);

    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState !== 4) return;
        if (xhr.status === 200) {
            try {
                var data = JSON.parse(xhr.responseText);
                if (data.error) {
                    container.innerHTML = '<span class="text-sm text-error">Error: ' + data.error + '</span>';
                } else {
                    gniza4cpCache[cacheKey] = data.options;
                    gniza4cpPopulateChecklist(containerId, hiddenId, data.options);
                }
            } catch(e) {
                container.innerHTML = '<span class="text-sm text-error">Failed to parse response</span>';
            }
        } else {
            container.innerHTML = '<span class="text-sm text-error">Request failed</span>';
        }
    };
    xhr.send();
}

function gniza4cpPopulateChecklist(containerId, hiddenId, options) {
    var container = document.getElementById(containerId);
    var hidden = document.getElementById(hiddenId);
    hidden.value = '';

    if (!options || options.length === 0) {
        container.innerHTML = '<span class="text-sm text-base-content/60">(none found)</span>';
        return;
    }

    var allLabels = {'dbname-list':'All Databases','dbusers-list':'All Database Users','email-list':'All Mailboxes','domains-list':'All Domains','ssl-list':'All Certificates'};
    var allLabel = allLabels[containerId] || 'All';
    var html = '<label class="flex items-center gap-2 cursor-pointer">'
        + '<input type="checkbox" class="checkbox checkbox-sm" onchange="gniza4cpToggleAll(\\'' + containerId + '\\',\\'' + hiddenId + '\\',this.checked)" data-all="1">'
        + '<span class="text-sm font-semibold">' + allLabel + '</span></label>';

    for (var i = 0; i < options.length; i++) {
        var v = options[i].replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
        html += '<label class="flex items-center gap-2 cursor-pointer">'
            + '<input type="checkbox" class="checkbox checkbox-sm" value="' + v + '" onchange="gniza4cpSyncHidden(\\'' + containerId + '\\',\\'' + hiddenId + '\\')" data-item="1">'
            + '<span class="text-sm">' + v + '</span></label>';
    }
    container.innerHTML = html;
}

function gniza4cpToggleAll(containerId, hiddenId, checked) {
    var container = document.getElementById(containerId);
    var hidden = document.getElementById(hiddenId);
    var items = container.querySelectorAll('input[data-item]');
    for (var i = 0; i < items.length; i++) {
        items[i].disabled = checked;
        if (checked) items[i].checked = false;
    }
    hidden.value = checked ? '__ALL__' : '';
}

function gniza4cpSyncHidden(containerId, hiddenId) {
    var container = document.getElementById(containerId);
    var hidden = document.getElementById(hiddenId);
    var items = container.querySelectorAll('input[data-item]:checked');
    var vals = [];
    for (var i = 0; i < items.length; i++) {
        vals.push(items[i].value);
    }
    hidden.value = vals.join(',');
}

function gniza4cpLoadPreview(type, containerId) {
    var ts = document.getElementById('timestamp').value;
    var cacheKey = type + ':' + ts;

    if (gniza4cpCache[cacheKey]) {
        gniza4cpPopulatePreview(containerId, gniza4cpCache[cacheKey], type);
        return;
    }

    var container = document.getElementById(containerId);
    container.innerHTML = '<span class="text-sm text-base-content/60"><span class="loading loading-spinner loading-xs"></span> Loading...</span>';

    var url = 'restore.cgi?restore_step=fetch_options'
        + '&remote=' + encodeURIComponent(gniza4cpRemote)
        + '&account=' + encodeURIComponent(gniza4cpAccount)
        + '&timestamp=' + encodeURIComponent(ts)
        + '&type=' + encodeURIComponent(type);

    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState !== 4) return;
        if (xhr.status === 200) {
            try {
                var data = JSON.parse(xhr.responseText);
                if (data.error) {
                    container.innerHTML = '<span class="text-sm text-error">Error: ' + data.error + '</span>';
                } else {
                    gniza4cpCache[cacheKey] = data.options;
                    gniza4cpPopulatePreview(containerId, data.options, type);
                }
            } catch(e) {
                container.innerHTML = '<span class="text-sm text-error">Failed to parse response</span>';
            }
        } else {
            container.innerHTML = '<span class="text-sm text-error">Request failed</span>';
        }
    };
    xhr.send();
}

function gniza4cpPopulatePreview(containerId, options, type) {
    var container = document.getElementById(containerId);
    if (!options || options.length === 0) {
        container.innerHTML = '<span class="text-sm text-base-content/60">(none found)</span>';
        return;
    }
    if (type === 'cron') {
        var html = '<pre class="text-xs font-mono bg-base-200 p-3 rounded-lg overflow-x-auto">';
        for (var i = 0; i < options.length; i++) {
            html += options[i].replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '\\n';
        }
        html += '</pre>';
        container.innerHTML = html;
    } else {
        var html = '<ul class="list-disc pl-5 text-sm">';
        for (var i = 0; i < options.length; i++) {
            html += '<li>' + options[i].replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</li>';
        }
        html += '</ul>';
        container.innerHTML = html;
    }
}

function gniza4cpAddExclude() {
    var input = document.getElementById('exclude-input');
    var val = input.value.trim();
    if (!val) return;
    gniza4cpAddExcludeTag(val);
    input.value = '';
    gniza4cpUpdateExcludeField();
}

function gniza4cpAddExcludeTag(text) {
    var container = document.getElementById('exclude-tags');
    // Skip duplicates
    var existing = container.querySelectorAll('.badge span');
    for (var i = 0; i < existing.length; i++) {
        if (existing[i].textContent === text) return;
    }
    var badge = document.createElement('span');
    badge.className = 'badge badge-warning gap-1';
    var span = document.createElement('span');
    span.textContent = text;
    badge.appendChild(span);
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'btn btn-xs btn-ghost btn-circle';
    btn.innerHTML = '\\u2715';
    btn.onclick = function() { badge.remove(); gniza4cpUpdateExcludeField(); };
    badge.appendChild(btn);
    container.appendChild(badge);
}

function gniza4cpUpdateExcludeField() {
    var tags = document.getElementById('exclude-tags').querySelectorAll('.badge span');
    var vals = [];
    for (var i = 0; i < tags.length; i++) {
        vals.push(tags[i].textContent);
    }
    document.getElementById('exclude_paths').value = vals.join(',');
}

function gniza4cpOpenExcludeModal() {
    document.getElementById('exclude-textarea').value = '';
    document.getElementById('exclude-modal').showModal();
}

function gniza4cpExcludeModalOk() {
    var text = document.getElementById('exclude-textarea').value;
    var lines = text.split('\\n');
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i].trim();
        if (line) gniza4cpAddExcludeTag(line);
    }
    gniza4cpUpdateExcludeField();
    document.getElementById('exclude-modal').close();
}

gniza4cpModeChanged();

function gniza4cpOpenFileBrowser() {
    fbSelected = '';
    document.getElementById('fb-select-btn').disabled = true;
    document.getElementById('fb-modal').showModal();
    gniza4cpLoadDir('');
}

function gniza4cpLoadDir(path) {
    var ts = document.getElementById('timestamp').value;
    var cacheKey = 'fb:' + ts + ':' + path;

    if (fbCache[cacheKey]) {
        gniza4cpRenderFileList(path, fbCache[cacheKey]);
        return;
    }

    document.getElementById('fb-loading').hidden = false;
    document.getElementById('fb-error').hidden = true;
    document.getElementById('fb-tbody').innerHTML = '';

    var url = 'restore.cgi?restore_step=fetch_options'
        + '&remote=' + encodeURIComponent(gniza4cpRemote)
        + '&account=' + encodeURIComponent(gniza4cpAccount)
        + '&timestamp=' + encodeURIComponent(ts)
        + '&type=files'
        + (path ? '&path=' + encodeURIComponent(path) : '');

    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState !== 4) return;
        document.getElementById('fb-loading').hidden = true;
        if (xhr.status === 200) {
            try {
                var data = JSON.parse(xhr.responseText);
                if (data.error) {
                    document.getElementById('fb-error').textContent = data.error;
                    document.getElementById('fb-error').hidden = false;
                } else {
                    fbCache[cacheKey] = data.options;
                    gniza4cpRenderFileList(path, data.options);
                }
            } catch(e) {
                document.getElementById('fb-error').textContent = 'Failed to parse response';
                document.getElementById('fb-error').hidden = false;
            }
        } else {
            document.getElementById('fb-error').textContent = 'Request failed';
            document.getElementById('fb-error').hidden = false;
        }
    };
    xhr.send();
}

function gniza4cpRenderBreadcrumbs(path) {
    var ul = document.createElement('ul');
    var li = document.createElement('li');
    var a = document.createElement('a');
    a.textContent = 'homedir';
    a.href = '#';
    a.onclick = function(e) { e.preventDefault(); gniza4cpLoadDir(''); };
    li.appendChild(a);
    ul.appendChild(li);

    if (path) {
        var parts = path.replace(/\\/\$/, '').split('/');
        var built = '';
        for (var i = 0; i < parts.length; i++) {
            built += (i > 0 ? '/' : '') + parts[i];
            li = document.createElement('li');
            if (i < parts.length - 1) {
                a = document.createElement('a');
                a.textContent = parts[i];
                a.href = '#';
                (function(p) { a.onclick = function(e) { e.preventDefault(); gniza4cpLoadDir(p); }; })(built);
                li.appendChild(a);
            } else {
                li.textContent = parts[i];
            }
            ul.appendChild(li);
        }
    }

    var bc = document.getElementById('fb-breadcrumbs');
    bc.innerHTML = '';
    bc.appendChild(ul);
}

function gniza4cpRenderFileList(currentPath, entries) {
    gniza4cpRenderBreadcrumbs(currentPath);
    fbSelected = '';
    document.getElementById('fb-select-btn').disabled = true;

    var tbody = document.getElementById('fb-tbody');
    tbody.innerHTML = '';

    if (!entries || entries.length === 0) {
        tbody.innerHTML = '<tr><td class="text-center text-base-content/60 py-4">(empty directory)</td></tr>';
        return;
    }

    for (var i = 0; i < entries.length; i++) {
        var entry = entries[i];
        var isDir = entry.endsWith('/');
        var displayName = entry;
        var fullPath = currentPath ? currentPath.replace(/\\/\$/, '') + '/' + entry : entry;

        var tr = document.createElement('tr');
        tr.className = 'cursor-pointer hover';
        tr.setAttribute('data-path', fullPath);

        var td = document.createElement('td');
        td.className = 'py-1';
        var icon = isDir ? '\\uD83D\\uDCC1 ' : '\\uD83D\\uDCC4 ';
        td.textContent = icon + displayName;
        tr.appendChild(td);

        (function(row, path, dir) {
            row.onclick = function() { gniza4cpHighlight(row, path); };
            if (dir) {
                row.ondblclick = function() { gniza4cpLoadDir(path.replace(/\\/\$/, '')); };
            }
        })(tr, fullPath, isDir);

        tbody.appendChild(tr);
    }
}

function gniza4cpHighlight(row, path) {
    var rows = document.getElementById('fb-tbody').querySelectorAll('tr');
    for (var i = 0; i < rows.length; i++) {
        rows[i].classList.remove('bg-primary/10');
    }
    row.classList.add('bg-primary/10');
    fbSelected = path;
    document.getElementById('fb-select-btn').disabled = false;
}

function gniza4cpSelectPath() {
    if (fbSelected) {
        document.getElementById('path').value = fbSelected;
    }
    document.getElementById('fb-modal').close();
}
</script>
JS

    print Gniza4cpWHM::UI::page_footer();
    Whostmgr::HTMLInterface::footer();
}

# ── Step 3: Summary + Confirm ────────────────────────────────

sub handle_step3 {
    my $remote    = $form->{'remote'}    // '';
    my $account   = $form->{'account'}   // '';
    my $timestamp = $form->{'timestamp'} // '';
    my $path         = $form->{'path'}         // '';
    my $dbnames      = $form->{'dbnames'}      // '';
    my $dbuser_names = $form->{'dbuser_names'} // '';
    my $emails       = $form->{'emails'}       // '';
    my $domain_names = $form->{'domain_names'} // '';
    my $ssl_names      = $form->{'ssl_names'}      // '';
    my $exclude_paths  = $form->{'exclude_paths'}  // '';
    my $terminate      = $form->{'terminate'}      // '';

    # Collect selected types from type_* checkboxes
    my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl);
    my @selected_types;
    for my $t (@all_type_keys) {
        push @selected_types, $t if ($form->{"type_$t"} // '') eq '1';
    }

    unless (@selected_types) {
        Gniza4cpWHM::UI::set_flash('error', 'Please select at least one restore type.');
        print "Status: 302 Found\r\n";
        print "Location: restore.cgi?restore_step=2&remote=" . _uri_escape($remote) . "&account=" . _uri_escape($account) . "\r\n\r\n";
        exit;
    }

    print "Content-Type: text/html\r\n\r\n";
    Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza4cp-whm/restore.cgi');

    print Gniza4cpWHM::UI::page_header('Restore from Backup');
    print Gniza4cpWHM::UI::render_nav('restore.cgi');

    my $esc_remote    = Gniza4cpWHM::UI::esc($remote);
    my $esc_account   = Gniza4cpWHM::UI::esc($account);
    my $esc_timestamp = Gniza4cpWHM::UI::esc($timestamp);

    my $types_display = join(', ', map { Gniza4cpWHM::UI::esc($TYPE_LABELS{$_} // $_) } @selected_types);

    print qq{<div class="card bg-white shadow-sm border border-base-300 mb-6">\n<div class="card-body">\n};
    print qq{<h2 class="card-title text-sm">Step 3: Confirm Restore</h2>\n};
    print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100"><table class="table">\n};
    print qq{<tr><td class="font-medium w-44">Remote</td><td>$esc_remote</td></tr>\n};
    print qq{<tr><td class="font-medium">Account</td><td>$esc_account</td></tr>\n};
    print qq{<tr><td class="font-medium">Snapshot</td><td>$esc_timestamp</td></tr>\n};
    print qq{<tr><td class="font-medium">Restore Types</td><td>$types_display</td></tr>\n};

    # Show sub-field details for applicable types
    if (grep { $_ eq 'files' } @selected_types) {
        my $path_display = $path ne '' ? Gniza4cpWHM::UI::esc($path) : 'All files';
        print qq{<tr><td class="font-medium">Path</td><td>$path_display</td></tr>\n};
    }
    if (grep { $_ eq 'database' } @selected_types) {
        my $db_display = ($dbnames eq '' || $dbnames eq '__ALL__') ? 'All databases' : Gniza4cpWHM::UI::esc($dbnames);
        print qq{<tr><td class="font-medium">Database</td><td>$db_display</td></tr>\n};
    }
    if (grep { $_ eq 'dbusers' } @selected_types) {
        my $dbu_display = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? 'All database users' : Gniza4cpWHM::UI::esc($dbuser_names);
        print qq{<tr><td class="font-medium">Database Users</td><td>$dbu_display</td></tr>\n};
    }
    if (grep { $_ eq 'mailbox' } @selected_types) {
        my $mb_display = ($emails eq '' || $emails eq '__ALL__') ? 'All mailboxes' : Gniza4cpWHM::UI::esc($emails);
        print qq{<tr><td class="font-medium">Mailbox</td><td>$mb_display</td></tr>\n};
    }
    if (grep { $_ eq 'domains' } @selected_types) {
        my $dom_display = ($domain_names eq '' || $domain_names eq '__ALL__') ? 'All domains' : Gniza4cpWHM::UI::esc($domain_names);
        print qq{<tr><td class="font-medium">Domains</td><td>$dom_display</td></tr>\n};
    }
    if (grep { $_ eq 'ssl' } @selected_types) {
        my $ssl_display = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? 'All certificates' : Gniza4cpWHM::UI::esc($ssl_names);
        print qq{<tr><td class="font-medium">SSL</td><td>$ssl_display</td></tr>\n};
    }

    if ($terminate eq '1' && grep { $_ eq 'account' } @selected_types) {
        print qq{<tr><td class="font-medium">Terminate First</td><td class="text-error font-medium">Yes — account will be removed before restore</td></tr>\n};
    }

    if ($exclude_paths ne '') {
        my $exclude_display = Gniza4cpWHM::UI::esc($exclude_paths);
        $exclude_display =~ s/,/, /g;
        print qq{<tr><td class="font-medium">Exclude</td><td>$exclude_display</td></tr>\n};
    }

    print qq{</table></div>\n};
    print qq{</div>\n</div>\n};

    print qq{<form method="POST" action="restore.cgi">\n};
    print qq{<input type="hidden" name="restore_step" value="4">\n};
    print qq{<input type="hidden" name="remote" value="$esc_remote">\n};
    print qq{<input type="hidden" name="account" value="$esc_account">\n};
    print qq{<input type="hidden" name="timestamp" value="$esc_timestamp">\n};
    for my $t (@selected_types) {
        print qq{<input type="hidden" name="type_$t" value="1">\n};
    }
    print qq{<input type="hidden" name="path" value="} . Gniza4cpWHM::UI::esc($path) . qq{">\n};
    print qq{<input type="hidden" name="dbnames" value="} . Gniza4cpWHM::UI::esc($dbnames) . qq{">\n};
    print qq{<input type="hidden" name="dbuser_names" value="} . Gniza4cpWHM::UI::esc($dbuser_names) . qq{">\n};
    print qq{<input type="hidden" name="emails" value="} . Gniza4cpWHM::UI::esc($emails) . qq{">\n};
    print qq{<input type="hidden" name="domain_names" value="} . Gniza4cpWHM::UI::esc($domain_names) . qq{">\n};
    print qq{<input type="hidden" name="ssl_names" value="} . Gniza4cpWHM::UI::esc($ssl_names) . qq{">\n};
    print qq{<input type="hidden" name="exclude_paths" value="} . Gniza4cpWHM::UI::esc($exclude_paths) . qq{">\n};
    print qq{<input type="hidden" name="terminate" value="} . Gniza4cpWHM::UI::esc($terminate) . qq{">\n};
    print Gniza4cpWHM::UI::csrf_hidden_field();

    print qq{<div class="flex items-center gap-2">\n};
    print qq{  <button type="submit" class="btn btn-error btn-sm" onclick="return confirm('Are you sure you want to restore? This may overwrite existing data.')">Execute Restore</button>\n};
    print qq{  <button type="button" class="btn btn-info btn-sm" onclick="location.href='restore.cgi'">Cancel</button>\n};
    print qq{</div>\n};
    print qq{</form>\n};

    print Gniza4cpWHM::UI::page_footer();
    Whostmgr::HTMLInterface::footer();
}

# ── Step 4: Execute + Show Output ────────────────────────────

sub handle_step4 {
    unless ($method eq 'POST' && Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
        Gniza4cpWHM::UI::set_flash('error', 'Invalid or expired form token.');
        print "Status: 302 Found\r\n";
        print "Location: restore.cgi\r\n\r\n";
        exit;
    }

    my $remote       = $form->{'remote'}       // '';
    my $account      = $form->{'account'}      // '';
    my $timestamp    = $form->{'timestamp'}    // '';
    my $path         = $form->{'path'}         // '';
    my $dbnames      = $form->{'dbnames'}      // '';
    my $dbuser_names = $form->{'dbuser_names'} // '';
    my $emails       = $form->{'emails'}       // '';
    my $domain_names = $form->{'domain_names'} // '';
    my $ssl_names      = $form->{'ssl_names'}      // '';
    my $exclude_paths  = $form->{'exclude_paths'}  // '';
    my $terminate      = $form->{'terminate'}      // '';

    # Collect selected types
    my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl);
    my @selected_types;
    for my $t (@all_type_keys) {
        push @selected_types, $t if ($form->{"type_$t"} // '') eq '1';
    }

    unless (@selected_types) {
        Gniza4cpWHM::UI::set_flash('error', 'No restore types selected.');
        print "Status: 302 Found\r\n";
        print "Location: restore.cgi\r\n\r\n";
        exit;
    }

    # Build commands for async execution
    my @commands;
    for my $type (@selected_types) {
        my %opts = (remote => $remote);
        $opts{timestamp} = $timestamp if $timestamp ne '';

        if ($SIMPLE_TYPES{$type}) {
            if ($type eq 'account') {
                $opts{exclude} = $exclude_paths if $exclude_paths ne '';
                $opts{terminate} = '1' if $terminate eq '1';
            }
            push @commands, ['restore', $type, [$account], {%opts}];
        } elsif ($type eq 'files') {
            $opts{path} = $path if $path ne '';
            $opts{exclude} = $exclude_paths if $exclude_paths ne '';
            push @commands, ['restore', 'files', [$account], {%opts}];
        } elsif ($type eq 'database') {
            my @dbs;
            if ($dbnames ne '' && $dbnames ne '__ALL__') {
                @dbs = split /,/, $dbnames;
            }
            if (@dbs) {
                for my $db (@dbs) {
                    push @commands, ['restore', 'database', [$account, $db], {%opts}];
                }
            } else {
                push @commands, ['restore', 'database', [$account], {%opts}];
            }
        } elsif ($type eq 'dbusers') {
            my @dbus;
            if ($dbuser_names ne '' && $dbuser_names ne '__ALL__') {
                @dbus = split /,/, $dbuser_names;
            }
            if (@dbus) {
                for my $dbu (@dbus) {
                    push @commands, ['restore', 'dbusers', [$account, $dbu], {%opts}];
                }
            } else {
                push @commands, ['restore', 'dbusers', [$account], {%opts}];
            }
        } elsif ($type eq 'mailbox') {
            my @mbs;
            if ($emails ne '' && $emails ne '__ALL__') {
                @mbs = split /,/, $emails;
            }
            if (@mbs) {
                for my $mb (@mbs) {
                    push @commands, ['restore', 'mailbox', [$account, $mb], {%opts}];
                }
            } else {
                push @commands, ['restore', 'mailbox', [$account], {%opts}];
            }
        } elsif ($type eq 'domains') {
            my @doms;
            if ($domain_names ne '' && $domain_names ne '__ALL__') {
                @doms = split /,/, $domain_names;
            }
            if (@doms) {
                for my $dom (@doms) {
                    push @commands, ['restore', 'domains', [$account, $dom], {%opts}];
                }
            } else {
                push @commands, ['restore', 'domains', [$account], {%opts}];
            }
        } elsif ($type eq 'ssl') {
            my @certs;
            if ($ssl_names ne '' && $ssl_names ne '__ALL__') {
                @certs = split /,/, $ssl_names;
            }
            if (@certs) {
                for my $cert (@certs) {
                    push @commands, ['restore', 'ssl', [$account, $cert], {%opts}];
                }
            } else {
                push @commands, ['restore', 'ssl', [$account], {%opts}];
            }
        }
    }

    my ($ok, $err) = Gniza4cpWHM::Runner::run_async(\@commands);
    if ($ok) {
        Gniza4cpWHM::UI::set_flash('success', 'Restore started in background. Watch progress below.');
        print "Status: 302 Found\r\n";
        print "Location: logs.cgi\r\n\r\n";
    } else {
        Gniza4cpWHM::UI::set_flash('error', "Failed to start restore: $err");
        print "Status: 302 Found\r\n";
        print "Location: restore.cgi\r\n\r\n";
    }
}
