# (c) cavaliba.com - IAM - group.py

import os
from datetime import datetime, timedelta
import time
import base64
import random
import re

import csv
import json
import yaml

from django.http import HttpResponse
from django.forms.models import model_to_dict
from django.utils import timezone
from django.utils.translation import gettext as _
from django.db.models import Q

from app_home.configuration import get_configuration
from app_home.log import log, DEBUG, INFO, WARNING, ERROR, CRITICAL

from .models import SireneUser
from .models import SireneGroup
from .models import SirenePermission

from .forms import GroupForm

from app_user.user import user_get_by_id
from app_user.user import user_get_by_login
from app_user.user import user_get_login_by_email

from app_data.data import Instance
from app_data.models import DataEAV

from app_data.permissions import has_group_read_permission
from app_data.permissions import has_group_create_permission
from app_data.permissions import has_group_update_permission
from app_data.permissions import has_group_delete_permission





def group_get_form(group=None):

    # blank
    if not group:
        return GroupForm(initial={})

    attributs =  [
        "keyname", "displayname", "description", "is_enabled", "is_builtin", "autogroup",
        ] 

    initial = {}
    for attrib in attributs:
        try:
            initial[attrib] = getattr(group,attrib, "")
        except:
            pass

    initial["users"] = [i for i in group_get_users(group, subgroups=False, computed=False)]
    initial["subgroups"] = [i for i in group.subgroups.all()]
    initial["permissions"] = [i for i in group.permissions.all()]

    return GroupForm(initial=initial) 





def group_search(query=None, page=1, size=10):

    m = re.compile(r'[a-zA-Z0-9()_/.-]*$')
    if query and not m.match(query):
        return []

    offset = (page - 1) * size
    limit = offset + size

    qs = SireneGroup.objects\
        .filter(is_role=False)\
        .prefetch_related("users")\
        .prefetch_related("subgroups")\
        .order_by('keyname')

    if query:
        qs = qs.filter(
            Q(keyname__icontains=query) |
            Q(displayname__icontains=query) |
            Q(description__icontains=query)
        )

    return qs[offset:limit]


def group_all_filtered(is_enabled=None, filter=None):
    # prefetch_related('').
    if is_enabled:
        return SireneGroup.objects.filter(is_enabled=True, is_role=False).all()
    else:
        return SireneGroup.objects.filter(is_role=False).all()

# X
def group_get_by_id(id):
    # prefetch_related('').
    return SireneGroup.objects.filter(pk=id, is_role=False).first()

# X
def group_get_by_name(name):
    return SireneGroup.objects.filter(keyname=name, is_role=False).prefetch_related('users').first()

# X
def group_get_by_data(data):
    
    keyname = data.get('keyname', None)
    if not keyname:
        return
    return  group_get_by_name(keyname)




# ------------------------------------
# expand group(s) objects to users
# Recursive
# ------------------------------------
# OBJECTS
def group_get_users(group, processed=None, 
                    direct=True, 
                    computed=True, 
                    subgroups=True, 
                    maxcount=999999999):

    if not processed:
        processed = []

    reply = []

    if not isinstance(group,SireneGroup):
        return []

    # 1. add all direct users from group g
    if direct:
        for u in group.users.all():
            reply.append(u)
            #reply += list(group.users.all())
    maxcount_left = maxcount-len(reply)
    if maxcount_left <= 0:
        return list(set(reply[:maxcount]))


    # 2. add computed users grom autogroup grammar
    if computed:
        reply += group_get_users_computed(group=group)
    maxcount_left = maxcount-len(reply)
    if maxcount_left <= 0:
        return list(set(reply[:maxcount]))

    # 3. recurse to subgroups
    if subgroups:

        if group in processed:
            return []

        processed.append(group)

        for sg in group.subgroups.all():
            subreply = group_get_users(
                sg,
                processed = processed, 
                direct=True, 
                computed=computed, 
                subgroups=True,
                maxcount = maxcount_left)
            reply += subreply
            # remove duplicate users
            reply = list(set(reply))
            maxcount_left = maxcount-len(reply)
            if maxcount_left <= 0:
                break

    # remove duplicate users
    reply = list(set(reply))
    return reply   


# ------------------------------------
#  autogroup
# ------------------------------------

def group_get_users_computed(group=None):
    '''
    compute group users from autogroup rule

    IN: group: SireneGroup object
    OUT: list of SireneUser object [u1, u2, u3]

    syntax:
       [G1] : autogroup = "test1:test1-01:*"
       [G2] : autogroup = "test1:*:my_user"

    v3.22
    '''

    reply = []
    if not type(group) is SireneGroup:
        return reply

    #  grammar in group.autogroup
    try:
        autogroup = group.autogroup
        (schema_name, keyname, fieldname) = autogroup.split(':')
    except:
        # invalid syntax in autogroup
        return reply

    g_users = []

    # G1: schemaname:*:fieldname
    if keyname == '*':
        g_users = group_get_users_computed_g1(group, schema_name, fieldname)


    # G2: schemaname:keyname:*
    elif fieldname == '*':
        g_users = group_get_users_computed_g2(group, schema_name, keyname)

    if g_users:
        for g in g_users:
            reply.append(g)

        
    return reply



# Grammars
# --------

# G1: schemaname:*:fieldname
#  new 3.22
def group_get_users_computed_g1(group, schema_name, fieldname):

    reply = []

    cached = DataEAV.objects.filter(
        classname=schema_name,
        fieldname=fieldname,
        format="user"
    ).values_list("value", flat=True)

    reply = list(SireneUser.objects.filter(login__in=cached))

    return reply



# G2: schemaname:keyname:*
#  new 3.22
def group_get_users_computed_g2(group, schema_name, keyname):

    reply = []

    instance = Instance.from_keyname(classname=schema_name, keyname=keyname, expand=False)
    if not instance:
        return []

    for fieldname,field in instance.fields.items():
        if field.dataformat == "user":            
            for login in field.value:
                user = user_get_by_login(login)
                if user:
                    reply.append(user)

    return reply

# update cache
def group_autogroup_update(groups=None):
    
    count = 0
    if not groups:
        groups = SireneGroup.objects.filter(is_enabled=True, is_role=False).all()
    for group in groups:
        users = group_get_users_computed(group)
        if len(users) > 0:
            count += 1
        group.autogroup_users.set(users)
        group.save()

    return count



# ------------------------------------
# recursive get of subgroups
# ------------------------------------

def group_get_subgroups(group, done=[]):
    ''' 
    Recurse  group Object to get all subgroups Objects
    group: single SireneGroup object
    reply: [] list of SireneGroup objects
        
    '''

    reply = []

    if not type(group) is SireneGroup:
        return reply
    
    for g in group.subgroups.all():
        if g in done:
            continue
        done.append(g)
        reply.append(g)
        reply += group_get_subgroups(g, done)
        
    reply = list(set(reply))
    return reply

# ------------------------------------
#  CRUD
# ------------------------------------

def group_init(data):

    keyname = data.get('keyname', None)
    if not keyname:
        return None, "group_init: missing keyname"

    # check if exists as role (not group)
    entry0 = SireneGroup.objects.filter(keyname=keyname, is_role=True).first()
    if entry0:
        return None, f"group_init: already exists as role {keyname}"

    # skip if exists
    entry = group_get_by_name(keyname)
    if entry:
        return entry, None

    entry = SireneGroup(is_role=False)
    return group_update(entry, data)
 


def group_create(data):

    keyname = data.get('keyname', None)
    if not keyname:
        return None, "group_create: missing keyname"

    # check if exists as role (not group)
    entry0 = SireneGroup.objects.filter(keyname=keyname, is_role=True).first()
    if entry0:
        return None, f"group_create: already exists as role {keyname}"

    # create or update
    entry1 = group_get_by_name(keyname)
    if not entry1:
        entry1 = SireneGroup(is_role=False)

    return group_update(entry1, data)



def group_update(group, data):
    ''' update Object with data dict info'''

    attributs =  [
        "keyname", "displayname", "is_enabled","description","autogroup",
    ] 

    #special_attributs = ["users", "subgroups", "permissions"]


    if not data:
        return None, "group_update: no data"

    for attrib in attributs:
        if attrib in data:
            try:
                setattr(group, attrib, data[attrib])
            except:
                pass
    
    group.last_update = timezone.now()
    group.save()


    # users
    if 'users' in data:
        # can be a Queryset (form) or list of login (import)
        group.users.clear()
        if data["users"]:
            for user in data["users"]:
                if type(user) is SireneUser:
                    group.users.add(user)
                else:
                    if '@' in user:
                        login2 = user_get_login_by_email(user)
                        user2 = user_get_by_login(login2)
                    else:
                        user2 = user_get_by_login(user)
                    if user2:
                        group.users.add(user2)
        group.last_update = timezone.now()                        
        group.save()       
    
    # subgroups
    if 'subgroups' in data:
        # can be a Queryset (form) or list of keynames (import)
        group.subgroups.clear()
        if data["subgroups"]:
            for subgroup in data["subgroups"]:
                if type(subgroup) is SireneGroup:
                    group.subgroups.add(subgroup)
                else:
                    subgroup2 = group_get_by_name(subgroup)
                    if subgroup2:
                        group.subgroups.add(subgroup2)
        group.last_update = timezone.now()
        group.save()       

    # NO permissions in groups ; use Role (groups w/ is_role=True)
    # if 'permissions' in data:
    #     # can be a Queryset (form) or list of keynames (import)
    #     group.permissions.clear()
    #     for perm in data["permissions"]:
    #         if type(perm) is SirenePermission:
    #             group.permissions.add(perm)
    #         else:
    #             perm2 = permission_get_by_name(perm)
    #             if perm2:
    #                 group.permissions.add(perm2)
    #     group.save()    

    return group, None



def group_update_by_data(data):

    group = group_get_by_data(data)
    if group:
        return group_update(group, data)
    return None, "group_update: group not found"


# X
def group_delete(gobj):

    if not gobj:
        return None, "group_delete:  error"

    if gobj.is_builtin:
        return None, "group_delete: is builtin"

    if gobj.is_role:
        return None, "group_delete: is role"
    
    try:
        gobj.delete()
        return gobj, None
    except Exception as e:
        return None, f"group_delete: {e}"

# X
def group_delete_by_id(gid):

    gobj = group_get_by_id(gid)
    return group_delete(gobj)

# X
def group_delete_by_data(data):

    group = group_get_by_data(data)
    if group:
        return group_delete_by_id(group.id)
    return None, "group_delete: not found"

# 0
def group_enable_by_data(data):
    group = group_get_by_data(data)
    if group:
        group.is_enabled = True
        group.save()
        return group, None
    return None, "group_enable: not found"

# 0
def group_disable_by_data(data):
    group = group_get_by_data(data)
    if group:
        group.is_enabled = False
        group.save()
        return group, None
    return None, "group_disable: not found"

# -------------------------------------------------------
# IMPORTS
# -------------------------------------------------------
def load_group(datadict=None, aaa=None):

    if not datadict:
        return f"load_group: missing data"

    if not aaa:
        log(WARNING, aaa=aaa, app="iam", view="group", action="load", status="DENY", data=_("Not allowed")) 
        return f"load_group: not allowed"

    keyname = datadict.get("keyname", None)
    if not keyname: 
        return f"load_group: missing keyname"

    action = datadict.get("_action", "create")

    err = None

    if action == "init":
        if not has_group_create_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="init", status="DENY", data=_("Not allowed")) 
            return f"load_group: init not allowed"
        # create/update only if not already exists
        g , err = group_init(datadict)

    elif action == "create":
        if not has_group_create_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="create", status="DENY", data=_("Not allowed")) 
            return f"load_group: create not allowed for {keyname}"
        # create and/or update
        g, err = group_create(datadict)

    elif action == "update":
        if not has_group_update_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="update", status="DENY", data=_("Not allowed")) 
            return f"load_group: update not allowed for {keyname}"
        # update only if exists
        g , err = group_update_by_data(datadict)

    elif action == "delete":
        if not has_group_delete_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="delete", status="DENY", data=_("Not allowed")) 
            return f"load_group: delete not allowed for {keyname}"
        g, err = group_delete_by_data(datadict)

    elif action == "enable":
        if not has_group_update_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="update", status="DENY", data=_("Not allowed")) 
            return f"load_group: enable not allowed for {keyname}"
        g, err = group_enable_by_data(datadict)

    elif action == "disable":
        if not has_group_update_permission(aaa=aaa):
            log(WARNING, aaa=aaa, app="iam", view="group", action="update", status="DENY", data=_("Not allowed")) 
            return f"load_group: disable not allowed for {keyname}"
        g, err = group_disable_by_data(datadict)

    else:
        return f"load_group: failed for {keyname} , action {action}"

    if err:
        return f"load_group: {err}"
    return

# -------------------------------------------------------
# EXPORTs
# -------------------------------------------------------


def group_to_dict(group):
    
    dict_attributs =  ["keyname", "displayname", "is_enabled","description", "autogroup"] 
    m = {}
    if group.is_role:
        m["classname"] = "_role"
    else:
        m["classname"] = "_group"
    m.update( model_to_dict(group, fields=dict_attributs) )
    
    # remove null values
    m2= {}
    for k,v in m.items():
        if not isinstance(v,bool):
            if v:
                m2[k] = v
        else:
            m2[k] = v


    #  add many-to-many objects 
    m2["users"] = [i.login for i in group.users.all()]
    m2["subgroups"]= [i.keyname for i in group.subgroups.all()]
    if group.is_role:
        m2["permissions"] = [i.keyname for i in group.permissions.all()]
    return m2


def group_to_yaml(group):
    data = group_to_dict(group)
    return yaml.dump([data], allow_unicode=True, Dumper=MyYamlDumper, sort_keys=False)





def group_listdict_format(groups):
    ''' groups to listdict = [ {}, {} .. ] '''

    dict_attributs =  ["keyname", "displayname", "is_enabled","description", "autogroup"] 

    datalist = []
    for group in groups:

        m = {}
        m["classname"] = "_group"
        m.update(model_to_dict(group, fields=dict_attributs))

        # remove null values
        m2= {}
        for k,v in m.items():
            if v:
                m2[k] = v
        m2["subgroups"]= [i.keyname for i in group.subgroups.all()]
        m2["users"] = [i.login for i in group_get_users(group, computed=False, subgroups=False)]
        #m2["permissions"] = [i.keyname for i in group.permissions.all()]
        datalist.append(m2)

    return datalist


# JSON
def group_json_response(groups):
    datalist = group_listdict_format(groups)
    filedata = json.dumps(datalist, indent=4, ensure_ascii = False)
    response = HttpResponse(filedata, content_type='text/json')  
    response['Content-Disposition'] = 'attachment; filename="groups.json"'
    return response

# YAML
class MyYamlDumper(yaml.SafeDumper):
    def write_line_break(self, data=None):
        super().write_line_break(data)
        if len(self.indents) < 2:
            super().write_line_break()

def group_yaml_response(groups):
    datalist = group_listdict_format(groups)
    filedata = yaml.dump(datalist, allow_unicode=True, Dumper=MyYamlDumper, sort_keys=False)
    response = HttpResponse(filedata, content_type='text/yaml')  
    response['Content-Disposition'] = 'attachment; filename="groups.yaml"'
    return response


# CSV
def group_csv_response(groups):

    csv_attributs =  [
        "classname", "keyname", "displayname", "is_enabled","description","autogroup", "users", "subgroups",
    ] 

    special_attributs = ["classname", "users", "subgroups","permissions"]

    delimiter = get_configuration(keyname="CSV_DELIMITER")

    response = HttpResponse(content_type='text/csv')  
    response['Content-Disposition'] = 'attachment; filename="groups.csv"'
    writer = csv.writer(response, delimiter=delimiter)

    # headers = []
    # for attrib in csv_attributs:
    #     headers.append(attrib)

    writer.writerow(csv_attributs)

    for item in groups:
        mye = ["_group"]
        # standard attributs
        for attrib in csv_attributs:
            if attrib in special_attributs:
                continue
            myvalue = getattr(item,attrib, "")
            mye.append(myvalue)


        # users - special attribut - don't include computed users
        mystr = '+'.join([i.login for i in group_get_users(item, computed=False, subgroups=False)])
        mye.append(mystr)

        # subgroups - special attribut
        mystr = '+'.join([i.keyname for i in item.subgroups.all()])
        mye.append(mystr)

        # NO permissions directly in groups ; use Roles
        # permissions - special attribut
        # mystr = '+'.join([i.keyname for i in item.permissions.all()])
        # mye.append(mystr)


        writer.writerow(mye)

    return response

