#!/usr/bin/perl

use strict;
use warnings;

use English;
use Getopt::Long;
use Pod::Usage;
use NGCP::API::Client;
use Readonly;
use XML::Simple;
use Template;
use Email::Sender::Simple qw();
use Email::Simple;
use Email::Simple::Creator;
use Email::Sender::Transport::Sendmail qw();
use POSIX qw(strftime);
use File::Pid;

Readonly my @required => qw();
Readonly my $config_file => '/etc/ngcp-panel/provisioning.conf';

my $PROGRAM_BASE = 'ngcp-fraud-notifier';
my $retcode = 0;
my $piddir  = '/run/ngcp-fraud-notifier';
my $pidfile = "$piddir/ngcp-fraud-notifier.pid";
my $pf      = File::Pid->new({ file => $pidfile });

local $PROGRAM_NAME = $PROGRAM_BASE;
local $OUTPUT_AUTOFLUSH = 1;

local $SIG{INT} = \&cleanup;

my $page_size = 100;

my $opts = {
    verbose => 0,
};

my $config;

GetOptions($opts,
    'help|h' => sub { usage() },
    'verbose',
) or usage();

sub DESTROY {
    cleanup();
}

sub check_params {
    my @missing;

    foreach my $param (@required) {
        push @missing, $param unless $opts->{$param};
    }
    usage(join(' ', @missing)) if scalar @missing;

    return;
}

sub usage {
    my $missing = shift;

    pod2usage(
        -exitval => $missing ? 1 : 0,
        -verbose => 1,
        -message => $missing ? "Missing parameters: $missing" : '',
    );

    return;
}

sub load_config {
    $config = XML::Simple->new()->XMLin($config_file, ForceArray => 0)
        or die "Cannot load config: $config_file: $ERRNO\n";

    return;
}

sub get_data {
    my ($uri, $link, $code) = @_;

    my $client = NGCP::API::Client->new(
        verbose => $opts->{verbose},
        page_rows => $page_size,
    );

    my @result = ();
    while (my $res = $client->next_page($uri)) {
        return [] unless check_api_error($res);
        my $res_hash = $res->as_hash;
        my $data = $res_hash->{_embedded}{'ngcp:' . $link};
        if ('ARRAY' eq ref $data) {
            push @result, @{$data};
        } elsif ($data) {
            push @result, $data;
        }
        if (defined $code and 'CODE' eq ref $code) {
            $code->(\@result);
            @result = ();
        }
    }
    return \@result;
}

sub check_api_error {
    my $res = shift;
    if ($res->is_success) {
        return 1;
    } else {
        die($res->result . "\n") if (
            $res->is_client_error
            or ($res->is_server_error and not scalar grep { $_ == $res->code; } qw(502 503 504))
        );
        return 0;
    }
}

sub get_email_template {
    my $event = shift;

    my $lock_type = $event->{interval_lock} ? 'lock' : 'warning';
    my $reseller_id = $event->{reseller_id};
    my @templates_data = ();
    foreach my $reseller ($event->{reseller_id}, 'NULL') {
        @templates_data = (
            @templates_data,
            @{get_data(sprintf('/api/emailtemplates/?reseller_id=%s', $reseller),
                       'emailtemplates')});
    }
    my $selected_template;
    foreach my $template (@templates_data) {
        next if $template->{name} ne 'customer_fraud_' . $lock_type . '_default_email'
             && $template->{name} ne 'customer_fraud_' . $lock_type . '_email';
        next if $template->{reseller_id} && $template->{reseller_id} != $reseller_id;
        $selected_template = $template;
        last if $template->{reseller_id};
    }

    unless ($selected_template) {
        die sprintf "No template 'customer_fraud_%s_default_email' OR 'customer_fraud_%s_email' is defined.\n",
                    $lock_type, $lock_type;
    }

    return $selected_template;
}

sub send_email {
    my ($event, $subscribers) = @_;

    my $template = get_email_template($event);

    my $vars = {
        adminmail => $config->{adminmail},
        customer_id => $event->{contract_id},
        interval => $event->{interval},
        interval_cost  => sprintf('%.2f', $event->{interval_cost} / 100),
        interval_limit => sprintf('%.2f', $event->{interval_limit} / 100),
        type => $event->{type} eq 'profile_limit'
                ? 'billing profile' : 'customer',
    };

    foreach my $subscriber (@{$subscribers}) {
        $vars->{subscribers} .= sprintf "%s\@%s %s\n",
                                @{$subscriber}{qw(username domain)},
                                $subscriber->{external_id}
                                ? '(' . $subscriber->{external_id} . ')'
                                : '';
    }

    my $tt = Template->new();
    foreach my $field (keys %{$template}) {
        my $out;
        $tt->process(\$template->{$field}, $vars, \$out);
        $template->{$field} = $out if $out;
    }

    die "'To' header is empty in the email\n" unless $event->{interval_notify};
    die "'From' header is empty in the email\n" unless $template->{from_email};
    die "'Subject' header is empty in the email\n" unless $template->{subject};

    my $transport = Email::Sender::Transport::Sendmail->new;
    my $email = Email::Simple->create(
        header => [
            To      => $event->{interval_notify},
            From    => $template->{from_email},
            Subject => $template->{subject},
        ],
        body => $template->{body},
    );

    Email::Sender::Simple->send($email, { transport => $transport })
        or die sprintf "Cannot send fraud daily lock notification to %s: $ERRNO\n",
                       $email->header('To');

    update_notify_status($event);

    return;
}


sub update_notify_status {
    my $event = shift;

    my $client = NGCP::API::Client->new(verbose => $opts->{verbose});

    my $now = strftime('%Y-%m-%d %H:%M:%S', localtime);
    my $uri = '/api/customerfraudevents/' . $event->{id};
    my $data = [
        {
            op => 'replace',
            path => '/notify_status',
            value => 'notified',
        },
        {
            op => 'replace',
            path => '/notified_at',
            value => $now,
        },
    ];

    my $res = $client->request('PATCH', $uri, $data);

    return;
}

sub main {
    if (my $num = $pf->running) {
        print "$PROGRAM_BASE is already running.\n";
        $retcode = 1;
        exit $retcode;
    }
    mkdir $piddir unless -e $piddir;
    $pf->write;

    check_params();
    load_config();

    get_data(
        sprintf('/api/customerfraudevents/?no_count=true&notify_status=%s', 'new'),
        'customerfraudevents',
        sub {
            my $events = shift;
            foreach my $event (@{$events}) {
                if ($event->{interval_notify}) {
                    my $subscribers = get_data(sprintf('/api/subscribers/?customer_id=%d',
                        $event->{contract_id}),
                        'subscribers');
                    next unless scalar @$subscribers;
                    eval {
                        send_email($event, $subscribers);
                    };
                    print $EVAL_ERROR if $EVAL_ERROR;
                }
            }
        }
    );
    return;
}

sub cleanup {
    $pf->remove;
    rmdir $piddir if $piddir;
}

eval { main() };

if ($EVAL_ERROR) {
    print STDERR $EVAL_ERROR;
    $retcode = 1;
}

exit $retcode;

__END__

=head1 NAME

ngcp-fraud-notifier - send fraud notifications for customers exceeding thresholds

=head1 SYNOPSIS

B<ngcp-fraud-nofifier> [I<options>...]

=head1 DESCRIPTION

B<This program> checks for contract balances above fraud limit warning
thresholds and sends email notifications about the incidents.

=head1 OPTIONS

=over 8

=item B<--verbose>

Show additional debug information. Default false.

=item B<--help>

Print a brief help message.

=back

=head1 EXIT STATUS

=over

=item B<0>

Command completed successfully.

=item B<1>

Command failed with errors.

=back

=head1 SEE ALSO

NGCP::API::Client

=head1 BUGS AND LIMITATIONS

Please report problems you notice to the Sipwise
Development Team <support@sipwise.com>.

=head1 AUTHOR

Kirill Solomko <ksolomko@sipwise.com>

=head1 LICENSE

Copyright (C) 2019 Sipwise GmbH, Austria

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=cut
