#!/usr/bin/perl -w
use strict;
use warnings FATAL => 'all';

use DBI;
use POSIX qw(:errno_h);
use Getopt::Long qw(:config no_ignore_case);
no if $] >= 5.017011, warnings => 'experimental::smartmatch';

sub help {
	return qq{
        Find all groups' members inconsistencies.
        --------------------------------------
        Available options:

        --vo        | -v Vo id in which inconsistencies will be looking for (optional)
        --user      | -u Username for Postgre DB (required)
        --password  | -p Password for Postgre DB (required)
        };
}

my ($user, $pwd, $input_vo);
my @already_processed_groups = ();

GetOptions ("help|h" => sub { print help(); exit 0;}, "vo|v=s" => \$input_vo,"user|u=s" => \$user, "password|w=s" => \$pwd) || die help();

if (!defined $pwd) { print "[ERROR] Password for Postgre DB is required! Use --help | -h to print help.\n"; exit 1; }
if (!defined $user) { print "[ERROR] Username for Postgre DB is required! Use --help | -h to print help.\n"; exit 1; }

my $dbh = DBI->connect('dbi:Pg:dbname=perun',$user,$pwd,{RaiseError=>1,AutoCommit=>0,pg_enable_utf8=>1}) or die EPERM," Connect";

my $sth_vos = $dbh->prepare(q{
	select id from vos;
});

my $sth_root_groups = $dbh->prepare(q{
	select id from groups where vo_id = ? and parent_group_id is null and id not in (select operand_gid from groups_groups) and name != 'members';
});

my $sth_child_groups = $dbh->prepare(q{
	select operand_gid from groups_groups where result_gid = ?
});

my $sth_indirect_group_members = $dbh->prepare(q{
	select member_id, source_group_id from groups_members where group_id = ? and membership_type = 2
});

my $sth_group_members = $dbh->prepare(q{
	select distinct member_id from groups_members where group_id = ?
});

if (defined $input_vo) {
	find_inconsistencies_in_vo($input_vo);
} else {
	$sth_vos->execute();
	while (my ($selected_vo) = $sth_vos->fetchrow_array()) {
		find_inconsistencies_in_vo($selected_vo);
	}
}
disconnect $dbh;

#
# Find group inconsistencies in specific vo
#
sub find_inconsistencies_in_vo {
	my $vo = shift;

	print("Process of finding group inconsistencies in vo:$vo has started.\n");

	my $root_groups = $dbh->selectcol_arrayref($sth_root_groups, {}, ($vo));
	my $root_indirect_members = prepare_structure_of_indirect_members($root_groups);
	check_against_next_group_level($root_indirect_members);

	print("Process of finding group inconsistencies in vo:$vo has finished.\n");
}

#
# Recursively check current level of groups against their child groups (unions included).
#
sub check_against_next_group_level {
	my $current_indirect_members = shift;
	return unless (%$current_indirect_members);

	my @all_sub_groups = ();

	foreach my $parent_group (sort keys %$current_indirect_members) {
		next if($parent_group ~~ @already_processed_groups);
		my @sub_groups = @{$dbh->selectcol_arrayref($sth_child_groups, {}, ($parent_group))};
		foreach my $sub_group (@sub_groups) {
			my @all_sub_group_members = @{$dbh->selectcol_arrayref($sth_group_members, {}, ($sub_group))};
			foreach my $sub_group_member (@all_sub_group_members) {
				unless ($sub_group_member ~~ @{$current_indirect_members->{$parent_group}->{$sub_group}}) {
					print("Inconsistency found. Member $sub_group_member should exist as Indirect in parent group $parent_group of group $sub_group.\n");
				}
			}
			foreach my $expected_sub_group_member (@{$current_indirect_members->{$parent_group}->{$sub_group}}) {
				unless ($expected_sub_group_member ~~ @all_sub_group_members) {
					print("Inconsistency found. Indirect member $expected_sub_group_member should exist as Direct in operand group $sub_group of parent group $parent_group.\n");
				}
			}
			push(@all_sub_groups, $sub_group);
		}
	}
	my $next_indirect_members = prepare_structure_of_indirect_members(\@all_sub_groups);
	push(@already_processed_groups, keys %$current_indirect_members);
	check_against_next_group_level($next_indirect_members);
}

#
# Create a structure, where main key is a group, subkeys are all group's child groups
# and the value is a list of indirect members of a main group, which have the child group as a source group.
#
sub prepare_structure_of_indirect_members {
	my $groups = shift;

	my %indirect_members = ();
	foreach (@$groups) {
		my @child_groups = @{$dbh->selectcol_arrayref($sth_child_groups, {}, ($_))};
		foreach my $child (@child_groups) {
			$indirect_members{$_}{$child} = ();
		}
		$sth_indirect_group_members->execute($_);
		while (my ($member_id, $source_group_id) = $sth_indirect_group_members->fetchrow_array()) {
			push(@{$indirect_members{$_}{$source_group_id}}, $member_id);
		}
	}

	return \%indirect_members;
}
