#!/usr/bin/python
import collections
import datetime
import os
import sys

if __name__ == '__main__':
    cur_file = os.path.abspath(__file__)
    django_dir = os.path.abspath(os.path.join(os.path.dirname(cur_file), '..'))
    django_dir_parent = os.path.abspath(os.path.join(os.path.dirname(cur_file), '../..'))
    sys.path.append(django_dir)
    sys.path.append(django_dir_parent)
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

from django.core.mail import EmailMessage
from django.core.urlresolvers import reverse
from django.db import connection
from django.db.models import Q
from django.template import Context, Template
from django.template.loader import get_template

import groups.diffs
import groups.models
import space.models
import util.emails

role = {
    'office': groups.models.OfficerRole.objects.get(slug='office-access'),
    'locker': groups.models.OfficerRole.objects.get(slug='locker-access'),
}

people_name = {} # username -> full name
people_id = {} # username -> MIT ID

all_spaces = {} # Space.pk -> Space

def bulk_fill_people(times):
    max_time = max(times)
    min_time = min(times)
    active_holders = groups.models.OfficeHolder.objects.filter(
        start_time__lte=max_time,
        end_time__gte=min_time,
        role__in=role.values(),
    )
    usernames = active_holders.values_list('person', flat=True,)
    people = groups.models.AthenaMoiraAccount.objects.filter(username__in=usernames)
    for person in people:
        people_name[person.username] = person.format()
        people_id[person.username] = person.mit_id

def fill_people(holder):
    if not holder.person in people_name:
        #print "Person %s not pre-cached" % (holder.person, )
        try:
            person = groups.models.AthenaMoiraAccount.objects.get(username=holder.person)
            people_name[holder.person] = person.format()
            people_id[holder.person] = person.mit_id
        except groups.models.AthenaMoiraAccount.DoesNotExist:
            people_name[holder.person] = "<%s>" % (holder.person, )
            people_id[holder.person] = None

class GroupInfo(object):
    def __init__(self, group, ):
        self.group = group
        self.offices = {}  # Space.pk -> (ID -> (Set name, Set name))
        self.locker_acl = {}
        self.locker_messages = []
        self.changes = False

    def learn_office_access(self, space_pk, old, new):
        group_pk = self.group.pk
        if group_pk in old:
            old_access = old[group_pk]
        else: old_access = {}
        if group_pk in new:
            new_access = new[group_pk]
        else: new_access = {}
        assert space_pk not in self.offices

        # Let's fill out the self.offices set.
        self.offices[space_pk] = collections.defaultdict(lambda: (set(), set()))
        space_data = self.offices[space_pk]
        for mit_id, old_set in old_access.items():
            space_data[mit_id][0].update(old_set)
        for mit_id, new_set in new_access.items():
            space_data[mit_id][1].update(new_set)

    def add_office_signatories_per_time(self, ind, time):
        group = self.group
        people = group.officers(as_of=time, role=role['office'])
        for holder in people:
            fill_people(holder)
        for office_id, office_data in self.offices.items():
            for holder in people:
                holder_name = people_name[holder.person]
                holder_id = people_id[holder.person]
                office_data[holder_id][ind].add(holder_name)

    def add_office_signatories(self, old_time, new_time, ):
        group = self.group
        self.add_office_signatories_per_time(0, old_time)
        self.add_office_signatories_per_time(1, new_time)

    def list_office_changes(self, ):
        systems_lines = {
            'cac-card': [],
            'none': [],
        }
        group_lines = []
        for space_pk, space_data in self.offices.items():
            lock_type = all_spaces[space_pk].lock_type
            system_lines = systems_lines[lock_type.db_update]
            def append_change(mit_id, verb, name):
                system_lines.append("%s:\t%s:\t%s" % (mit_id, verb, name))
                group_lines.append("%s:\t%s" % (verb, name))

            line = "Changes in %s:" % (all_spaces[space_pk].number, )
            system_lines.append(line)
            group_lines.append(line)

            if lock_type.db_update == 'none':
                tmpl =  'Warning: You submitted changes affecting this space, but this space is ' + \
                        'a "%s" space, and is not managed through the ASA DB. See ' + \
                        'https://asa.mit.edu%s for details on how to update spaces of this type.'
                line = tmpl % (lock_type.name, reverse('space-lock-type'), )
                group_lines.append(line)

            for mit_id, (old_names, new_names) in space_data.items():
                if mit_id is None: mit_id = "ID unknown"
                if old_names == new_names:
                    pass
                else:
                    self.changes = True
                    for name in old_names:
                        if name in new_names:
                            append_change(mit_id, "Keep", name)
                        else:
                            append_change(mit_id, "Remove", name)
                    for name in new_names:
                        if name in old_names:
                            pass
                        else:
                            append_change(mit_id, "Add", name)
            system_lines.append("")
            group_lines.append("")

        systems_msg = dict([
            (system, '\n'.join(lines), ) for (system, lines) in systems_lines.items()
        ])
        group_msg = "\n".join(group_lines)
        return systems_msg, group_msg

    def add_locker_signatories(self, space_access, time):
        # space_access: ID -> (Name -> (Set Group.pk))
        if time in self.locker_acl:
            locker_acl = self.locker_acl[time]
        else:
            locker_acl = self.group.officers(as_of=time, role=role['locker'])
            self.locker_acl[time] = locker_acl
        for holder in locker_acl:
            fill_people(holder)
            holder_name = people_name[holder.person]
            holder_id = people_id[holder.person]
            space_access[holder_id][holder_name].add(self.group.pk)

def init_groups(the_groups, assignments):
    for assignment in assignments:
        group = assignment.group
        if group.id not in the_groups:
            the_groups[group.id] = GroupInfo(group)

def flip_dict(dct):
    new = collections.defaultdict(set)
    for key, vals in dct.items():
        for val in vals:
            new[val].add(key)
    return new

def joint_keys(dct1, dct2):
    return set(dct1.keys()).union(dct2.keys())

class LockerAccessChangeEntry(object):
    def __init__(self, mit_id, verb, name, groups):
        self.mit_id = mit_id
        self.verb = verb
        self.name = name
        self.cac_msgs = ""
        self.group_msgs = ""
        self.groups = groups

    def cac_format(self):
        return "%s\t%s\t%s\t%s" % (self.mit_id, self.verb, self.name, self.cac_msgs)

    def group_format(self):
        return "%s\t%s\t%s" % (self.verb, self.name, self.group_msgs)

def safe_add_change_real(change_by_name, change):
    """Add a new change to our dict of pending changes.

    If a different change has already been added for this person (eg, "Remove"
    instead of "Keep", or with a different list of groups), error.  This should
    always succeed; if it doesn't, the code is buggy. We worry about this
    because we want to be really sure that the email that goes to just CAC is
    compatible with the emails that go to each groups. Since we iterate over
    the changes once per group, we want to be sure that for each group
    iteration we're building compatible information.
    """

    name = change.name
    if name in change_by_name:
        if change_by_name[name].verb != change.verb or change_by_name[name].groups != change.groups:
            print "change_by_name=%s" % (change_by_name, )
            print "change: old=%s; new=%s" % (change_by_name[name], change)
            assert False
    else:
        change_by_name[name] = change

def locker_access_diff(the_space, group_data, old_access, new_access, ):
    cac_msgs = [] # [String]
    for mit_id in joint_keys(old_access, new_access):
        change_by_name = {} # name -> LockerAccessChangeEntry
        def safe_add(change):
            safe_add_change_real(change_by_name, change)
        old_by_names = old_access[mit_id]
        new_by_names = new_access[mit_id]
        old_by_group = flip_dict(old_by_names)
        new_by_group = flip_dict(new_by_names)
        unchanged = (old_by_names == new_by_names)
        if unchanged: continue
        print "ID=%s (%s):\n\t%s\t(%s)\n\t%s\t(%s)\n" % (mit_id, unchanged, old_by_names, old_by_group, new_by_names, new_by_group, ),
        for group_pk in joint_keys(old_by_group, new_by_group):
            # TODO: Do we need to do an iteration for each group? This seems
            # slightly questionable. Can we just loop over all known names?

            old_names = old_by_group[group_pk]
            new_names = new_by_group[group_pk]
            for name in old_names.union(new_names):
                changed_groups = old_by_names[name] ^ new_by_names[name]

                def mkchange(verb):
                    change = LockerAccessChangeEntry(
                        mit_id=mit_id,
                        verb=verb,
                        name=name,
                        groups=changed_groups,
                    )
                    if verb == "Keep":
                        change.group_msgs = "(other groups involved)"
                    change.cac_msgs = "(groups: %s -> %s)" % (old_by_names[name], new_by_names[name])
                    return change

                if name in old_names and name in new_names: # keep
                    safe_add(mkchange("Keep"))
                elif name in old_names: # remove from this group
                    if new_by_names[name]: # keep b/c other groups
                        safe_add(mkchange("Keep"))
                    else:
                        safe_add(mkchange("Remove"))
                elif name in new_names: # add for this group
                    if old_by_names[name]: # keep b/c other groups
                        safe_add(mkchange("Keep"))
                    else:
                        safe_add(mkchange("Add"))
                else:
                    assert False, "in old_names or new_names, but not in both, one, or the other..."

        # Handle reporting the results...
        for change in change_by_name.values():
            cac_msgs.append(change.cac_format())
            group_msg = "%s\t%s" % (the_space.number, change.group_format())
            for group_pk in change.groups:
                group_data[group_pk].locker_messages.append(group_msg)
                group_data[group_pk].changes = True

    return cac_msgs

def space_specific_access(the_space, group_data, old_time, new_time, ):
    old_data = the_space.build_access(time=old_time)
    new_data = the_space.build_access(time=new_time)
    all_spaces[the_space.pk] = the_space
    init_groups(group_data, old_data[2])
    init_groups(group_data, new_data[2])
    for group_pk, group_info in group_data.items():
        if group_pk in old_data[0] or group_pk in new_data[0]:
            if the_space.merged_acl:
                group_info.add_locker_signatories(old_data[1], old_time)
                group_info.add_locker_signatories(new_data[1], new_time)
            else:
                group_info.learn_office_access(the_space.pk, old_data[0], new_data[0])
    cac_msgs = []
    if the_space.merged_acl:
        cac_msgs = locker_access_diff(the_space, group_data, old_data[1], new_data[1])
    return cac_msgs

def space_access_diffs():
    new_time = datetime.datetime.utcnow()
    old_time = new_time - datetime.timedelta(days=1, minutes=15)
    bulk_fill_people([old_time, new_time])
    group_data = {} # Group.pk -> GroupInfo
    cac_locker_msgs = []

    process_spaces =  space.models.Space.objects.all().select_related('lock_type')
    for the_space in process_spaces:
        new_cac_msgs = space_specific_access(the_space, group_data, old_time, new_time)
        if new_cac_msgs:
            cac_locker_msgs.append("%s\n%s\n" % (the_space.number, "\n".join(new_cac_msgs)))

    changed_groups = []
    cac_chars = 0
    for group_pk, group_info in group_data.items():
        group_info.add_office_signatories(old_time, new_time)
        systems_changes, group_office_changes = group_info.list_office_changes()
        if group_info.changes:
            cac_chars += len(systems_changes['cac-card'])
            changed_groups.append((group_info.group, systems_changes['cac-card'], group_office_changes, group_info.locker_messages, ))

    asa_rcpts = ['asa-space-access@mit.edu']
    if cac_chars > 0 or cac_locker_msgs:
        util.emails.email_from_template(
            tmpl='space/cac-change-email.txt',
            context={'changed_groups': changed_groups, 'locker_msgs':cac_locker_msgs, },
            subject="Space access updates",
            to=['caclocks@mit.edu'],
            cc=asa_rcpts,
            from_email='asa-db-auto@mit.edu',
        ).send()
    group_email_cc = asa_rcpts + ['caclocks@mit.edu']
    for group, cac_msg, group_office_msg, group_locker_msgs in changed_groups:
        util.emails.email_from_template(
            tmpl='space/group-change-email.txt',
            context={
                'group':group,
                'office_msg':group_office_msg,
                'locker_msgs':group_locker_msgs,
            },
            subject="[ASA DB] Space access updates for %s" % (group.name, ),
            to=[group.officer_email],
            cc=group_email_cc,
            from_email='asa-db-auto@mit.edu',
        ).send()


if __name__ == "__main__":
    space_access_diffs()
