#!/usr/bin/perl

# Includes
use strict;
use warnings;
use Readonly;
use POSIX ":sys_wait_h";
use POSIX qw(WNOHANG setsid);
use IO::File;
use Cache::Memcached;
use File::Basename;
use Config::IniFiles;
use Getopt::Long;
use Time::HiRes qw( gettimeofday tv_interval );
use Date::Manip;

# Use own definitions
use Mailtrace::Daemon::Parsing;
use Mailtrace::Database::Layer;
use Mailtrace::System::Logging;
use Mailtrace::System::Basic;

# Define constants
Readonly my $LOGFACILITY => 'daemon';

# Variable definitions
my $quit = 1;
my $pid_file;
my $help;
my $nofollow;
my $benchmark;
my $startTime;
my $config_path = "/etc/mailtrace";
my $database;
my $max_age = undef;
my $monitoring = undef;
my $clean = undef;
my $FULL_DEBUG = undef;

# -------------------------- Prerequisites --------------------------
# Get the progname, cut the .pl and make it global
my $program_name = basename("$0");
if ($program_name =~ /^(.*).pl$/x) {
	$program_name = $1
}

# Generate our basic global objects
my $logging     = Mailtrace::System::Logging->new($program_name, $LOGFACILITY);
my $basic_tasks = Mailtrace::System::Basic->new;

# Set the signal handler (the handler object is global)
eval {
    $SIG{'CHLD'} = sub { while ( waitpid(-1, WNOHANG)>0 ) {} };
    $SIG{'TERM'} = $SIG{'INT'} = \&sig_termhandler;
};


# Check if the config file is present
if (not -f $config_path."/mailtraced.cfg") {
	$basic_tasks->alert(0, "Could not find my config file!");
}

# Read the configuration file and save the needed values to separate variables
my %configuration;
tie %configuration, 'Config::IniFiles', ( -file => $config_path."/mailtraced.cfg" );

# Read the database configuration (if some exists)
my $db_configuration = read_database_configuration();

# If something went wrong, just log it, but continue (maybe we have values in direct host configuration)
if (!$db_configuration) {
    $db_configuration = undef;
    $logging->log_syslog('info', "Unable to retrieve configuration from database. See previous errors.");
}

# Create the memcached object
my $memd = Cache::Memcached->new({
    servers	    => [ "127.0.0.1:11211" ],
    debug       => 0
});

# Check: memcached must be up and running
check_memcached();

# Check if we are allowed to start
if (!$configuration{general}{operational} eq 'no') {
    $basic_tasks->alert(0, "Daemon disabled by host configuration!");
#} elsif ((defined $db_configuration) && (!${$db_configuration}{'hmt.daemon.global.start'})) {
} elsif ((defined $db_configuration) && (${$db_configuration}{'hmt.daemon.global.start'} eq 'no')) {
    $basic_tasks->alert(0, "Daemon disabled by global database configuration!");
}

my $log_file          = $configuration{general}{logfile_path};
my $log_idle_time     = $configuration{general}{logfile_read_idle_time_seconds};
my $cache_expire_time = $configuration{general}{cache_expire_time_seconds};
my $DEBUG             = $configuration{general}{debug};

# Get the long commandline options
GetOptions( "debug"        => \$DEBUG,
            "full-debug"   => \$FULL_DEBUG,
            "clean"        => \$clean,
			"expire=s"     => \$cache_expire_time,
			"nofollow"     => \$nofollow,
			"max-age=s"    => \$max_age,
			"benchmark"    => \$benchmark,
			"help"         => \$help,
			"idle=s"       => \$log_idle_time,
			"logfile=s"    => \$log_file,
			"monitoring=s" => \$monitoring,
			"pid=s"        => \$pid_file );

# Print out help if wanted
print_help() if defined $help;

# Activate debugging if wished
$logging->activate_debug() if ($DEBUG == 1);

# If monitoring is set do only this, regardless which further options are defined
if (defined $monitoring) {
    # Control hash
    my %monitoring_options;
    
    # Convert the string into an hash
    for (my $i=0; $i<length($monitoring);$i++) {
        $monitoring_options{substr($monitoring, $i,1)} = 1;
    }
    
    # Call the monitoring procedure
    print_monitoring_data(\%monitoring_options);
    
    exit;
}

# Overwrite the value in configuration file, if a commandline switch exists
if (defined $clean) {
    # Take the commandline value, if one exist
	if (defined $max_age) {
		clean_database($max_age)
		  if $max_age > 0;
		
	# Else try to find an value in configuration file
	} elsif (exists $configuration{database}{db_max_age}) {
		clean_database($configuration{database}{db_max_age})
		  if $configuration{database}{db_max_age} > 0;
		
    # Or try to find one from database
	} elsif (exists ${$db_configuration}{'hmt.daemon.global.database.db_max_age'}) {
	    clean_database(${$db_configuration}{'hmt.daemon.global.database.db_max_age'})
	       if ${$db_configuration}{'hmt.daemon.global.database.db_max_age'} > 0;
	    
	# If no value was found, fallback to 14 days
	} else {
	    clean_database(14);
	}	
}

# Get the start time if duration option is active
if ($benchmark) { $startTime = [gettimeofday] };

# Validate if the logfile to analyse exist
if (not -f $log_file) {
	$basic_tasks->alert(0, "Could not find the logfile to analyze!");
}

# Set an default pidfile of nothing is given
if (not defined $pid_file) {
	# Get the time in second format
	my $timestamp = time();
	
	# Define the name of the pid file
	$pid_file = "/var/run/mailtraced.".$timestamp.".pid";
}

# Safely open the PID-file
my $file_handle = open_pidfile($pid_file);

# Daemonize
my $pid = become_daemon();
print $file_handle $pid;
close $file_handle;

# Write to syslog, that the server is running
$logging->log_syslog('info', "Mailtrace ready for processing...");

# ---------------------------------- Main -----------------------------------------

# Is the memcached running?
check_memcached();

# Open the logfile
open (FILEHANDLE, $log_file);

# Start working
analyze_file(\*FILEHANDLE);
	
# Close the file
close(FILEHANDLE);

# Get the runtime if duration option is active
if ($benchmark) {
	my $elapsed = tv_interval ($startTime, [gettimeofday]);
	
	$logging->log_syslog('info', "Mailtrace durationtime was: $elapsed seconds / ".($elapsed/60)." minutes");
};

# Wait a bit
sleep 1;
	
# Log the termination in syslog
$logging->log_syslog('info', "Mailtrace is down ...");

# Finally quit
exit;

# ---------------------------- Functions ----------------------------
# Simple file analyzation without any additional logic
sub analyze_file {
	my $log_file_handle = shift;
	$logging->debug(1, "Going into normal file analyzation...");
	
	# Create the object for analyzing the lines
	my $analyze = Mailtrace::Daemon::Parsing->new($basic_tasks, $logging, \%configuration, \$memd, $cache_expire_time) or do {
		$basic_tasks->alert(0, "Unable to create object for normal line analyzation");
	};
	
	# Go into the daemon loop
	while ($quit) {
	    # Activate the parsing flag
	    $memd->set('parsing_active', '1');
	    
		# While the handle is defined process the gathered data
		while (<$log_file_handle>) {
			# Analyze the given line
			$analyze->analyze_line(\$_);
		}
		
		# Exit if followmode is not wished
		if ($nofollow) { last; }
		
		# Deactivate the parsing flag
		$memd->set('parsing_active', '0');
		
		# Make a keep alive ping
	    $memd->set('keep_alive', '1', $configuration{general}{keep_alive_seconds});
		
		# If the filehandle quits, sleep for a defined amount of time
		sleep $log_idle_time;
		
		# Unset the flag that prevents further reading
		$log_file_handle->clearerr();
	}
	
	return 1;
}

# Read configuration settings from the database
sub read_database_configuration {
    my $db_configuration;
    
    # Create the database object
	my $database = Mailtrace::Database::Layer->new($basic_tasks, $logging, \%configuration) or do {
		$basic_tasks->alert(0, "Unable to create object for database handling");
	};
	
	# Read out the configuration
	$db_configuration = $database->read_configuration();
	
	# Destroy the temporary needed database object
	$database->DESTROY;
    
    # Value defaulting
    # If no start key exist in database default it to 1 (start)
    ${$db_configuration}{'hmt.daemon.global.start'} = 1
        if not exists ${$db_configuration}{'hmt.daemon.global.start'};
    
    return $db_configuration;
}

# Do an database-cleanup and exit
sub clean_database {
	# Get the parameter
	my $max_age = shift;
	
	# Create the database object
	my $database = Mailtrace::Database::Layer->new($basic_tasks, $logging, \%configuration) or do {
		$basic_tasks->alert(0, "Unable to create object for database handling");
	};
	
	# Do the cleanup
	$database->clean_tables($max_age);
	
	# Exit mailtraced
	exit;
}

# Generate monitoring data
sub print_monitoring_data {
    my $options = shift;
    
    # Is memcached running?
    check_memcached();
       
    # Create the memcached object
    my $memd = Cache::Memcached->new({
        servers	    => [ "127.0.0.1:11211" ],
        debug       => 0
    });
    
    # Status of the daemon
    if (exists ${$options}{s} && ${$options}{s}) {
        if (defined $memd->get('keep_alive')) {
            print "DAEMON: running\n";
        } else {
            print "DAEMON: not running\n";
        }
    }
    
    # Backlog time
    if (exists ${$options}{b} && ${$options}{b}) {
        # Check if a backlog currently exist
        if ($memd->get('parsing_active')) {
            # Get the current time and last processed time
            my $current_time = time();
            my $last_processed_time = UnixDate(ParseDate($memd->get('last_processed_time')), "%s");
            
            # Calculate the difference
            my $backlog_time = $current_time - $last_processed_time;
           
            # Print out the result
            print "BACKLOG: ".$backlog_time."s\n";
           
        } else {
            # Print out the result
            print "BACKLOG: no\n";
        }
    }
    
    return 1;
}

sub check_memcached {
    # Check if the memcached is running
    # if (qx(ps aux | grep memcached | grep -v grep | wc -l) == 0) {
    my $test = $memd->set('testkey', '1');
    
    if ($test eq '0') {
           	$logging->log_syslog('info', "No memcached running -- fatal error and exiting");
                $basic_tasks->alert(0, "\nNo memcached running. memcached has to be started in front of mailtrace!");
    }
    
    # If the daemon ist running return TRUE
    return 1;
}

# Print out help and exit
sub print_help {
	print "mailtraced <OPTIONS>\n\n";
	
	print "   --debug \t\t Start mailtraced in debugging modus (more verbose in syslog)\n";
	print "   --full-debug \t Redirect every output of perl to /root/mailtraced.debug\n";
	print "\t\t\t BE CAREFUL: This file could grow very fast!\n";
	print "   --expire=seconds \t Define after which amount of seconds the cached data should be discarded\n";
	print "   --nofollow \t\t Stop the daemon if end of file is reached\n";
	print "   --clean \t\t Trigger the clean function (database cleanup)\n";
	print "   --max-age=N \t\t Delete entries older than N days from now. Works only in combination with --clean\n";
	print "   --benchmark \t\t Print the duration of daemon runtime to syslog. Only reasonable with the --nofollow option\n";
	print "   --help \t\t Print this text\n";
	print "   --idle=seconds \t Define the idle time before retrying to fetch new lines when\n";
	print "\t\t\t the end of logfile is reached\n";
	print "   --logfile=path \t Specify the logfile which should be analysed\n";
	print "   --monitoring=<string> Define an string for monitoring output behaviour:\n";
	print "   \t\t\t s: Daemon status (running, not running)\n";
	print "   \t\t\t b: Print out backlog time (difference between workstatus an current time in seconds)\n";
	print "   --pid=pidfile \t Define an alternative pidfile (default is /var/run/mailtraced.pid)\n\n";
	
        print "PLEASE NOTE: Define options in /etc/mailtrace/mailtraced.cfg!\n\n";
        	
	exit;
}

# Daemonize mailtrace
sub become_daemon {
	# Do the fork
	$basic_tasks->alert(0, "Can't fork") unless defined (my $child = fork);
	
	# Let the parent die
	exit 0 if $child;
	
	# Become the session leader
	setsid();
	
	# Output redirection
	if (defined $FULL_DEBUG) {
        # Redirect STD* to /tmp/mailtrace.debug
        open(STDIN, "</root/mailtraced.debug");
        open(STDOUT,">/root/mailtraced.debug");
        open(STDERR,">&STDOUT");
	} else {
        # Redirect STD* to /dev/null
        open(STDIN, "</dev/null");
        open(STDOUT,">/dev/null");
        open(STDERR,">&STDOUT");
    }
	
	# Change the working directory
	chdir '/';
	
	# Forget the file mode creation mask
	umask(0);
	
	# Reset the environment path
	$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
	
	# Return the current PID
	return $$;
}

# PID file creation
sub open_pidfile {
	# Get the parameters
	my $file = shift;
	
	# If the PID file already exist do this
	if (-e $file) {
		my $fh = IO::File->new($file) || return;
		my $pid = <$fh>;
		$basic_tasks->alert(0, "Server already running with PID $pid") if kill 0 => $pid;
		warn "Removing PID file for defunct server process $pid.\n";
		$basic_tasks->alert(0, "Can't unlink PID file $file") unless -w $file && unlink $file;
	}
	
	# Open the file and return the handle
	return IO::File->new($file,O_WRONLY|O_CREAT|O_EXCL,0644)
		or $basic_tasks->alert(0, "Can't create $file: $!\n");
}

# Let all active processes do their job and then exit
sub sig_termhandler {
    $logging->debug(1, "Received SIGTERM");
    #$SIG{INT} = 'DEFAULT';
    $quit = 0;
    die;
    return 1;
}

# Remember, in the starting process the $pid variable isn't set
END { if ( defined $pid ) { unlink $pid_file if $$ == $pid } }
