#!/usr/bin/perl

use strict;
use warnings;
use Carp;
use Config::Tiny;
use DBI;
use English qw( -no_match_vars );
use Getopt::Long;
use List::Util qw(any);
use Pod::Usage;

my $dbhost      = 'localhost';
my $dbfile      = '/etc/ngcp-config/cfg_schema.db';
my $dbcredsfile = '/etc/mysql/sipwise_extra.cnf';
my $rolesfile   = '/etc/default/ngcp-roles';
my $dbname      = 'ngcp';
my $debug       = 0;
my $exit_code   = 0;
my $help        = 0;
my $man         = 0;
my $node        = undef;
my $result_date = undef;
my $result_host = undef;
my $result_id   = undef;
my $result_rev  = undef;
my $result_rel  = undef;
my $result_site = undef;
my $site_id     = 1;
my $is_db_node  = 'no';
my @revision;
my @result_list;
my $schema = undef;
my $socket = undef;
my $dbh = undef;
my $dsn;

GetOptions(
    'dbhost=s'       => \$dbhost,
    'dbfile=s'       => \$dbfile,
    'dbname=s'       => \$dbname,
    'debug|d'        => \$debug,
    'help|?'         => \$help,
    'man'            => \$man,
    'node=s'         => \$node,
    'revision=s{1,}' => \@revision,
    'schema=s'       => \$schema,
    'socket=s'       => \$socket,
) or pod2usage(2);

pod2usage( -exitval => 2, -verbose => 0 ) if $help;
pod2usage( -exitval => 2, -verbose => 2 ) if $man;
pod2usage(2) if !@revision;
pod2usage(2) if !defined $schema;

my $rolesconf = Config::Tiny->read($rolesfile);

if (ref $rolesconf && defined $rolesconf->{_}) {
    $site_id = $rolesconf->{_}{NGCP_SITE_ID} // $site_id;
    $is_db_node = $rolesconf->{_}{NGCP_IS_DB} // $is_db_node;
    $is_db_node =~ s/"//g;
}

if ($schema eq 'cfg_schema') {
    $dsn = "dbi:SQLite:$dbfile";
}
elsif (length $socket) {
    $dsn = "dbi:mysql:dbname=$dbname;mysql_socket=$socket";
}
else {
    $dsn = "dbi:mysql:dbname=$dbname;host=$dbhost;mysql_read_default_file=$dbcredsfile";
}
$dbh = DBI->connect($dsn, '', '')
    or croak "Couldn't connect to database: " . DBI->errstr;

my $sql_clause = "";    # if no node is specified then use empty SQL statement
if ( defined $node ) {
    $sql_clause = "where node = ?";
}

my $q_schema = $dbh->quote_identifier($schema);

my $sth = $dbh->prepare("SELECT * FROM $q_schema $sql_clause")
  or croak "Couldn't prepare statement: " . $dbh->errstr;

if ( defined $node ) {
    $sth->bind_param( 1, $node );
}

$sth->execute()
  or croak "Couldn't execute statement: " . $sth->errstr;

if ($dbname eq 'ngcp' and $schema eq 'db_schema') {
    my $rows = $dbh->selectall_arrayref(<<SQL, undef, $dbname, $schema);
SELECT column_name
  FROM information_schema.columns
 WHERE table_schema = ?
   AND table_name = ?
   AND column_name IN ('release', 'site_id')
SQL

    my @bind_cols = (\$result_id, \$result_rev, \$result_host, \$result_date);

    foreach my $row (@{$rows}) {
        push(@bind_cols, \$result_rel) if $row->[0] eq 'release';
        push(@bind_cols, \$result_site) if $row->[0] eq 'site_id';
    }

    $sth->bind_columns(@bind_cols);
} else {
    $sth->bind_columns( \$result_id, \$result_rev, \$result_host, \$result_date);
}

while ( $sth->fetch ) {
    foreach my $rev (@revision) {

        my $short_rev = _sanitize_rev($rev);

        if ( $result_rev =~ /^${short_rev}$/msx ) {
            if ( defined $node ) {
                if ( $result_host =~ /$node/msx ) {
                    if ($schema eq 'db_schema' && $is_db_node eq 'yes') {
                        if (!$result_site || $result_site == $site_id) {
                            push @result_list, $result_rev;
                        }
                    }
                    else {
                        push @result_list, $result_rev;
                    }
                }
            }
            else {
                push @result_list, $result_rev;
            }
        }
    }
}

foreach my $rev (@revision) {
    my $short_rev = _sanitize_rev($rev);
    if ( any { /^${short_rev}$/msx } @result_list ) {
        if ( defined $node ) {
            print "Revision $rev already executed on host $node\n";
        }
        else {
            print "Revision $rev already executed\n";
        }
    }
    else {
        if ( defined $node ) {
            print "No match for revision $rev on host $node\n";
            $exit_code = 1;
        }
        else {
            print "No match for revision $rev\n";
            $exit_code = 1;
        }
    }
}

$sth->finish;
$dbh->disconnect;

exit($exit_code);

sub _sanitize_rev {
  
  # adjust for MySQL handling (we're interested only in the ID in MySQL speak)
  # the regex handling is ugly, some love to it would be nice...  
  my $short_rev = shift;
  $short_rev =~ s/.*\///;    # drop anything until and incl. the first slash
  $short_rev =~ s/\..*//;    # drop anything starting at the first '.'
  $short_rev =~ s/_.*//;     # drop anything starting at the first '_'
  $short_rev =~ s/^0+//;     # strip any trailing zeros from the id
  return $short_rev;
  
}

__END__

=head1 NAME

ngcp-check-rev-applied - check which db/config revisions have been executed

=head1 SYNOPSIS

B<ngcp-check-rev-applied> [I<options>...] I<required-arguments>

=head1 DESCRIPTION

This program queries the ngcp database to check for db/config revisions that
have been applied. If the specified ID is present (or when multiple IDs are
specified: all of them are present) the exit code is 0, otherwise (the specified
ID is not present, or when multiple IDs are specified:: one of them is not
present) the exit code is 1.

=head1 REQUIRED ARGUMENTS

=over 8

=item B<--schema> I<schema>

Schema to check. 'cfg_schema' or 'db_schema'

=item B<--revision> I<id>

Revision to be checked. Is this revision already applied?

=back

=head1 OPTIONS

=over 8

=item B<--dbhost> I<host>

Query specified host instead of the default (being "localhost").

=item B<--dbfile> I<file.db>

Query specified sqlite3 file instead of the default (being
"/etc/ngcp-config/cfg_schema.db").
Used only if --schema is 'cfg_schema'

=item B<--dbname> I<db>

Use specified database instead of the default (being "ngcp").

=item B<--node> I<name>

Restrict querying ID to specified node name.
This is relevant only in high availability environments
to check for (non-)replicated revisions.

=item B<--revision> I<id>...

Query database for specified ID. Multiple IDs can be specified white-space
separated (e.g.: '--revision 11 23 42').

=item B<--schema> I<name>

Use specified schema as table name to look for specified ID.
Supported table names: cfg_schema, db_schema

=item B<--debug>

Be more verbose during execution.

=item B<--help>

Print help message and exit.

=item B<--man>

Display manpage using pod2man.

=back

=head1 USAGE EXAMPLES

=over 8

=item Check db_schema table for revision 23:

  % ngcp-check-rev-applied --schema db_schema --revision 23

=item Check cfg_schema table for revision 42 and use output of ngcp-hostname
to limit the specified ID to the current host:

  % ngcp-check-rev-applied --schema cfg_schema --revision 42 --node $(ngcp-hostname)

=item Check db_schema table for revisions 11, 23 and 42:

  % ngcp-check-rev-applied --schema db_schema --revision 11 23 42 --debug
  No match for revision 11.
  No match for revision 23.
  Revision 42 already executed.

=back

=head1 EXIT STATUS

=over 8

=item B<0>

The requested ID is present. If multiple IDs have been specified all of them are present.

=item B<1>

The requested ID is *NOT* present. If multiple IDs have been specified at least
one of the specified IDs it *NOT* present.

=item B<2>

Error or invalid usage of command line options.

=item B<255>

Error while running database query.

=back

=head1 DIAGNOSTICS

=over 8

=item "Couldn't connect to database: "

=item "Couldn't prepare statement: "

=item "Couldn't execute statement: "

=item "No match for revision \$rev on host \$node\n"

=item "No match for revision \$rev\n"

=back

=head1 BUGS AND LIMITATIONS

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

=head1 AUTHOR

Michael Prokop <mprokop@sipwise.com>

=head1 LICENSE

Copyright (c) 2012-2015 Sipwise GmbH, Austria.
All rights reserved. You may not copy, distribute
or modify without prior written permission from
Sipwise GmbH, Austria.

=cut
