package Gniza4cpWHM::Cron;
# Per-schedule cron manipulation and gniza4cp schedule CLI wrappers.

use strict;
use warnings;
use IPC::Open3;
use Symbol 'gensym';

use Gniza4cpWHM::Config;

my $GNIZA4CP_BIN       = '/usr/local/bin/gniza4cp';
my $GNIZA4CP_TAG       = '# gniza4cp:';
my $SCHEDULES_DIR   = '/etc/gniza4cp/schedules.d';

# get_current_schedules()
# Reads crontab for gniza4cp entries tagged with "# gniza4cp:<name>".
# Returns hashref of { name => cron_line }.
sub get_current_schedules {
    my %schedules;

    my $crontab = '';
    if (open my $pipe, '-|', 'crontab', '-l') {
        local $/;
        $crontab = <$pipe> // '';
        close $pipe;
    }

    my @lines = split /\n/, $crontab;
    for (my $i = 0; $i < @lines; $i++) {
        if ($lines[$i] =~ /^\Q$GNIZA4CP_TAG\E(.+)$/) {
            my $name = $1;
            if ($i + 1 < @lines) {
                $schedules{$name} = $lines[$i + 1];
                $i++;  # skip the command line
            }
        }
    }

    return \%schedules;
}

# install_schedules()
# Runs: /usr/local/bin/gniza4cp schedule install
# Returns ($success, $stdout, $stderr).
sub install_schedules {
    return _run_gniza4cp_command('schedule', 'install');
}

# remove_schedules()
# Runs: /usr/local/bin/gniza4cp schedule remove
# Returns ($success, $stdout, $stderr).
sub remove_schedules {
    return _run_gniza4cp_command('schedule', 'remove');
}

# show_schedules()
# Runs: /usr/local/bin/gniza4cp schedule show
# Returns ($success, $stdout, $stderr).
sub show_schedules {
    return _run_gniza4cp_command('schedule', 'show');
}

# install_schedule($name)
# Builds a cron entry for a single schedule and installs it.
# Returns ($success, $error_message).
sub install_schedule {
    my ($name) = @_;

    my $conf_path = "$SCHEDULES_DIR/$name.conf";
    return (0, "Schedule config not found: $name") unless -f $conf_path;

    my $conf = Gniza4cpWHM::Config::parse($conf_path, 'schedule');
    my $schedule = $conf->{SCHEDULE} // '';
    return (0, "SCHEDULE not set in $name") unless $schedule;

    # Build 5-field cron expression
    my ($cron_expr, $err) = _schedule_to_cron($conf);
    return (0, $err) unless defined $cron_expr;

    # Build full cron command line
    my $extra_flags = '';
    my $remotes = $conf->{REMOTES} // '';
    $remotes =~ s/^\s+|\s+$//g;
    if ($remotes ne '') {
        $extra_flags .= " --remote=$remotes";
    }
    if (($conf->{SYSBACKUP} // '') eq 'yes') {
        $extra_flags .= " --sysbackup";
    }
    if (($conf->{SKIP_SUSPENDED} // '') eq 'yes') {
        $extra_flags .= " --skip-suspended";
    }
    my $cmd_line = "$cron_expr $GNIZA4CP_BIN backup${extra_flags} >/dev/null 2>&1";

    # Read current crontab, strip existing entry for this schedule, append new
    my $crontab = _read_crontab();
    $crontab = _strip_schedule_entries($crontab, $name);

    # Append new entry
    $crontab .= "\n" if $crontab ne '' && $crontab !~ /\n$/;
    $crontab .= "$GNIZA4CP_TAG$name\n$cmd_line\n";

    return _write_crontab($crontab);
}

# remove_schedule($name)
# Removes the cron entry for a single schedule.
# Returns ($success, $error_message).
sub remove_schedule {
    my ($name) = @_;

    my $crontab = _read_crontab();
    my $new_crontab = _strip_schedule_entries($crontab, $name);

    return (1, undef) if $new_crontab eq $crontab;  # nothing to remove
    return _write_crontab($new_crontab);
}

# cron_to_human($cron_line)
# Converts a gniza4cp cron line to human-readable timing and remotes.
# Returns ($timing, $remotes) in list context.
# e.g. "0 * * * * /usr/local/bin/gniza4cp backup --remote=rasp ..." => ("Every hour", "rasp")
sub cron_to_human {
    my ($cron_line) = @_;

    # Extract the 5 cron fields
    my @parts = split /\s+/, $cron_line, 6;
    return ($cron_line, '') if @parts < 6;
    my ($min, $hour, $dom, $mon, $dow) = @parts[0..4];

    # Build timing description
    my $timing;
    my @dow_names = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday);

    if ($hour eq '*' && $dom eq '*' && $mon eq '*' && $dow eq '*') {
        $timing = 'Every hour';
    }
    elsif ($dom eq '*' && $mon eq '*' && $dow eq '*') {
        $timing = sprintf('Daily at %02d:%02d', $hour, $min);
    }
    elsif ($dom eq '*' && $mon eq '*' && $dow =~ /^[0-6]$/) {
        my $day_name = $dow_names[$dow] // "day $dow";
        $timing = sprintf('Every %s at %02d:%02d', $day_name, $hour, $min);
    }
    elsif ($mon eq '*' && $dow eq '*' && $dom =~ /^\d+$/) {
        my $suffix = ($dom == 1 || $dom == 21 || $dom == 31) ? 'st'
                   : ($dom == 2 || $dom == 22) ? 'nd'
                   : ($dom == 3 || $dom == 23) ? 'rd'
                   : 'th';
        $timing = sprintf('%d%s of each month at %02d:%02d', $dom, $suffix, $hour, $min);
    }
    else {
        $timing = "$min $hour $dom $mon $dow";
    }

    # Extract target remotes from --remote= flag
    my $remotes = 'All';
    if ($cron_line =~ /--remote=(\S+)/) {
        $remotes = $1;
    }

    return ($timing, $remotes);
}

# -- Private --

# Only these exact commands are allowed.
my %ALLOWED_COMMANDS = (
    'schedule install' => 1,
    'schedule show'    => 1,
    'schedule remove'  => 1,
);

sub _run_gniza4cp_command {
    my (@args) = @_;
    my $cmd_key = join(' ', @args);

    unless ($ALLOWED_COMMANDS{$cmd_key}) {
        return (0, '', "Command not allowed: gniza4cp $cmd_key");
    }

    my $err = gensym;
    my $pid = open3(my $in, my $out, $err, $GNIZA4CP_BIN, @args);
    close $in;

    my $stdout = do { local $/; <$out> } // '';
    my $stderr = do { local $/; <$err> } // '';
    close $out;
    close $err;

    waitpid($pid, 0);
    my $exit_code = $? >> 8;

    return ($exit_code == 0, $stdout, $stderr);
}

# _schedule_to_cron(\%conf)
# Converts schedule config to a 5-field cron expression.
# Returns ($expr, undef) on success or (undef, $error) on failure.
sub _schedule_to_cron {
    my ($conf) = @_;

    my $schedule = $conf->{SCHEDULE} // '';
    my $time     = $conf->{SCHEDULE_TIME} // '02:00';
    my $day      = $conf->{SCHEDULE_DAY} // '';
    my $cron_raw = $conf->{SCHEDULE_CRON} // '';

    my ($hour, $minute) = (2, 0);
    if ($time =~ /^(\d{1,2}):(\d{2})$/) {
        ($hour, $minute) = ($1 + 0, $2 + 0);
    }

    if ($schedule eq 'hourly') {
        return ("$minute * * * *", undef);
    } elsif ($schedule eq 'daily') {
        return ("$minute $hour * * *", undef);
    } elsif ($schedule eq 'weekly') {
        return (undef, "SCHEDULE_DAY required for weekly") if $day eq '';
        return ("$minute $hour * * $day", undef);
    } elsif ($schedule eq 'monthly') {
        return (undef, "SCHEDULE_DAY required for monthly") if $day eq '';
        return ("$minute $hour $day * *", undef);
    } elsif ($schedule eq 'custom') {
        return (undef, "SCHEDULE_CRON required for custom") if $cron_raw eq '';
        return ($cron_raw, undef);
    }

    return (undef, "Unknown schedule type: $schedule");
}

# _read_crontab()
# Returns current crontab as string (empty string if none).
sub _read_crontab {
    my $crontab = '';
    if (open my $pipe, '-|', 'crontab', '-l') {
        local $/;
        $crontab = <$pipe> // '';
        close $pipe;
    }
    return $crontab;
}

# _write_crontab($content)
# Writes new crontab via 'crontab -'. Returns ($success, $error).
sub _write_crontab {
    my ($content) = @_;

    open my $pipe, '|-', 'crontab', '-'
        or return (0, "Failed to open crontab for writing: $!");
    print $pipe $content;
    close $pipe;

    if ($? != 0) {
        return (0, "crontab command failed with exit code " . ($? >> 8));
    }
    return (1, undef);
}

# _strip_schedule_entries($crontab, $name)
# Removes the tag line and following command line for a specific schedule.
sub _strip_schedule_entries {
    my ($crontab, $name) = @_;

    my @lines = split /\n/, $crontab, -1;
    my @out;
    my $i = 0;
    while ($i < @lines) {
        if ($lines[$i] eq "$GNIZA4CP_TAG$name") {
            # Skip tag line and the following command line
            $i++;
            $i++ if $i < @lines;  # skip command line
            next;
        }
        push @out, $lines[$i];
        $i++;
    }

    return join("\n", @out);
}

1;
