#!/usr/bin/perl
use strict;
use warnings;

use Data::Dumper;
use Time::Local;
use Time::HiRes "gettimeofday";
use SKM::Common;
use NextCAM::ELClient;
use Node::Conf;
use SKM::DB;
use File::Basename qw(dirname);
use lib dirname(__FILE__).'/../lib';              # find  SM::Config here
use SM::Config ":all";

# CONS
my $APL = $ENV{APL};
my $APL_VAR = "$ENV{APL_VAR}";
my $JOB_DIR = "$APL_VAR/sm/cloud";
my $QUEUE = "$JOB_DIR/incoming";
my $STAGE = "$JOB_DIR/inprogress";
my $DONE  = "$JOB_DIR/migrated";
my $OLD   = "$JOB_DIR/completed";
my $STORE = "/vasm/store/".SM_VER;
my $WIPE = "$APL/sm/bin/sm_wipe";
my $EXPORT_MD = "$APL/mgears/bin/storage_export_metadata";
my $IDENTITY_OBJ = 53;
my $CHUNK_SIZE = 30;
my $CLOUD_PREFIX = "store";
my $JOBID_PAT = qr/^\d+_\w+_\d{6}_\d\d\.\d{4}-\d\d$/;
my $BS=512;
my $BUCKET = UNI;

# VAR
my $dbm;
my %dbs;
my %Jobs;
my %EvtMark;
my %CloudConf;
my $ELClient;
my $sigterm;

# SIG
$SIG{TERM} = sub { $sigterm = 1 }; # SIGTERM terminates mover

# SUB
sub ts2utc 
{
	my $ts = shift;
        my ($year, $mon, $mday, $hour, $min, $sec) = $ts=~/^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/;
        $year += 100;
        $mon -= 1;
        my $utc = timegm($sec, $min, $hour, $mday, $mon, $year);
        return $utc;
}

sub span_intersect
{
	my ($s1, $e1, $s2, $e2) = @_; # UNIX timestamps
	if ($s2 < $e1 && $s1 < $e2) {
		return 1;
	}
	return 0;
}

sub file_size 
{
	my $file = shift;
	return 0 if ! -f $file;
	return (stat($file))[12]*$BS/1024;# get KB-size
}

sub db_master
{
	eval { 
		$dbm->disconnect() if $dbm; 
		$dbm=''; 
	}; # disconnect if defined
	
	for (my $i=1; $i<6; $i++) {
		eval {
			$dbm = DBMaster({PrintError=>1,RaiseError => 1});
			
			$dbs{INSERT_CHUNK}=$dbm->prepare('insert into cloud_chunks values (?,?,?)');
			$dbs{UPDATE_TS}=$dbm->prepare("update _obj_attr set val=? where obj=? and attr='CLOUD_SEPARATOR_TS'");
		};
		if ($@) {
			SM_LOG->logdie("Attempt $i (final). Cannot connect to master: $@") if $i>=5;
    			SM_LOG->error("Attempt $i. Cannot connect to master: $@");
    			SM_LOG->error('Sleep '. $i*30 . ' before next attempt');
    			SM_error('',  "Cannot connect to master:$@");
    			sleep($i*30);
		}
		else {
			last;
		}
	}
	
	$dbm->{FetchHashKeyName} = 'NAME_uc';
	$dbm->{ShowErrorStatement}=1;
	$dbm->{AutoCommit} = 0;
	SM_LOG->debug("Connected to master db");
}

sub check_wipe
{
	if (open PIPE, "ps ax | grep $WIPE | grep -v grep | cut -c 1-6 |") {
		my $pid = <PIPE>;
		close PIPE;
		return 1 if $pid;
	}
	else {
		die "Cannot open pipe: $!";
	}
	return 0;
}

sub check_cloud
{
	eval {
		my $c = $dbm->selectall_arrayref( qq {
			select attr,val from _obj_attr 
			where obj=$IDENTITY_OBJ and attr in
			('CLOUD_STORAGE','CLOUD_STORAGE_ENABLED','CLOUD_KEY_ID', 
			 'CLOUD_SECRET_KEY', 'CLOUD_BUCKET')
		});
		foreach my $av (@$c) {
			$CloudConf{$av->[0]} = $av->[1];
		}
	};
	if ($@) {
		SM_LOG->logdie("DB_MASTER: $@");
	}
	return ($CloudConf{CLOUD_STORAGE} ne 'none' && $CloudConf{CLOUD_STORAGE_ENABLED} eq 'yes') ? 1 : 0;
}

sub cloud_init
{
	eval {
		no strict 'subs';
		require SM::Cloud;
		SM::Cloud->import($CloudConf{CLOUD_STORAGE});
		Cloud_Init(
			$CloudConf{CLOUD_KEY_ID},
		        $CloudConf{CLOUD_SECRET_KEY}
		);
		
		# If CLOUD_BUCKET system setting is specified 
		# it overrides bucket value which defaults to node UNI
		#
		$BUCKET = $CloudConf{CLOUD_BUCKET} if $CloudConf{CLOUD_BUCKET};
	
		my @buckets = Cloud_ListBuckets();
		my $found = 0;
		foreach my $bucket (@buckets) {
			if ($bucket eq $BUCKET) {
				$found = 1;
				last;
			}
		}
		die "Bucket $BUCKET is missing\n" if not $found;;
	};
	if ($@) {
		SM_LOG->logdie("Cloud init failed: $@");
	}
}

sub mark_event
{
	my $eventid = shift;
	my $location = shift;
	
	$ELClient->updateEvent({
		"eventid" => $eventid,
		"property.CLOUD_LOCATION" => $location
	}, 1); # Async call
}

sub cleanup_completed_jobs
{
        # Simply erase old jobs
        `find $OLD  -ctime +2 -delete`;
}

sub scan_new_jobs
{
	opendir DH, $QUEUE or die "Cannot open $QUEUE: $!";
	my @jobs = grep {/$JOBID_PAT/} readdir DH;
	closedir DH;
	
	foreach my $jobid (@jobs) {
		next unless $jobid;
		# Insert new job
		my ($objid) = $jobid=~/^\d+_\D?(\d+)_/;
		$Jobs{$jobid} = {
			ID     => $jobid,
			OBJID  => $objid,
			STATUS => 'NEW',
			CHUNKS => {},
		};
	}
}

sub scan_unfinished_jobs
{
	opendir DH, $STAGE or die "Cannot open $STAGE: $!";
	my @jobs = grep {/$JOBID_PAT/} readdir DH;
	closedir DH;	
	
	foreach my $jobid (@jobs) {
		next unless $jobid;
		my $job_file = "$STAGE/$jobid";
		if ($Jobs{$jobid}) { # Job already exist
			my $ex_status = $Jobs{$jobid}{STATUS};
			SM_LOG->warn("Collision detected when scanning inprogress jobs: ".
				    "job $jobid already exist with $ex_status status");
			unlink $job_file;
			next;
		}
		if (open FH, $job_file) {
			my $has_sep = 0;
			my %chunks;
			my %job;
			while (<FH>) {
				chomp;
				next if not /^\d{12}/;
				my ($chunk,$events) = split(/\s+/, $_);
				next if not $chunk or not $events;
				my @events = split(/,/, $events);
				$chunks{$chunk} = {events=>\@events,migrated=>1};
			}
			close FH;
			my ($objid) = $jobid=~/^\d+_\D?(\d+)_/;
			$job{ID} = $jobid;
			$job{OBJID} = $objid;
			$job{CHUNKS} = \%chunks;
			$job{STATUS} = 'INPROGRESS';
			$Jobs{$jobid} = \%job;
		}
		else {
			SM_LOG->warn("Error opening job file inprogress/$jobid: $!");
			unlink $job_file;
		}
	}
}

sub scan_jobs
{
	# Read new jobs from 'incoming'
	#
	scan_new_jobs;
	
	# Read unfinished jobs from 'inprogress'
	#
	scan_unfinished_jobs;
	
	if (not %Jobs) {
		SM_LOG->info("Empty jobs list");
		return;
	}
	
	# List chunks in every hour directory for new and unfinished jobs
	#
	foreach my $jobid (keys %Jobs) {
		my $job = $Jobs{$jobid};
		my ($ts,$dev,$day,$hour) = split(/_/, $jobid);
		my $path = $STORE . "/$dev/$day/$hour";
		if (opendir DH, $path) {
			my @chunks = grep {/^\d{12}\.\w+$/} readdir DH;
			closedir DH;
			foreach my $chunk (@chunks) {
				next if $job->{CHUNKS}{$chunk};
				$job->{CHUNKS}{$chunk} = {
					events => [],
					migrated => 0
				};
			}
		}
	}
	
	# Find events associated with jobs
	#
	foreach my $jobid (keys %Jobs) {
		my $job = $Jobs{$jobid};
		my @chunks = sort keys %{$job->{CHUNKS}};
		my @events;
		next if not @chunks;
		
		my ($ts_from) = $chunks[0]=~/^(\d{12})\./;
		my ($ts_to) = $chunks[$#chunks]=~/^(\d{12})\./;
		my $utc_from = ts2utc($ts_from);
		my $utc_to = ts2utc($ts_to);
		$utc_to += $CHUNK_SIZE;
		
		eval {
			my $events = $dbm->selectall_arrayref(qq {
				select e.eventid, EXTRACT(EPOCH from e.utc_from at time zone 'UTC'),
				    EXTRACT(EPOCH from e.utc_to at time zone 'UTC')
				from eventwitness w 
				    inner join event e on e.eventid = w.eventid
				where w.objid=$job->{OBJID} and e.eventtype = 1
				    and EXTRACT(EPOCH from e.utc_from) < $utc_to
				    and EXTRACT(EPOCH from e.utc_to) > $utc_from
			});
			foreach my $e (@$events) {
				push @events, $e; # Push event arrayref
			}
			
		};
		die ("DB_MASTER: $@") if $@;
		
		# Filter chunks according to events found
		#
		foreach my $chname (keys %{$job->{CHUNKS}}) {
			my $chunk = $job->{CHUNKS}{$chname};
			next if @{$chunk->{events}}; # Skip already migrated (and mapped) chunks
			foreach my $event (@events) {
				my ($ch_ts) = $chname=~/^(\d{12})\./;
				my $ch_utc_from = ts2utc($ch_ts);
				my $ch_utc_to   = $ch_utc_from + $CHUNK_SIZE;
				my ($eventid, $ev_utc_from, $ev_utc_to) = @$event;
				
				if (span_intersect($ch_utc_from, $ch_utc_to, $ev_utc_from, $ev_utc_to)) {
					push @{$chunk->{events}}, $eventid;
				}
			}
			
			# Remove chunk from processing if it isn't covered by any event
			#
			delete $job->{CHUNKS}{$chname} unless @{$chunk->{events}};
		}
	}
}

sub log_job
{
	my ($job, @msg) = @_;
	
	my $jobid = $job->{ID};
	return if $job->{STATUS} ne 'INPROGRESS';
	chomp @msg;
	open FH, ">>$STAGE/$jobid"
		or die "[$jobid] Error opening job file: $!";
	print FH $_."\n" foreach @msg;
	close FH or die "[$jobid] Close error: $!";
}

sub start_job
{
	my $job = shift;
	
	my $jobid = $job->{ID};
	$job->{STATUS} = 'INPROGRESS';
	if (-f "$QUEUE/$jobid") {
		rename "$QUEUE/$jobid", "$STAGE/$jobid" 
			or die "job $jobid start failed: $!";
	}
	log_job($job, "INFO: started at ".gmtime);
}

sub complete_job
{
	my $job = shift;
	
	my $jobid = $job->{ID};
	log_job($job, "MIGRATED: ".gmtime);
	$job->{STATUS} = 'MIGRATED';
	rename "$STAGE/$jobid", "$DONE/$jobid"
	    or die "job $jobid complete failed: $!";
}

sub do_job
{
	my $job = shift;
	
	my $jobid = $job->{ID};
	my $objid = $job->{OBJID};
	my ($ts, $dev, $day, $hour) = split(/_/, $jobid);
	my ($streamnum) = $hour =~ /^\d\d\.\d{4}-(\d\d)$/;
	my $chunk_dir = $STORE . "/" . "$dev/$day/$hour";
	my $cloud_dir = $CLOUD_PREFIX . "/" . "$dev/$streamnum";
	my $migrated_size = 0;
	my @upload_rate = 0;
	my $last_chunk_migrated;
	my $err = 0;
	
	# Skip jobs with empty chunks list
	#
	if (not %{$Jobs{$jobid}{CHUNKS}}) {
		SM_LOG->info("[$jobid] empty chunk list");
	}
	else {
		foreach my $chname (sort keys %{ $job->{CHUNKS} }) {
			my $chunk = $job->{CHUNKS}{$chname};
			my $chpath = "$chunk_dir/$chname";
			next if $chunk->{migrated};
			my $idx = "/tmp/${jobid}_${chname}.idx";
			eval {
			        my $ts_start = gettimeofday;
				Cloud_PutFile($BUCKET, $chpath, "$cloud_dir/$chname");
				my $ts_end = gettimeofday;
				my $fsize = file_size($chpath);
				$migrated_size += $fsize;
				push @upload_rate, int($fsize / ($ts_end - $ts_start));
				# Export metadata to temporary location and upload it
				unlink $idx if -f $idx;
				system("$EXPORT_MD $chpath 2>/dev/null 1>$idx");
				if ($?) {
					unlink $idx;
					die "Metadata export failed: code ".($?>>8) if $?;
				}
				Cloud_PutFile($BUCKET, $idx, "$cloud_dir/${chname}.idx");
				unlink $idx;
				# Insert record into 'cloud_chunks'
				foreach my $eventid (@{$chunk->{events}}) {
					$dbs{INSERT_CHUNK}->execute($eventid, $objid, "$streamnum-$chname");
				}
				$dbm->commit;
				$chunk->{migrated} = 1;
				($last_chunk_migrated) = $chname=~/^(\d{12})\./;
				# Mark events as migrated to Cloud
				foreach my $eventid (@{$chunk->{events}}) {
					if (not $EvtMark{$eventid}) {	
						mark_event($eventid, 'online');
						$EvtMark{$eventid} = 1;
					}
				}
				my $ev_str = join(',', @{$chunk->{events}});
				log_job($job, $chname." ".$ev_str);
	    		};
			if ($@) {
				my $errmsg = $@;
				chomp $errmsg;
				SM_LOG->error("[$jobid] Error migrating chunk $chname: $errmsg");
				log_job($job, "ERROR: Failed to migrate chunk $chname: $errmsg");
				$err++;
				eval { $dbm->rollback } if $dbm;
				die "SIGTERM" if $errmsg =~ /SIGTERM/i or $sigterm;
				next;
			}
			else {
				SM_LOG->info("[$jobid] Successfully migrated chunk $chname");
			}
		}
	}
	
	# Log statistics
	my $upload_rate_avg = 0;
	$upload_rate_avg += $_ foreach @upload_rate;
	$upload_rate_avg = int($upload_rate_avg / @upload_rate);
	log_job($job, "SIZE: ".$migrated_size."K", "RATE: ".$upload_rate_avg."K/S");
	
	if ($err == 0) {
		complete_job($job);
	} else {
		log_job($job, "WARN: errors occured. Job postponed");
	}
	
	# Update per-camera attribute with last migrated chunk timestamp
	if ($last_chunk_migrated) {
		eval {
			$dbs{UPDATE_TS}->execute(ts2utc($last_chunk_migrated), $objid);
			$dbm->commit;
		};
		if ($@) {
			my $errmsg = $@;
			eval { $dbm->rollback } if $dbm;
			die "DB_MASTER: $errmsg";
		}
	}
}

sub migrate
{
	foreach my $jobid (sort keys %Jobs) {
		my $job = $Jobs{$jobid};
		
		eval {
		        die "SIGTERM" if $sigterm;
			start_job($job);
			do_job($job);
		};
		if ($@) {
			SM_LOG->error("Error completing job $jobid: $@");
			die "SIGTERM" if $@=~/SIGTERM/i or $sigterm;
		}
		else {
			SM_LOG->info("Job $jobid completed successfully");
		}
	}
}

sub main 
{
	SM_LOG->info("Cloud mover started");
	
        # Pid control
        #
        SM_LOG->logdie("Concurrent run detected!") if CheckPid;
        #die "Cannot start until wipe is running!\n" if check_wipe;
        WritePid;
        
        # Connect to Master DB
        # 
        db_master;
        
        SM_LOG->logdie("Cloud is disabled") unless check_cloud;
        
        # Connect to Cloud storage and validate its state
        #
        cloud_init;
        
        # Init ELog client
        #
        $ELClient = new ELClient;
        SM_LOG->logdie("ELClient init failure") unless $ELClient;
        
        # Scan directory for completed jobs and cleanup old ones
        #
        cleanup_completed_jobs;
        
        # Scan jobs created by sm_wipe and migrate corresponding chunks
        #
        eval {
                scan_jobs;
                migrate if %Jobs;
        };
        if ($@) {
                if ($@ =~ /SIGTERM/) {
                        SM_LOG->warn("TERMINATED by request. The task is not completed");
                        exit 1;
                }
                elsif ($@ =~ /DB_MASTER/) {
                        SM_LOG->logdie("TERMINATED. $@");
                }
                else {
                        SM_LOG->logdie("TERMINATED: Unknown error: $@");
                }
        }
}

# MAIN
main;

END {
	RemovePid;
	eval { $dbm->disconnect } if $dbm;
	SM_LOG->info("sm_cloud_mover finished");
}

# TODO: Move failed jobs to another directory and process them separately

# TODO: More error handling

# TODO: Signal handling
