#!/usr/bin/perl
use strict;
use warnings;
use Cwd;
use SKM::DB qw(DBMaster DBLocal);
use NextCAM::Conf qw(GetCfgs WriteCfg);
use Node::Conf;
use Data::Dumper;
use Master::Zmk;
use Log::Log4perl "get_logger";

require "$ENV{APL}/common/bin/logger.patrol";
# ------------------------------------- Constants -------------------------------------------------
my $APL 	 = $ENV{APL};
my $DOMAINDIR    = "$APL/www/domain";
my $CONFDIR      = "$ENV{APL_CONF}";
my $NODESDIR	 = "$CONFDIR/master/nodes";
my $LICDIR       = "$APL/var/license";
my $LICFILE      = "$LICDIR/license.key";
my $LIVEPATH     = "/tmp";
my $STILLPATH    = "$CONFDIR/still";
my $VARPATH      = "$APL/var";
my $REMOVED      = "$APL/var/sm/removed"; #list of removed devices
my $CHECK_MASTER = "$APL/pm/bin/check_master";
my $PM_OPTIONAL  = "$APL/pm/bin/pm_optional";
my $KOWTOW       = "$APL/conf/bin/conf_kowtow"; # handshake with master
my $KOWTOW_WAIT  = 3;	# Time to wait for kowtow to complete
my $DOMAIN_ACCESS= "$ENV{APL}/conf/bin/rebuild_domain_access";
my $EVENT_INTEGRITY="$APL/elog/bin/event_integrity_check >/dev/null 2>&1 &";#do not have to wait till finishes
my $SYSTEM_STATUS="$APL/sdi/bin/system_status >/dev/null 2>&1 &";          #do not have to wait till finishes
my $MASTER = -f "$CONFDIR/master/s_master";
my $DB_MAX_CONNECTS = 6;
my $DB_CONNECT_INTERVAL = 10; # Timeout between attempts
# --------------------------------------- Vars ----------------------------------------------------

my $log = get_logger('NEXTCAM::CONF::CONF_SETUP');
my $dbm;
my $dbl;
my %dbs;
my %conf;	# Device configuration extracted from flat files (keys are DEVIDs)
my %db_conf;	# Device configuration from DB (keys are OBJIDs)
my @block;      # analytics data located in 'obj_block' table
my $NodeOBJ;
my $DoRestore = 0;
# --------------------------------------- Routines ------------------------------------------------

sub db_local {
    for (my $i = 1; $i <= $DB_MAX_CONNECTS; $i++) {
	eval {
	    $dbl=DBLocal({PrintError=>0,RaiseError => 1});
	};
	if($@) {
	    if($i>=$DB_MAX_CONNECTS) {
		$log->error("DB_LOCAL: Attempt $i (final). Cannot connect to localhost: $@");
		return 0;
	    }
    	    $log->error("Attempt $i. Cannot connect to localhost: $@");
    	    $log->error('Sleep '. $i*$DB_CONNECT_INTERVAL . ' before next attempt');
    	    sleep($i*$DB_CONNECT_INTERVAL);
	} else {
	    last;
	}
    }
    eval {
	$dbl->{FetchHashKeyName} = 'NAME_uc';
	$dbl->{ShowErrorStatement}=1;
	# Get node objid and rtime
	my $uni_arr = $dbl->selectall_arrayref(
	    "SELECT obj,rtime FROM _objs WHERE name=? AND otype='D' AND subtype='N'",
	    undef, UNI
	);
	$DoRestore = 1 if @$uni_arr == 1 and not defined $uni_arr->[0][1];
    };
    if($@) {
	$log->error("Can not fetch data from LOCAL DB: $@");
	return 0;
    }
    return 1;

}

sub db_master {
    for (my $i = 1; $i <= $DB_MAX_CONNECTS; $i++) {
	eval {
	    $dbm=DBMaster({PrintError=>0,RaiseError => 1});
	};
	if($@) {
	    if($i>=$DB_MAX_CONNECTS) {
		$log->error("DB_MASTER: Attempt $i (final). Cannot connect to master: $@");
		return 0;
	    }
    	    $log->error("Attempt $i. Cannot connect to master: $@");
    	    $log->error('Sleep '. $i*$DB_CONNECT_INTERVAL . ' before next attempt');
    	    sleep($i*$DB_CONNECT_INTERVAL);
	} else {
	    last;
	}
    }
    eval {
	$dbm->{FetchHashKeyName} = 'NAME_uc';
	$dbm->{ShowErrorStatement}=1;
	# Get node objid and rtime
	$dbs{GET_NODE_ATTR} = $dbm->prepare("SELECT obj,rtime FROM _objs WHERE name=? AND otype='D' AND subtype='N'");
	$dbs{GET_NODE_ATTR}->execute(UNI);
	my $uni_arr = $dbs{GET_NODE_ATTR}->fetchall_arrayref;
	die "Can not determine NODE_ID for UNI=".UNI if @$uni_arr != 1;
	$NodeOBJ = $uni_arr->[0][0];
	$DoRestore = 1 unless defined $uni_arr->[0][1];
	# Prepare queries
	$dbs{GET_OBJS} = $dbm->prepare("SELECT * FROM _objs WHERE otype='D' AND node_id='$NodeOBJ' AND deleted=0");
	$dbs{GET_OBJ_ATTR} = $dbm->prepare("SELECT * FROM _obj_attr WHERE obj=?");
	$dbs{SET_NODE_RTIME} = $dbm->prepare("UPDATE _objs SET rtime=now() at time zone 'UTC' WHERE obj=?");
	$dbs{CLEAR_OBJ_RTIME} = $dbm->prepare("UPDATE _objs SET rtime=null WHERE obj=?");
	$dbs{GET_OBJ_BLOCK} = $dbm->prepare("SELECT * FROM obj_block WHERE obj=$NodeOBJ");
    };
    if($@) {
	$log->error("Can not prepare MASTER database statement: $@");
	return 0;
    }
    return 1;
}

sub remove_dev { ### Let patrol process device removal ###########
    my $dev = shift;
    
    unlink "$CONFDIR/conf/$dev.conf.tmp" if -f  "$CONFDIR/conf/$dev.conf.tmp";
    my $ok = open(CF,">$CONFDIR/conf/$dev.conf.tmp");
    unless($ok) {
        $log->error("Cannot open $CONFDIR/conf/$dev.conf.tmp for writing: $!");
        return;
    }
    $conf{$dev}{LOCATION} = '@garbage';
    print CF "$_=$conf{$dev}{$_}\n" foreach sort keys %{$conf{$dev}};
    close CF;
    unlink "$CONFDIR/conf/$dev.conf" if -f  "$CONFDIR/conf/$dev.conf";
    rename "$CONFDIR/conf/$dev.conf.tmp", "$CONFDIR/conf/$dev.conf";
    
    delete $conf{$dev};
}

sub check_master {
    my $ok = system($CHECK_MASTER) == 0;
    $log->error("check_master_failed") unless $ok;
    return $ok;
}


sub pm_optional {
    system("$PM_OPTIONAL >/dev/null 2>&1"); #setup optional processes
}

sub kowtow {
    my $kidpid;
    eval {
	local $SIG{ALRM} = sub { die "ALARM" };
	local $SIG{INT} = 'IGNORE';
	alarm $KOWTOW_WAIT;
	if($kidpid = fork) {
	    waitpid($kidpid, 0);
	} else {
	    die "Cannot fork: $!" unless defined $kidpid;
	    close STDOUT;
	    close STDERR;
	    exec($KOWTOW) or die "Can't exec: $!\n";
	}
    };
    alarm 0;
    if($@ && $@ =~ /ALARM/) {
	$log->info("KOWTOW continues working after $KOWTOW_WAIT sec wait");
	return 2;
    }
    my $ok = $??0:1;
    $log->error("kowtow failed") unless $ok;
    return $ok;
}

sub rebuild_domain_access {
    # Synchronize master domain configuration files if in 'restore' mode
    if ($DoRestore) {
	$log->info("Restore master configuration from DB");
	my %nconf;
	my %uni;
	eval {
	    my $ra = $dbl->selectall_arrayref(
		"SELECT obj,name as uni FROM _objs
		WHERE otype='D' AND subtype='N' AND deleted=0"
	    );
	    $uni{$_->[0]} = $_->[1] foreach @$ra;
	    $ra = $dbl->selectall_arrayref(
		"SELECT o.obj,a.attr,a.val 
		FROM _objs o INNER JOIN _obj_attr a
		    ON o.obj=a.obj
		WHERE o.otype='D' and o.subtype='N' and o.deleted=0"
	    );
	    foreach my $row (@$ra) {
		my ($obj,$attr,$val) = @$row;
		$nconf{$obj}{$attr} = $val;
	    }
	    die "No nodes found!" unless %nconf;
	    
	    # DB -> Files
	    system("rm -f $NODESDIR/*");
	    foreach my $objid (keys %nconf) {
		if (not exists $uni{$objid}) {
		    $log->error("UNI missing! Node OBJID=$objid was not found in _objs!");
		    next;
		}
		my $uni = $uni{$objid};
		my $conf = $nconf{$objid};
		$conf->{UNI} = $uni;
		
		if (open FH, ">$NODESDIR/$uni") {
		    print FH "$_=$conf->{$_}\n" foreach sort keys %$conf;
		    close FH or $log->error("Error writing to node conf for $uni: $!");
		    $log->info("Write configuration file for node $uni");
		}
		else {
		    $log->error("Error writing configuration for node $uni: $!");
		    next;
		}
	    }
	};
	$log->logdie("Domain configuration sync error: $@") if $@;
    }
    my $ret = `$DOMAIN_ACCESS 2>&1`;
    $log->info($ret);
    return $??0:1;
}

sub event_integrity {          # checks event integrity (master only)
    system($EVENT_INTEGRITY);
}
sub set_system_status {        # update system status. will be STARTING
    system($SYSTEM_STATUS);
}

sub setup_license {
    opendir (DH, $LICDIR) or return;
    my @lic = sort grep {/^license\.key/} readdir DH;
    closedir DH;
    return unless @lic; # System is not licensed
    my $licfile;
    foreach my $f (@lic) {
	$licfile=$f,last if Master::Zmk::VADE("$LICDIR/$f");
    }
    $log->warn("No valid license files found"), return unless defined $licfile;
    return if $licfile eq 'license.key'; # current license is valid
    unlink $LICFILE if -l $LICFILE;
    if (-f $LICFILE) { # Rename current license file if not a symlink
	my $i = 1;
	++$i while -e "$LICDIR/license.key.$i";
	rename $LICFILE,"$LICFILE.$i";
    }
    # Create/Modify symlink
    $log->warn("New license file selected: $licfile");
    my $cwd = cwd;
    chdir $LICDIR;
    symlink($licfile,"license.key");
    $log->error("Cannot symlink license file: $!") if $!;
    chdir $cwd;
}

sub setup_conf {
    $log->info("Setup device configuration");
    %conf = GetCfgs;
    eval {
	$dbs{GET_OBJS}->execute;
	%db_conf = %{ $dbs{GET_OBJS}->fetchall_hashref('OBJ') };
	$dbs{GET_OBJ_BLOCK}->execute;
	@block = @{ $dbs{GET_OBJ_BLOCK}->fetchall_arrayref({}) };
    };
    if($@) {
	$log->error("Cannot fetch objects from DB: $@");
	return 0;
    }
    foreach my $dev (keys %conf) {
	next if not $dev =~ /^\w?\d+/;
        next if not defined $conf{$dev}{DEVID};
        next if not defined $conf{$dev}{OBJID} or $conf{$dev}{OBJID}+0 < 100;
        next if ! ( $dev =~ /^(d|a|r|s|w|v|j)?\d+$/ );
	my %attr = ();
	my $objid = $conf{$dev}{OBJID};
	unless(exists $db_conf{$objid}) {
	    $log->info("Remove device: $dev");
	    remove_dev($dev);
	    next;
	}
	# Compare DEVICETYPE in files and DB. Replace device if they differ
	my $ra;
	eval {
	    $dbs{GET_OBJ_ATTR}->execute($objid);
	    $ra = $dbs{GET_OBJ_ATTR}->fetchall_arrayref({});
	};
	if($@) {
	    $log->error("Fetch object attr failed for ObjID=$objid: $@");
	    next;
	}
        $attr{ $_->{OBJ} }{ $_->{ATTR} } = $_->{VAL} foreach (@$ra);
        my $devtype = $attr{$objid}{DEVICETYPE};
        if(not defined $devtype or $devtype ne $conf{$dev}{DEVICETYPE}) {
    	    $log->info("DEVICETYPE changed for $dev. Scheduled for replacement");
    	    remove_dev($dev);
    	    eval { WriteCfg($attr{$objid}) };
    	    $log->error("WriteCfg failed for OBJID $objid: $@") if $@;
        }
    }
    # Fix replication problems if any
    foreach my $objid (keys %db_conf) {
	my $dbobj = $db_conf{$objid};
	next if $dbobj->{SUBTYPE} =~ /^(E|N)$/;
	my $dev = $dbobj->{SUBTYPE} eq 'C' ? $objid : lc($dbobj->{SUBTYPE}).$objid;
	if ($dbobj->{RTIME} && !exists($conf{$dev})) {
	    $log->warn("Object OBJID=$objid is present in DB with RTIME!=null but isn't replicated");
	    # Clear object RTIME
	    eval { $dbs{CLEAR_OBJ_RTIME}->execute($objid) };
	    $log->error("Clear RTIME for OBJ=$objid failed. DB error: $@") if $@;
	}
    }
    # Recover packed data from obj_block table
    my $bck_cert;
    my $bck_vae;
    foreach my $block (@block) {
	my $name = $block->{NAME};
	$bck_cert = $block->{BLOCK} if $name eq 'BACKUP_CERT';
	$bck_vae = $block->{BLOCK} if $name eq 'BACKUP_CONF_VAE';
    }
    # recover certificate
    if (defined $bck_cert and length $bck_cert) {
	if (open TAR, "| /bin/tar -C $CONFDIR -xz &>/dev/null") {
	    print TAR $bck_cert;
	    close TAR;
	    $log->error("Error unpacking certificate: exit code " . ($? >> 8)) if $?;
	}
	else {
	    $log->error("Cannot unpack certificate: $!");
	}
    }
    else {
	$log->warn("None or empty certificate in backup");
    }
    # recover VAE configs
    if (defined $bck_vae and length $bck_vae) {
	# Remove existing content
	system("/bin/rm -rf $CONFDIR/*/vae/* &>/dev/null");
	
	if (open TAR, "| /bin/tar -C $CONFDIR -xz &>/dev/null") {
	    print TAR $bck_vae;
	    close TAR;
	    $log->error("Error unpacking VAE configs: exit code " . ($? >> 8)) if $?;
	}
	else {
	    $log->error("Cannot unpack VAE configs: $!");
	}
    }
    
    
    # Set node rtime
    eval { $dbs{SET_NODE_RTIME}->execute($NodeOBJ) };
    return $@?0:1;
}

sub setup_master {
    check_master;
    kowtow;
    db_local;
    rebuild_domain_access;
    sleep 3; # Wait for postmaster to restart
    db_master or exit(-10);
    setup_conf if $DoRestore;
    event_integrity;
    set_system_status;
    pm_optional;

}

sub setup_node {
    check_master;
    kowtow;
    sleep 3;
    db_master or exit(-10);
    setup_conf if $DoRestore;
    pm_optional;
}

sub main {
    $log->info("Configuration setup starts");
    
    if ($MASTER) {
	setup_master;
    }
    else {
	setup_node;
    }
}

# ------------------------------------ Main ------------------------------------------------
main;


END {
    eval { $dbm->disconnect } if $dbm;
    eval { $dbl->disconnect } if $dbl;
}
