# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later

from io import StringIO
import re

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Field, HTML, Layout, Submit
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils.translation import gettext_lazy as _
import pandas
from pypdf import PdfReader
from registration.models import VolunteerRegistration

from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview


class TeamForm(forms.ModelForm):
    """
    Form to create a team, with the name and the trigram,...
    """
    def clean_name(self):
        if "name" in self.cleaned_data:
            name = self.cleaned_data["name"]
            if Team.objects.filter(name=name).exclude(pk=self.instance.pk).exists():
                raise ValidationError(_("This name is already used."))
            return name

    def clean_trigram(self):
        if "trigram" in self.cleaned_data:
            trigram = self.cleaned_data["trigram"].upper()
            if not re.match("[A-Z]{3}", trigram):
                raise ValidationError(_("The trigram must be composed of three uppercase letters."))

            if Team.objects.filter(trigram=trigram).exclude(pk=self.instance.pk).exists():
                raise ValidationError(_("This trigram is already used."))
            return trigram

    class Meta:
        model = Team
        fields = ('name', 'trigram',)


class JoinTeamForm(forms.ModelForm):
    """
    Form to join a team by the access code.
    """
    def clean_access_code(self):
        access_code = self.cleaned_data["access_code"]
        if not Team.objects.filter(access_code=access_code).exists():
            raise ValidationError(_("No team was found with this access code."))
        else:
            team = Team.objects.get(access_code=access_code)
            if team.participation.valid is not None:
                raise ValidationError(_("The team is already validated or the validation is pending."))
        return access_code

    def clean(self):
        cleaned_data = super().clean()
        if "access_code" in cleaned_data:
            team = Team.objects.get(access_code=cleaned_data["access_code"])
            self.instance = team
        return cleaned_data

    class Meta:
        model = Team
        fields = ('access_code',)


class ParticipationForm(forms.ModelForm):
    """
    Form to update the problem of a team participation.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if settings.SINGLE_TOURNAMENT:
            del self.fields['tournament']
        self.helper = FormHelper()
        idf_text = _(
            'For the tournaments in the region "Île-de-France": registration is '
            'unified for each tournament. By choosing a tournament "Île-de-France", '
            "you're accepting that your team may be selected for one of these tournaments. "
            'In case of date conflict, please write them in your motivation letter.'
        )

        idf_warning_banner = f"""
<div class=\"alert alert-warning\">
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
{idf_text}
</div>
"""
        unified_registration_tournament_ids = ",".join(
            str(tournament.id) for tournament in Tournament.objects.filter(
                unified_registration=True).all())
        self.helper.layout = Layout(
            'tournament',
            Div(
                HTML(idf_warning_banner),
                css_id="idf_warning_banner",
                data_tid_unified=unified_registration_tournament_ids,
            ),
            'final',
        )
        self.helper.form_tag = False

    class Meta:
        model = Participation
        fields = ('tournament', 'final',)


class MotivationLetterForm(forms.ModelForm):
    def clean_motivation_letter(self):
        if "motivation_letter" in self.files:
            file = self.files["motivation_letter"]
            if file.size > 2e6:
                raise ValidationError(_("The uploaded file size must be under 2 Mo."))
            if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
                raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
            return self.cleaned_data["motivation_letter"]

    class Meta:
        model = Team
        fields = ('motivation_letter',)


class RequestValidationForm(forms.Form):
    """
    Form to ask about validation.
    """
    _form_type = forms.CharField(
        initial="RequestValidationForm",
        widget=forms.HiddenInput(),
    )

    engagement = forms.BooleanField(
        label=_("I engage myself to participate to the whole tournament."),
        required=True,
    )


class ValidateParticipationForm(forms.Form):
    """
    Form to let administrators to accept or refuse a team.
    """
    _form_type = forms.CharField(
        initial="ValidateParticipationForm",
        widget=forms.HiddenInput(),
    )

    message = forms.CharField(
        label=_("Message to address to the team:"),
        widget=forms.Textarea(),
    )


class TournamentForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if settings.NB_ROUNDS < 3:
            del self.fields['date_third_phase']
            del self.fields['solutions_available_third_phase']
            del self.fields['reviews_third_phase_limit']
        if not settings.PAYMENT_MANAGEMENT:
            del self.fields['price']

    class Meta:
        model = Tournament
        exclude = ('notes_sheet_id', )
        widgets = {
            'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
            'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
            'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
            'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
            'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
            'date_first_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
            'reviews_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
                                                             format='%Y-%m-%d %H:%M'),
            'date_second_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
            'reviews_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
                                                              format='%Y-%m-%d %H:%M'),
            'date_third_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
            'reviews_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
                                                             format='%Y-%m-%d %H:%M'),
            'organizers': forms.SelectMultiple(attrs={
                'class': 'selectpicker',
                'data-live-search': 'true',
                'data-live-search-normalize': 'true',
                'data-width': 'fit',
            })
        }


class SolutionForm(forms.ModelForm):
    def clean_file(self):
        if "file" in self.files:
            file = self.files["file"]
            if file.size > 5e6:
                raise ValidationError(_("The uploaded file size must be under 5 Mo."))
            if file.content_type != "application/pdf":
                raise ValidationError(_("The uploaded file must be a PDF file."))
            pdf_reader = PdfReader(file)
            pages = len(pdf_reader.pages)
            if pages > 30:
                raise ValidationError(_("The PDF file must not have more than 30 pages."))
            return self.cleaned_data["file"]

    def save(self, commit=True):
        """
        Don't save a solution with this way. Use a view instead
        """

    class Meta:
        model = Solution
        fields = ('problem', 'file',)


class PoolForm(forms.ModelForm):
    class Meta:
        model = Pool
        fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'jury_president', 'juries',)
        widgets = {
            "jury_president": forms.Select(attrs={
                'class': 'selectpicker',
                'data-live-search': 'true',
                'data-live-search-normalize': 'true',
            }),
            "juries": forms.SelectMultiple(attrs={
                'class': 'selectpicker',
                'data-live-search': 'true',
                'data-live-search-normalize': 'true',
            }),
        }


class AddJuryForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['first_name'].required = True
        self.fields['last_name'].required = True
        self.fields['email'].required = True
        self.helper = FormHelper()
        self.helper.form_class = 'form-inline'
        self.helper.layout = Div(
            Div(
                Div(
                    Field('email', autofocus="autofocus", list="juries-email"),
                    css_class='col-md-5 px-1',
                ),
                Div(
                    Field('first_name', list="juries-first-name"),
                    css_class='col-md-3 px-1',
                ),
                Div(
                    Field('last_name', list="juries-last-name"),
                    css_class='col-md-3 px-1',
                ),
                Div(
                    Submit('submit', _("Add")),
                    css_class='col-md-1 py-md-4 px-1',
                ),
                css_class='row',
            )
        )

    def clean_email(self):
        """
        Ensure that the email address is unique.
        """
        email = self.data["email"]
        if User.objects.filter(email=email).exists():
            self.instance = User.objects.get(email=email)
            if self.instance.registration.participates:
                self.add_error(None, _("This user already exists, but is a participant."))
                return
        return email

    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email',)


class UploadNotesForm(forms.Form):
    file = forms.FileField(
        label=_("Spreadsheet file:"),
        validators=[FileExtensionValidator(allowed_extensions=["csv", "ods"])],
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['file'].widget.attrs['accept'] = 'text/csv,application/vnd.oasis.opendocument.spreadsheet'

    def clean(self):
        cleaned_data = super().clean()

        if 'file' in cleaned_data:
            file = cleaned_data['file']
            if file.name.endswith('.csv'):
                with file:
                    try:
                        data: bytes = file.read()
                        try:
                            content = data.decode()
                        except UnicodeDecodeError:
                            # This is not UTF-8, grrrr
                            content = data.decode('latin1')

                        table = pandas.read_csv(StringIO(content), sep=None, header=None)
                        self.process(table, cleaned_data)
                    except UnicodeDecodeError:
                        self.add_error('file', _("This file contains non-UTF-8 and non-ISO-8859-1 content. "
                                                 "Please send your sheet as a CSV file."))
            elif file.name.endswith('.ods'):
                table = pandas.read_excel(file, header=None, engine='odf')
                self.process(table, cleaned_data)

        return cleaned_data

    def process(self, df: pandas.DataFrame, cleaned_data: dict):
        parsed_notes = {}
        pool_size = 0
        line_length = 0
        for line in df.values.tolist():
            # Remove NaN
            line = [s for s in line if s == s]
            # Strip cases
            line = [str(s).strip() for s in line if str(s)]
            if line and line[0] in ["Problème", "Problem"]:
                pool_size = len(line) - 1
                line_length = 2 + (8 if df.iat[1, 8] == "Observer" else 6) * pool_size
                continue

            if pool_size == 0 or len(line) < line_length:
                continue

            name = line[0]
            if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe",
                                "role", "juree", "average", "coefficient", "subtotal", "team"]:
                continue
            notes = line[2:line_length]
            if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
                continue
            notes = list(map(lambda x: int(float(x)), notes))

            max_notes = pool_size * [20 if settings.TFJM_APP == "TFJM" else 10,
                                     20 if settings.TFJM_APP == "TFJM" else 10,
                                     10, 10, 10, 10, 10, 10]
            for n, max_n in zip(notes, max_notes):
                if n > max_n:
                    self.add_error('file',
                                   _("The following note is higher of the maximum expected value:")
                                   + str(n) + " > " + str(max_n))

            # Search by volunteer id
            jury = VolunteerRegistration.objects.filter(pk=int(float(line[1])))
            if jury.count() != 1:
                raise ValidationError({'file': _("The following user was not found:") + " " + name})
            jury = jury.get()
            parsed_notes[jury] = notes

        print(parsed_notes)

        cleaned_data['parsed_notes'] = parsed_notes

        return cleaned_data


class PassageForm(forms.ModelForm):
    def clean(self):
        cleaned_data = super().clean()
        if "reporter" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
                and len({cleaned_data["reporter"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
            self.add_error(None, _("The reporter, the opponent and the reviewer must be different."))
        if "reporter" in self.cleaned_data and "solution_number" in self.cleaned_data \
                and not Solution.objects.filter(participation=cleaned_data["reporter"],
                                                problem=cleaned_data["solution_number"]).exists():
            self.add_error("solution_number", _("This reporter did not work on this problem."))
        return cleaned_data

    class Meta:
        model = Passage
        fields = ('position', 'solution_number', 'reporter', 'opponent', 'reviewer', 'opponent', 'reporter_penalties',)


class WrittenReviewForm(forms.ModelForm):
    def clean_file(self):
        if "file" in self.files:
            file = self.files["file"]
            if file.size > 2e6:
                raise ValidationError(_("The uploaded file size must be under 2 Mo."))
            if file.content_type != "application/pdf":
                raise ValidationError(_("The uploaded file must be a PDF file."))
            pdf_reader = PdfReader(file)
            pages = len(pdf_reader.pages)
            if pages > 2:
                raise ValidationError(_("The PDF file must not have more than 2 pages."))
            return self.cleaned_data["file"]

    def save(self, commit=True):
        """
        Don't save a written review with this way. Use a view instead
        """

    class Meta:
        model = WrittenReview
        fields = ('file',)


class NoteForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not settings.HAS_OBSERVER:
            del self.fields['observer_writing']
            del self.fields['observer_oral']

    class Meta:
        model = Note
        fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
                  'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', )
