import logging
import ldap

from django.contrib.auth import get_user_model, load_backend
from django.core.management.base import BaseCommand
from django_auth_ldap.config import LDAPSearch
import django.conf

from urllib.error import HTTPError

from postorius.models import List, MailmanUser
from postorius.utils import get_mailman_client

logger = logging.getLogger(__name__)

class Command(BaseCommand):
	can_import_settings = True
	help = 'Synchronize mailing list memberships from a LDAP server'

	def handle(self, *args, **options):
		ldap_backend = load_backend('django_auth_ldap.backend.LDAPBackend')
		conn = ldap_backend.ldap.initialize(ldap_backend.settings.SERVER_URI, bytes_mode=False)
		for opt, value in ldap_backend.settings.CONNECTION_OPTIONS.items():
			conn.set_option(opt, value)
		if ldap_backend.settings.START_TLS:
			conn.start_tls_s()
		conn.simple_bind_s(ldap_backend.settings.BIND_DN, ldap_backend.settings.BIND_PASSWORD)

		client = get_mailman_client()

		user_search = django.conf.settings.AUTH_LDAP_USER_SEARCH_ALL_NAME
		results = user_search.execute(conn)
		ldap_users = [list(attr.values())[0][0] for dn, attr in results ]
		django_users = get_user_model().objects.all()
		backref_mapping = {'member': 'members', 'moderator': 'moderators', 'owner': 'owners'}
		mm_users = {}

		membership_settings = getattr(django.conf.settings, 'LDAP_MEMBERSHIP_SYNC', {})

		# create all django user in mailman
		for user in django_users:
			mm_users[user.username] = MailmanUser.objects.get_or_create_from_django(user)
			if mm_users[user.username].display_name != user.get_full_name():
				logger.warning("update display_name on {} to {}".format(user.username, user.get_full_name()))
				mm_users[user.username].display_name = user.get_full_name()
				mm_users[user.username].save()

		mailman_id2username = {mm_users[i].user_id: i for i in mm_users}

		for list_name in membership_settings:
			ldap_setting = membership_settings[list_name].get('ldap', {})
			for membership_type in ldap_setting:
				if not ldap_setting[membership_type].get('enabled'):
					continue
				ldap_members = LDAPSearch(
						ldap_setting[membership_type]['dn'], ldap.SCOPE_SUBTREE,
						ldap_setting[membership_type]['filter'],
						[ldap_setting[membership_type]['username_attr']]).execute(conn)
				ldap_member_names = [list(attr.values())[0][0] for dn, attr in ldap_members ]

				# we refetch the mm_list each time because of wired caching problems
				mm_list = client.get_list(list_name)

				mm_members = getattr(mm_list, backref_mapping[membership_type], [])
				for mm_member in mm_members:
					try:
						username = mailman_id2username.get(mm_member.user.user_id, None)
						if not username or not username in ldap_member_names:
							# user should not be subscribed -> remove
							logger.warning("remove {} ( {} ) as {} on {}".format(username, mm_member.user, membership_type, list_name))
							mm_member.unsubscribe()
					except Exception as e:
						logger.exception(e)
						continue

				for username in mm_users:
					if not username in ldap_member_names:
						continue
					try:
						mm_user = mm_users[username]
						displayname = mm_users[username].display_name or username
						try:
							# user might not be subscribed but should be subscribed -> subscribe
							# we do not test if he is subscibed already because the mailman api is inconsistent and reports wrong state information for this...
							# instead we always try to subscribe and catch the error if we are subscribed already
							for address in mm_user.addresses[0]:
								user_mail = address.email
								if membership_type == 'member':
									# discard a subscription request before trying to subscribe else it will fail
									for request in mm_list.requests:
										if request['email'] == user_mail:
											mm_list.discard_request(request['token'])
									mm_list.subscribe(user_mail,
											display_name=displayname,
											pre_verified=True,
											pre_confirmed=True,
											pre_approved=True)
								elif membership_type == 'moderator':
									mm_list.add_moderator(user_mail,
											display_name=displayname)
								elif membership_type == 'owner':
									mm_list.add_moderator(user_mail,
											display_name=displayname)
								logger.warning("subscribe {} ( {} ) as {} on {}".format(username, user_mail, membership_type, list_name))
						except HTTPError as e:
							# We get a http error if the user is already subscribed.
							# It is silently ignored
							if e.code == 409 and e.reason == b'Member already subscribed':
								continue
					except Exception as e:
						logger.exception(e)
						continue