#!/usr/local/cpanel/3rdparty/bin/perl
# gniza4cp WHM Plugin — Schedule CRUD
use strict;
use warnings;

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

use Whostmgr::HTMLInterface ();
use Cpanel::Form            ();
use File::Copy              ();
use POSIX                   ();
use Gniza4cpWHM::Config;
use Gniza4cpWHM::Validator;
use Gniza4cpWHM::Cron;
use Gniza4cpWHM::UI;

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

# Route to handler
if    ($action eq 'add')         { handle_add()         }
elsif ($action eq 'edit')        { handle_edit()        }
elsif ($action eq 'delete')      { handle_delete()      }
elsif ($action eq 'toggle_cron')      { handle_toggle_cron()      }
elsif ($action eq 'toggle_sysbackup')      { handle_toggle_sysbackup()      }
elsif ($action eq 'toggle_skip_suspended') { handle_toggle_skip_suspended() }
elsif ($action eq 'run_now')               { handle_run_now()               }
else                             { handle_list()        }

exit;

# ── List ─────────────────────────────────────────────────────

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

    print Gniza4cpWHM::UI::page_header('Schedule Management');
    print Gniza4cpWHM::UI::render_nav('schedules.cgi');
    print Gniza4cpWHM::UI::render_flash();

    # Configured schedules
    my @schedules = Gniza4cpWHM::UI::list_schedules();
    my $cron_schedules = Gniza4cpWHM::Cron::get_current_schedules();

    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">Configured Schedules</h2>\n};

    if (@schedules) {
        print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100"><table class="table">\n};
        print qq{<thead><tr><th>Name</th><th>Type</th><th>Time</th><th>Day</th><th>Remotes</th><th>Sys Backup</th><th>Skip Suspended</th><th>Active</th><th>Actions</th></tr></thead>\n};
        print qq{<tbody>\n};
        for my $name (@schedules) {
            my $conf = Gniza4cpWHM::Config::parse(Gniza4cpWHM::UI::schedule_conf_path($name), 'schedule');
            my $esc_name  = Gniza4cpWHM::UI::esc($name);
            my $esc_sched = Gniza4cpWHM::UI::esc($conf->{SCHEDULE} // '');
            my $esc_time  = Gniza4cpWHM::UI::esc($conf->{SCHEDULE_TIME} // '02:00');
            my $esc_day   = Gniza4cpWHM::UI::esc($conf->{SCHEDULE_DAY} // '-');
            my $esc_remotes = Gniza4cpWHM::UI::esc($conf->{REMOTES} // '(all)');
            $esc_remotes = '(all)' if $esc_remotes eq '';

            my $in_cron = exists $cron_schedules->{$name};
            my $checked = $in_cron ? ' checked' : '';

            my $sysbackup_on = (($conf->{SYSBACKUP} // '') eq 'yes');
            my $sysbackup_checked = $sysbackup_on ? ' checked' : '';

            my $skip_suspended_on = (($conf->{SKIP_SUSPENDED} // '') eq 'yes');
            my $skip_suspended_checked = $skip_suspended_on ? ' checked' : '';

            print qq{<tr class="hover">};
            print qq{<td><strong>$esc_name</strong></td>};
            print qq{<td>$esc_sched</td><td>$esc_time</td><td>$esc_day</td><td>$esc_remotes</td>};
            print qq{<td>};
            print qq{<input type="checkbox" class="toggle toggle-sm toggle-success" data-schedule="$esc_name" onchange="gniza4cpToggleSysbackup(this)"$sysbackup_checked>};
            print qq{</td>};
            print qq{<td>};
            print qq{<input type="checkbox" class="toggle toggle-sm toggle-success" data-schedule="$esc_name" onchange="gniza4cpToggleSkipSuspended(this)"$skip_suspended_checked>};
            print qq{</td>};
            print qq{<td>};
            print qq{<input type="checkbox" class="toggle toggle-sm toggle-success" data-schedule="$esc_name" onchange="gniza4cpToggleCron(this)"$checked>};
            print qq{</td>};
            print qq{<td>};
            print qq{<div class="flex items-center gap-2">};
            print qq{<form method="POST" action="schedules.cgi" class="inline">};
            print qq{<input type="hidden" name="action" value="run_now">};
            print qq{<input type="hidden" name="name" value="$esc_name">};
            print Gniza4cpWHM::UI::csrf_hidden_field();
            print qq{<button type="submit" class="btn btn-secondary btn-sm" onclick="return confirm('Run backup for schedule $esc_name now?')">Run Now</button>};
            print qq{</form>};
            print qq{<button type="button" class="btn btn-primary btn-sm" onclick="location.href='schedules.cgi?action=edit&amp;name=$esc_name'">Edit</button>};
            print qq{<form method="POST" action="schedules.cgi" class="inline">};
            print qq{<input type="hidden" name="action" value="delete">};
            print qq{<input type="hidden" name="name" value="$esc_name">};
            print Gniza4cpWHM::UI::csrf_hidden_field();
            print qq{<button type="submit" class="btn btn-error btn-sm" onclick="return confirm('Delete schedule $esc_name?')">Delete</button>};
            print qq{</form>};
            print qq{</div>};
            print qq{</td>};
            print qq{</tr>\n};
        }
        print qq{</tbody>\n</table></div>\n};
    } else {
        print qq{<p>No schedules configured. Add a schedule to define when backups run.</p>\n};
    }
    print qq{</div>\n</div>\n};

    # CSRF token + AJAX toggle script
    my $csrf_token = Gniza4cpWHM::UI::generate_csrf_token();
    print qq{<script>
var gniza4cpCsrf = '} . Gniza4cpWHM::UI::esc($csrf_token) . qq{';
function gniza4cpToggleCron(el) {
    var name = el.getAttribute('data-schedule');
    el.disabled = true;
    var fd = new FormData();
    fd.append('action', 'toggle_cron');
    fd.append('name', name);
    fd.append('gniza4cp_csrf', gniza4cpCsrf);
    fetch('schedules.cgi', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            gniza4cpCsrf = d.csrf;
            el.checked = d.active;
            el.disabled = false;
        })
        .catch(function() {
            el.checked = !el.checked;
            el.disabled = false;
        });
}
function gniza4cpToggleSysbackup(el) {
    var name = el.getAttribute('data-schedule');
    el.disabled = true;
    var fd = new FormData();
    fd.append('action', 'toggle_sysbackup');
    fd.append('name', name);
    fd.append('gniza4cp_csrf', gniza4cpCsrf);
    fetch('schedules.cgi', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            gniza4cpCsrf = d.csrf;
            el.checked = d.active;
            el.disabled = false;
        })
        .catch(function() {
            el.checked = !el.checked;
            el.disabled = false;
        });
}
function gniza4cpToggleSkipSuspended(el) {
    var name = el.getAttribute('data-schedule');
    el.disabled = true;
    var fd = new FormData();
    fd.append('action', 'toggle_skip_suspended');
    fd.append('name', name);
    fd.append('gniza4cp_csrf', gniza4cpCsrf);
    fetch('schedules.cgi', { method: 'POST', body: fd })
        .then(function(r) { return r.json(); })
        .then(function(d) {
            gniza4cpCsrf = d.csrf;
            el.checked = d.active;
            el.disabled = false;
        })
        .catch(function() {
            el.checked = !el.checked;
            el.disabled = false;
        });
}
</script>\n};

    # Action buttons
    print qq{<div class="flex gap-2 mb-6">\n};
    print qq{  <button type="button" class="btn btn-primary btn-sm" onclick="location.href='schedules.cgi?action=add'">Add Schedule</button>\n};
    print qq{</div>\n};

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

# ── Add ──────────────────────────────────────────────────────

sub handle_add {
    my @errors;

    if ($method eq 'POST') {
        unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
            push @errors, 'Invalid or expired form token. Please try again.';
        }

        my $name = $form->{'schedule_name'} // '';
        my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
        push @errors, $name_err if $name_err;

        if (!@errors && -f Gniza4cpWHM::UI::schedule_conf_path($name)) {
            push @errors, "A schedule named '$name' already exists.";
        }

        my %data;
        for my $key (@Gniza4cpWHM::Config::SCHEDULE_KEYS) {
            $data{$key} = $form->{$key} // '';
        }

        if (!@errors) {
            my $validation_errors = Gniza4cpWHM::Validator::validate_schedule_config(\%data);
            push @errors, @$validation_errors;
        }

        if (!@errors) {
            my $dest = Gniza4cpWHM::UI::schedule_conf_path($name);
            my $example = Gniza4cpWHM::UI::schedule_example_path();
            if (-f $example) {
                File::Copy::copy($example, $dest)
                    or do { push @errors, "Failed to create schedule file: $!"; goto RENDER_ADD; };
            }
            my ($ok, $err) = Gniza4cpWHM::Config::save($dest, \%data, \@Gniza4cpWHM::Config::SCHEDULE_KEYS);
            if ($ok) {
                my ($cron_ok, $cron_err) = Gniza4cpWHM::Cron::install_schedule($name);
                if ($form->{'wizard'}) {
                    Gniza4cpWHM::UI::set_flash('success', 'Setup complete! Your remote and schedule are configured.');
                    print "Status: 302 Found\r\n";
                    print "Location: index.cgi\r\n\r\n";
                    exit;
                }
                if ($cron_ok) {
                    Gniza4cpWHM::UI::set_flash('success', "Schedule '$name' created and activated.");
                } else {
                    Gniza4cpWHM::UI::set_flash('warning', "Schedule '$name' created but cron activation failed: $cron_err");
                }
                print "Status: 302 Found\r\n";
                print "Location: schedules.cgi\r\n\r\n";
                exit;
            } else {
                push @errors, "Failed to save schedule config: $err";
            }
        }
    }

    RENDER_ADD:

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

    print Gniza4cpWHM::UI::page_header('Add Schedule');
    print Gniza4cpWHM::UI::render_nav('schedules.cgi');

    if (@errors) {
        print Gniza4cpWHM::UI::render_errors(\@errors);
    }

    my $conf = {};
    if ($method eq 'POST') {
        for my $key (@Gniza4cpWHM::Config::SCHEDULE_KEYS) {
            $conf->{$key} = $form->{$key} // '';
        }
    } elsif ($form->{'remote_name'}) {
        $conf->{REMOTES} = $form->{'remote_name'};
    }
    my $name_val = Gniza4cpWHM::UI::esc($form->{'schedule_name'} // '');

    render_schedule_form($conf, $name_val, 0, $form->{'wizard'} ? 1 : 0);

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

# ── Edit ─────────────────────────────────────────────────────

sub handle_edit {
    my $name = $form->{'name'} // '';
    my @errors;

    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        Gniza4cpWHM::UI::set_flash('error', "Invalid schedule name.");
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    my $conf_path = Gniza4cpWHM::UI::schedule_conf_path($name);
    unless (-f $conf_path) {
        Gniza4cpWHM::UI::set_flash('error', "Schedule '$name' not found.");
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    if ($method eq 'POST') {
        unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
            push @errors, 'Invalid or expired form token. Please try again.';
        }

        my %data;
        for my $key (@Gniza4cpWHM::Config::SCHEDULE_KEYS) {
            $data{$key} = $form->{$key} // '';
        }

        if (!@errors) {
            my $validation_errors = Gniza4cpWHM::Validator::validate_schedule_config(\%data);
            push @errors, @$validation_errors;
        }

        if (!@errors) {
            my ($ok, $err) = Gniza4cpWHM::Config::save($conf_path, \%data, \@Gniza4cpWHM::Config::SCHEDULE_KEYS);
            if ($ok) {
                # Always reinstall cron so changes take effect immediately
                Gniza4cpWHM::Cron::install_schedule($name);
                Gniza4cpWHM::UI::set_flash('success', "Schedule '$name' updated successfully.");
                print "Status: 302 Found\r\n";
                print "Location: schedules.cgi\r\n\r\n";
                exit;
            } else {
                push @errors, "Failed to save schedule config: $err";
            }
        }
    }

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

    print Gniza4cpWHM::UI::page_header("Edit Schedule: " . Gniza4cpWHM::UI::esc($name));
    print Gniza4cpWHM::UI::render_nav('schedules.cgi');

    if (@errors) {
        print Gniza4cpWHM::UI::render_errors(\@errors);
    }

    my $conf;
    if (@errors && $method eq 'POST') {
        $conf = {};
        for my $key (@Gniza4cpWHM::Config::SCHEDULE_KEYS) {
            $conf->{$key} = $form->{$key} // '';
        }
    } else {
        $conf = Gniza4cpWHM::Config::parse($conf_path, 'schedule');
    }

    render_schedule_form($conf, Gniza4cpWHM::UI::esc($name), 1);

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

# ── Delete ───────────────────────────────────────────────────

sub handle_delete {
    if ($method ne 'POST') {
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    unless (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: schedules.cgi\r\n\r\n";
        exit;
    }

    my $name = $form->{'name'} // '';
    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        Gniza4cpWHM::UI::set_flash('error', 'Invalid schedule name.');
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    my $conf_path = Gniza4cpWHM::UI::schedule_conf_path($name);
    if (-f $conf_path) {
        Gniza4cpWHM::Cron::remove_schedule($name);
        unlink $conf_path;
        Gniza4cpWHM::UI::set_flash('success', "Schedule '$name' deleted.");
    } else {
        Gniza4cpWHM::UI::set_flash('error', "Schedule '$name' not found.");
    }

    print "Status: 302 Found\r\n";
    print "Location: schedules.cgi\r\n\r\n";
    exit;
}

# ── Run Now ──────────────────────────────────────────────────

sub handle_run_now {
    if ($method ne 'POST') {
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    unless (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: schedules.cgi\r\n\r\n";
        exit;
    }

    my $name = $form->{'name'} // '';
    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        Gniza4cpWHM::UI::set_flash('error', 'Invalid schedule name.');
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    my $conf_path = Gniza4cpWHM::UI::schedule_conf_path($name);
    unless (-f $conf_path) {
        Gniza4cpWHM::UI::set_flash('error', "Schedule '$name' not found.");
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    my $conf = Gniza4cpWHM::Config::parse($conf_path, 'schedule');
    my $remotes = $conf->{REMOTES} // '';
    $remotes =~ s/^\s+|\s+$//g;

    # Build command
    my @cmd = ('/usr/local/bin/gniza4cp', 'backup');
    if ($remotes ne '') {
        push @cmd, "--remote=$remotes";
    }
    if (($conf->{SYSBACKUP} // '') eq 'yes') {
        push @cmd, '--sysbackup';
    }
    if (($conf->{SKIP_SUSPENDED} // '') eq 'yes') {
        push @cmd, '--skip-suspended';
    }

    my $log_file = "/var/log/gniza4cp/cron-${name}.log";

    # Double-fork to fully detach from CGI process group
    my $pid = fork();
    if (!defined $pid) {
        Gniza4cpWHM::UI::set_flash('error', "Failed to fork backup process: $!");
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    if ($pid == 0) {
        # First child: new session, then fork again
        POSIX::setsid();
        my $pid2 = fork();
        exit 0 if $pid2;   # first child exits immediately
        # Grandchild: fully detached daemon
        close STDIN;
        open STDIN, '<', '/dev/null';
        close STDOUT;
        open STDOUT, '>>', $log_file;
        close STDERR;
        open STDERR, '>&', \*STDOUT;
        exec @cmd;
        exit 1;
    }

    waitpid($pid, 0);  # Reap first child (exits immediately)

    # Parent: redirect
    Gniza4cpWHM::UI::set_flash('success', "Backup started for schedule '$name'. Check Logs for progress.");
    print "Status: 302 Found\r\n";
    print "Location: schedules.cgi\r\n\r\n";
    exit;
}

# ── Toggle Cron ──────────────────────────────────────────────

sub handle_toggle_cron {
    if ($method ne 'POST') {
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
        my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();
        _json_response(0, 0, 'Invalid or expired form token.', $new_csrf);
    }

    my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();

    my $name = $form->{'name'} // '';
    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        _json_response(0, 0, 'Invalid schedule name.', $new_csrf);
    }

    my $cron_schedules = Gniza4cpWHM::Cron::get_current_schedules();
    my $is_active = exists $cron_schedules->{$name};

    if ($is_active) {
        my ($ok, $err) = Gniza4cpWHM::Cron::remove_schedule($name);
        if ($ok) {
            _json_response(1, 0, "Cron disabled for '$name'.", $new_csrf);
        } else {
            _json_response(0, 1, "Failed to remove cron: $err", $new_csrf);
        }
    } else {
        my ($ok, $err) = Gniza4cpWHM::Cron::install_schedule($name);
        if ($ok) {
            _json_response(1, 1, "Cron enabled for '$name'.", $new_csrf);
        } else {
            _json_response(0, 0, "Failed to install cron: $err", $new_csrf);
        }
    }
}

# ── Toggle Sysbackup ────────────────────────────────────────────

sub handle_toggle_sysbackup {
    if ($method ne 'POST') {
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
        my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();
        _json_response(0, 0, 'Invalid or expired form token.', $new_csrf);
    }

    my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();

    my $name = $form->{'name'} // '';
    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        _json_response(0, 0, 'Invalid schedule name.', $new_csrf);
    }

    my $conf_path = Gniza4cpWHM::UI::schedule_conf_path($name);
    unless (-f $conf_path) {
        _json_response(0, 0, "Schedule '$name' not found.", $new_csrf);
    }

    my $conf = Gniza4cpWHM::Config::parse($conf_path, 'schedule');
    my $is_on = (($conf->{SYSBACKUP} // '') eq 'yes');

    # Toggle the value
    $conf->{SYSBACKUP} = $is_on ? '' : 'yes';
    my ($ok, $err) = Gniza4cpWHM::Config::save($conf_path, $conf, \@Gniza4cpWHM::Config::SCHEDULE_KEYS);

    if ($ok) {
        # Reinstall cron so --sysbackup flag is updated
        Gniza4cpWHM::Cron::install_schedule($name);
        my $new_state = $is_on ? 0 : 1;
        my $label = $new_state ? 'enabled' : 'disabled';
        _json_response(1, $new_state, "System backup $label for '$name'.", $new_csrf);
    } else {
        _json_response(0, $is_on ? 1 : 0, "Failed to update config: $err", $new_csrf);
    }
}

# ── Toggle Skip Suspended ──────────────────────────────────────

sub handle_toggle_skip_suspended {
    if ($method ne 'POST') {
        print "Status: 302 Found\r\n";
        print "Location: schedules.cgi\r\n\r\n";
        exit;
    }

    unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
        my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();
        _json_response(0, 0, 'Invalid or expired form token.', $new_csrf);
    }

    my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();

    my $name = $form->{'name'} // '';
    my $name_err = Gniza4cpWHM::Validator::validate_schedule_name($name);
    if ($name_err) {
        _json_response(0, 0, 'Invalid schedule name.', $new_csrf);
    }

    my $conf_path = Gniza4cpWHM::UI::schedule_conf_path($name);
    unless (-f $conf_path) {
        _json_response(0, 0, "Schedule '$name' not found.", $new_csrf);
    }

    my $conf = Gniza4cpWHM::Config::parse($conf_path, 'schedule');
    my $is_on = (($conf->{SKIP_SUSPENDED} // '') eq 'yes');

    # Toggle the value
    $conf->{SKIP_SUSPENDED} = $is_on ? '' : 'yes';
    my ($ok, $err) = Gniza4cpWHM::Config::save($conf_path, $conf, \@Gniza4cpWHM::Config::SCHEDULE_KEYS);

    if ($ok) {
        # Reinstall cron so --skip-suspended flag is updated
        Gniza4cpWHM::Cron::install_schedule($name);
        my $new_state = $is_on ? 0 : 1;
        my $label = $new_state ? 'enabled' : 'disabled';
        _json_response(1, $new_state, "Skip suspended $label for '$name'.", $new_csrf);
    } else {
        _json_response(0, $is_on ? 1 : 0, "Failed to update config: $err", $new_csrf);
    }
}

sub _json_response {
    my ($ok, $active, $message, $csrf) = @_;
    # Escape for JSON
    $message =~ s/\\/\\\\/g;
    $message =~ s/"/\\"/g;
    my $active_str = $active ? 'true' : 'false';
    my $ok_str     = $ok     ? 'true' : 'false';
    print "Content-Type: application/json\r\n\r\n";
    print qq({"ok":$ok_str,"active":$active_str,"message":"$message","csrf":"$csrf"});
    exit;
}

# ── Shared Form Renderer ────────────────────────────────────

sub render_schedule_form {
    my ($conf, $name_val, $is_edit, $wizard) = @_;

    my $action_val = $is_edit ? 'edit' : 'add';
    print qq{<form method="POST" action="schedules.cgi">\n};
    print qq{<input type="hidden" name="action" value="$action_val">\n};
    if ($wizard) {
        print qq{<input type="hidden" name="wizard" value="1">\n};
    }
    print Gniza4cpWHM::UI::csrf_hidden_field();

    if ($is_edit) {
        print qq{<input type="hidden" name="name" value="$name_val">\n};
    }

    # Schedule name
    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">Schedule Identity</h2>\n};
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="schedule_name">Schedule Name</label>\n};
    if ($is_edit) {
        print qq{  <input type="text" class="input input-bordered input-sm w-full max-w-xs" value="$name_val" disabled>\n};
    } else {
        print qq{  <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="schedule_name" name="schedule_name" value="$name_val" required>\n};
        print qq{  <span class="text-xs text-base-content/60 ml-2">Letters, digits, hyphens, underscores</span>\n};
    }
    print qq{</div>\n};
    print qq{</div>\n</div>\n};

    # Schedule settings
    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">Schedule Settings</h2>\n};

    my $sched = $conf->{SCHEDULE} // '';
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="SCHEDULE">Schedule Type</label>\n};
    print qq{  <select class="select select-bordered select-sm w-full max-w-xs" id="SCHEDULE" name="SCHEDULE" onchange="gniza4cpScheduleChange()">\n};
    for my $opt ('hourly', 'daily', 'weekly', 'monthly', 'custom') {
        my $sel = ($sched eq $opt) ? ' selected' : '';
        print qq{    <option value="} . Gniza4cpWHM::UI::esc($opt) . qq{"$sel>$opt</option>\n};
    }
    print qq{  </select>\n};
    print qq{</div>\n};

    _sched_field($conf, 'SCHEDULE_TIME', 'Time (HH:MM)', 'Default: 02:00');

    print qq{<div id="gniza4cp-schedule-day">\n};
    _sched_field($conf, 'SCHEDULE_DAY', 'Day', 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)');
    print qq{</div>\n};

    print qq{<div id="gniza4cp-schedule-cron">\n};
    _sched_field($conf, 'SCHEDULE_CRON', 'Cron Expression', '5-field cron (for custom only)');
    print qq{</div>\n};

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

    # Target remotes
    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">Target Remotes</h2>\n};
    print qq{<p class="text-xs text-base-content/60 mb-3">Select which remotes this schedule targets. Leave all unchecked to target all remotes.</p>\n};

    my @remotes = Gniza4cpWHM::UI::list_remotes();
    my %selected_remotes;
    my $remotes_str = $conf->{REMOTES} // '';
    for my $r (split /,/, $remotes_str) {
        $r =~ s/^\s+|\s+$//g;
        $selected_remotes{$r} = 1 if $r ne '';
    }

    if (@remotes) {
        for my $rname (@remotes) {
            my $esc_name = Gniza4cpWHM::UI::esc($rname);
            my $checked = $selected_remotes{$rname} ? ' checked' : '';
            print qq{<label class="flex items-center gap-2 mb-1 cursor-pointer">\n};
            print qq{  <input type="checkbox" class="toggle toggle-sm toggle-success" name="remote_$esc_name" value="1"$checked>\n};
            print qq{  <span class="text-sm">$esc_name</span>\n};
            print qq{</label>\n};
        }
        # Hidden field to collect selected remotes via JS
        print qq{<input type="hidden" name="REMOTES" id="remotes_hidden" value="} . Gniza4cpWHM::UI::esc($remotes_str) . qq{">\n};
    } else {
        print qq{<p class="text-sm">No remotes configured. <a href="remotes.cgi?action=add" class="link">Add a remote</a> first.</p>\n};
    }

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

    # Options toggles
    my $sysbackup_val = $conf->{SYSBACKUP} // '';
    my $sysbackup_checked = ($sysbackup_val eq 'yes') ? ' checked' : '';
    my $skip_suspended_val = $conf->{SKIP_SUSPENDED} // '';
    my $skip_suspended_checked = ($skip_suspended_val eq 'yes') ? ' checked' : '';
    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">Options</h2>\n};
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-52 font-medium text-sm whitespace-nowrap" for="SYSBACKUP">Include system backup <span class="tooltip tooltip-top" data-tip="Back up WHM/cPanel config, installed packages, and cron jobs after all account backups">&#9432;</span></label>\n};
    print qq{  <input type="checkbox" class="toggle toggle-sm toggle-success" id="SYSBACKUP" name="SYSBACKUP" value="yes"$sysbackup_checked>\n};
    print qq{</div>\n};
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-52 font-medium text-sm whitespace-nowrap" for="SKIP_SUSPENDED">Skip suspended accounts <span class="tooltip tooltip-top" data-tip="Exclude cPanel accounts that are currently suspended from this backup schedule">&#9432;</span></label>\n};
    print qq{  <input type="checkbox" class="toggle toggle-sm toggle-success" id="SKIP_SUSPENDED" name="SKIP_SUSPENDED" value="yes"$skip_suspended_checked>\n};
    print qq{</div>\n};
    print qq{</div>\n</div>\n};

    # Submit
    print qq{<div class="flex items-center gap-2 mt-4">\n};
    my $btn_label = $is_edit ? 'Save Changes' : 'Create Schedule';
    print qq{  <button type="submit" class="btn btn-primary btn-sm" onclick="return gniza4cpCollectRemotes()">$btn_label</button>\n};
    print qq{  <button type="button" class="btn btn-info btn-sm" onclick="location.href='schedules.cgi'">Cancel</button>\n};
    print qq{</div>\n};

    print qq{</form>\n};

    # JS for schedule field visibility and remote collection
    print <<'JS';
<script>
function gniza4cpScheduleChange() {
    var sel = document.getElementById('SCHEDULE').value;
    var dayDiv  = document.getElementById('gniza4cp-schedule-day');
    var cronDiv = document.getElementById('gniza4cp-schedule-cron');
    dayDiv.style.display  = (sel === 'hourly' || sel === 'weekly' || sel === 'monthly') ? '' : 'none';
    cronDiv.style.display = (sel === 'custom') ? '' : 'none';
    var dayLabel = dayDiv.querySelector('label');
    var dayInput = dayDiv.querySelector('input');
    var dayHint  = dayDiv.querySelector('.text-xs');
    if (sel === 'hourly') {
        if (dayLabel) dayLabel.textContent = 'Every N hours';
        if (dayHint)  dayHint.textContent = '1-23 (default: 1 = every hour)';
    } else {
        if (dayLabel) dayLabel.textContent = 'Day';
        if (dayHint)  dayHint.textContent = 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)';
    }
}
function gniza4cpCollectRemotes() {
    var checks = document.querySelectorAll('input[type="checkbox"][name^="remote_"]');
    var selected = [];
    for (var i = 0; i < checks.length; i++) {
        if (checks[i].checked) {
            selected.push(checks[i].name.replace('remote_', ''));
        }
    }
    var hidden = document.getElementById('remotes_hidden');
    if (hidden) { hidden.value = selected.join(','); }
    return true;
}
gniza4cpScheduleChange();
</script>
JS
}

sub _sched_field {
    my ($conf, $key, $label, $hint) = @_;
    my $val = Gniza4cpWHM::UI::esc($conf->{$key} // '');
    my $hint_html = $hint ? qq{ <span class="text-xs text-base-content/60 ml-2">$hint</span>} : '';
    print qq{<div class="flex items-center gap-3 mb-2.5">\n};
    print qq{  <label class="w-44 font-medium text-sm" for="$key">$label</label>\n};
    print qq{  <input type="text" class="input input-bordered input-sm w-full max-w-xs" id="$key" name="$key" value="$val">\n};
    print qq{  $hint_html\n} if $hint;
    print qq{</div>\n};
}
