#!/usr/bin/perl

use strict;
use warnings;
use Switch;
use Net::LDAPS;
use Net::LDAP::Entry;
use Net::LDAP::Message;
use Net::LDAP::LDIF;
use Encode;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED );
use String::Random qw( random_string );
no if $] >= 5.017011, warnings => 'experimental::smartmatch';

# Import shared AD library
use ADConnector;
use ScriptLock;

sub process_add;
sub process_update;
sub process_disable;
sub process_groups;

# define service
my $service_name = "ad_ceitec";

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

# BASE DN for Service accounts
open my $file_s, '<', "$service_files_dir/baseDNService";
my $base_dn_service = <$file_s>;
chomp($base_dn_service);
close $file_s;

# 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 was already running.";

# init configuration
my @conf = init_config($namespace);
my $ldap_location = $conf[0]; # we can't publicly resolve PDC for CEITEC, use fixed value
my $ldap = ldap_connect($ldap_location);

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

# filter
my $filter = '(objectClass=user)';
my $filter_groups = '(objectClass=group)';

# log counters
my $counter_add = 0;
my $counter_updated = 0;
my $counter_disabled = 0;
my $counter_renamed = 0;
my $counter_fail = 0;
my $counter_group_updated = 0;
my $counter_group_failed = 0;

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

# previously take entries only with 'samAccountName' now if they have 'cn'

# load normal user entries
my @ad_entries = load_ad($ldap, $base_dn, $filter, ['displayName','cn','sn','givenName','mail','company','eduPersonPrincipalNames','samAccountName','userPrincipalName','userAccountControl']);
# load service user entries
my @ad_entries_service = load_ad($ldap, $base_dn_service, $filter, ['displayName','cn','sn','givenName','mail','company','eduPersonPrincipalNames','samAccountName','userPrincipalName','userAccountControl']);
# group them together
push (@ad_entries, @ad_entries_service);

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 data
process_add();
process_update();
process_disable();
process_groups();

# disconnect
ldap_unbind($ldap);

# log results
ldap_log($service_name, "Added: " . $counter_add . " entries.");
ldap_log($service_name, "Updated: " . $counter_updated . " entries.");
ldap_log($service_name, "Disabled: " . $counter_disabled . " entries.");
ldap_log($service_name, "Renamed: " . $counter_renamed . " entries.");
ldap_log($service_name, "Failed: " . $counter_fail. " entries.");
ldap_log($service_name, "Group updated: " . $counter_group_updated . " entries.");
ldap_log($service_name, "Group failed: " . $counter_group_failed . " entries.");

# print results
print "Added: " . $counter_add . " entries.\n";
print "Updated: " . $counter_updated . " entries.\n";
print "Disabled: " . $counter_disabled . " entries.\n";
print "Renamed: " . $counter_renamed . " entries.\n";
print "Failed: " . $counter_fail. " entries.\n";
print "Group updated: " . $counter_group_updated . " entries.\n";
print "Group failed: " . $counter_group_failed . " entries.\n";

$lock->unlock();

# END of main script

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

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

	foreach my $perun_entry (@perun_entries) {

		my $login = $perun_entry->get_value('samAccountName');
		unless (exists $ad_entries_map{$login}) {

			# Generate also random password
			my $password = '"' . random_string("CCcc!ccnCn") . '"';
			my $converted_pass = encode("UTF-16LE",'"'.$password.'"');
			$perun_entry->add(
				unicodePwd => $converted_pass
			);

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

		}
	}

}


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

	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','company','eduPersonPrincipalNames');
			# 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
					);
				}
			}

			# check UAC
			my $ad_entry_uac = $ad_entry->get_value('userAccountControl');

			# if disabled -> enable it
			unless (is_uac_enabled($ad_entry_uac) == 1) {

				my $original_ad_entry_uac = $ad_entry_uac;
				my $new_ad_entry_uac = enable_uac($ad_entry_uac);
				push( @entry_changed, "userAccountControl | $original_ad_entry_uac => $new_ad_entry_uac" );
				$ad_entry->replace(
					'userAccountControl' => $new_ad_entry_uac
				);

			}

			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, "Updated: " . $ad_entry->dn() . " | " . $log_message);
					}
					$counter_updated++;
				} else {
					# FAIL
					ldap_log($service_name, "NOT updated: " . $ad_entry->dn() . " | " . $response->error());
					ldap_log($service_name, $ad_entry->ldif());
					$counter_fail++;
				}
			}

			# If CN changed update DN of entry (move it)
			my $ad_cn = $ad_entry->get_value('cn');
			my $perun_cn = $perun_entry->get_value('cn');
			unless ($ad_cn eq $perun_cn) {
				my $ad_dn = $ad_entry->dn();
				my $perun_dn = $perun_entry->dn();
				my $response = $ldap->moddn($ad_entry, newrdn => "cn=" . $perun_cn , deleteoldrdn => 1);
				unless ($response->is_error()) {
					# SUCCESS
					ldap_log($service_name, "Renamed: " . $ad_dn . " => " . $perun_dn);
					$counter_renamed++;
				} else {
					# FAIL
					ldap_log($service_name, "NOT renamed: " . $ad_dn . " | " . $response->error());
					ldap_log($service_name, $ad_entry->ldif());
					$counter_fail++;
				}
			}
		}
	}
}

#
# Disable entries in AD
#
#
# Disable user entries in AD which are not present in Perun
#
sub process_disable() {

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

			my $ad_entry_uac = $ad_entry->get_value('userAccountControl');

			# if enabled -> disable it
			if (is_uac_enabled($ad_entry_uac) == 1) {

				# disable entry in AD
				$ad_entry->replace( userAccountControl => disable_uac($ad_entry_uac) );
				my $response = $ad_entry->update($ldap);
				unless ($response->is_error()) {
					ldap_log($service_name, "Disabled entry: " . $ad_entry->dn());
					$counter_disabled++;
				} else {
					ldap_log($service_name, "NOT disabled: " . $ad_entry->dn() . " | " . $response->error());
					ldap_log($service_name, $ad_entry->ldif());
					$counter_fail++;
				}
			}

		}
	}

}

#
# Update group membership in AD
#
sub process_groups() {

	foreach my $perun_entry (@perun_entries_groups) {

		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());
			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) {

			# 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_groups, scope => 'base' );
			unless ($response_ad->is_error()) {
				# SUCCESS
				my $ad_entry = $response_ad->entry(0);
				$ad_entry->replace(
					'member' => \@per_val
				);

				# Update entry in AD
				my $response = $ad_entry->update($ldap);

				if ($response) {
					unless ($response->is_error()) {
						# SUCCESS (group updated)
						$counter_group_updated++;
						ldap_log($service_name, "Group members updated: " . $ad_entry->dn() . " | \n" . join(",\n",@sorted_ad_val) .  "\n=>\n" . join(",\n",@sorted_per_val));
					} else {
						# FAIL (to update group)
						$counter_group_failed++;
						ldap_log($service_name, "Group members NOT updated: " . $ad_entry->dn() . " | " . $response->error());
						ldap_log($service_name, $ad_entry->ldif());
					}
				}
			} else {
				# FAIL (to get group from AD)
				$counter_group_failed++;
				ldap_log($service_name, "Group members NOT updated: " . $perun_entry->dn() . " | " . $response_ad->error());
			}
		}

		# group is unchanged

	}

}
