import functools
import ldap
import re
import flask
import flask_wtf
import wtforms

import webapp
from webapp import app, context, config, ldaputils, email

bp = flask.Blueprint('admin', __name__)

def admin_required_impl(f):
    @functools.wraps(f)
    def func(*a, **kw):
        # TODO: Actually check for admin perms
        if not flask.session['is_admin']:
            flask.abort(403)

        return f(*a, **kw)
    return func

def admin_required(f):
    return webapp.auth.login_required(admin_required_impl(f))

def _get_user_list(conn, query='&'):
    """Returns List[Tuple[username, full name]] for query"""
    all_users = []

    results = conn.search_s(config.ldap_people, ldap.SCOPE_SUBTREE, f'(&(uid=*)(cn=*){ldaputils.wrap(query)})', attrlist=['uid', 'cn'])
    for user, attrs in results:
        user_uid = attrs['uid'][0].decode()
        user_cn = attrs['cn'][0].decode()
        all_users.append((user_uid, user_cn))

    all_users.sort(key=lambda user: user[0].lower())
    return all_users

def _get_groupped_user_list(conn):
    """Returns all users (uid, full name), groupped by active groups"""
    groupped_users = [
        (group.capitalize(), _get_user_list(conn, f'memberOf={ldaputils.group_dn(group)}'))
        for group in config.ldap_active_groups
    ]

    inactive_filter = ldaputils._not(
        ldaputils.member_of_any(config.ldap_active_groups)
    )

    groupped_users.append(
        ('Inactive users', _get_user_list(conn, inactive_filter))
    )

    return groupped_users

@bp.route('/admin/')
@admin_required
def admin_view():
    return flask.render_template('admin/index.html')

@bp.route('/admin/users/')
@admin_required
def admin_users_view():
    conn = context.get_connection()
    groups = _get_groupped_user_list(conn)

    return flask.render_template('admin/users.html', groups=groups)

def _get_profile(conn, uid):
    results = conn.search_s(ldaputils.user_dn(uid), ldap.SCOPE_SUBTREE)
    return ldaputils.normalized_entries(results)[0]

def _format_profile(profile):
    rendered = []
    dn, attrs = profile
    for attr, value in attrs:
        attr_sanitized = attr.lower()
        attr_full_name = config.full_name.get(attr_sanitized, attr_sanitized)
        attr_readable_name = config.readable_names.get(attr_full_name)
        rendered.append((attr, attr_readable_name, value))

    # Attributes with readable names first, then by name
    rendered.sort(key=lambda profile: profile[0])
    rendered.sort(key=lambda profile: profile[1] is None)
    return rendered

def _get_groups_of(conn, uid):
    filter = ldaputils.groups_of_user(uid)
    groups = [
        attrs['cn'][0].decode()
        for group_dn, attrs in
        conn.search_s(config.ldap_base, ldap.SCOPE_SUBTREE, filter)
    ]

    return groups

def _is_user_protected(conn, uid, groups):
    return any(group in config.ldap_protected_groups for group in groups)

@bp.route('/admin/users/<uid>')
@admin_required
def admin_user_view(uid):
    ldaputils.validate_name(uid)
    conn = context.get_connection()

    profile = _get_profile(conn, uid)
    groups = _get_groups_of(conn, uid)
    is_protected = _is_user_protected(conn, uid, groups)

    return flask.render_template('admin/user.html', uid=uid, profile=_format_profile(profile), groups=groups, is_protected=is_protected)

# TODO: Deduplicate this modification logic with webapp/vcard.py

class AddMifareIDHash(flask_wtf.FlaskForm):
    value = wtforms.fields.StringField(label=config.readable_names.get('mifareidhash'))

class DelForm(flask_wtf.FlaskForm):
    pass

@bp.route('/admin/users/<uid>/add_mifareidhash')
@admin_required
def admin_user_view_add_mifareidhash(uid):
    form = AddMifareIDHash()
    return flask.render_template('admin/ops/add_mifareidhash.html', uid=uid, form=form)

@bp.route('/admin/users/<uid>/del_mifareidhash')
@admin_required
def admin_user_view_del_mifareidhash(uid):
    form = DelForm()
    value = flask.request.args.get('value')
    return flask.render_template('admin/ops/del_mifareidhash.html', uid=uid, form=form, value=value)

def _modify_mifareidhash(uid, form, modify_func):
    ldaputils.validate_name(uid)
    conn = context.get_connection()

    groups = _get_groups_of(conn, uid)
    is_protected = _is_user_protected(conn, uid, groups)

    redirect_url = flask.url_for('admin.admin_user_view', uid=uid)
    if is_protected:
        flask.flash('Cannot modify protected user', 'danger')
        return flask.redirect(redirect_url)

    try:
        if form.validate_on_submit():
            dn = ldaputils.user_dn(uid)
            modify_func(conn, dn)

            context.refresh_profile(dn)
            flask.flash('Added mifareidhash', category='info')
            return flask.redirect(redirect_url)

        for field, errors in form.errors.items():
            for error in errors:
                flask.flash("Error in the {} field - {}".format(
                    getattr(form, field).label.text,
                    error
                ), 'danger')

        return flask.redirect(redirect_url)
    except ldap.LDAPError as e:
        print('LDAP error:', e)
        flask.flash(f'Could not modify profile due to LDAP error: {e}', 'danger')
        return flask.redirect(redirect_url)


@bp.route('/admin/users/<uid>/add_mifareidhash', methods=['POST'])
@admin_required
def admin_user_add_mifareidhash(uid):
    form = AddMifareIDHash()
    def modify_func(conn, dn):
        new_value = form.value.data

        email.send_papertrail(
            f'Adding mifareIDHash for user {uid}',
            f'New mifareIDHash: {new_value}'
        )

        conn.modify_s(dn, [(ldap.MOD_ADD, 'mifareidhash', new_value.encode('utf-8'))])

    return _modify_mifareidhash(uid, form, modify_func)

@bp.route('/admin/users/<uid>/del_mifareidhash', methods=['POST'])
@admin_required
def admin_user_del_mifareidhash(uid):
    form = DelForm()
    def modify_func(conn, dn):
        old_value = flask.request.args.get('value')

        email.send_papertrail(
            f'Deleting mifareIDHash for user {uid}',
            f'Deleted mifareIDHash: {old_value}'
        )

        conn.modify_s(dn, [(ldap.MOD_DELETE, 'mifareidhash', old_value.encode('utf-8'))])

    return _modify_mifareidhash(uid, form, modify_func)

@bp.route('/admin/groups/')
@admin_required
def admin_groups_view():
    conn = context.get_connection()

    # no obvious way to filter out groups that are just a per-user-group
    # (not super useful to look at them)
    # so we'll filter them out by name yolo
    all_users = _get_user_list(conn)
    all_uids = set([uid for uid, cn in all_users])

    groups = [
        attrs['cn'][0].decode()
        for group_dn, attrs in
        conn.search_s(config.ldap_base, ldap.SCOPE_SUBTREE, 'objectClass=groupOfUniqueNames')
    ]

    filter_groups = filter((lambda cn: cn not in all_uids), groups)

    return flask.render_template('admin/groups.html', groups=filter_groups)

def _get_group(conn, name):
    results = conn.search_s(ldaputils.group_dn(name), ldap.SCOPE_SUBTREE)
    return ldaputils.normalized_entries(results)[0]

@bp.route('/admin/groups/<name>')
@admin_required
def admin_group_view(name):
    ldaputils.validate_name(name)
    conn = context.get_connection()

    group_attrs = _get_group(conn, name)
    members = _get_user_list(conn, f'memberOf={ldaputils.group_dn(name)}')

    is_protected = name in config.ldap_protected_groups

    return flask.render_template('admin/group.html', name=name, attributes=_format_profile(group_attrs), members=members, is_protected=is_protected)
