#!/usr/bin/perl

my @today = Today();
my $groups_grace_period_date = Date_to_Text(Add_Delta_Days(@today, 30));  # grace period for groups
#@today = Add_Delta_Days(@today, 155);  #DEBUG - add more day 
#@today = (2016, 1, 1); #DEBUG - set fo fixed date



use strict;
use warnings;
use utf8;
use DBI;
use Data::Dumper;
use File::Path qw(make_path);
use Switch;
use MIME::Lite;

    package MIME::Lite::IO_Handle {
        no warnings "redefine";
        sub print {
            my $self = shift;
            binmode $$self => ":utf8";
            print {$$self} @_;
        }
    };

use Net::LDAPS;
use Net::LDAP::Entry;
use Net::LDAP::Message;
use Net::LDAP::LDIF;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED );
use File::Copy;
use Date::Calc qw/ Today Delta_Days Add_Delta_Days Date_to_Days Date_to_Text Decode_Date_EU /;
no if $] >= 5.017011, warnings => 'experimental::smartmatch';


binmode STDOUT, ":utf8";

# Import shared AD library
use ADConnector;
use ScriptLock;

sub process_add_user;
sub process_update_user;
sub process_ous;
sub process_groups;
sub process_groups_members;
sub process_licenses_groups;

sub ping_password_setter;
sub fill_from_ad;
sub add_to_license_group;
sub remove_from_license_group;

# define service
my $service_name = "ad_mu";

ldap_log($service_name, "-- Propagation of $service_name service started.");
END { ldap_log($service_name, "-- Propagation of $service_name service finished."); };


my $R_NONE = 0x00;

my $SUCCESS = 1;
my $FAIL = 0;


my $facility_name = $ARGV[0];
chomp($facility_name);

## init and "global" variables

## end - init

# GEN folder location
my $service_files_base_dir="../gen/spool";
my $service_files_dir="$service_files_base_dir/$facility_name/$service_name";

# BASE DN
open my $file, '<', "$service_files_dir/baseDN";
my $base_dn = <$file>;
chomp($base_dn);
close $file;

# BASE DN for Groups
open my $file_g, '<', "$service_files_dir/baseDNGroups";
my $base_dn_groups = <$file_g>;
chomp($base_dn_groups);
close $file_g;

# propagation destination
my $namespace = $ARGV[1];
chomp($namespace);

# create service lock
my $lock = ScriptLock->new($facility_name . "_" . $service_name . "_" . $namespace);
($lock->lock() == 1) or die "Unable to get lock, service propagation is already running.";

# init configuration
my @conf = init_config($namespace);
my @ldap_locations = resolve_domain_controlers($conf[0]);
my $ldap = ldap_connect_multiple_options(\@ldap_locations);

# bind
ldap_bind($ldap, $conf[1], $conf[2]);

# filter
my $filter = '(objectClass=person)';
my $filter_groups = '(objectClass=group)';
my $filter_ou = '(objectClass=organizationalunit)';

my @licencesDN = ("CN=O365Lic_A3s_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_A3s-2_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_A3z_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_A1s_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_A1z_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_A1p_group.muni.cz,OU=licenses," . $base_dn_groups,
		"CN=O365Lic_Abs_group.muni.cz,OU=licenses," . $base_dn_groups);

# operations results
my $RESULT_ERRORS = "errors";
my $RESULT_CHANGED = "changed";
my $RESULT_UNCHANGED = "unchanged";

# extension attributes for groups (filled by Perun)

# This attribute can be 'TRUE' or 'FALSE'. The first one means that a group exists in AD.
# The second one serves as a sign that a group was removed. We do not remove groups physically,
# because if we would want to recreate it, AD would have not recognized that it is a group which existed before.
my $extension_attr_one = 'extensionAttribute1';

# This attribute contains a date, after which will be group removed (extensionAttribute1 = 'FALSE'), or is not defined.
# It is not filled by the gen script. The grace period can be set only in the send script, when a group should be removed.
# When such group is recreated in Perun, this attribute is set to undef in the update process.
my $extension_attr_two = 'extensionAttribute2';

# log counters
my $counter_add = 0;
my $counter_updated = 0;
my $counter_disabled = 0;
my $counter_fail = 0;
my $counter_fail_password = 0;
my $counter_add_ous = 0;
my $counter_fail_ous = 0;
my $counter_group_added = 0;
my $counter_group_not_added = 0;
my $counter_group_attributes_updated = 0;
my $counter_group_attributes_not_updated = 0;
my $counter_group_members_updated = 0;
my $counter_group_members_updated_with_errors = 0;
my $counter_group_members_not_updated = 0;
my $counter_group_not_emptied = 0;
my $counter_group_grace_period_set = 0;
my $counter_group_grace_period_not_set = 0;
my $counter_group_removed = 0;
my $counter_group_not_removed = 0;

# load all data
my @perun_entries = load_perun($service_files_dir . "/" . $service_name . ".ldif");

# load normal user entries
my @ad_entries = load_ad($ldap, $base_dn, $filter, ['displayName','cn','sn','givenName','mail','samAccountName','userPrincipalName','userAccountControl','ProxyAddresses','MailNickName','c', 'preferredLanguage', 'msDS-cloudExtensionAttribute1', 'msDS-cloudExtensionAttribute2','msDS-cloudExtensionAttribute3', 'targetaddress']);

my %ad_entries_map = ();
my %perun_entries_map = ();

foreach my $ad_entry (@ad_entries) {
	my $login = $ad_entry->get_value('samAccountName');
	$ad_entries_map{ $login } = $ad_entry;
}
foreach my $perun_entry (@perun_entries) {
	my $login = $perun_entry->get_value('samAccountName');
	$perun_entries_map{ $login } = $perun_entry;
}

# PROCESS USERS
process_add_user();
process_update_user();
# we do not disable users, it's just removed from all groups by groups update

# PROCESS OUs - GROUPS ARE PROCESSED BY EACH OU
process_ous();

# disconnect
ldap_unbind($ldap);

# log results
ldap_log($service_name, "User added: " . $counter_add . " entries.");
ldap_log($service_name, "User updated: " . $counter_updated . " entries.");
ldap_log($service_name, "User disabled: " . $counter_disabled . " entries.");
ldap_log($service_name, "User failed: " . $counter_fail. " entries.");
ldap_log($service_name, "Failed to set password: " . $counter_fail_password . " entries.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "OU added: " . $counter_add_ous . " entries.");
ldap_log($service_name, "OU failed: " . $counter_fail_ous . " entries.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "Group added (without members): " . $counter_group_added . " entries.");
ldap_log($service_name, "Group failed to add: " . $counter_group_not_added . " entries.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "Group set as removed: " . $counter_group_removed . " entries.");
ldap_log($service_name, "Group grace period set: " . $counter_group_grace_period_set . " entries.");
ldap_log($service_name, "Group failed to empty during removal: " . $counter_group_not_emptied . " entries.");
ldap_log($service_name, "Group grace period failed to set: " . $counter_group_grace_period_not_set . " entries.");
ldap_log($service_name, "Group failed to set as removed: " . $counter_group_not_removed . " entries.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "Group updated (attributes): " . $counter_group_attributes_updated . " entries.");
ldap_log($service_name, "Group failed to update (attributes): " . $counter_group_attributes_not_updated . " entries.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "Group updated (members): " . $counter_group_members_updated . " entries.");
ldap_log($service_name, "Group updated with errors (members)" . $counter_group_members_updated_with_errors . " entries.");
ldap_log($service_name, "Group failed to update (members): " . $counter_group_members_not_updated . " entries.");

# print results
print "User added: " . $counter_add . " entries.\n";
print "User updated: " . $counter_updated . " entries.\n";
print "User disabled: " . $counter_disabled . " entries.\n";
print "User failed: " . $counter_fail. " entries.\n";
print "Failed to set password: " . $counter_fail_password . " entries.\n";
print "-------------------------------------------------------------------------------------------------------\n";
print "OU added: " . $counter_add_ous . " entries.\n";
print "OU failed: " . $counter_fail_ous . " entries.\n";
print "-------------------------------------------------------------------------------------------------------\n";
print "Group added (without members): " . $counter_group_added . " entries.\n";
print "Group failed to add: " . $counter_group_not_added . " entries.\n";
print "-------------------------------------------------------------------------------------------------------\n";
print "Group set as removed: " . $counter_group_removed . " entries.\n";
print "Group grace period set: " . $counter_group_grace_period_set . " entries.\n";
print "Group failed to empty during removal: " . $counter_group_not_emptied . " entries.\n";
print "Group grace period failed to set: " . $counter_group_grace_period_not_set . " entries.\n";
print "Group failed to set as removed: " . $counter_group_not_removed . " entries.\n";
print "-------------------------------------------------------------------------------------------------------\n";
print "Group updated (attributes): " . $counter_group_attributes_updated . " entries.\n";
print "Group failed to update (attributes): " . $counter_group_attributes_not_updated . " entries.\n";
print "-------------------------------------------------------------------------------------------------------\n";
print "Group updated (members): " . $counter_group_members_updated . " entries.\n";
print "Group updated with errors (members): " . $counter_group_members_updated_with_errors . " entries.\n";
print "Group failed to update (members): " . $counter_group_members_not_updated . " entries.\n";

$lock->unlock();

if ($counter_fail or $counter_fail_password or $counter_fail_ous or
	$counter_group_not_added or $counter_group_members_not_updated or
	$counter_group_members_updated_with_errors or $counter_group_attributes_not_updated or
	$counter_group_not_removed or $counter_group_grace_period_not_set or $counter_group_not_emptied) {
	# some update of AD failed, tell it to the engine to re-schedule the service.
	exit 1;
}

# END of main script

###########################################
#
# Main processing functions
#
###########################################

#
# Add new user entries to AD
#
sub process_add_user() {

	foreach my $perun_entry (@perun_entries) {

		my $login = $perun_entry->get_value('samAccountName');

		unless (exists $ad_entries_map{$login}) {

			# Add new entry to AD
			my $response = $perun_entry->update($ldap);
			unless ($response->is_error()) {
				# SUCCESS
				ldap_log($service_name, "User added: " . $perun_entry->dn());
				$counter_add++;
				# tell IS to set Password to AD
				ping_password_setter($login);
			} else {
				# FAIL
				ldap_log($service_name, "User NOT added: " . $perun_entry->dn() . " | " . $response->error());
				ldap_log($service_name, $perun_entry->ldif());
				$counter_fail++;
			}

		}

	}
}

#
# Update existing entries in AD
#
sub process_update_user() {

	foreach my $perun_entry (@perun_entries) {

		if (exists $ad_entries_map{$perun_entry->get_value('samAccountName')}) {

			my $ad_entry = $ad_entries_map{$perun_entry->get_value('samAccountName')};

			# attrs without cn since it's part of DN to be updated
			my @attrs = ('displayName','sn','givenName','mail','MailNickName','ProxyAddresses','c', 'preferredLanguage','msDS-cloudExtensionAttribute1', 'msDS-cloudExtensionAttribute2','msDS-cloudExtensionAttribute3', 'targetaddress');
			# stored log messages to check if entry should be updated
			my @entry_changed = ();

			# check each attribute
			foreach my $attr (@attrs) {
				if (compare_entry($ad_entry , $perun_entry , $attr) == 1) {
					# store value for log
					my @ad_val = $ad_entry->get_value($attr);
					my @perun_val = $perun_entry->get_value($attr);
					push(@entry_changed, "$attr | " . join(", ",sort(@ad_val)) .  " => " . join(", ",sort(@perun_val)));
					# replace value
					$ad_entry->replace(
						$attr => \@perun_val
					);
				}
			}

			# we never touch UAC or move entry !!

			if (@entry_changed) {
				# Update entry in AD
				my $response = $ad_entry->update($ldap);
				unless ($response->is_error()) {
					# SUCCESS
					foreach my $log_message (@entry_changed) {
						ldap_log($service_name, "User updated: " . $ad_entry->dn() . " | " . $log_message);
					}
					$counter_updated++;
				} else {
					# FAIL
					ldap_log($service_name, "User NOT updated: " . $ad_entry->dn() . " | " . $response->error());
					ldap_log($service_name, $ad_entry->ldif());
					$counter_fail++;
				}
			}

		}
	}
}

#
# Create new OUs and process groups per-each OU
# (we allow ou=licenses to be created too)
#
sub process_ous() {

	my @perun_ou_entries = load_perun($service_files_dir."/".$service_name."_ous.ldif");
	my @ad_ou_entries = load_ad($ldap, $base_dn_groups, $filter_ou, ['ou']);

	my %ad_ou_entries_map = ();
	my %perun_ou_entries_map = ();

	foreach my $ad_entry (@ad_ou_entries) {
		my $ouName = $ad_entry->get_value('ou');
		$ad_ou_entries_map{ $ouName } = $ad_entry;
	}
	foreach my $perun_entry (@perun_ou_entries) {
		my $ouName = $perun_entry->get_value('ou');
		$perun_ou_entries_map{ $ouName } = $perun_entry;
	}

	# ADD NEW OUs | UPDATE OUs


	foreach my $perun_entry (@perun_ou_entries) {


		my $ouName = $perun_entry->get_value('ou');

		unless (exists $ad_ou_entries_map{$ouName}) {

			# Add new entry to AD
			my $response = $perun_entry->update($ldap);
			unless ($response->is_error()) {
				# SUCCESS
				ldap_log($service_name, "OU added: " . $perun_entry->dn());
				$counter_add_ous++;

				# PROCESS OU GROUPS
				process_groups($ouName);

			} else {
				# FAIL
				ldap_log($service_name, "OU NOT added: " . $perun_entry->dn() . " | " . $response->error());
				ldap_log($service_name, $perun_entry->ldif());
				$counter_fail_ous++;
			}

		} else {

			# OU already exist - update it's groups
			process_groups($ouName);

		}

	}

}

#
# Create and update GROUPS per OU
# (we allow to create new groups in ou=licenses too)
#
sub process_groups() {

	my $ouName = shift;

	my @perun_entries_groups = load_perun($service_files_dir."/".$service_name."_groups_".$ouName.".ldif");
	my @ad_entries_groups = load_ad($ldap, "OU=".$ouName.",".$base_dn_groups, $filter_groups,
		[ 'cn', 'samAccountName', 'displayName', 'MailNickName', 'msExchRequireAuthToSendTo', 'publicDelegates' , 'ProxyAddresses', 'mail', $extension_attr_one, $extension_attr_two]);

	my %ad_entries_group_map = ();
	my %perun_entries_group_map = ();

	foreach my $ad_entry (@ad_entries_groups) {
		my $cn = $ad_entry->get_value('cn');
		$ad_entries_group_map{ $cn } = $ad_entry;
	}
	foreach my $perun_entry (@perun_entries_groups) {
		my $cn = $perun_entry->get_value('cn');
		$perun_entries_group_map{ $cn } = $perun_entry;
	}

	# ADD groups
	foreach my $perun_entry (@perun_entries_groups) {

		my $cn = $perun_entry->get_value('cn');
		unless (exists $ad_entries_group_map{$cn}) {

			my @attrs_to_add = ('cn', 'samAccountName', 'displayName', 'MailNickName', 'msExchRequireAuthToSendTo', $extension_attr_one, 'objectClass');
			my $new_ad_entry = clone_entry_with_specific_attributes($perun_entry, \@attrs_to_add);
			# Add new entry to AD
			my $response = $new_ad_entry->update($ldap);
			unless ($response->is_error()) {
				# SUCCESS
				ldap_log($service_name, "Group added (without members): ".$perun_entry->dn());
				push(@ad_entries_groups, $new_ad_entry);
				$counter_group_added++;
			} else {
				# FAIL
				ldap_log($service_name, "Group NOT added: ".$perun_entry->dn()." | ".$response->error());
				ldap_log($service_name, $new_ad_entry->ldif());
				$counter_group_not_added++;
			}

		}
	}

	#
	# WE UPDATE ALL GROUPS / REMOVE ONLY NORMAL/PART_LICENSES GROUPS
	#

	# UPDATE groups

	foreach my $ad_entry (@ad_entries_groups) {
		my $cn = $ad_entry->get_value('cn');

		if (exists $perun_entries_group_map{$cn}) {

			my $perun_entry = $perun_entries_group_map{$cn};

			# attrs without cn!
			my @attrs = ('samAccountName', 'displayName', 'MailNickName', 'msExchRequireAuthToSendTo', 'publicDelegates', 'ProxyAddresses', 'mail', $extension_attr_one, $extension_attr_two);
			# stored log messages to check if entry should be updated
			my @entry_changed = ();

			# check each attribute
			foreach my $attr (@attrs) {
				if (compare_entry($ad_entry, $perun_entry, $attr) == 1) {
					# store value for log
					my @ad_val = $ad_entry->get_value($attr);
					my @perun_val = $perun_entry->get_value($attr);
					push(@entry_changed, "$attr | ".join(", ", sort(@ad_val))." => ".join(", ", sort(@perun_val)));
					# replace value
					$ad_entry->replace(
						$attr => \@perun_val
					);
				}
			}

			# we never touch UAC or move entry !!

			if (@entry_changed) {
				# Update entry in AD
				my $response = $ad_entry->update($ldap);
				unless ($response->is_error()) {
					# SUCCESS
					foreach my $log_message (@entry_changed) {
						ldap_log($service_name, "Group attributes updated: ".$ad_entry->dn()." | ".$log_message);
					}
					$counter_group_attributes_updated++;
				} else {
					# FAIL
					ldap_log($service_name, "Group attributes NOT updated: ".$ad_entry->dn()." | ".$response->error());
					ldap_log($service_name, $ad_entry->ldif());
					$counter_group_attributes_not_updated++;
				}
			}

			#
			# ONLY MEMBERS OF NORMAL GROUPS ARE PROCESSED IMMEDIATELLY
			#
			unless ($ouName eq "licenses") {
				process_groups_members($perun_entry);
			}

		}
	}

	# REMOVE groups (empty group and don't delete it !!)

	foreach my $ad_entry (@ad_entries_groups) {
		my $cn = $ad_entry->get_value('cn');
		unless (exists $perun_entries_group_map{$cn} || $ad_entry->get_value($extension_attr_one) eq 'FALSE') {

			# Prevent clearing main license groups !!
			# compare using smart-match (perl 5.10.1+)
			unless ($ad_entry->dn() ~~ @licencesDN) {
				# clear members
				# load members of a group from AD
				my @to_be_removed = load_group_members($ldap, $ad_entry->dn(), $filter_groups);
				my $response_remove = remove_members_from_entry($ldap, $service_name, $ad_entry, \@to_be_removed);
				unless ($response_remove == $SUCCESS) {
					ldap_log($service_name, "Failed to remove all members from group during the group removal process: ".$ad_entry->dn());
					$counter_group_not_emptied++;
				} else {
					my $grace_period = $ad_entry->get_value($extension_attr_two);
					unless (defined $grace_period) {
						$ad_entry->replace(
							$extension_attr_two => $groups_grace_period_date
						);
						my $response = $ad_entry->update($ldap);
						unless ($response->is_error()) {
							ldap_log($service_name, "Set grace period for group: ".$ad_entry->dn(). "to ". $groups_grace_period_date);
							$counter_group_grace_period_set++;
						} else {
							ldap_log($service_name, "Failed to set grace period for group: ".$ad_entry->dn()." | ".$response->error());
							$counter_group_grace_period_not_set++;
						}
					} else {
						my @grace_period_array = Decode_Date_EU($grace_period);
						# if group's grace period passed, set the group as removed.
						if(Delta_Days(@today, @grace_period_array) < 0) {
							# set this attribute to FALSE
							$ad_entry->replace(
								$extension_attr_one => 'FALSE'
							);
							my $response = $ad_entry->update($ldap);
							unless ($response->is_error()) {
								ldap_log($service_name, "Group set as removed: ".$ad_entry->dn());
								$counter_group_removed++;
							} else {
								ldap_log($service_name, "Failed to set group as removed: ".$ad_entry->dn()." | ".$response->error());
								$counter_group_not_removed++;
							}
						}
					}
				}
			}

		}
	}

	#
	# Process groups from ou licenses separately
	#
	if ($ouName eq "licenses") {
		process_licenses_groups(\%perun_entries_group_map);
	}

}

#
# ADD and REMOVE group members
# can be used only for normal groups !!, not for groups from "ou=licenses" !!
#
sub process_groups_members() {

	my $perun_entry = shift;

	my @per_val = $perun_entry->get_value('member');

	# load members of a group from AD based on DN in Perun => Group must exists in AD
	my @ad_val = load_group_members($ldap, $perun_entry->dn(), $filter_groups);

	if ($? != 0) {
		ldap_log($service_name, "Unable to load Perun group members from AD: " . $perun_entry->dn());
		$counter_group_members_not_updated++;
		return;
	}

	# sort to compare
	my @sorted_ad_val = sort(@ad_val);
	my @sorted_per_val = sort(@per_val);

	# compare using smart-match (perl 5.10.1+)
	unless(@sorted_ad_val ~~ @sorted_per_val) {

		my %ad_val_map = map { $_ => 1 } @sorted_ad_val;
		my %per_val_map = map { $_ => 1 } @sorted_per_val;

		# we must get reference to real group from AD in order to call "replace"
		my $response_ad = $ldap->search( base => $perun_entry->dn(), filter => $filter_groups, scope => 'base' );
		unless ($response_ad->is_error()) {
			# SUCCESS
			my $ad_entry = $response_ad->entry(0);
			my $result = update_group_membership($ldap, $service_name, $ad_entry, \%ad_val_map, \%per_val_map);
			$counter_group_members_updated++ if ($result eq $RESULT_CHANGED);
			$counter_group_members_updated_with_errors++ if ($result eq $RESULT_ERRORS);
		} else {
			# FAIL (to get group from AD)
			$counter_group_members_not_updated++;
			ldap_log($service_name, "Group members NOT updated: " . $perun_entry->dn() . " | " . $response_ad->error());
		}
	}

}

#
# Update groups from ou=licenses !!!
#
# Method asume, that they exists, since new OUs and Groups are added to AD during standard group processing.
#
sub process_licenses_groups() {

	my $perun_entries = shift;

	my $ouName = "licenses";

	my @ad_entries_groups = load_ad($ldap, "OU=" . $ouName . "," . $base_dn_groups, $filter_groups, ['cn','displayName']);
	my %ad_entries_group_map = ();

	# store by DNs, will be easier for update logic
	foreach my $ad_entry (@ad_entries_groups) {
		my $dn = $ad_entry->dn();
		$ad_entries_group_map{ $dn } = $ad_entry;
	}

	my $ad_state; # $ad_state->{group_dn}->{user_dn} = 1;

	# AD state can be filled from AD
	$ad_state = fill_from_ad(\%ad_entries_group_map);

	my %add_result = map { $_ => $RESULT_UNCHANGED } keys %{$perun_entries};
	my %remove_result = map { $_ => $RESULT_UNCHANGED } keys %{$perun_entries};

	# add members to licence groups
	foreach my $group_cn (sort keys %{$perun_entries}) {
		if (defined $perun_entries->{$group_cn}->get_value('member')){
			my $group_dn = $perun_entries->{$group_cn}->dn();
			my @members =  $perun_entries->{$group_cn}->get_value('member');
			$add_result{$group_cn} = add_to_license_group($ad_entries_group_map{$group_dn}, $ad_state->{$group_dn}, \@members);
		}
	}

	foreach (keys %{$perun_entries}) {
		if ($add_result{$_} eq $RESULT_ERRORS) {
			$counter_group_members_updated_with_errors++;
			die "Failed to add members to one or more main license groups. Check logs for more information.";
		}
	}

	# remove members from licence groups
	foreach my $group_cn (sort keys %{$perun_entries}) {
		my @members = ();
		if (defined $perun_entries->{$group_cn}->get_value('member')){
			@members =  $perun_entries->{$group_cn}->get_value('member');
		}
		my $group_dn = $perun_entries->{$group_cn}->dn();
		$remove_result{$group_cn} = remove_from_license_group($ad_entries_group_map{$group_dn}, $ad_state->{$group_dn}, \@members);
	}

	# update counters
	foreach (keys %{$perun_entries}) {
		if ($remove_result{$_} eq $RESULT_ERRORS) {
			$counter_group_members_updated_with_errors++;
		} elsif ($add_result{$_} eq $RESULT_CHANGED or $remove_result{$_} eq $RESULT_CHANGED) {
			$counter_group_members_updated++;
		}
	}
}


#
# Add members to the license group -Check if ad_members_state keys are in perun_members_state array and add missing members to the group in AD.
#
# 1. param - AD_ENTRY
# 2. param - hash of current AD group members (user_dn=>1)
# 3. param - array of perun group members
#
sub add_to_license_group() {

	my $ad_entry = shift;
	my $ad_members_state = shift;
	my $perun_members_state = shift;

	my @to_be_added = ();

	foreach (@$perun_members_state) {
		unless (defined $ad_members_state->{$_}) {
			push (@to_be_added, $_);
		}
	}

	@to_be_added = sort @to_be_added;


	if (@to_be_added) {
		if (add_members_to_entry($ldap, $service_name, $ad_entry, \@to_be_added) == $SUCCESS) {
			return $RESULT_CHANGED;
		} else {
			return $RESULT_ERRORS;
		}
	}

	return $RESULT_UNCHANGED;
}

#
# Remove members from the license group - Check if ad_members_state keys are in perun_members_state array and remove extra members from the group in AD.
#
# 1. param - AD_ENTRY
# 2. param - hash of current AD group members (user_dn=>1)
# 3. param - array of perun group members
#
sub remove_from_license_group() {

	my $ad_entry = shift;
	my $ad_members_state = shift;
	my $perun_members_state = shift;

	my @to_be_removed;

	foreach (keys %{$ad_members_state}) {
		# compare using smart-match (perl 5.10.1+)
		unless ($_ ~~ $perun_members_state) {
			push (@to_be_removed,$_);
		}
	}

	@to_be_removed = sort @to_be_removed;

	if (@to_be_removed) {
		if (remove_members_from_entry($ldap, $service_name, $ad_entry, \@to_be_removed)  == $SUCCESS) {
			return $RESULT_CHANGED;
		} else {
			return $RESULT_ERRORS;
		}
	}

	return $RESULT_UNCHANGED;

}


#
# Ping IS that it must set password for user to AD
#
sub ping_password_setter() {

	my $login = shift;

	my $username;
	my $password;
	my $db_name;
	my $table_name;

	my $configPath = "/etc/perun/services/ad_mu/DB";
	open FILE, $configPath or die "Could not open config file $configPath: $!";
	while(my $line = <FILE>) {
		if($line =~ /^username: .*/) {
			$username = ($line =~ m/^username: (.*)$/)[0];
		} elsif($line =~ /^password: .*/) {
			$password = ($line =~ m/^password: (.*)$/)[0];
		} elsif($line =~ /^tablename: .*/) {
			$table_name = ($line =~ m/^tablename: (.*)$/)[0];
		} elsif($line =~ /^dbname: .*/) {
			$db_name = ($line =~ m/^dbname: (.*)$/)[0];
		}
	}

	if(!defined($password) || !defined($username) || !defined($table_name) || !defined($db_name)) {
		print "Can't get config data from config file.\n";
		exit 14;
	}

	my $dbh = DBI->connect("dbi:Oracle:$db_name",$username, $password,{RaiseError=>1,AutoCommit=>0,LongReadLen=>65536, ora_charset => 'AL32UTF8'}) or die "Connect to database $db_name Error!\n";

	my $changeExists = $dbh->prepare(qq{select 1 from $table_name where uin=?});
	unless ($changeExists->execute($login)) {
		ldap_log($service_name, "Couldn't execute select statement: " . $changeExists->errstr . "for login " . $login);
		$counter_fail_password++;
		$changeExists->finish();
		commit $dbh;
		$dbh->disconnect();
		return;
	}

	unless($changeExists->fetch) {

		my $insert = $dbh->prepare(qq{INSERT INTO $table_name (uin, import_time) VALUES (?, sysdate)});
		unless ($insert->execute($login)) {
			ldap_log($service_name, "Couldn't execute insert statement: " . $insert->errstr . "for login " . $login);
			$counter_fail_password++;
		}
		$insert->finish();

	}

	$changeExists->finish();
	commit $dbh;
	$dbh->disconnect();

}
#
# Return hash strucure of AD license groups like $ad_state->{group_dn}->{user_dn} = 1;
# DIE the script if unable to load all data !!
#
sub fill_from_ad() {

	my $ad_entries_group_map = shift;

	my $ad_state;  # $ad_state->{group_dn}->{user_dn} = 1

	# for each AD group, get members
	foreach my $group_dn (sort keys %{$ad_entries_group_map}) {

		# load members of a group from AD based on DN in Perun => Group must exists in AD
		my @ad_val = load_group_members($ldap, $group_dn, $filter_groups);

		if ($? != 0) {
			ldap_log($service_name, "Unable to load Perun group members from AD: " . $group_dn);
			die "Unable to load AD state to resolve license changes!";
		}

		foreach (@ad_val) {
			$ad_state->{$group_dn}->{$_} = 1;
		}

	}

	return $ad_state;
}
