#!/usr/bin/perl

# $Id$

use 5.010;
use strict;
use warnings;
use utf8;

# just to run on working copy
use lib::abs "../lib"; 

use YAML;
use Getopt::Long;
use Sys::Hostname;  
use Pid::File::Flock qw/:auto/;
use Log::Any '$log';

use Yandex::HTTP qw/http_fetch/;
use Yandex::SendMail;


#my $CONF_DIR = '../example';
#my $STATUS_DIR = '.';

my $CONF_DIR = '/etc/total-monitor.d';
my $STATUS_DIR = '/var/spool/total-monitor/status';


my %MODULE = (
    cocaine  => 'TotalMonitor::CocaineApp',
#    dbconfig => 'TotalMonitor::DBConfig',
#    dpkg     => 'TotalMonitor::Dpkg',
);

my %TRANSPORT = (
    dump => {
        diff_method => 'get_text_diff',
        reporter => \&dump_changes,
    },
    email => {
        diff_method => 'get_text_diff',
        reporter => \&email_changes,
    },
    versionica => {
        diff_method => 'get_changed_versions',
        reporter => \&update_versionica,
    },
);



run() if !caller();
exit;


sub run
{
    GetOptions(
        "help" => sub {system("podselect -section NAME -section DESCRIPTION $0 | pod2text-utf8 >&2"); exit 0;},
        "status-dir=s" => \$STATUS_DIR,
        "conf-dir=s" => \$CONF_DIR,
    ) || die "Error occured";

    my $confs = read_configs();
    exit if !@$confs;

    CONF:
    for my $conf (@$confs) {
        $log->notice("Processing task '$conf->{title}'");
        $log->debug(Dump($conf))  if $log->is_debug;

        my $module = $MODULE{$conf->{type}};
        die "Unknown type: $conf->{type}"  if !$module;
        eval "require $module; 1"
            or die "Failed to load $module: $@";

        my $plugin = $module->new( %{ $conf->{params} || {} } );

        my $state_time = time;
        my $state = eval { $plugin->get_current_state() };
        if (!$state) {
            $log->warn($@);
            next CONF;
        }

        REPORT:
        for my $report ( @{$conf->{reports}} ) {
            my $transport = $report->{transport};
            my $transport_info = $TRANSPORT{$transport}
                or die "Unknown transport: $transport";

            my $state_id = get_report_id($conf, $report);
            my $old_data = get_saved_state($state_id);
            my $old_state = $old_data->{data}->{state};

            my $should_resave = 1;
            STEP: {
                my $diff_method = $report->{diff_method} || $transport_info->{diff_method};
                my $diff = $plugin->$diff_method($old_state, $state, $report);
                if ( !$diff ) {
                    $should_resave = 0;
                    last STEP;
                }

                my $reporter = $transport_info->{reporter};
                eval { $reporter->($diff, $report, $conf); 1 }
                    and last STEP;

                warn "Transport failed for $state_id: $@";
                next REPORT;
            }

            my $data_to_save = $should_resave ? { state => $state } : undef;
            save_state($state_id, $data_to_save, $state_time);
        }
    }

    return;
}





sub dump_changes
{
    my ($diff, $report, $conf) = @_;
    say ref $diff ? Dump($diff) : $diff;
    return;
}

sub email_changes
{
    my ($text, $report, $conf) = @_;

    my $subj = $report->{subj} || "<host>: $conf->{title} changed";
    $subj =~ s/<host>/hostname()/gexms; # !!! to extend

    my $to = $report->{to}
        or die "Email recipient is not defined";
    local $Yandex::SendMail::FROM_FIELDS_FOR_ALERTS = $report->{from} || $to;
    send_alert($text, $subj, $to);
    
    return;
}

sub update_versionica
{
    my ($diff, $report, $conf) = @_;

    my %to_post = map {($_->[0] => $_->[1] // '<none>')} @$diff;
    return if !%to_post;

    my $url = $report->{url};
    http_fetch(POST => $url, \%to_post, timeout => 20, headers => {'Content-Type' => 'application/x-www-form-urlencoded'});

    return;
}



sub save_state
{
    my ($state_id, $data, $starttime) = @_;
    my $file = "$STATUS_DIR/$state_id.yml";
    YAML::DumpFile($file, $data)  if $data;
    utime $starttime, $starttime, $file;
    return;
}

sub get_saved_state
{
    my ($state_id) = @_;
    my $file = "$STATUS_DIR/$state_id.yml";
    my $saved_state = {
        file => $file,
        time => [stat($file)]->[9],
        data => -f $file ? scalar YAML::LoadFile($file) : {},
    };
    return $saved_state;
}

sub get_report_id
{
    my ($conf, $report) = @_;
    my $report_id = $report->{id} || $report->{transport};
    return "$conf->{id}-$report_id";
}

sub read_configs
{
    my @confs;
    opendir(my $c_dh, $CONF_DIR) || die "Can't open dir $CONF_DIR: $!";
    for my $file (sort grep {!/\./} readdir($c_dh)) {
        $log->info("Reading config file $CONF_DIR/$file");
        my $conf = load_config("$CONF_DIR/$file");
        $conf->{id} ||= $file;
        push @confs, $conf;
    }
    closedir($c_dh) || die "Can't close dir $CONF_DIR: $!";

    return \@confs;
}


sub load_config
{
    my ($file) = @_;
    my $conf = YAML::LoadFile($file);
    
    my @errors = validate_conf($conf);
    die "Error in $file: " . join(', ', @errors)  if @errors;

    my $module = $MODULE{$conf->{type}}
        or die "Unknown config type: $conf->{type}";

    return $conf;
}

sub validate_conf 
{
    my $conf = shift;
    my @required = qw/ type reports /;
    my @optional = qw/ id title params /;

    my @err;
    for my $key (@required) {
        push @err, "Required key <$key> is missing"  if !defined $conf->{$key};
    }

    my $keys_re = join q{|}, map {quotemeta} sort ( @required, @optional );
    if (my @unknown = sort grep { !/^(?: $keys_re )$/xms } keys %$conf) {
        push @err, "Unknown keys: ".join(',', @unknown);
    }
    return @err;
}


