#!/usr/bin/perl
use strict;
use warnings;
no if $] >= 5.017011, warnings => 'experimental::smartmatch';
use Net::LDAPS;
use Net::LDAP::Entry;
use Net::LDAP::Message;
use Net::LDAP::LDIF;

# Import shared AD library
use ADConnector;
use ScriptLock;

sub process_add;
sub process_remove;
sub process_update;

# log counters
my $counter_group_added = 0;
my $counter_group_not_added = 0;
my $counter_group_not_emptied = 0;
my $counter_group_removed = 0;
my $counter_group_not_removed = 0;
my $counter_group_members_updated = 0;
my $counter_group_members_updated_with_errors = 0;
my $counter_group_members_not_updated = 0;

# define service
my $service_name = "ad_group_mu_ucn";

# operations results
my $RESULT_ERRORS = "errors";
my $RESULT_CHANGED = "changed";
my $RESULT_UNCHANGED = "unchanged";
my $SUCCESS = 1;
my $FAIL = 0;

# GEN folder location
my $facility_name = $ARGV[0];
chomp($facility_name);
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;

# 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 was 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);
my $filter = '(objectClass=group)';
my @baseAttrs = ('cn', 'samAccountName', 'objectClass', 'info', 'description');


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

# load all data
my @perun_entries = load_perun($service_files_dir . "/" . $service_name . ".ldif");
my @groups_ad_entries = load_ad($ldap, "OU=Groups,OU=MU,DC=ucn,DC=muni,DC=cz", $filter, ['cn', 'info', 'description']);
my @types_ad_entries = load_ad($ldap, "OU=Types,OU=MU,DC=ucn,DC=muni,DC=cz", $filter, ['cn', 'info', 'description']);
my @perun_groups_ad_entries = load_ad($ldap, "OU=Groups,OU=Perun,OU=MU,DC=ucn,DC=muni,DC=cz", $filter, ['cn', 'info', 'description']);
my @ad_entries = (@groups_ad_entries, @perun_groups_ad_entries);
@ad_entries = (@ad_entries, @types_ad_entries);

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

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

# process data
process_add();
process_remove();
process_update();

# disconnect
ldap_unbind($ldap);

# log results
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 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.");
ldap_log($service_name, "-------------------------------------------------------------------------------------------------------");
ldap_log($service_name, "Group removed: " . $counter_group_removed . " entries.");
ldap_log($service_name, "Group failed to empty during removal: " . $counter_group_not_emptied . " entries.");
ldap_log($service_name, "Group failed to remove: " . $counter_group_not_removed. " entries.");

# print results for TaskResults in GUI
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 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";
print "-------------------------------------------------------------------------------------------------------\n";
print "Group removed: " . $counter_group_removed . " entries.\n";
print "Group failed to empty during removal: " . $counter_group_not_emptied . " entries.\n";
print "Group failed to remove: " . $counter_group_not_removed. " entries.\n";

$lock->unlock();

if ($counter_group_not_added or $counter_group_members_not_updated or $counter_group_not_removed or $counter_group_not_emptied) {
	die "Some entries failed to process.\nSee log at: ~/send/logs/$service_name.log";
}
if ($counter_group_members_updated_with_errors > 0) { die "Some members were not updated.\nSee log at: ~/send/logs/$service_name.log";}

# END of main script

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

#
# Add new group entries to AD
#
sub process_add() {

	foreach my $perun_entry (@perun_entries) {

		my $cn = $perun_entry->get_value('cn');
		unless (exists $ad_entries_map{lc $cn}) {
			my $new_ad_entry = clone_entry_with_specific_attributes($perun_entry, \@baseAttrs);
			# 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());
				$counter_group_added++;
			} else {
				# FAIL
				print STDERR "Group NOT added: " . $perun_entry->dn() . " | " . $response->error() . "\n";
				ldap_log($service_name, "Group NOT added: " . $perun_entry->dn() . " | " . $response->error());
				ldap_log($service_name, $new_ad_entry->ldif());
				$counter_group_not_added++;
			}

		}
	}

}

#
# Remove group entries in AD
#
sub process_remove() {

	foreach my $ad_entry (@ad_entries) {
		my $cn = $ad_entry->get_value('cn');
		unless (exists $perun_entries_map{ lc $cn }) {

			# clear members
			# load members of a group from AD
			my @to_be_removed = load_group_members($ldap, $ad_entry->dn(), $filter);

			## HACK: do not remove GUEST- or L- accounts from groups
			@to_be_removed = grep(!/CN=(Guest|L).*/, @to_be_removed);
			# Focus only on users
			#@to_be_removed = grep(/OU=(Users).*/, @to_be_removed);
			my $response_remove = remove_members_from_entry($ldap, $service_name, $ad_entry, \@to_be_removed);
			unless ($response_remove == $SUCCESS) {
				print STDERR "Failed to remove all members from group during the group removal process: ".$ad_entry->dn() . "\n";
				ldap_log($service_name, "Failed to remove all members from group during the group removal process: ".$ad_entry->dn());
				$counter_group_not_emptied++;
			}
			# NOT REMOVING GROUPS IN UCN AD
			# else {
			#         my $response = $ldap->delete($ad_entry->dn());
			#         unless ($response->is_error()) {
			#                 ldap_log($service_name, "Group removed: " . $ad_entry->dn());
			#                 $counter_group_removed++;
			#         }
			#         else {
			#                 ldap_log($service_name, "Group NOT removed: " . $ad_entry->dn() . " | " . $response->error());
			#                 ldap_log($service_name, $ad_entry->ldif());
			#                 $counter_group_not_removed++;
			#         }
			# }

		}
	}

}

#
# Update group members in AD
#
sub process_update() {
	my %params;
	foreach my $perun_entry (@perun_entries) {

		%params = (
			info => $perun_entry->get_value('info'),
			description => $perun_entry->get_value('description')
		);

		if (exists $ad_entries_map{ lc $perun_entry->get_value('cn') }){
			update_info(clone_entry_with_specific_attributes($perun_entry, \@baseAttrs), $ad_entries_map{ lc $perun_entry->get_value('cn') }, \%params);
		}

		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);
		## HACK: do not remove GUEST- or L- accounts from groups
		@ad_val = grep(!/CN=(Guest|L).*/, @ad_val);

		if ($? != 0) {
			print STDERR "Unable to load Perun group members from AD: " . $perun_entry->dn() . "\n";
			ldap_log($service_name, "Unable to load Perun group members from AD: " . $perun_entry->dn());
			next;
		}

		# 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;

			# members of group are not equals
			# 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, 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++;
				print STDERR "Group members NOT updated: " . $perun_entry->dn() . " | " . $response_ad->error() . "\n";
				ldap_log($service_name, "Group members NOT updated: " . $perun_entry->dn() . " | " . $response_ad->error());
			}
		}

		# group is unchanged

	}

}

sub update_info {

	my $entry = shift;
	my $update_entry = shift;
	my $params = shift;

	my $params_updated = 0;
	foreach (keys %{$params}){
		if (compare_entry($update_entry, $entry, $_) == 1){
			$update_entry->replace(
				"$_" => $params->{$_},
			);
			$params_updated = 1;
		}
	}
	if ($params_updated == 1){
		my $response = $update_entry->update($ldap);
		if ($response) {
			unless ($response->is_error()) {
				ldap_log($service_name, "Group Attributes Updated: " . $update_entry->dn() . " | ");
				return 0;
			} else {
				print STDERR "Group Attributes NOT updated: " . $update_entry->ldif("change" => "true") . " | " . $response->error() . "\n";
				ldap_log($service_name, "Group Attributes NOT updated: " . $update_entry->ldif("change" => "true") . " | " . $response->error() . " | ");
				return 1;
			}
		}
	}
}
