import dataclasses
import datetime

import caldav
from django.conf import settings
from django.http import Http404
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.text import slugify
from django.views.generic.base import RedirectView, TemplateView
from django.views.generic.dates import MonthMixin, YearMixin
from lxml.etree import XMLSyntaxError


class ReportError(Exception):
    pass


@dataclasses.dataclass
class Holiday:
    start: datetime.date
    end: datetime.date
    half: bool = False

    def __str__(self):
        if self.half:
            return '0,5j le %s' % date_format(self.start, 'd/m')
        elif self.start == self.end:
            return 'le %s' % date_format(self.start, 'd/m')
        elif (self.end - self.start).days == 1:
            return 'les %s et %s' % (date_format(self.start, 'd'), date_format(self.end, 'd/m'))

        return 'du %s au %s' % (date_format(self.start, 'd'), date_format(self.end, 'd/m'))


class WorkersMonthlyReportRedirect(RedirectView):
    pattern_name = 'admin:conges-workers-monthly-report'

    def get_redirect_url(self, *args, **kwargs):
        today = datetime.date.today()
        kwargs['year'] = today.year
        kwargs['month'] = today.month
        return super().get_redirect_url(*args, **kwargs)


workers_monthly_report_redirect = WorkersMonthlyReportRedirect.as_view()


class WorkersMonthlyReport(MonthMixin, YearMixin, TemplateView):
    template_name = 'eo_conges/workers_monthly_report.html'

    def dispatch(self, *args, **kwargs):
        try:
            self.date = datetime.date(self.get_year(), self.get_month(), 1)
        except ValueError:
            raise Http404()

        self.last_date = (self.date + datetime.timedelta(days=40)).replace(day=1) - datetime.timedelta(days=1)

        return super().dispatch(*args, **kwargs)

    def get_previous_month_url(self):
        previous_month = self.date - datetime.timedelta(days=1)
        return reverse(
            'admin:conges-workers-monthly-report',
            kwargs={'year': previous_month.year, 'month': previous_month.month},
        )

    def get_next_month_url(self):
        next_month = self.last_date + datetime.timedelta(days=1)
        return reverse(
            'admin:conges-workers-monthly-report', kwargs={'year': next_month.year, 'month': next_month.month}
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context['errors'] = self.errors = []
        try:
            context['workers'] = self.build_workers_data()
        except ReportError as e:
            context['fatal_error'] = e

        return context

    def get_events(self):
        config = settings.WORKERS_CALENDAR_CONFIG
        client = caldav.DAVClient(config['url'], username=config['username'], password=config['password'])
        try:
            calendar = client.principal().calendar(name=config['calendar'])
        except caldav.error.DAVError as e:
            raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)

        try:
            events = calendar.search(event=True, start=self.date, end=self.last_date)
        except caldav.error.DAVError as e:
            raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)
        except (XMLSyntaxError, TypeError):
            try:
                events = calendar.search(event=True, start=self.date, end=self.last_date)
            except (XMLSyntaxError, TypeError):
                raise ReportError('Erreur lors de la récupération du calendrier : %s' % e)

        return [x.vobject_instance.vevent for x in events]

    def build_workers_data(self):
        events = self.get_events()
        holidays_events = [x for x in events if self.is_holiday(x)]
        day_off_events = [x for x in events if self.is_day_off(x)]

        workers_data = [
            self.get_data_for_worker(worker, day_off_events, holidays_events)
            for worker in settings.WORKERS_CALENDAR_CONFIG['workers']
        ]
        workers_data = [x for x in workers_data if not x['ignore']]

        events_no_match = [
            x.summary.value for x in holidays_events + day_off_events if not hasattr(x, 'match')
        ]
        if events_no_match:
            self.errors.append(
                'Aucun·e coopérateurice trouvé·e pour les évènements : %s.' % ', '.join(events_no_match)
            )

        return sorted(workers_data, key=lambda x: x['number'])

    def is_day_off(self, event):
        if not hasattr(event, 'categories'):
            return False

        return bool(settings.WORKERS_CALENDAR_CONFIG['day_off_category'] in event.categories.value)

    def is_holiday(self, event):
        if not hasattr(event, 'categories'):
            return False

        return bool(settings.WORKERS_CALENDAR_CONFIG['holiday_category'] in event.categories.value)

    def get_data_for_worker(self, worker, day_off_events, holidays_events):
        name = worker['display_name']
        first_name, last_name = name.split()
        worker_possible_names = [
            worker['username'],  # jdupont
            first_name,  # Jean
            first_name + last_name[0],  # JeanD
            first_name[0] + last_name[0],  # JD
        ]
        worker_possible_names.extend(worker.get('aliases', []))  # ponpon
        worker_possible_names = [slugify(x) for x in worker_possible_names]

        errors = []
        days_off = None
        days_off = [x for x in day_off_events if self.is_event_for_worker(x, worker_possible_names)]
        half_days_off = [
            x
            for x in days_off
            if hasattr(x.dtstart.value, 'time')
            and (
                x.dtstart.value.time() > datetime.time(12, 00)
                or x.dtend.value.time() <= datetime.time(15, 00)
            )
        ]
        if len(days_off) > 2 or len(days_off) == 2 and len(half_days_off) != 2:
            errors.append(
                '%s a trop de jours off : %s.' % (name, ', '.join(x.summary.value for x in days_off))
            )
        elif not days_off:
            errors.append("%s n'a pas de jour off." % name)
        else:
            days_off = [x.getrruleset()[0].weekday() for x in days_off]

        def get_date_end(event):
            if hasattr(event, 'rrule'):
                return event.getrruleset()[-1].date() + datetime.timedelta(days=1)

            return event.dtend.value

        holidays = [
            (x.dtstart.value, get_date_end(x))
            for x in holidays_events
            if self.is_event_for_worker(x, worker_possible_names)
        ]
        holidays = self.normalize_holidays(holidays, days_off)

        ignore = worker.get('ignore', False)
        if not ignore:
            self.errors.extend(errors)

        return {
            'number': worker['number'],
            'ignore': ignore,
            'is_parisian': worker.get('is_parisian', False),
            'display_name': name,
            'holidays': holidays,
        }

    def is_event_for_worker(self, event, worker_possible_names):
        splitted_summary = event.summary.value.split()

        worker_match = any(slugify(x) in worker_possible_names for x in splitted_summary)
        if worker_match:
            if hasattr(event, 'match'):
                self.errors.append(
                    'L\'évènement %s correspond à plusieurs coopérateurices : %s et %s.'
                    % (event.summary.value, worker_possible_names[0], event.match)
                )
            event.__dict__['match'] = worker_possible_names[0]

        return worker_match

    def normalize_holidays(self, worker_holidays, days_off=None):
        def make_holiday(start, end):
            if type(start) != type(end):
                start = start.date() if hasattr(start, 'date') else start
                end = end.date() if hasattr(end, 'date') else end

            half = False
            if isinstance(start, datetime.datetime):
                if start.date() == end.date():
                    if start.time() > datetime.time(12, 00):
                        half = True
                    elif end.time() <= datetime.time(15, 00):
                        half = True
                start = start.date()
                end = end.date()
            else:
                end -= datetime.timedelta(days=1)

            start = max(start, self.date)
            end = min(end, self.last_date)

            return Holiday(start, end, half)

        def split_holidays_on_day_off(holiday):
            start, end = holiday.start, holiday.end
            if start == end:
                if start.weekday() not in days_off:
                    yield holiday
                elif len(days_off) == 2:
                    holiday.half = True
                    yield holiday
                return

            if len(days_off) == 1 and start.weekday() == days_off[0]:
                start += datetime.timedelta(days=1)

            interval_start = start
            for i in range((end - start).days + 1):
                interval_end = start + datetime.timedelta(days=i)
                if interval_end.weekday() in days_off:
                    yield Holiday(interval_start, interval_end - datetime.timedelta(days=1))
                    if len(days_off) == 2:
                        yield Holiday(interval_end, interval_end, half=True)
                    interval_start = interval_end + datetime.timedelta(days=1)

            if interval_start <= interval_end:
                yield Holiday(interval_start, interval_end)

        def strip_weekend(holiday):
            if holiday.start.weekday() in (5, 6):
                holiday.start += datetime.timedelta(days=7 - holiday.start.weekday())
            if holiday.end.weekday() in (5, 6):
                holiday.end -= datetime.timedelta(days=holiday.end.weekday() - 4)
            if holiday.start <= holiday.end:
                return holiday

        worker_holidays = [make_holiday(*dates) for dates in worker_holidays]

        if days_off is not None:
            worker_holidays = [
                sub_holiday
                for holiday in worker_holidays
                for sub_holiday in split_holidays_on_day_off(holiday)
            ]

        worker_holidays = [
            holiday_no_weekend
            for holiday in worker_holidays
            if (holiday_no_weekend := strip_weekend(holiday))
        ]

        return worker_holidays


workers_monthly_report = WorkersMonthlyReport.as_view()
