#!/usr/bin/perl -w

use strict;
use warnings;
use Redis;
use Config::Tiny;
use Getopt::Long qw(GetOptions);
use List::Util qw(none);
use Pod::Usage qw(pod2usage);
use IPC::Cmd qw(run);
use Text::Table;
use Time::Piece;
use XMLRPC::Lite;
use JSON;

###### General Variables ######
my $NGCP_SYSTEM_TOOLS_PATH = "/etc/ngcp-system-tools/ngcp-active-calls.conf";
my $SEMS_XMLRPC_PORT ;
my $REDIS_PORT;
my $REDIS_DB;
my $MAX_ROWS = 50;
my $help;
my $caller;
my $callee;
my $min_time = 0;
my %hosts;
my $json_obj = JSON->new->utf8;

#----------------------------------------------------------------------
# Get Nodes services IPs and ports
sub init_nodes {
    # load ngcp_check_tools
    if (! -f $NGCP_SYSTEM_TOOLS_PATH) {
        error("cannot read $NGCP_SYSTEM_TOOLS_PATH");
    }
    my $config = Config::Tiny->read($NGCP_SYSTEM_TOOLS_PATH);
    $REDIS_DB = $config->{_}->{'redis-db'};
    $REDIS_PORT = $config->{_}->{'redis-port'};
    $SEMS_XMLRPC_PORT = $config->{_}->{'sems-xmlrpc-port'};
    my $HOST_LIST = $config->{_}->{'proxy-hosts'};
    if (length $REDIS_DB == 0) {
        error("can't define Redis DB for Dialogs.");
    }
    if (length $REDIS_PORT == 0) {
        error("can't define Redis DB port.");
    }
    if (length $SEMS_XMLRPC_PORT == 0) {
        error("can't define Sems XMLRPC port.");
    }
    if (length $HOST_LIST == 0) {
        error("can't define NGCP proxy hosts.");
    }
    foreach my $node (split ' ', $HOST_LIST) {
        $hosts{$node}->{'redis_ip'} = $config->{"proxy-$node"}->{'redis-ip'};
        $hosts{$node}->{'sems_ip'} = $config->{"proxy-$node"}->{'sems-ip'};
    }
}
#----------------------------------------------------------------------
# Check if caller and/or callee are correctly provided
sub check_call_data {
    $callee ||= 'any';
    $caller ||= 'any';
    if (( $caller eq "any" ) and ( $callee eq "any" )) {
        error("you can not set caller and callee equal to any.");
    };
}
#----------------------------------------------------------------------
# Error message
sub error {
    my $msg = shift;
    print { *STDERR } "Error: $msg\n";
    exit 1;
}
#----------------------------------------------------------------------
# Check number format: all numbers with optional + at the start
sub check_number {
    my $sequence = shift;
    if ($sequence =~ /^\+?[0-9]+$/) {
        return 1;
    } else {
        return 0;
    }
}
#----------------------------------------------------------------------
# Compute elapsed time
sub elapsed_time {
    my $epoch_start_time = shift ;
    my $this_time = time();
    my $elapsed = Time::Piece->strptime($this_time-$epoch_start_time, '%s');
    return $elapsed->epoch, $elapsed->hms;
}
#----------------------------------------------------------------------
# Connect to Sems and drops call with provided tag. Return error status and message.
sub drop_call {
    my $url = shift;
    my $tag = shift;
    my $result = XMLRPC::Lite
        -> proxy("http://$hosts{$url}->{sems_ip}:$SEMS_XMLRPC_PORT")
        -> call('di', ("sbc","postControlCmd", $tag, "teardown"))
        -> result;

    return @$result[0], @$result[1];
}
#----------------------------------------------------------------------
# Let user select which calls must be torn down from the call list
sub select_and_drop {
    my (%Active_Calls) = @_;
    my $success;
    my $cmd;

    print "Which call(s) do you wanto to drop? ";
    my $data = <STDIN>;
    chomp $data;
    my @ToDrop = split(/,/, $data);
    my $length = scalar @ToDrop;
    if ( $length > 0 ) {
        if ( $ToDrop[0] eq "all" ) {
            print "Every call on the list will be dropped. Are you sure? [y/N]:";
            my $answer = <STDIN>;
            chomp $answer;
            if ( $answer ne "y" ) {
                exit 0;
            }
            foreach my $row (keys %Active_Calls)
            {
                my ($Code , $Message) = drop_call($Active_Calls{$row}[0],$Active_Calls{$row}[1]);
                if  ($Code<200 or $Code>=300){
                    error("can not drop call with to_tag: $Active_Calls{$row}[1]");
                }
            }
        } else {
            foreach my $item (@ToDrop) {
                my $index = $item-1;
                if ( defined($Active_Calls{$index}) and ($index >= 0) ) {
                    my ($Code , $Message) = drop_call($Active_Calls{$index}[0],$Active_Calls{$index}[1]);
                    if  ($Code<200 or $Code>=300){
                        error("can not drop call with index: $item");
                    }
                } else {
                    error("call with index $item does not exist anymore.");
                }
            }
        }
    } else {
        print "No call to drop!\n";
        exit 0;
    }
}
#----------------------------------------------------------------------
# Show calls matching user defined criteria
sub find_calls {
    my $task = shift;
    my $calls_checker = shift;
    my $rows = 0;
    my $redis_dbh;
    my %Active_Calls;
    my %json_data;
    my $call_table = Text::Table->new(
        \'|',
        {
            title => 'INDEX',
            align => 'center',
            align_title => 'center'
        },
        \"|",
        {
            title => 'FROM uri',
            align => 'center',
            align_title => 'center'
        },
        \"|",
        {
            title => 'REQUEST uri',
            align => 'center',
            align_title => 'center'
        },
        \"|",
        {
            title => 'TIME',
            align => 'center',
            align_title => 'center'
        },
        \"|",
        {
            title => 'CALL ID',
            align => 'center',
            align_title => 'center'
        },
        \'|',
    );

    foreach my $node (sort keys %hosts) {
        $redis_dbh = Redis->new(
            server => "$hosts{$node}->{redis_ip}:$REDIS_PORT",
            reconnect => 1,
            every => 500_000,
            cnx_timeout => 3,
            write_timeout => 3
        );
        $redis_dbh->select($REDIS_DB);
        my $cursor = 0;
        do {
            my $res = $redis_dbh->scan($cursor, MATCH => 'dialog:entry:*', COUNT => 1000);
            $cursor = shift @$res;
            my $keys = shift @$res;
            foreach my $i (@$keys) {
                my $from_user;
                my $req_user;
                my $start_time;
                my $to_tag;
                my $clid;
                my $elapsed;
                my $minutes;
                my @redis_values;
                @redis_values = $redis_dbh->hmget($i, qw(from_uri req_uri start_time to_tag callid)) ;
                next if none { defined } @redis_values;
                if (defined($redis_values[0]) and $redis_values[0] =~ /^sip:([^@]+)@[^@]+$/) {
                    $from_user = $1
                }
                if (defined($redis_values[1]) and $redis_values[1] =~ /^sip:([^@]+)@[^@]+$/) {
                	$req_user = $1
                }
                if (defined($redis_values[2])) {
                    $start_time = int($redis_values[2]);
                    ($elapsed, $minutes) = elapsed_time($start_time);
                }
                if (defined($redis_values[3])) {
                    $to_tag = $redis_values[3]
                }
                if (defined($redis_values[4])) {
                    $clid = $redis_values[4]
                }
                if ($calls_checker->($from_user, $req_user)) {
                    if ( defined($to_tag) and ( $elapsed >= $min_time )) {
                        $Active_Calls{$rows}[0] = $node;
                        $Active_Calls{$rows}[1] = $to_tag;
                        $json_data{$rows} = {
                            caller => $from_user,
                            callee => $req_user,
                            to_tag => $to_tag,
                            time=>$minutes,
                            clid=>$clid
                        };
                        $rows++;
                        if (($task eq "show") or ($task eq "drop")) {
                            $call_table->add( $rows, $from_user, $req_user, $minutes, $clid );
                        }
                    }
                }
                if ( $rows >= $MAX_ROWS ) {
                    last
                }
            }
            if ( $rows >= $MAX_ROWS ) {
                last
            }
        } while ($cursor);
        $redis_dbh->quit;
    }
    if ($rows == 0 ) {
        print "Call not found, sorry!\n";
        return 1;
    }
    if ($task eq "show") {
        print $call_table->rule( '-', '-' );
        print $call_table->title();
        print $call_table->rule( '-', '|' );
        print $call_table->body();
        print $call_table->body_rule( '-', '-' );
        return 0;
    }
    if ($task eq "json") {
        print $json_obj->pretty->encode(\%json_data);
        return 0;
    }
    if ($task eq "drop") {
        print $call_table->rule( '-', '-' );
        print $call_table->title();
        print $call_table->rule( '-', '|' );
        print $call_table->body();
        print $call_table->body_rule( '-', '-' );
        select_and_drop(%Active_Calls);
        return 0;
    }
}

#----------------------------------------------------------------------
# MAIN

GetOptions(
    "help|h" => \$help,
    "caller|s=s" => \$caller,
    "callee|d=s" => \$callee,
    "min_time|t=i" => \$min_time,
);

if ($help) {
    pod2usage(-verbose => 3 );
    exit(0);
}
my $action = $ARGV[0];
my @actions = qw(show drop json);
my $calls_checker;
my $exit_status;
init_nodes;
check_call_data;
if ( defined($action) and (grep { /$action/ } @actions)) {
    my $caller_correct = check_number($caller);
    my $callee_correct = check_number($callee);
    # CASE 1: Caller numeric, callee numeric
    if ( $caller_correct and $callee_correct ) {
        $calls_checker = sub {
            my ($from, $to) = @_;
            return ($from eq $caller and $to eq $callee);
        };
    # CASE 2: Caller numeric, callee any
    } elsif ( $caller_correct and $callee eq "any" ) {
        $calls_checker = sub {
            my ( $from, undef ) = @_;
            return ($from eq $caller);
        };
    # CASE 3: Caller any, callee numeric
    } elsif ( $caller eq "any" and $callee_correct ) {
        $calls_checker = sub {
            my (undef, $to) = @_;
            return ($to eq $callee);
        };
    } else {
        error("Caller or Callee has an invalid syntax.");
    }
} else {
    print "Use action show or drop or json.\n\n";
    pod2usage(-verbose => 1);
    exit 0;
}

$exit_status = find_calls( $action, $calls_checker );

exit $exit_status;

# vim: ts=4 sw=4 et

__END__

=encoding utf-8

=head1 NAME

ngcp-active-calls - show/drop active calls matching search criteria

=head1 SYNOPSIS

B<ngcp-active-calls> [I<command>...] [I<option>...]

=head1 DESCRIPTION

B<This program> retrieves all active calls with defined caller and/or
callee. If caller or callee are not specified, it assumes any caller
or any callee. Of course, at least caller or callee must be specified.
A minimum duration time can be set to restrict the shown calls to a
duration equal or greater than the one specified.

=head1 COMMANDS

=over 8

=item B<--help>

This help message.

=back

=over 8

=item B<show>

To get a list of calls matching serching criteria.

=back

=over 8

=item B<drop>

To get a list of calls matching serching criteria and select which calls to drop.

=back

=over 8

=item B<json>

To get a list of calls matching serching criteria in json format.

=back

=head1 OPTIONS

=over 8

=item B<-s> I<caller-number>

Select calls whit specified caller number. Use 'any' for any number.

=item B<-d> I<callee-number>

Select calls whit specified callee number. Use 'any' for any number.

=item B<-t> I<seconds>

Shows only calls with duration equal or greater than I<seconds>.

=back

=head1 CAVEATS

Since it could increase system load, it is NOT possible to set caller and callee both equal to 'any'.

=head1 AUTHOR

Christian Bergamaschi <cbergamaschi@sipwise.com>

=head1 LICENSE

Copyright © 2020 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

