#!/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), '..'))
    sys.path.append(django_dir)
    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

from django.core.mail import EmailMessage
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, ):
        cac_lines = []
        group_lines = []
        def append_change(mit_id, verb, name):
            cac_lines.append("%s:\t%s:\t%s" % (mit_id, verb, name))
            group_lines.append("%s:\t%s" % (verb, name))
        for space_pk, space_data in self.offices.items():
            line = "Changes in %s:" % (all_spaces[space_pk].number, )
            cac_lines.append(line)
            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)
            cac_lines.append("")
            group_lines.append("")

        cac_msg = "\n".join(cac_lines)
        group_msg = "\n".join(group_lines)
        return cac_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):
    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):
            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)
    bulk_fill_people([old_time, new_time])
    group_data = {} # Group.pk -> GroupInfo
    cac_locker_msgs = []

    process_spaces =  space.models.Space.objects.all()
    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 = []
    for group_pk, group_info in group_data.items():
        group_info.add_office_signatories(old_time, new_time)
        cac_changes, group_office_changes = group_info.list_office_changes()
        if group_info.changes:
            changed_groups.append((group_info.group, cac_changes, group_office_changes, group_info.locker_messages, ))

    asa_rcpts = ['asa-space@mit.edu', 'asa-db@mit.edu', ]
    if changed_groups:
        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,
        ).send()
    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",
            to=[group.officer_email],
            cc=asa_rcpts,
        ).send()


if __name__ == "__main__":
    space_access_diffs()
