#!/bin/env python3
# -*- coding: utf-8 -*-
#    TOMUSS: The Online Multi User Simple Spreadsheet
#    Copyright (C) 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

"""
Add analyser column type.

Launch SCRIPT/analyse.py on a sandboxed remote host to do the grading


nom = STUDENTS.add_competence('Compétence Nom', 2)
prenom = STUDENTS.add_competence('Compétence Prénom', 1)

for student in STUDENTS:
    for cell in student:
        if cell.new.value.lower() == 'thierry':
            student.grade(prenom, 1, 'Super')
        if cell.new.value.lower() == 'excoffier':
            student.grade(nom, 2, 'Génial')

************** Analyser : cycle mémoire ******************


"""

# Test binome

import json
import io
import zipfile
import csv
import subprocess
import os
import html
import time
import re
from . import notation
from .. import plugin
from .. import document
from .. import utilities
from .. import configuration
from ..TEMPLATES import config_table

class Analyser(notation.Notation):
    """Automatic grading with a teacher script"""
    type_type = 'computed'
    attributes_visible = notation.Notation.attributes_visible  + ('analyser_config', 'columns')

SCRIPT = """
cat >$$.zip
mkdir $$
cd $$
unzip -qq ../$$.zip
rm ../$$.zip
ulimit -t $(expr {} '*' $(wc -l <students.csv))
ulimit -v {}
python3 analyser.py
cd ..
rm -rf $$
"""

config_table.variable_list.append("analyser_logins")
configuration.analyser_logins = ''

config_table.variable_list.append("analyser_max_time")
configuration.analyser_max_time = 1

config_table.variable_list.append("analyser_max_memory")
configuration.analyser_max_memory = 537395200

def launch_process(host):
    """
    Launch the analyser process locally (unsafe) or on a remote host.
    """
    script = SCRIPT.format(configuration.analyser_max_time, configuration.analyser_max_memory)
    if configuration.real_regtest:
        return subprocess.Popen(script,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                shell=True)
    return subprocess.Popen(['ssh', host, script], # pragma: no cover
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)

configuration.analyser_in_use = set()

@utilities.add_a_lock
def get_host():
    """Get an unused host"""
    if configuration.real_regtest:
        return ''
    while True: # pragma: no cover
        free = set(re.split('[ \t\n]+', configuration.analyser_logins.strip())
                  ) - configuration.analyser_in_use
        if free:
            host = free.pop()
            configuration.analyser_in_use.add(host)
            return host
        time.sleep(1)



class Analysing: # pylint: disable=too-many-instance-attributes
    """Analyser state"""
    def __init__(self, server, check, trace, lines=None, column=None): #pylint: disable=too-many-arguments
        self.server = server
        self.check = check
        self.trace = trace
        self.table = document.table(server.the_year, server.the_semester, server.the_ue,
                                    create=False)
        if column:
            self.column = column
        else:
            self.column = self.table.columns[int(server.something)]

        if not self.column.readable_by(server.ticket.user_name): # pragma: no cover
            raise ValueError('Hacker')

        self.columns = [self.table.columns.from_title(title)
                        for title in self.column.get_columns()]
        if lines is None:
            lines = tuple(self.table.lines)
        self.lines = lines

        analyser = json.loads(self.column.analyser_config)
        if check:
            self.script = analyser['script_U']
            self.upload = analyser['script_U_upload']
            self.old_upload = analyser['script_U_old_upload']
            if analyser['script_G_on_upload']:
                analyser_add_background(self)
        else:
            self.script = analyser['script_G']
            self.upload = analyser['script_G_upload']
            self.old_upload = analyser['script_G_old_upload']
        self.script = self.script.strip()

    def log(self, txt):
        """Browser feedback if not student upload"""
        if len(self.lines) > 1 and self.trace:
            self.server.the_file.write(txt)

    def fill_zip(self, archive): # pylint: disable=too-many-locals
        """Fill the archive file with uploads and student data"""
        csv_file = io.StringIO()
        writer = csv.writer(csv_file)

        def add_upload(old='', idx=[1]): # pylint: disable=dangerous-default-value
            """Add the uploaded file content and path the CSV line to add its name"""
            filename = os.path.join(configuration.container_path(col), line_id + old) # pylint: disable=no-member,undefined-loop-variable
            if os.path.exists(filename):
                csv_line[-1] = str(idx[0])
                archive.writestr(str(idx[0]), utilities.read_file(filename, encoding='bytes'))
                idx[0] += 1

        nr_student_sent = 0
        self.log(self.server._("MSG_analyser_sent"))
        for line_id in self.lines:
            line = self.table.lines[line_id]
            if line[0].value == '':
                continue
            csv_line = [line_id]
            for col in self.columns:
                if not col.readable_by(self.server.ticket.user_name):
                    continue # pragma: no cover
                cell = line[col.data_col]
                csv_line.extend((cell.value, cell.author, cell.comment, cell.date, ''))
                if self.upload:
                    add_upload()

                if cell.history:
                    csv_line.extend((cell.previous_value(), cell.previous_author(),
                                     cell.previous_comment(), cell.previous_date(), ''))
                    if self.old_upload:
                        add_upload('~')
                else:
                    csv_line.extend(('', '', '', '', ''))
            writer.writerow(csv_line)
            nr_student_sent += 1
            self.log(f' {nr_student_sent}/{len(self.lines)}')
        archive.writestr('students.csv', csv_file.getvalue().encode('utf-8'))

    def get_zip(self, file):
        """Store the ZIP"""
        to_analyse = zipfile.ZipFile(file, mode="w", compression=zipfile.ZIP_STORED,
                                     allowZip64=True)
        self.fill_zip(to_analyse)
        to_analyse.writestr('YourScript.py', self.script.encode('utf-8'))
        to_analyse.writestr('analyser.py', utilities.read_file('SCRIPTS/analyser.py', 'bytes'))
        to_analyse.close()

    def get_result(self):
        """
        Compute the grades.
        """
        if not self.script:
            return {} # No script

        host = get_host()
        try:
            process = launch_process(host)
            self.get_zip(process.stdin)
            process.stdin.close()
            result = process.stdout.read().decode('utf-8')
            errors = process.stderr.read().decode('utf-8')
            process.wait()
        finally:
            if host:
                configuration.analyser_in_use.remove(host) # pragma: no cover
        if errors:
            return {'error': errors}
        return json.loads(result)

configuration.Analysing = Analysing


ANALYSER_TODO = []

def analyser_batch():
    """Grade one student a a time"""
    test_time = None
    while ANALYSER_TODO:
        analyser = ANALYSER_TODO.pop(0)
        analyser = Analysing(analyser.server, False, False, analyser.lines, analyser.column)
        result = analyser.get_result()
        column = analyser.column
        if result == {}:
            pass
        elif 'error' in result:
            utilities.send_mail_in_background(
                configuration.User(column.author).mail,
                str(column) + ':' + column.type.name + ' BUG',
                result['error'])
        else:
            table = column.table
            page = table.get_a_page_for((column.author,))
            with table.the_lock:
                is_ok = table.column_attr(page, column.the_id, 'comment', result['competences'])
            if is_ok != 'ok.png':
                # must not raise an error here to process all the grading
                pass # pragma: no cover
            for line_id, grade in result['students'].items():
                with table.the_lock:
                    is_ok = table.comment_change(page, column.the_id, line_id, grade)
                    if is_ok != 'ok.png':
                        # must not raise an error here to process all the grading
                        pass # pragma: no cover

        test_time = time.time()
    return test_time

def analyser_add_background(analyser):
    """Only one grading job on upload"""
    ANALYSER_TODO.append(analyser)
    if configuration.real_regtest:
        analyser_batch()
    else:
        utilities.start_job(analyser_batch, 1, important='analyser') # pragma: no cover

def analyser_grade(server):
    """Grade students list"""
    analyser = Analysing(server, False, True,
                         server.uploaded.getfirst('line_ids').split(' '))
    result = analyser.get_result()
    if 'error' in result:
        server.the_file.write('<pre>' + html.escape(result['error']) + '</pre>')
        return

    page = analyser.table.get_a_page_for((server.ticket.user_name,))
    with analyser.table.the_lock:
        is_ok = analyser.table.column_attr(page, analyser.column.the_id, 'comment',
                                           result['competences'])
    if is_ok != 'ok.png': # pragma: no cover
        server.the_file.write('<p>BUG: Column update impossible')
        utilities.send_backtrace(str(analyser.column), "Analyser: column update impossible")
        return

    server.the_file.write('<p>')
    for line_id, grade in result['students'].items():
        with analyser.table.the_lock:
            is_ok = analyser.table.comment_change(page, analyser.column.the_id, line_id, grade)
            if is_ok == 'ok.png':
                server.the_file.write(' ' + analyser.table.lines[line_id][0].value)
            else:
                server.the_file.write(' <span style="background: #F00">'
                                      + analyser.table.lines[line_id][0].value
                                      + '</span>')

plugin.Plugin('analyser_grade', '/{Y}/{S}/{U}/analyser_grade/{?}',
              upload_max_size=100000,
              function=analyser_grade, launch_thread=True)

def analyser_zip(server):
    """Let the teacher get the ZIP to test its script"""
    analyser = Analysing(server, False, False)
    analyser.script = """# Put your checking or grading script in this file
# To check the execution, run: « python3 analyser.py »
    """
    with os.fdopen(server.the_file.fileno(), 'wb') as file:
        analyser.get_zip(file)
        server.close_connection_now() # XXX Rare random socket problem if not here

plugin.Plugin('analyser_zip', '/{Y}/{S}/{U}/analyser_zip/{?}',
              mimetype="application/zip",
              function=analyser_zip, launch_thread=True)
