package PVE::VZDump::LXC;

use strict;
use warnings;

use File::Basename;
use File::Path;
use POSIX qw(strftime);

use PVE::Cluster qw(cfs_read_file);
use PVE::GuestHelpers;
use PVE::INotify;
use PVE::LXC::Config;
use PVE::LXC::Namespaces;
use PVE::LXC;
use PVE::Storage;
use PVE::Tools;
use PVE::VZDump;

use base qw (PVE::VZDump::Plugin);

my $default_mount_point = "/mnt/vzsnap0";

my $rsync_vm = sub {
    my ($self, $task, $to, $text) = @_;

    my $disks = $task->{disks};
    my $from = $disks->[0]->{dir};
    $self->loginfo("starting $text sync $from to $to");

    my $opts = $self->{vzdump}->{opts};

    my @xattr = $task->{no_xattrs} ? () : ('-X', '-A');

    my $rsync = [
        'rsync',
        '--stats',
        '-h',
        @xattr,
        '--numeric-ids',
        '-aH',
        '--delete',
        '--no-whole-file',
        '--sparse',
        '--one-file-system',
        '--relative',
    ];
    push @$rsync, "--bwlimit=$opts->{bwlimit}" if $opts->{bwlimit};
    push @$rsync, map { "--exclude=$_" } @{ $self->{vzdump}->{findexcl} };
    push @$rsync, map { "--exclude=$_" } @{ $task->{exclude_dirs} };

    # See the rsync(1) manpage for --relative in conjunction with /./ in paths.
    # This is the only way to have exclude-dirs work together with the
    # --one-file-system option.
    # This way we can pass multiple source paths and tell rsync which directory
    # they're supposed to be relative to.
    # Otherwise with eg. using multiple rsync commands means the --exclude
    # directives need to be modified for every command as they are meant to be
    # relative to the rootdir, while rsync treats them as relative to the
    # source dir.
    foreach my $disk (@$disks) {
        push @$rsync, "$from/.$disk->{mp}";
    }

    my $transferred = '';
    my $outfunc = sub {
        return if $_[0] !~ /^Total transferred file size: (.+)$/;
        $transferred = $1;
    };

    my $errfunc = sub { $self->logerr($_[0]); };

    my $starttime = time();
    PVE::Tools::run_command([@$rsync, $to], outfunc => $outfunc, errfunc => $errfunc);
    my $delay = time() - $starttime;

    $self->loginfo("$text sync finished - transferred $transferred in ${delay}s");
};

sub new {
    my ($class, $vzdump) = @_;

    PVE::VZDump::check_bin('lxc-stop');
    PVE::VZDump::check_bin('lxc-start');

    my $self = bless {}, $class;

    $self->{vzdump} = $vzdump;
    $self->{storecfg} = PVE::Storage::config();

    $self->{vmlist} = PVE::LXC::config_list();

    return $self;
}

sub type {
    return 'lxc';
}

sub vm_status {
    my ($self, $vmid) = @_;

    my $running = PVE::LXC::check_running($vmid) ? 1 : 0;

    return wantarray ? ($running, $running ? 'running' : 'stopped') : $running;
}

my $check_mountpoint_empty = sub {
    my ($mountpoint) = @_;

    die "mount point '$mountpoint' is not a directory\n" if !-d $mountpoint;

    PVE::Tools::dir_glob_foreach(
        $mountpoint,
        qr/.*/,
        sub {
            my $entry = shift;
            return if $entry eq '.' || $entry eq '..';
            die "mount point '$mountpoint' not empty\n";
        },
    );
};

sub prepare {
    my ($self, $task, $vmid, $mode) = @_;

    my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::Config->load_config($vmid);
    my $storage_cfg = $self->{storecfg};

    $self->loginfo("CT Name: $conf->{hostname}")
        if defined($conf->{hostname});

    my $running = PVE::LXC::check_running($vmid);

    my $disks = $task->{disks} = [];
    my $exclude_dirs = $task->{exclude_dirs} = [];

    $task->{hostname} = $conf->{'hostname'} || "CT$vmid";

    my ($id_map, $root_uid, $root_gid) = PVE::LXC::parse_id_maps($conf);
    $task->{userns_cmd} = PVE::LXC::userns_command($id_map);
    $task->{id_map} = $id_map;
    $task->{root_uid} = $root_uid;
    $task->{root_gid} = $root_gid;

    my $volids = $task->{volids} = [];

    my $backup_volumes = PVE::LXC::Config->get_backup_volumes($conf);

    for my $volume (@{$backup_volumes}) {
        my $name = $volume->{key};
        my $included = $volume->{included};
        my $volume_config = $volume->{volume_config};

        my $volid = $volume_config->{volume};
        my $mount = $volume_config->{mp};

        next if !$volid || !$mount;

        if (!$included) {
            my $type = $volume_config->{type};
            push @$exclude_dirs, $mount;
            $self->loginfo(
                "excluding $type mount point $name ('$mount') from backup ($volume->{reason})");
            next;
        }

        $volume_config->{name} = $name;

        # immutable raw base images need RO mount
        if ($conf->{template} && !defined($volume_config->{ro})) {
            $volume_config->{ro} = 1;
        }

        $self->loginfo("including mount point $name ('$mount') in backup");
        push @$disks, $volume_config;
        push @$volids, $volid if $included;
    }

    if ($mode eq 'snapshot') {
        if (!PVE::LXC::Config->has_feature('snapshot', $conf, $storage_cfg, undef, undef, 1)) {
            die "mode failure - some volumes do not support snapshots\n";
        }

        if ($conf->{snapshots} && $conf->{snapshots}->{vzdump}) {
            $self->loginfo("found old vzdump snapshot (force removal)");
            PVE::LXC::Config->lock_config(
                $vmid,
                sub {
                    $self->unlock_vm($vmid);
                    PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 1);
                    $self->lock_vm($vmid);
                },
            );
        }

        my $rootdir = $default_mount_point;
        mkpath $rootdir;
        &$check_mountpoint_empty($rootdir);

        # set snapshot_count (freezes CT if snapshot_count > 1)
        $task->{snapshot_count} = scalar(@$volids);
    } elsif ($mode eq 'stop') {
        my $rootdir = $default_mount_point;
        mkpath $rootdir;
        &$check_mountpoint_empty($rootdir);
    } elsif ($mode eq 'suspend') {
        my $pid = PVE::LXC::find_lxc_pid($vmid);
        foreach my $disk (@$disks) {
            $disk->{dir} = "/proc/$pid/root$disk->{mp}";
        }
        $task->{snapdir} = $task->{tmpdir};
    } else {
        unlock_vm($self, $vmid);
        die "unknown mode '$mode'\n"; # should not happen
    }

    if ($mode ne 'suspend') {
        # If we perform mount operations, let's unshare the mount namespace
        # to not influence the running host.
        PVE::Tools::unshare(PVE::Tools::CLONE_NEWNS);
        PVE::Tools::run_command(['mount', '--make-rslave', '/']);
    }
}

sub lock_vm {
    my ($self, $vmid) = @_;

    PVE::LXC::Config->set_lock($vmid, 'backup');
}

sub unlock_vm {
    my ($self, $vmid) = @_;

    PVE::LXC::Config->remove_lock($vmid, 'backup');
}

sub snapshot {
    my ($self, $task, $vmid) = @_;

    $self->loginfo("create storage snapshot 'vzdump'");

    # todo: freeze/unfreeze if we have more than one volid
    PVE::LXC::Config->lock_config(
        $vmid,
        sub {
            $self->unlock_vm($vmid);
            PVE::LXC::Config->snapshot_create($vmid, 'vzdump', 0, "vzdump backup snapshot");
            $self->lock_vm($vmid);
        },
    );
    $task->{cleanup}->{remove_snapshot} = 1;

    # reload config
    my $conf = $self->{vmlist}->{$vmid} = PVE::LXC::Config->load_config($vmid);
    die "unable to read vzdump snapshot config - internal error"
        if !($conf->{snapshots} && $conf->{snapshots}->{vzdump});

    my $disks = $task->{disks};
    my $volids = $task->{volids};

    my $rootdir = $default_mount_point;
    my $storage_cfg = $self->{storecfg};

    PVE::Storage::activate_volumes($storage_cfg, $volids, 'vzdump');
    foreach my $disk (@$disks) {
        $disk->{dir} = "${rootdir}$disk->{mp}";
        PVE::LXC::mountpoint_mount(
            $disk, $rootdir, $storage_cfg, 'vzdump', $task->{root_uid}, $task->{root_gid},
        );
    }

    $task->{snapdir} = $rootdir;
}

sub copy_data_phase1 {
    my ($self, $task) = @_;

    if (my $mntinfo = PVE::VZDump::get_mount_info($task->{snapdir})) {
        if ($mntinfo->{fstype} =~ /^nfs4?/) {
            $self->loginfo("temporary directory is on NFS, disabling xattr and acl"
                . " support, consider configuring a local tmpdir via"
                . " /etc/vzdump.conf\n");
            $task->{no_xattrs} = 1;
        }
    }

    $self->$rsync_vm($task, $task->{snapdir}, "first");
}

sub copy_data_phase2 {
    my ($self, $task) = @_;

    $self->$rsync_vm($task, $task->{snapdir}, "final");
}

sub stop_vm {
    my ($self, $task, $vmid) = @_;

    my $opts = $self->{vzdump}->{opts};
    my $timeout = $opts->{stopwait} * 60;

    PVE::LXC::vm_stop($vmid, 0, $timeout);
}

sub start_vm {
    my ($self, $task, $vmid) = @_;

    my $conf = PVE::LXC::Config->load_config($vmid);
    PVE::LXC::vm_start($vmid, $conf);
}

sub suspend_vm {
    my ($self, $task, $vmid) = @_;

    PVE::LXC::freeze($vmid);
}

sub resume_vm {
    my ($self, $task, $vmid) = @_;

    PVE::LXC::thaw($vmid);
}

sub assemble {
    my ($self, $task, $vmid) = @_;

    my $opts = $self->{vzdump}->{opts};

    my $conf = PVE::LXC::Config->load_config($vmid);
    delete $conf->{lock};
    delete $conf->{snapshots};
    delete $conf->{parent};
    delete $conf->{pending};

    my $tmpdir = $task->{tmpdir};

    mkpath "$tmpdir/etc/vzdump/";

    PVE::Tools::file_set_contents(
        "$tmpdir/etc/vzdump/pct.conf",
        PVE::LXC::Config::write_pct_config("/lxc/$vmid.conf", $conf),
    );

    my $firewall = "/etc/pve/firewall/$vmid.fw";
    my $fwconftmp = "$tmpdir/etc/vzdump/pct.fw";

    if ($self->{vzdump}->{opts}->{pbs}) {
        # fixme: do not store pct.conf and fw.conf into $tmpdir
        if (-e $firewall) {
            PVE::Tools::file_copy($firewall, $fwconftmp);
        }
    } else {
        if (-e $firewall) {
            PVE::Tools::file_copy($firewall, $fwconftmp);
        } else {
            PVE::Tools::file_set_contents($fwconftmp, '');
        }
        $task->{fw} = 1;
    }
}

sub archive {
    my ($self, $task, $vmid, $filename, $comp) = @_;

    my $disks = $task->{disks};
    my @sources;

    if ($task->{mode} eq 'stop') {
        my $storage_cfg = $self->{storecfg};

        PVE::Storage::activate_volumes($storage_cfg, $task->{volids});

        my $rootdir = $default_mount_point;
        foreach my $disk (@$disks) {
            $disk->{dir} = "${rootdir}$disk->{mp}";
            PVE::LXC::mountpoint_mount(
                $disk, $rootdir, $storage_cfg, undef, $task->{root_uid}, $task->{root_gid},
            );
            # add every enabled mountpoint (since we use --one-file-system)
            # mp already starts with a / so we only need to add the dot
            push @sources, ".$disk->{mp}";
        }
        $task->{snapdir} = $rootdir;
    } elsif ($task->{mode} eq 'snapshot') {
        # mounting the vzdump snapshots and setting $snapdir is already done,
        # but we need to include all mountpoints here!
        foreach my $disk (@$disks) {
            push @sources, ".$disk->{mp}";
        }
    } else {
        # the data was rsynced to a temporary location, only use '.' to avoid
        # having mountpoints duplicated
        push @sources, '.';
    }

    my $opts = $self->{vzdump}->{opts};
    my $snapdir = $task->{snapdir};
    my $tmpdir = $task->{tmpdir};

    my $userns_cmd = $task->{userns_cmd};
    my $findexcl = $self->{vzdump}->{findexcl};

    if (my $backup_provider = $self->{vzdump}->{'backup-provider'}) {
        $self->loginfo("starting external backup via " . $backup_provider->provider_name());

        if (!scalar($task->{id_map}->@*) || $task->{root_uid} == 0 || $task->{root_gid} == 0) {
            $self->log(
                "warn",
                "external backup of privileged container can only be restored as"
                    . " unprivileged which might not work in all cases",
            );
        }

        my $mechanism = $backup_provider->backup_get_mechanism($vmid, 'lxc');
        die "mechanism '$mechanism' requested by backup provider is not supported for containers\n"
            if $mechanism ne 'directory';

        $self->loginfo("using backup mechanism '$mechanism'");

        my $guest_config = PVE::Tools::file_get_contents("$tmpdir/etc/vzdump/pct.conf");
        my $firewall_file = "$tmpdir/etc/vzdump/pct.fw";

        my $info = {
            directory => $snapdir,
            sources => [@sources],
            'backup-user-id' => $task->{root_uid},
        };
        $info->{'firewall-config'} = PVE::Tools::file_get_contents($firewall_file)
            if -e $firewall_file;
        $info->{'bandwidth-limit'} = $opts->{bwlimit} * 1024 if $opts->{bwlimit};

        $backup_provider->backup_container_prepare($vmid, $info);

        if (scalar($task->{id_map}->@*)) {
            PVE::LXC::Namespaces::run_in_userns(
                sub {
                    $backup_provider->backup_container($vmid, $guest_config, $findexcl, $info);
                },
                $task->{id_map},
            );
        } else {
            $backup_provider->backup_container($vmid, $guest_config, $findexcl, $info);
        }
    } elsif ($self->{vzdump}->{opts}->{pbs}) {

        my $param = [];
        push @$param, "pct.conf:$tmpdir/etc/vzdump/pct.conf";

        my $fw_conf = "$tmpdir/etc/vzdump/pct.fw";
        if (-f $fw_conf) {
            push @$param, "fw.conf:$fw_conf";
        }

        my $rootdir = $snapdir;
        push @$param, "root.pxar:$rootdir";

        foreach my $disk (@sources) {
            push @$param, '--include-dev', "$snapdir/$disk";
        }

        push @$param, '--skip-lost-and-found' if $userns_cmd;
        push @$param, map { "--exclude=$_" } @$findexcl;

        push @$param, '--backup-type', 'ct';
        push @$param, '--backup-id', $vmid;
        push @$param, '--backup-time', $task->{backup_time};
        push @$param, '--change-detection-mode', $opts->{"pbs-change-detection-mode"}
            if $opts->{"pbs-change-detection-mode"};

        if (my $entries_max = $opts->{"performance"}->{"pbs-entries-max"}) {
            push $param->@*, '--entries-max', $entries_max;
            $self->loginfo(
                "set max number of entries in memory for file-based backups to $entries_max");
        }

        my @storage = ($opts->{scfg}, $opts->{storage});

        my $logfunc = sub { my $line = shift; $self->loginfo($line) };
        PVE::Storage::PBSPlugin::run_raw_client_cmd(
            @storage, 'backup', $param,
            logfunc => $logfunc,
            userns_cmd => $userns_cmd,
        );

        # FIXME: move to a pve-common helper, storage has a similar one
        my $time_iso8601 = strftime('%FT%TZ', gmtime($task->{backup_time}));
        my $snapshot_id = "ct/$vmid/$time_iso8601";
        my $res =
            eval { PVE::Storage::PBSPlugin::run_client_cmd(@storage, 'files', [$snapshot_id]) }
            // [];
        $task->{size} = 0;
        $task->{size} += $_->{size} for @$res;

    } else {

        my $tar = [
            @$userns_cmd,
            'tar',
            'cpf',
            '-',
            '--totals',
            @PVE::Storage::Plugin::COMMON_TAR_FLAGS,
            '--one-file-system',
            '--warning=no-file-ignored',
        ];

        # note: --remove-files does not work because we do not
        # backup all files (filters). tar complains:
        # Cannot rmdir: Directory not empty
        # we disable this optimization for now
        #if ($snapdir eq $task->{tmpdir} && $snapdir =~ m|^$opts->{dumpdir}/|) {
        #       push @$tar, "--remove-files"; # try to save space
        #}

        # The directory parameter can give an alternative directory as source.
        # the second parameter gives the structure in the tar.
        push @$tar, "--directory=$tmpdir", './etc/vzdump/pct.conf';
        push @$tar, "./etc/vzdump/pct.fw" if $task->{fw};
        push @$tar, "--directory=$snapdir";

        my @findexcl_no_anchored = ();
        my @findexcl_anchored = ();
        foreach my $pattern (@{$findexcl}) {
            if ($pattern !~ m|^/|) {
                push @findexcl_no_anchored, $pattern;
            } else {
                push @findexcl_anchored, $pattern;
            }

            # NOTE rsync and proxmox-backup-client will match directories, but not files when there
            # is a trailing slash, tar won't match either. For suspend mode, rsync already did the
            # exclusion, so no need to warn.
            # TODO PVE 9 - consider matching "$pattern*" and "$pattern.*" in this case, which will
            # only include the empty directory to more closely align the behavior between different
            # modes. Don't forget to update the docs!
            $self->log("warn", "tar does not match exclusion with a trailing slash '$pattern'")
                if $pattern =~ m|/$| && $task->{mode} ne 'suspend';
        }

        push @$tar, '--no-anchored';
        push @$tar, '--exclude=lost+found' if $userns_cmd;
        push @$tar, map { "--exclude=$_" } @findexcl_no_anchored;

        push @$tar, '--anchored';
        push @$tar, map { "--exclude=.$_" } @findexcl_anchored;

        push @$tar, @sources;

        my $cmd = [$tar];

        my $bwl = $opts->{bwlimit} * 1024; # bandwidth limit for cstream
        push @$cmd, ['cstream', '-t', $bwl] if $opts->{bwlimit};
        push @$cmd, [split(/\s+/, $comp)] if $comp;

        if ($opts->{stdout}) {
            $self->cmd($cmd, output => ">&" . fileno($opts->{stdout}));
        } else {
            push @{ $cmd->[-1] }, \(">" . PVE::Tools::shellquote($filename));
            $self->cmd($cmd);
        }
    }
}

sub cleanup {
    my ($self, $task, $vmid) = @_;

    my $conf = PVE::LXC::Config->load_config($vmid);

    if ($task->{mode} ne 'suspend') {
        my $disks = $task->{disks};
        foreach my $disk (reverse @$disks) {
            PVE::Tools::run_command(['umount', '-l', '-d', $disk->{dir}]) if $disk->{dir};
        }
    }

    if ($task->{cleanup}->{remove_snapshot}) {
        my $lock_obtained;
        my $do_delete = sub {
            $lock_obtained = 1;
            $self->loginfo("cleanup temporary 'vzdump' snapshot");
            PVE::LXC::Config->snapshot_delete($vmid, 'vzdump', 0);
        };

        eval {
            eval { PVE::GuestHelpers::guest_migration_lock($vmid, 1, $do_delete); };
            if (my $err = $@) {
                die $err if $lock_obtained;

                $self->loginfo("waiting for active replication or other operation..");
                PVE::GuestHelpers::guest_migration_lock($vmid, 600, $do_delete);
            }
        };
        # will be cleaned up by next backup run anyway
        warn "snapshot 'vzdump' was not (fully) removed - $@" if $@;
    }
}

1;
