#!/usr/bin/env perl

# Use mandatory external modules
use strict;
use Cwd;
use File::Basename;
use Data::Dumper;
use POSIX;
use Config;
use Getopt::Long;

# Determine installation dir name
our $SELF_DIR = dirname(dirname(Cwd::abs_path(__FILE__)));

# Include parts of the system
require $SELF_DIR . '/lib/config.pm';
require $SELF_DIR . '/lib/log.pm';
require $SELF_DIR . '/lib/db.pm';

#-----------------------------------------------------------------
#Parse options

my $config_file = "mmm_lvm.conf";
my $backup_dir;
my $restore_mode = "single-single";
my $version = '';
my $override_destdir = '';

my $skip_mysqld = 0;
my $dry_run = 0;

GetOptions( "config=s"     => \$config_file,
            "backup-dir=s" => \$backup_dir,
            "mode=s"       => \$restore_mode,
            "data-dir=s"   => \$override_destdir,
            "version=s"    => \$version,
            "dry-run"      => \$dry_run);

$skip_mysqld = 1 if ($restore_mode eq 'data-only');

my @known_modes = qw(
    data-only

    single-single
    slave-single
    master-single

    master-slave
    slave-slave
);

#-----------------------------------------------------------------
# Read config file
our $config = ReadConfig($config_file);

# Parse clone dirs
my @clone_dirs : shared = split(/\s*\,\s*/, $config->{clone_dirs});
$config->{clone_dirs} = \@clone_dirs;

# Override datadir
$config->{dest_dir} = $override_destdir if ($override_destdir ne '');

# Check bak dir
PrintUsage("Invalid backup directory!") unless (CheckBackupDir($backup_dir));

# Check mode
PrintUsage("Invalid restore mode!") unless (grep(/^$restore_mode$/, @known_modes));

# Print info
LogNotice("Backup directory: '$backup_dir'");
LogNotice("DataDir: '$config->{dest_dir}'");
LogNotice("Dry Run: " . ($dry_run ? 'yes' : 'no'));

LogNotice("Incremental Version: $version") if ($version ne '');
LogNotice("Dirs to restore: '" . join(', ', @clone_dirs) . "'");
LogNotice("Restore mode: '$restore_mode'");
LogNotice("Skip mysqld operations: " . ($skip_mysqld ? 'yes' : 'no'));

#-----------------------------------------------------------------
# Create config shortcuts
my $this = $config->{this};
my $host = $config->{host}->{$this};

LogNotice("-----------------------------------------------------------------");
#Loading status info from data dir
my $status = {};
my $res = LoadStatusInfo(\$status);
unless ($res =~ /^OK/) {
    LogError("Error: Can't load status info from backup data dir: $res");
    exit(1);
}
LogDebug("Status: " . Dumper($status));

my $backup_method = $status->{copy_method};
my $copy_method = $config->{copy_method}->{$backup_method};
unless ($copy_method) {
    LogError("Unsupported backup method!");
    exit(0);
} 

LogNotice("-----------------------------------------------------------------");
if ($version eq 'list') {
    ListIncrementalVersions();
    exit(0);
}

if ($dry_run) {
    LogNotice("Exiting - Dry Run...");
    exit(0);
}

LogNotice("-----------------------------------------------------------------");
# Check local mysql process and shutdown it if it is running
unless ($skip_mysqld) {
    my $res = ShutdownLocalMysql();
    unless ($res =~ /^OK/) {
        LogError("Can't stop local mysql daemon: $res");
        exit(1);
    }
} else {
    LogNotice("Skipping MySQL operations...");
}

LogNotice("-----------------------------------------------------------------");
my $res = system("mkdir -p $config->{dest_dir}");
if ($res) {
    LogError("Can't access datadir: $!");
    exit(1);
}

if ($version ne '') {
    my $res = RestoreDataFilesFromIncremental($version);
    unless ($res =~ /^OK/) {
        LogError("Error: Can't restore data files from incremental backup: $res");
        exit(1);
    }
} else {
    my $res = RestoreDataFiles();
    unless ($res =~ /^OK/) {
        LogError("Error: Can't copy data files from backup: $res");
        exit(1);
    }
}

LogNotice("-----------------------------------------------------------------");
# Cleaning dump from master.info and binary logs
my $res = CleanupDataDir();
unless ($res =~ /^OK/) {
    LogError("Error: Can't cleanup data dir: $res");
    exit(1);
}

LogNotice("-----------------------------------------------------------------");
# Run local local mysql process
unless ($skip_mysqld) {
    my $res = StartLocalMysql();
    unless ($res =~ /^OK/) {
        LogError("Can't start local mysql daemon: $res");
        exit(1);
    }
} else {
    LogNotice("Not starting mysqld - skipped...");
}

LogNotice("-----------------------------------------------------------------");
LogNotice("Setting up replication if needed...");

unless ($skip_mysqld) {
    my $res = SetupReplication();
    unless ($res =~ /^OK/) {
        LogError("Replication setup error: $res");
        exit(1);
    }
} else {
    LogNotice("Replication setup skipped!");
}

LogNotice("-----------------------------------------------------------------");
LogNotice("Restore process done!\n\n\n");

exit(0);

#-----------------------------------------------------------------
sub PrintUsage($) {
    my $error = shift;
    print "ERROR: $error\n\n";
    print "Usage: $0 [--config <config file>] --backup-dir <dir> [--mode <mode>] [--data-dir <dir>] [--version <version | list>] --dry-run\n";
    print "Where:\n";
    print "  Backup Dir: directory where backup resides\n";
    print "  Data Dir: datadir of local mysql installation\n";
    print "  Mode: " . join(', ', @known_modes) . "\n";
    print "  Version: \n";
    print "    - when run with 'list' parameter, displays available versions of incremental backups\n";
    print "    - if version is specified, tries to restore backup for specified version of datadir\n";
    print "  Dry-run: check everything and exit without any changes\n\n";
    exit(1);
}

#-----------------------------------------------------------------
sub CheckBackupDir($) {
    my $dir = shift;
    return 0 unless $dir;
    
    print "Checking Directory ('$dir'):\n";
    
    print "Check name..."; return 0 unless ($dir); print "OK\n";
    print "Check permissions..."; return 0 unless (-d $dir && -r $dir && -x $dir); print "OK\n";
    print "Check if empty..."; return 0 unless (scalar(glob("$dir/*"))); print "OK\n";
    print "Check _mmm dir..."; return 0 unless (-d "$dir/_mmm" && -r "$dir/_mmm" && -x "$dir/_mmm"); print "OK\n";
    print "Check _mmm/status.txt file..."; return 0 unless (-f "$dir/_mmm/status.txt" && -r "$dir/_mmm/status.txt"); print "OK\n";
    print "Check _mmm/copy_method.txt file..."; return 0 unless (-f "$dir/_mmm/copy_method.txt" && -r "$dir/_mmm/copy_method.txt"); print "OK\n";
    
    print "\n\n";
    
    return 1;
}

#-----------------------------------------------------------------
sub LoadStatusInfo($) {
    my $status_info = shift;
    
    LogDebug("Loading status info...");

    my $status_file = $backup_dir . "/_mmm/status.txt";
    unless (-f $status_file && -r $status_file) {
        return "ERROR: Status file ($status_file) does not exists or is not readable!";
    }
    
    my $status_data = `cat $status_file`;

    my $VAR1;
    eval($status_data);
    if ($@) {
        return "ERROR: Can't parse status info: $@";
    }
    
    $$status_info = $VAR1;
    
    my $method_file = $backup_dir . "/_mmm/copy_method.txt";
    chomp(my $copy_method = `cat $method_file`);
    $$status_info->{copy_method} = $copy_method;
    
    return "OK!";
}

#-----------------------------------------------------------------
sub ListIncrementalVersions() {
    unless ($copy_method->{incremental} eq 'yes') {
        LogError("Invalid backup directory for incremental operations");
        exit(0);
    }
    
    #FIXME: Need to add support for other incremental methods except rdiff
    if ($backup_method ne 'rdiff') {
        LogError('Only rdiff backup method supports version lists now. Sorry.');
        exit(0);
    }
    
    # List versions
    my $res = open(RDIFF, "rdiff-backup --parsable-output -l '$backup_dir'|");
    unless ($res) {
        LogError("Can't start rdiff-backup to read versions info from backup!");
        exit(1);
    }
    print "Following backup versions are available:\n";
    print " Version   | Date\n";
    print "-----------|---------------------\n";
    while(<RDIFF>) {
        chomp;
        my ($ts, $type) = split /\s+/;
        print("$ts | " . localtime($ts) . "\n");
    }
    close(RDIFF);
}

#-----------------------------------------------------------------
sub ShutdownLocalMysql() {
    my $pid_file = $host->{pid_file};
    
    # Check pid file
    if ($pid_file eq "") {
        LogWarn("Warning: Can't find pid-file option in config file.");
        $pid_file = "/var/run/mysqld/mysqld.pid";
    }
    
    my $cnt;
    my $pid;
    if (-f $pid_file) {
        # Read mysql pid
        open(PID, $pid_file) || return "ERROR: Can't read mysql pid file ($pid_file)";
        chomp($pid = <PID>);
        close(PID);
    
        # Check process
        $cnt = kill(0, $pid);
    }
    
    if ($cnt == 0) {
        LogWarn("MySql is not running now, skipping shutdown step...");
        return  "OK: Mysql is not running";
    }
    
    LogNotice("MySql is running now. Going to stop it...");
    
    #Stop mysql
    my $rc_script = $host->{rc_script};
    $rc_script = "/etc/init.d/mysql" if ($rc_script eq "");
    
    my $res = system($rc_script, "stop");
    if ($res) {
        return "ERROR: Can't stop local MySql server!";
    }

    # Check process
    my $wait = 15;
    LogDebug("Waiting mysql process with $pid to shutdown: ");
    while ($wait--) {
        $cnt = kill(0, $pid);
        last if ($cnt == 0);
        LogDebug(".");
        sleep(1);
    }

    if ($cnt != 0) {
        PrintError("MySql Shutdown Failed!");
        return "ERROR: MySql is running with PID $pid after shutdown request!";
    }
    
    LogDebug("Mysql Shutdown done: OK");
    return "OK: MySql server is down now!";
}

#-----------------------------------------------------------------
sub StartLocalMysql() {
    # Get pid file location
    my $pid_file = $host->{pid_file};

    # Check pid file
    if ($pid_file eq "") {
        LogWarn("Warining: Can't find pid-file option in config file.");
        $pid_file = "/var/run/mysqld/mysqld.pid";
    }
    
    if (-f $pid_file) {
        # Read mysql pid
        open(PID, $pid_file) || return "ERROR: Can't read mysql pid file ($pid_file)";
        chomp(my $pid = <PID>);
        close(PID);
    
        # Check process
        my $cnt = kill(0, $pid);
        if ($cnt) {
            return "ERROR: Local mysql is running now with pid = $pid!";
        }
    }
    
    LogNotice("MySql is not running now. Going to start it...");
    
    # Start mysql
    my $rc_script = $host->{rc_script};
    $rc_script = "/etc/init.d/mysql" if ($rc_script eq "");
    
    my $res = system($rc_script, "start");
    if ($res) {
        return "ERROR: Can't start local MySql server!";
    }
    
    return "OK: MySql server has been started!";
}

#-----------------------------------------------------------------
sub CleanupDataDir() {
    LogNotice("Cleaning data dir from master.info and binary logs...");
    
    my $master_log = $status->{master}->{File};
    unless ($master_log =~ /^(.*)\.(\d+)$/) {
        return "ERROR: Unknown master binary log file name format ($master_log)!";
    }
    
    LogDebug("Deleting master binary logs: $1.*");
    system("find $config->{dest_dir} -name $1.* | xargs rm -vf");
    
    my $slave_log = $status->{slave}->{Relay_Log_File};
    unless ($slave_log =~ /^(.*)\.(\d+)$/) {
        return "ERROR: Unknown relay binary log file name format ($slave_log)!";
    }
    
    LogDebug("Deleting relay binary logs: $1.*");
    system("find $config->{dest_dir} -name $1.* | xargs rm -vf");
    
    LogDebug("Deleting .info and .pid  files...");
    system("find $config->{dest_dir} -name master.info | xargs rm -vf");
    system("find $config->{dest_dir} -name relay-log.info | xargs rm -vf");
    system("find $config->{dest_dir} -name *.pid | xargs rm -vf");
    
    LogDebug("Changing permissions on mysql data dir...");
    system("chown -R mysql:mysql $config->{dest_dir}");
    
    return "OK";
}
#-----------------------------------------------------------------
sub RestoreDataFiles() {
    if ($copy_method->{incremental} eq 'yes') {
        LogError("This is incremental backup dir! Use --version option to restore specific version");
        exit(0);
    }

    my $restore_command = $copy_method->{restore_command};
    unless ($restore_command) {
        LogError("Copy method without restore command! Please, check your config file!");
        exit(0);
    }
    
    LogDebug("Performing template substitutions on command...");
    $restore_command =~ s/%DATA_DIR%/$config->{dest_dir}/ig;
    $restore_command =~ s/%BACKUP_DIR%/$backup_dir/ig;
    LogDebug("Final command: '$restore_command'");
    
    my $res = system($restore_command);
    return "ERROR: Restoration error: $!" if ($res);
    return "OK";
}

#-----------------------------------------------------------------
sub RestoreDataFilesFromIncremental($version) {
    if (!$copy_method->{incremental} || $copy_method->{incremental} eq 'no') {
        LogError("Error: This is not incremental backup dir ($backup_dir backup method is $backup_method)!");
        exit(0);
    }

    my $restore_command = $copy_method->{restore_command};
    unless ($restore_command) {
        LogError("Copy method without restore command! Please, check your config file!");
        exit(0);
    }
    
    LogDebug("Performing template substitutions on command...");
    $restore_command =~ s/%DATA_DIR%/$config->{dest_dir}/ig;
    $restore_command =~ s/%BACKUP_DIR%/$backup_dir/ig;
    $restore_command =~ s/%VERSION%/$version/ig;
    LogDebug("Final command: '$restore_command'");
    
    my $res = system($restore_command);
    return "ERROR: Restoration error: $!" if ($res);
    return "OK";
}

#-----------------------------------------------------------------
sub SetupReplication() {
    $restore_mode =~ /(\w+)\-(\w+)/;
    my $src_mode = $1;
    my $dst_mode = $2;
    
    if ($dst_mode eq "single") {
        LogNotice("Skipping replication setup because destination configuration is 'single'");
        return "OK: Skipped";
    }
    
    # Get remote host info
    my $src_host = $config->{host}->{$status->{host}};
    
    # Prepare params for local host
    my $my_master_info = {};
    $my_master_info->{'master_user'} = $config->{'replication_user'};
    $my_master_info->{'master_pass'} = $config->{'replication_password'};

    # Prepare params for remote host
    my $peer_master_info = {};
    $peer_master_info->{'master_user'} = $config->{'replication_user'};
    $peer_master_info->{'master_pass'} = $config->{'replication_password'};
    
    # Restore slave from slave - use the same replication params
    if ($src_mode eq 'slave' && $dst_mode eq 'slave') {
        $my_master_info->{'master_host'} = $status->{slave}->{'Master_Host'};
        $my_master_info->{'master_port'} = $status->{slave}->{'Master_Port'};
        $my_master_info->{'master_log'} = $status->{slave}->{'Relay_Master_Log_File'};
        $my_master_info->{'master_pos'} = $status->{slave}->{'Exec_Master_Log_Pos'};
    }
        
    # Restore slave from master - use master as replication source
    if ($src_mode eq 'master' && $dst_mode eq 'slave') {
        $my_master_info->{'master_host'} = $src_host->{'mysql_host'};
        $my_master_info->{'master_port'} = $src_host->{'mysql_port'};
        $my_master_info->{'master_log'} = $status->{master}->{'File'};
        $my_master_info->{'master_pos'} = $status->{master}->{'Position'};
    }

    # Restore master from slave - not implemented
    if ($src_mode eq 'slave' && $dst_mode eq 'master') {
        LogWarn("FIXME: Don't know how to restore master from slave");
        return "OK: Not implemented!";
    }

    # Restore master from master - not implemented
    if ($src_mode eq 'master' && $dst_mode eq 'master') {
        LogWarn("FIXME: Don't know how to restore master from master");
        return "OK: Not implemented!";
    }

    LogDebug("My Master info: " . Dumper($my_master_info));

    # Setup replication on local host
    my $res = ChangeMasterTo($config->{this}, $my_master_info);
    return $res unless ($res =~ /^OK/);
    
    # Setup replication on remote host
    if ($dst_mode eq 'master') {
#        my $res = ChangePeerMasterTo();
#        unless ($res =~ /^OK/) {
#            LogError("Error: Can't set master for peer server: $res");
#            exit(1);
#        }
    }
    
    return "OK: Done";
}

#-----------------------------------------------------------------
sub ChangeMasterTo($$) {
    my $host_name = shift;
    my $master = shift;
    my $host = $config->{host}->{$host_name};
    
    LogNotice("Changing master info for $host_name server...");

    # get connection info
    my $my_host = $host->{mysql_host};
    my $my_port = $host->{mysql_port};
    my $my_user = $host->{mysql_user};
    my $my_pass = $host->{mysql_password};

    my $dbh = MysqlConnect($my_host, $my_port, $my_user, $my_pass);
    if (!$dbh) {
        return "ERROR: Can't connect to $host_name mysql (host = $my_host:$my_port, user = $my_user)!";
    }

    # Stop slave
    my $res = ExecuteQuery($dbh, "STOP SLAVE");
    return "ERROR: SQL Query Error: " . $dbh->errstr unless($res);

    # Change master
    my $sql = "CHANGE MASTER TO " .
              "  MASTER_HOST='$master->{master_host}'," .
              "  MASTER_PORT=$master->{master_port}," .
              "  MASTER_USER='$master->{master_user}'," .
              "  MASTER_PASSWORD='$master->{master_pass}'";

    if ($master->{master_log} && $master->{master_pos}) {
        $sql .= ", MASTER_LOG_FILE='$master->{master_log}'," .
                "  MASTER_LOG_POS=$master->{master_pos}";
    }
    
    my $res = ExecuteQuery($dbh, $sql);
    return "ERROR: SQL Query Error: " . $dbh->errstr unless($res);
    
    # Start slave
    $res = ExecuteQuery($dbh, "START SLAVE");
    return "ERROR: SQL Query Error: " . $dbh->errstr unless($res);

    # disconnect
    $dbh->disconnect;

    return "OK";
}

