#!/bin/env python3
# -*- coding: utf-8 -*-
#    TOMUSS: The Online Multi User Simple Spreadsheet
#    Copyright (C) 2008,2010,2021 Thierry EXCOFFIER, Universite Claude Bernard
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    Contact: Thierry.EXCOFFIER@univ-lyon1.fr

"""
Attendance management tools
"""

import time
import os
import json
import random
import collections
import re
import io
import urllib.request
import html
import qrcode as qrcodelib
from TOMUSS.ATTRIBUTES import tableinvitation
from TOMUSS.TEMPLATES import config_cron
from . import note
from . import text
from .. import files
from .. import plugin
from .. import utilities
from .. import document
from .. import column
from .. import configuration
from .. import abj

class Prst(note.Note):
    """Attendance column type"""
    human_priority = -9
    tip_cell = "TIP_cell_Prst"
    cell_test = 'test_prst'
    formatte = text.Text.formatte
    formatte_suivi = "prst_format_suivi"
    ondoubleclick = 'toggle_prst'
    tip_filter = "TIP_filter_Prst"
    tip_test = ''
    should_be_a_float = 0
    attributes_visible = ('url_import', 'groupcolumn', 'repetition', 'weight',
                          'qrcode_prst')
    cell_completions = "prst_completions"

if not hasattr(configuration, 'qrcodes'): # In case of plugin reload
    configuration.qrcodes = {} # type: Dict[int, QRCode]

if not hasattr(configuration, 'FEEDBACK_CACHE'): # In case of plugin reload
    configuration.FEEDBACK_CACHE = {}

files.add('FILES', 'qr-creator.min.js')
files.add('COLUMN_TYPES', 'prst.html')

def etape_from_ue(table):
    """Returns the most used etape of registered students"""
    counters = collections.defaultdict(int)
    for user in configuration.Users(table.logins_valid()):
        for etape in user.etapes_list:
            counters[etape] += 1
    return max(counters, key=lambda i: counters[i])

class QRCode: # pylint: disable=too-many-instance-attributes
    """A running instance of QRCode.
    Multiple running QRCode for the same table+column are possible

    Negative data_col indicates that attendance is recorded in a '/P/etape' table
    The negative data_col format is -YYYYMMDDHHMMduration
    """
    def __init__(self, server):
        year, semester, code_ue = server.the_year, server.the_semester, server.the_ue
        self.table = document.table(year, semester, code_ue, create=False)
        self.datacol = int(server.something)
        if self.datacol >= 0:
            self.column = self.table.columns[self.datacol]
            self.col_id = self.column.the_id
            assert self.datacol < len(self.table.columns)
            assert self.table.columns[self.datacol].type.name == 'Prst'
        else:
            self.table_P = document.table(
                utilities.university_year(year, semester), 'P', etape_from_ue(self.table))
            i = server.something
            time_slot = f'{i[1:5]}-{i[5:7]}-{i[7:9]}_{i[9:11]}:{i[11:13]}'
            duration = i[13:]
            with self.table_P.the_lock:
                self.column = self.table_P.columns.from_title(time_slot)
                if not self.column:
                    last_time_column = None
                    columns = sorted(self.table_P.columns, key = lambda c: c.position) # Screen order
                    for j, a_column in enumerate(columns):
                        if re.match('....-..-.._..:..', a_column.title):
                            if not last_time_column or (
                                    a_column.title > last_time_column.title
                                    and a_column.title < time_slot
                            ):
                                last_time_column = a_column
                                if a_column.title > time_slot:
                                    # The new date is before the first one
                                    position = (columns[j-1].position + a_column.position)/2
                                else:
                                    if len(columns) == j+1:
                                        # The new date is after the last one
                                        position = a_column.position + 1
                                    else:
                                        position = (a_column.position + columns[j+1].position)/2
                    page = self.table_P.get_nobody_page()
                    self.column = self.table_P.add_empty_column(page)
                    column.ColumnAttr.attrs['type'].set(
                        self.table_P, page, self.column, 'Prst', i[1:13])
                    column.ColumnAttr.attrs['title'].set(
                        self.table_P, page, self.column, time_slot, i[1:13])
                    if last_time_column:
                        column.ColumnAttr.attrs['position'].set(
                            self.table_P, page, self.column, position, i[1:13])
            qrcode_id = configuration.get_qrcode_id(self.table_P, 1)
            # 2 spaces because no group
            self.comment_P = f'{code_ue} {server.ticket.user_name}  {semester} {duration}#{qrcode_id}'

        self.end_of_life = 0
        self.prst = set()
        if server.the_path and server.the_path[0] == 'grpseq':
            grp_col = self.table.columns.get_grp()
            seq_col = self.table.columns.get_seq()
            self.filtered = set()
            if len(server.the_path) == 3:
                # Deprecated, no more used
                grp = server.the_path[2]
                seq = server.the_path[1]
                for line_id, line in self.table.lines.items():
                    if grp_col and grp and line[grp_col].value != grp:
                        continue # bad grp
                    if seq_col and seq and line[seq_col].value != seq:
                        continue # bad seq
                    self.filtered.add(line_id)
            else:
                # grpseqs JSON: it is a OR between conditions and AND between GRP/SEQ.
                # [["group value",
                #   "seq value" or null for any,
                #   "optional: grp col name",
                #   "optional: seq col name"],
                #  ...
                # ]
                grpseqs = json.loads(server.the_path[1])
                if grpseqs:
                    if self.datacol < 0:
                        # Insert groups
                        groups = []
                        for grpseq in grpseqs:
                            grp = (len(grpseq) > 2 and grpseq[2] or '') + (grpseq[0] or '')
                            seq = (len(grpseq) > 3 and grpseq[3] or '') + (grpseq[1] or '')
                            groups.append(seq + grp)
                        self.comment_P = self.comment_P.replace(
                            '  ', ' ' + '+'.join(groups) + ' ')
                    for grpseq in grpseqs:
                        while len(grpseq) < 4:
                            grpseq.append(None)
                        if grpseq[2]:
                            grpseq[2] = self.table.columns.from_title(grpseq[2]).data_col
                        else:
                            grpseq[2] = grp_col
                        if grpseq[3]:
                            grpseq[3] = self.table.columns.from_title(grpseq[3]).data_col
                        else:
                            grpseq[3] = seq_col
                    for line_id, line in self.table.lines.items():
                        for grp, seq, grp_col, seq_col in grpseqs:
                            if (grp == line[grp_col].value
                                    and (seq == line[seq_col].value
                                         or seq is None)):
                                self.filtered.add(line_id)
                                break
                else:
                    self.filtered.update(self.table.lines)
        else:
            self.filtered = set(server.the_path) # Only these line_id
        self.filtered.discard('')
        self.qrcode_id = hex(random.randrange(1<<64))
        configuration.qrcodes[self.qrcode_id] = self
        self.table.do_not_unload_add('qrcode')

    def authorized(self, server, line):
        """Is the user allowed to modify the cell"""
        user = configuration.User(line[0].value)
        if user.ID_ in self.prst:
            return True # Student has scanned a QRCode
        if self.datacol < 0:
            attendances = tuple(self.table_P.get_lines(line[0].value))
            if not attendances:
                return True # student not in the P table
            return self.table.authorized(
                server.ticket.user_name,
                attendances[0][self.column.data_col], self.column,
                attendances[0], is_a_teacher=True)
        return self.table.authorized(
            server.ticket.user_name, line[self.datacol], self.column,
            line, is_a_teacher=True)

    def set_value(self, server, login, value, student=False):
        """Set value by the teacher or the student"""
        user = configuration.User(login)
        try:
            line_id_T = next(iter(self.table.the_keys()[user.ID_]))
        except StopIteration:
            return 'bug.png'
        if self.datacol < 0:
            attendance_table = self.table_P
            attendance_datacol = self.column.data_col
            attendance_col_id = self.column.the_id
            try:
                line_id_P = next(iter(self.table_P.the_keys()[user.ID_]))
            except StopIteration:
                # Add the student
                with self.table_P.the_lock:
                    page = self.table_P.get_rw_page() # get_a_page_for((the_column.author,))
                    line_id_P = self.table_P.create_line_id()
                    self.table_P.cell_change(page, '0', line_id_P, user.student_id)
            attendance_line_id = line_id_P
        else:
            attendance_table = self.table
            attendance_datacol = self.datacol
            attendance_col_id = self.col_id
            line_id_P = None
            attendance_line_id = line_id_T
        old_value = attendance_table.lines[attendance_line_id][attendance_datacol].value
        if old_value == value:
            return 'ok.png'
        if student:
            force = old_value == ''
            if not force  or  self.filtered and line_id_T not in self.filtered:
                return 'bad.png'
        else:
            force = user.ID_ in self.prst # Teacher allowed to erase PRST
        if value == configuration.pre and student:
            self.prst.add(user.ID_)
        elif old_value == configuration.pre:
            self.prst.discard(user.ID_)
        with attendance_table.the_lock:
            page = attendance_table.new_page(
                server.ticket.ticket, server.ticket.user_name,
                server.ticket.user_ip, server.ticket.user_browser, recycle=True)
            ok = attendance_table.cell_change(
                page, attendance_col_id, attendance_line_id, value, force_update=force)
            if self.datacol < 0:
                attendance_table.comment_change(
                    page, attendance_col_id, attendance_line_id, self.comment_P)
            return ok

    def start(self):
        """Get 60 seconds to flash the QRCode"""
        self.end_of_life = time.time() + 61
        utilities.important_job_add("qrcode-{}".format(self.qrcode_id))
    def stop(self):
        """Deactivate temporarely QRCode"""
        if self.end_of_life:
            self.end_of_life = 0
            utilities.important_job_remove("qrcode-{}".format(self.qrcode_id))
    def remove(self):
        """The QRCode is no more used, the teacher closed the browser tab"""
        self.stop()
        self.table.do_not_unload_remove('qrcode')
        configuration.qrcodes.pop(self.qrcode_id)

def qrcode_new_real(server, qrcode): # pylint: disable=too-many-branches
    """Create a new QRCode"""
    server.the_file.write(document.the_head.get_content())
    server.the_file.write(files.files["prst.html"].get_content())
    status = {}
    mtime = qrcode.table.mtime
    if qrcode.datacol > 0:
        attendance_table = qrcode.table
        attendance_column = qrcode.datacol
    else:
        attendance_table = qrcode.table_P
        attendance_column = qrcode.column.data_col

    for line_id, line in qrcode.table.lines.items():
        if qrcode.filtered and line_id not in qrcode.filtered:
            continue
        if line[0].value:
            if qrcode.datacol > 0:
                value = line[qrcode.datacol].value
            else:
                try:
                    line_P = next(iter(qrcode.table_P.get_lines(line[0].value)))
                    value = line_P[qrcode.column.data_col].value
                except StopIteration:
                    value = ''

            with_da = False
            for da in abj.Abj(attendance_table.year, attendance_table.semester, line[0].value).da:
                if attendance_table.ue_code.startswith(da[0]):
                    with_da = True
                    break

            status[line_id] = [
                line[2].value, # Surname
                line[1].value, # Firstname
                line[0].value, # Login
                value, # status
                qrcode.authorized(server, line),
                with_da
                ]
    prefs_table = document.get_preferences(server.ticket.user_name, False,
                                           the_ticket=server.ticket)
    server.the_file.write(
        '''{}<script>
        var students_status = {};
        var qrcode = {};
        var ticket = {};
        var url = {};
        var code = {};
        var title = {};
        var column = {};
        var column_comment = {};
        var link_P = {};
        students_init();
        </script>'''.format(
            document.translations_init(prefs_table['language']),
            json.dumps(sorted(status.values())),
            utilities.js(qrcode.qrcode_id),
            utilities.js(server.ticket.ticket),
            utilities.js(configuration.server_url),
            utilities.js(qrcode.table.ue),
            utilities.js(qrcode.table.table_title),
            utilities.js(qrcode.column.title),
            utilities.js(qrcode.column.comment),
            json.dumps(''
                if qrcode.datacol > 0
                else
                f'<a target="_blank" href="{server.ticket.url(configuration.server_url + "/" + str(qrcode.table_P))}">{qrcode.table_P}</a>'
            )))
    if configuration.real_regtest:
        server.close_connection_now()
    while True:
        texts = []
        if attendance_table.mtime != mtime:
            mtime = attendance_table.mtime
            for line_id, line in qrcode.table.lines.items():
                if line_id not in status or not line[0].value:
                    continue
                if qrcode.datacol < 0:
                    try:
                        line = next(iter(attendance_table.get_lines(line[0].value)))
                    except StopIteration:
                        continue
                if status[line_id][3] != line[attendance_column].value:
                    status[line_id][3] = line[attendance_column].value
                    texts.append('update({},{},{});'.format(
                        utilities.js(line[0].value),
                        utilities.js(line[attendance_column].value),
                        int(qrcode.authorized(server, line))
                        ))
        ttl = int(qrcode.end_of_life - time.time())
        if ttl > 0:
            texts.append('update_time({});'.format(ttl))
        else:
            if qrcode.end_of_life:
                qrcode.stop()
                texts.append('update_time(0);')
        if texts:
            texts = '<script>{}update_stats({});</script>'.format(
                ''.join(texts), len(qrcode.prst))
            if configuration.real_regtest:
                with open('xxx.qrcode', 'a', encoding="utf-8") as trace:
                    trace.write(texts)
                if 'update_time(0);' in texts:
                    break
            else: # pragma: no cover
                server.the_file.write(texts)
        else:
                # Check if connection is open
            if not configuration.real_regtest:
                server.the_file.write(' ') # pragma: no cover
        time.sleep(0.01 if configuration.real_regtest else 1)

FEEDBACK_KEEP_TIME = 3*3600

def qrcode_clear_cache(): # pragma: no cover
    """Remove the student PRST cache"""
    keep_after = time.time() - FEEDBACK_KEEP_TIME + 1
    for key, when in tuple(configuration.FEEDBACK_CACHE.items()):
        if when < keep_after:
            configuration.FEEDBACK_CACHE.pop(key, None)

def qrcode_new(server):
    """Create and delete the interactive QRCode"""
    qrcode = QRCode(server)
    try:
        qrcode_new_real(server, qrcode)
    except (BrokenPipeError, OSError): # pragma: no cover
        ttl = qrcode.end_of_life - time.time()
        if ttl > 0:
            time.sleep(ttl)
    finally:
        utilities.start_job(qrcode_clear_cache, 2 * FEEDBACK_KEEP_TIME)
        qrcode.remove()

def qrcode_update(server):
    """QRCode: change timing or close it"""
    qrcode = configuration.qrcodes[server.something]
    result = 'ok.png'
    if server.the_path[0] == 'set':
        result = qrcode.set_value(server, server.the_path[1], server.the_path[2])
    elif server.the_path[0] == 'start':
        qrcode.start()
    elif server.the_path[0] == 'stop':
        qrcode.end_of_life = 1
    server.wfile.write(files.files[result].bytes())

FEEDBACK_BODY = '<body style="background:{};font-size:10vw;text-align:center;color:{}">{}</body>'

def qrcode_check(server):
    """QRCode: the student scanned a QRCode"""
    key = (server.something, server.ticket.user_name)
    message = ''
    if key in configuration.FEEDBACK_CACHE:
        result = 'ok.png'
    else:
        result = 'bad'
        try:
            qrcode = configuration.qrcodes[server.something]
            if time.time() <= qrcode.end_of_life:
                result = qrcode.set_value(
                    server, server.ticket.user_name, configuration.pre, student=True)
            message = '<br><br>' + html.escape(str(qrcode.column)).replace('/', '<br>')
        except KeyError:
            pass
        if result == 'ok.png':
            configuration.FEEDBACK_CACHE[key] = time.time()
    if result == 'ok.png':
        result = FEEDBACK_BODY.format('#0F0', '#000', configuration.pre + message)
    elif result == 'bug.png':
        result = FEEDBACK_BODY.format('#F00', '#FFF', server._("MSG_tablelinear_registered_no"))
    else:
        result = FEEDBACK_BODY.format('#F00', '#FFF', server._('MSG_QRCode_fail'))
    server.the_file.write(result)

plugin.Plugin('qrcode_new', '/{Y}/{S}/{U}/qrcode_new/{?}/{*}', qrcode_new,
              launch_thread=True, keep_open=True, group="staff", unsafe=False)

plugin.Plugin('qrcode_update', '/qrcode_update/{?}/{*}', qrcode_update, group="staff",
              mimetype='image/png', cached=False)

plugin.Plugin('qrcode_check', '/qrcode_check/{?}', function=qrcode_check,
              priority=-10, unsafe=False)

def qrcode_debug(server):
    """Debug"""
    server.the_file.write("<title>Debug QRCode</title>")
    for qrcode in configuration.qrcodes.values():
        server.the_file.write(f'{qrcode.qrcode_id} {qrcode.column}<br>')
    server.the_file.write('<br>')
    now = time.time()
    for key, timestamp in configuration.FEEDBACK_CACHE.items():
        server.the_file.write(f'{key} {int(now - timestamp)}<br>')

plugin.Plugin('qrcode_debug', '/qrcode_debug', function=qrcode_debug, launch_thread=True)


###############################################################################
###############################################################################
###############################################################################
# Long running Start/Stop QRCode
# It has nothing common with the interactive QRCodes above.
#
# For P Template table, the comment is:
#     UE Teacher Groups Semester Duration
#
# For QRCode not in a P table, the comment is:
#     Teacher Groups
#
# For more explanations: lok at 'def SVC'
#
###############################################################################
###############################################################################
###############################################################################

def log(txt):
    """For debugging"""
    with open(os.path.join('LOGS', 'SVC.log'), 'a') as file:
        file.write(txt)

def statistics(the_column, qrcode_id):
    """Compute the number of values in the cells scanned with this QRCode"""
    datacol = the_column.data_col
    tag = '#{}'.format(qrcode_id)
    comment = ''
    for line in the_column.table.lines.values():
        if line.cells[datacol].comment.endswith(tag):
            comment = line.cells[datacol].comment.split('#')[0]
            break
    if comment.count(' ') > 2:
        students = get_students_from_group(the_column.table, comment)
        ue, _teacher, grp, _ = comment.split(' ', 3)
        group = ' ∉ ' + ue
        if grp:
            group += '(' + grp + ')'
    else:
        students = None
    values = collections.defaultdict(int)
    for line in the_column.table.lines.values():
        if line.cells[datacol].comment.endswith(tag):
            if students is None or utilities.the_login(line.cells[0].value) in students:
                values[line.cells[datacol].value] += 1
            else:
                values[line.cells[datacol].value + group] += 1
    return values

def get_comment(qrcode_id, the_column, first=False):
    """Return the QRCode comment"""
    table = the_column.table
    data_col = the_column.data_col
    comments = collections.defaultdict(int)
    qrcode = f'#{qrcode_id}'
    for line in table.lines.values():
        comment = line[data_col].comment
        if comment.endswith(qrcode):
            if first:
                return comment
            comments[comment] += 1
    if not comments:
        return ''
    if len(comments) > 1:
        utilities.send_backtrace(f"Comments: {comments}", f"QRCode BUG in {table}")
    return max(comments, key=lambda x: comments[x])

@utilities.add_a_cache
def get_students_from_group_(infos): # pylint: disable=too-many-branches,too-many-locals
    """All the students of the group list as A1+Group=B+2-A"""
    presence_year, presence_semester, qrcode_comment = infos
    (code_ue, _teacher, groups, semester, _duration) = (qrcode_comment + '    ').split(' ', 4)
    year = presence_year
    if presence_semester == 'P':
        year -= configuration.semesters_year[configuration.semesters.index(semester)]
    else:
        semester = semester or presence_semester
    table = document.table(year, semester, code_ue, create=False)
    if not table:
        return ()
    if not groups:
        return set(utilities.the_login(line[0].value)
                   for line in table.lines.values()
                   if line[0].value
                  )
    grp_col = table.columns.get_grp()
    seq_col = table.columns.get_seq()
    students = set()
    for group in groups.split('+'):
        if '=' in group:
            column_name, expected = re.split('=+', group)
            col = table.columns.from_title(column_name)
            if not col:
                continue
            data_col = col.data_col
        else:
            expected = group
            data_col = grp_col
            if not data_col:
                continue # pragma: no cover
        # Spaces in Grp name were replaced by unsecable spaces
        expected = expected.replace(' ', ' ') # Restore space in group name
        if '-' in expected:
            if seq_col:
                for line in table.lines.values():
                    if line[seq_col].value + '-' + line[data_col].value == expected:
                        students.add(utilities.the_login(line[0].value)) # pragma: no cover
        else:
            for line in table.lines.values():
                if line[data_col].value == expected:
                    students.add(utilities.the_login(line[0].value))
    return students

def get_students_from_group(presence_table, qrcode_comment):
    """In order to use a cached function"""
    if qrcode_comment.count(' ') >= 3: # In the P template table
        return get_students_from_group_((
            presence_table.year, presence_table.semester, qrcode_comment))
    return get_students_from_group_((
        presence_table.year, presence_table.semester,
        presence_table.ue + ' ' + qrcode_comment))

def remove_present(the_column, students):
    """Retrieve the empty cells for the student group"""
    students = set(students)
    table = the_column.table
    data_col = the_column.data_col
    for line in table.lines.values():
        if line[data_col].value:
            students.discard(utilities.the_login(line[0].value))
    return students

def justified_leave(year, semester, user) -> bool:
    """The student has an active justification"""
    now = time.time()
    for justified in abj.Abj(year, semester, user.ID_).abjs:
        end = utilities.date_to_time(justified[1][:-1])
        if justified[1][-1] == 'M':
            end += 14 * 3600
        else:
            end += 24 * 3600
        if now > end:
            continue # After ABJ
        start = utilities.date_to_time(justified[0][:-1])
        if justified[0][-1] == 'A':
            start += 14 * 3600
        if now < start:
            continue # Before ABJ
        return True
    return False

def record_abi(table, the_column, qrcode_id, mailresp): # pylint: disable=too-many-locals,too-many-statements,too-many-branches
    """Indicates ABI for the non present students"""
    full_comment = get_comment(qrcode_id, the_column)
    comment = full_comment.split('#')[0]
    if comment.count(' ') < 3:
        return
    (code_ue, teacher, group, semester) = comment.split(' ')[:4]
    if table.semester == 'P':
        options = json.loads(table.columns[4].comment) # XXX see P.py
    else:
        options = {'pc_abi': '0.4001'}
    all_students = get_students_from_group(table, comment)
    abis = remove_present(the_column, all_students)
    if all_students:
        percent = len(abis) / len(all_students)
    else:
        percent = 0
    log('record_abi expected={} abi={} percent={} max_percent={}\n'.format(
        len(all_students), len(abis), percent, options['pc_abi']))
    max_percent = float(options['pc_abi'].strip())
    if mailresp or percent > max_percent:
        if table.masters:
            masters = configuration.Users(table.masters)
            masters_mail = [master.mail for master in masters]
        else:
            masters_mail = ()
    if percent > max_percent:
        if masters_mail:
            masters = configuration.Users(table.masters)
            percent = int(100*percent)
            max_percent = int(100*max_percent)
            utilities.send_mail_in_background(
                masters_mail,
                '[{}] {}'.format(configuration.abi, table),
                utilities._('P_ABI_not_recorded').format(**locals()))
        return
    # Record ABI in the table
    users = configuration.Users(abis)
    status = {}
    is_justified = set()
    with table.the_lock:
        # RW page so the student may register with another QRCode
        # and any teacher can remove the ABINJ
        page = table.get_rw_page() # get_a_page_for((the_column.author,))
        for user in users:
            lin_id = table.the_key_dict.get(user.ID_, (None,))[0]
            if lin_id is None:
                lin_id = table.create_line_id()
                table.cell_change(page, table.columns[0].the_id, lin_id, user.student_id)
            if table.semester == 'P':
                # XXX see P.py
                status[user.ID_] = table.lines[lin_id][3].value or table.columns[3].empty_is or user.status
            if justified_leave(table.year, table.semester, user):
                is_justified.add(user.ID_)
                table.cell_change(page, the_column.the_id, lin_id, configuration.abj)
            else:
                table.cell_change(page, the_column.the_id, lin_id, configuration.abi)
            table.comment_change(page, the_column.the_id, lin_id, full_comment)
    # Send mail to the ABI students
    if table.semester != 'P':
        return
    short_teacher = teacher.split('@')[0]
    message = f"{configuration.abi} {code_ue} {short_teacher}"
    mail_content = options['abi_content'].format(
        code_ue + ' / ' + short_teacher + ' (' + table.ue + ' ' + table.table_title + ')', short_teacher)
    abi_cc = options.get('abi_cc', '').strip()
    abi_list = []
    for user in users:
        if user.mail:
            if re.match(options['student_rule'], status[user.ID_]):
                if user.ID_ in is_justified:
                    log('record_abj {}\n'.format(user.ID_))
                    abi_list.append([configuration.abj, user])
                    continue
                abi_list.append([configuration.abi, user])
                if mailresp:
                    cc_mails = masters_mail
                else:
                    cc_mails = ()
                if abi_cc:
                    cc_mails = list(cc_mails) + re.split('[ \n]+', abi_cc)
                log('record_abi {} {} {}\n'.format(user.ID_, user.mail, cc_mails))
                utilities.send_mail_in_background(
                    user.mail, message, mail_content, cc=cc_mails, show_to=True,
                    frome='no-reply-' + configuration.maintainer)
            else:
                log('record_abi no mail needed (student_rule) for «{}»\n'
                    .format(user.ID_)) # pragma: no cover
        else:
            log('record_abi unknown mail for «{}»\n'.format(user.ID_)) # pragma: no cover
    if '@' in teacher and abi_list:
        message = f"{configuration.abi} {code_ue} {group}"
        content = [
            # Course Table URL
            utilities._("TH_home_ue"), ': ',
            '%s/%s/%s/%s\n\n' % (
            configuration.server_url, table.year, semester, code_ue),
            # Presence Table URL, only the good column
            utilities._("MSG_P_title").format(code_ue + ' ' + group), ': ',
            '%s/%s/P/%s/=columns_filter=%s\n\n' % (
            configuration.server_url, table.year, table.ue, the_column.title),
        ]
        for what, user in abi_list:
            content.append(f'{what} {user.surname} {user.firstname}\n')
        utilities.send_mail_in_background(
            teacher, message, ''.join(content), show_to=True)


SVC_dir = os.path.join('TMP', 'SVC') # pylint: disable=invalid-name

class SVCQRCode: # pylint: disable=too-many-instance-attributes
    """Utility to manage a QRCode"""
    presences = None # The Presences table (may be P template or not)
    line_id = None # Student line_id in the presence table
    column = None # The presence column
    def __init__(self, server, datacol, qrcode_id, value, comment, checksum): # pylint: disable=too-many-arguments
        self.year = server.the_year
        self.semester = server.the_semester
        self.user = server.ticket.user
        self.ue = server.the_ue # pylint: disable=invalid-name
        self.ticket = server.ticket
        self.server = server
        self.datacol = datacol
        self.qrcode_id = int(qrcode_id)
        self.value = value
        self.comment = comment
        self.checksum = checksum
        self.do_close = self.value in '_' and self.comment in '_' # 'in' to keep old QRCode working
        self.create_column = self.datacol in 'cCD'
        self.presence_table = server.the_semester == 'P'
        self.link = '/'.join(
            (str(self.year), self.semester, self.ue, datacol, qrcode_id, value, comment))
    def valid(self):
        """The checksum is valid"""
        return tableinvitation.checksum_short(self.link) == self.checksum
    def closed(self):
        """This QRCode is no more usable"""
        return -self.qrcode_id in self.column.qrcode_prst
    def expired(self):
        """This QRCode is expired"""
        return self.datacol in 'cC' and self.column.title[:10] != time.strftime('%Y-%m-%d')
    def record_qrcode_prst(self, qrcode_prst):
        "The active QRCode list has been updated: record the new one"
        with self.presences.the_lock:
            page = self.presences.get_a_page_for((self.column.author,))
            result = column.ColumnAttr.attrs['qrcode_prst'].set(
                self.presences, page, self.column, tuple(qrcode_prst),
                time.strftime('%Y%m%d%H%M%S'))
            assert result == 'ok.png'
    def record_close(self):
        """Close the QRCode"""
        qrcode_prst = list(self.column.qrcode_prst)
        if self.qrcode_id in qrcode_prst:
            qrcode_prst.remove(self.qrcode_id)
        qrcode_prst.append(-self.qrcode_id)
        try:
            record_abi(self.presences, self.column, self.qrcode_id, self.datacol == 'c')
            self.record_qrcode_prst(qrcode_prst)
        except: # pylint: disable=bare-except # pragma: no cover
            # To be sure that a bug will not make recording fail
            utilities.send_backtrace('', 'SVC recording ABI')
        self.active_file_delete()

    def active_file_name(self):
        """This filename exists if the QRCode is active"""
        comment = self.comment.split(' ')
        if len(comment) == 5:
            duration = int(comment[4])
        else:
            comment = get_comment(self.qrcode_id, self.column, first=True).split(' ')
            if len(comment) == 5:
                duration = int(comment[4].split('#')[0])
            else:
                duration = 0
        return os.path.join(SVC_dir, '{}#{}#{}#{}#{}#{}'.format(
            self.year, self.semester, self.ue, self.datacol, duration, self.qrcode_id))
    def active_file_create(self):
        """Create the file"""
        if self.presence_table:
            filename = self.active_file_name()
            if not os.path.exists(filename):
                utilities.mkpath(SVC_dir, create_init=False)
                open(filename, "w").close()

    def active_file_delete(self):
        """Delete the file"""
        if self.presence_table:
            filename = self.active_file_name()
            if os.path.exists(filename):
                os.unlink(filename)

    def record_prst(self):
        """Record the value (PRST) in the table"""
        self.active_file_create()
        for comment in self.comment.split(' '):
            if '==' in comment:
                in_a_good_group = False
                errors = []
                for title_value in comment.split('+'):
                    title, value = title_value.split('==', 1)
                    # Spaces in Grp name were replaced by unsecable spaces
                    value = value.replace(' ', ' ') # Restore space in group name
                    col = self.presences.columns.from_title(title)
                    if col:
                        grp = self.presences.lines[self.line_id][col.data_col].value.strip('_')
                        if grp == value:
                            in_a_good_group = True
                            break
                        errors.append(
                            html.escape(title) + ' '
                            + html.escape(value) + '≠' +  html.escape(grp))
                if not in_a_good_group:
                    return '<br>'.join(errors)
        with self.presences.the_lock:
            page = self.presences.new_page(
                self.ticket.ticket, self.ticket.user_name,
                self.ticket.user_ip, self.ticket.user_browser, recycle=True)
            if self.value:
                result = self.presences.cell_change(
                    page, self.column.the_id, self.line_id, self.value)
            result2 = self.presences.comment_change(
                page, self.column.the_id, self.line_id,
                self.comment + '#{}'.format(self.qrcode_id))
            if result2 != 'ok.png':
                result = result2
        if result == 'ok.png':
            if self.qrcode_id not in self.column.qrcode_prst:
                self.record_qrcode_prst(self.column.qrcode_prst + (self.qrcode_id,))
            # XXX case '==' and groups ending with '_' for enumeration + repetition
            if '==' not in self.comment and self.user.ID_ not in get_students_from_group(self.presences, self.comment):
                result = '<BADGROUP>'
        return result

    def line_id_is_fine(self):
        """Return the line_id of the student in the presence table.
        If it does not exists, create one.
        Returns None
        """
        self.init_presences()
        try:
            self.line_id = next(iter(self.presences.the_keys()[self.user.ID_]))
        except StopIteration:
            if not self.do_close:
                # Add a line with the student
                if not self.create_column:
                    self.server.the_file.write(self.server._("MSG_tablelinear_registered_no"))
                    return False
                with self.presences.the_lock:
                    page = self.presences.get_nobody_page()
                    self.line_id = self.presences.create_line_id()
                    self.presences.cell_change(page, self.presences.columns[0].the_id,
                                               self.line_id, self.user.student_id)
        return True

    def init_presences(self):
        """Load the table in which presences are registered"""
        if not self.presences:
            self.presences = document.table(self.year, self.semester, self.ue) # May create

    def search_column(self): # pylint: disable=too-many-branches
        """
        If datacol=='D' column names are yet created and must have the format:
            YYYY-mm-dd_HH:MM
        Typical names are:
            2021-05-25_08:00 First registration possible until (08:00 + 09:45) / 2
            2021-05-25_09:45
            2021-05-25_11:30
            2021-05-25_14:00

        If datacol in 'cC', columns are created when needed.
        """
        if not self.create_column:
            self.column = self.presences.columns[int(self.datacol)]
            return
        self.init_presences()
        # Use a cache because there are many columns
        if not hasattr(self.presences, 'qrcode_cache'):
            self.presences.qrcode_cache = {}
        if self.qrcode_id in self.presences.qrcode_cache:
            self.column = self.presences.qrcode_cache[self.qrcode_id]
            return

        day, hour, minute = time.strftime('%Y-%m-%d_%H_%M').split('_')
        now = 60 * int(hour) + int(minute)
        previous_time = 0
        previous_column = None
        for a_column in self.presences.columns:
            if self.qrcode_id in a_column.qrcode_prst or -self.qrcode_id in a_column.qrcode_prst:
                # no cover because table.qrcode_cache is used if the server is not rebooted.
                previous_column = a_column # pragma: no cover
                break # pragma: no cover
            if not a_column.title.startswith(day):
                continue
            try:
                new_time = 60 * int(a_column.title[-5:-3]) + int(a_column.title[-2:])
            except ValueError: # pragma: no cover
                new_time = None
            if now < (previous_time + new_time) // 2:
                # This is only possible if the columns are created by hand
                break # pragma: no cover
            previous_time = new_time
            previous_column = a_column
        else:
            # No break, so starting a new qrcode_id on or after the last column
            if self.datacol in 'cC':
                if previous_column:
                    # Check if the LAST column is too old
                    if now - new_time > 60:
                        previous_column = None # pragma: no cover
                if previous_column is None:
                    # Create a new column
                    date = time.strftime('%Y%m%d%H%M%S')
                    now = time.strftime('%Y-%m-%d_%H:%M')
                    minutes = int(int(now[-2:]) / 15 + 0.5) * 15
                    if minutes == 60: # pragma: no cover
                        hours = int(now[-5:-3]) + 1 # May not increment day
                        now = f"{now[:-5]}{hours:02d}:00"
                    else: # pragma: no cover
                        now = f"{now[:-2]}{minutes:02d}"
                    with self.presences.the_lock:
                        page = self.presences.get_nobody_page()
                        previous_column = self.presences.add_empty_column(page)
                        column.ColumnAttr.attrs['type'].set(
                            self.presences, page, previous_column, 'Prst', date)
                        column.ColumnAttr.attrs['title'].set(
                            self.presences, page, previous_column, now, date)

        self.column = self.presences.qrcode_cache[self.qrcode_id] = previous_column

    def get_html(self, message): # pylint: disable=too-many-branches
        """Create the message to display to the human (on the smartphone)"""
        lines = [
            '''<style>
            BODY { font-size: 5vw ; padding: 0.5em }
            .status { border: 8px solid black ; text-align: center }
            </style>
            '''
            ]
        if configuration.real_regtest:
            self.comment += '#{}'.format(self.qrcode_id)
        if self.comment != '_':
            lines.append(self.comment)
        stats = statistics(self.column, self.qrcode_id)
        if len(stats) > 1:
            lines.append('<ul>')
            for value, nr_values in stats.items():
                lines.append('<li> {} {}'.format(nr_values, value))
            lines.append('</ul>')
        elif len(stats) == 1:
            value, nr_values = next(iter(stats.items()))
            lines.append('<br><b>{} {}</b>'.format(nr_values, value))
        name = '{} {}'.format(self.user.firstname.title(), self.user.surname)
        if self.line_id:
            lines.append('<div class="status">')
            lines.append(name)
            lines.append('<br>')
            lines.append(self.presences.lines[self.line_id][self.column.data_col].value or '???')
            lines.append('</div>')
        else:
            lines.append(name)
        if message == "EXPIRED": # pragma: no cover
            lines.append(
                '<div style="background: #FAA">'
                + self.server._("MSG_QRCode_expired")
                + '</div>')
        elif message == "CLOSED":
            lines.append(
                '<div style="background: #FAA">'
                + self.server._("MSG_QRCode_stopped")
                + '</div>')
        elif message == "<BADGROUP>":
            lines.append(
                '<div style="background: #FAA">'
                + self.server._("MSG_QRCode_unexpected")
                + '</div>')
        elif message == "ok.png":
            pass
        elif message.endswith('.png'):
            lines.append(
                '<div style="background: #F00">'
                + self.server._("ERROR_value_not_modifiable")
                + '</div>')
        else:
            lines.append('<div style="background: #F00">' + message + '</div>')
        img = '<img src="{}" style="width:100%"></a>'.format(
            configuration.picture(self.user.student_id, self.ticket))
        if configuration.rgpd_link:
            img = '<a href="{}">{}</a>'.format(configuration.rgpd_link, img)
        lines.append(img)
        return '\n'.join(lines)

def SVC(server): # pylint: disable=invalid-name
    """
    Signed URL allowing to modify any table cell value/comment if it is allowed.
    The table does not need to be visible on the suivi.
    Only the value/comment defined by the URL is allowed.
    Thr signed URL can be expired/invalidated by another one.

    The URL syntax is:
    /SVC/{Y}/{S}/{U}/datacol|C|D/qrcode_id/value/comment/checksum

    If the 'datacol' is 'D' then the column title to fill must match the current date and hour.
    On first use the column is fixed and will not change.

    If the 'datacol' is 'C' is works as 'D' but the column is automaticaly created.
    One column per hour may be created.

    If the 'datacol' is 'c' is works as 'C' but the ABI mail is
    also sent to the table manager.

    'qrcode_id' is an integer but not zero.

    Multiple values for value/comment are possible for the same 'qrcode_id'
    Empty value and comment close forever the possibility to use this 'qrcode_id'.

    The comment may contain 'columnTitle==cellValue'
    The PRST recording is done only is the expected value match the student one.
    So the student must flashes the QRCode of the good group to be PRST.
    If cellValue contains spaces, they are replaced by non-secable spaces
    in the URL and cell comments to not break comment parsing.
    Comment for P template table:  UE Teacher Groups Semester Duration
    Comment for normal table : Teacher Groups

    'column.qrcode_prst = [2, -1]' indicates that qrcode 2 is running and 1 is closed
    """
    datacol, qrcode_id, value, comment, checksum = server.the_path
    qrcode = SVCQRCode(server, datacol, qrcode_id, value, comment, checksum)
    if not qrcode.valid():
        message = "HACKER"
        server.the_file.write(message)
        return
    if not qrcode.line_id_is_fine():
        return
    qrcode.search_column()
    if qrcode.column is None:
        server.the_file.write('No column')
        return
    if qrcode.closed():
        message = "CLOSED"
    elif qrcode.expired():
        message = "EXPIRED" # pragma: no cover # Not the same day
    elif qrcode.do_close:
        message = "CLOSED"
        qrcode.record_close()
    else:
        message = qrcode.record_prst()

    log('("{}",{},{},{})\n'.format(
        time.strftime('%Y%m%d%H%M%S'), repr(qrcode.link), repr(server.ticket.user_name),
        repr(message)))

    server.the_file.write(
        qrcode.get_html(message)
        + '<img width=1 height=1 src="%s">' % configuration.authenticator.logout(server)
        ) # Last to not be stopped by exception
    # Let some time to load the student picture
    utilities.start_job(lambda: server.ticket.remove_this_ticket(), 1) # pylint: disable=unnecessary-lambda


plugin.Plugin('SVC', '/SVC/{Y}/{S}/{U}/{*}', function=SVC,
              priority=-10, unsafe=False)


config_cron.default_threads += (
    ('SVC', ('2000-01-01 00:00', '1', '', 'python:configuration.expire_qrcode(6*3600)')),
)

def expire_qrcode(ttl):
    """Automatically close QRCode after TTL seconds"""
    if not os.path.exists(SVC_dir):
        return # pragma: no cover
    class Server: # pylint: disable=too-few-public-methods
        """Fake class"""
        class ticket: # pylint: disable=invalid-name
            """Fake class"""
            user = None
    now = time.time()
    for filename in os.listdir(SVC_dir):
        start = os.path.getmtime(os.path.join(SVC_dir, filename))
        (Server.the_year, Server.the_semester, Server.the_ue, datacol, duration, qrcode_id
        ) = filename.split('#')
        if ttl and duration != '0': # pragma: no cover
            ttl = 60 * int(duration) + 15*60 # some more time
        if now - start > ttl:
            qrcode = SVCQRCode(Server, datacol, qrcode_id, '', 'U T G S {}'.format(duration), '')
            qrcode.search_column()
            qrcode.record_close()

configuration.expire_qrcode = expire_qrcode

# Only for regression tests
plugin.Plugin('expire_qrcode', '/expire_qrcode',
              function=lambda server: expire_qrcode(0),
              group='roots')


###############################################################################
###############################################################################
# A single random QRCode pair to print
###############################################################################
###############################################################################

def get_pair(server):
    """Returns the QRCode image and URL"""
    if len(server.the_path) == 4:
        datacol, start, qrcode_id, more = server.the_path
        more = ' ' + more
    else:
        datacol, start, qrcode_id = server.the_path
        more = ''
    assert qrcode_id.isdigit()
    path = [str(server.the_year), server.the_semester, server.the_ue, datacol, qrcode_id]
    if int(start):
        path.extend((configuration.pre, server.ticket.user_name + more))
    else:
        path.extend(('_', '_'))
    url = '%s/SVC/%s/%s' % (
        configuration.server_url,
        '/'.join(urllib.request.quote(i, safe='') for i in path),
        tableinvitation.checksum_short('/'.join(path)))
    image = io.BytesIO()
    qrcodelib.make(url, box_size=1, border=1).save(image, format='PNG')
    server.the_file.write(image.getvalue())
    server.close_connection_now()

plugin.Plugin('get_pair', '/{Y}/{S}/{U}/get_pair/{*}', function=get_pair,
              mimetype='image/png')
