package org.synyx.urlaubsverwaltung.calendar;

import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.ParameterList;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.parameter.Cn;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.RefreshInterval;
import net.fortuna.ical4j.model.property.Sequence;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.XProperty;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.synyx.urlaubsverwaltung.absence.Absence;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;

import static java.lang.invoke.MethodHandles.lookup;
import static java.util.Date.from;
import static java.util.stream.Collectors.toList;
import static net.fortuna.ical4j.model.parameter.Role.REQ_PARTICIPANT;
import static net.fortuna.ical4j.model.property.CalScale.GREGORIAN;
import static net.fortuna.ical4j.model.property.Method.CANCEL;
import static net.fortuna.ical4j.model.property.Version.VERSION_2_0;
import static org.slf4j.LoggerFactory.getLogger;
import static org.synyx.urlaubsverwaltung.calendar.ICalType.CANCELLED;
import static org.synyx.urlaubsverwaltung.calendar.ICalType.PUBLISHED;


@Service
public class ICalService {

    private static final Logger LOG = getLogger(lookup().lookupClass());

    private final CalendarProperties calendarProperties;

    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

    @Autowired
    ICalService(CalendarProperties calendarProperties) {
        this.calendarProperties = calendarProperties;
    }

    public File getCalendar(String title, List<Absence> absences) {
        final File file = generateCalenderFile(title);
        final Calendar calendar = generateCalendar(title, absences);
        return writeCalenderIntoFile(calendar, file);
    }

    public File getSingleAppointment(Absence absence, ICalType method) {
        final File file = generateCalenderFile("appointment");
        final Calendar calendar = generateForSingleAppointment(absence, method);
        return writeCalenderIntoFile(calendar, file);
    }

    private Calendar generateCalendar(String title, List<Absence> absences) {
        final Calendar calendar = prepareCalendar(absences, PUBLISHED);
        calendar.getProperties().add(new XProperty("X-WR-CALNAME", title));
        calendar.getProperties().add(new RefreshInterval(new ParameterList(), calendarProperties.getRefreshInterval()));
        return calendar;
    }

    private Calendar generateForSingleAppointment(Absence absence, ICalType method) {
        return prepareCalendar(List.of(absence), method);
    }

    private Calendar prepareCalendar(List<Absence> absences, ICalType method) {
        final Calendar calendar = new Calendar();
        calendar.getProperties().add(VERSION_2_0);
        calendar.getProperties().add(new ProdId("-//Urlaubsverwaltung//iCal4j 1.0//DE"));
        calendar.getProperties().add(GREGORIAN);
        calendar.getProperties().add(new XProperty("X-MICROSOFT-CALSCALE", GREGORIAN.getValue()));

        if (method == CANCELLED) {
            calendar.getProperties().add(CANCEL);
        }

        final List<VEvent> absencesVEvents = absences.stream()
            .map(absence -> this.toVEvent(absence, method))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(toList());
        calendar.getComponents().addAll(absencesVEvents);

        return calendar;
    }

    private Optional<VEvent> toVEvent(Absence absence, ICalType method) {

        final ZonedDateTime startDateTime = absence.getStartDate();
        final ZonedDateTime endDateTime = absence.getEndDate();

        final VEvent event;
        if (absence.isAllDay()) {
            try {
                final Date startDate = new Date(startDateTime.format(formatter));
                if (isSameDay(startDateTime, endDateTime)) {
                    event = new VEvent(startDate, absence.getEventSubject());
                } else {
                    final Date endDate = new Date(endDateTime.format(formatter));
                    event = new VEvent(new Date(startDate), new Date(endDate), absence.getEventSubject());
                }
            } catch (ParseException e) {
                LOG.warn("Could not generate all day ical event for absence {}", absence, e);
                return Optional.empty();
            }

            event.getProperties().add(new XProperty("X-MICROSOFT-CDO-ALLDAYEVENT", "TRUE"));
        } else {
            final DateTime start = new DateTime(from(startDateTime.toInstant()));
            start.setUtc(true);
            final DateTime end = new DateTime(from(endDateTime.toInstant()));
            end.setUtc(true);

            event = new VEvent(start, end, absence.getEventSubject());
        }

        event.getProperties().add(new Uid(generateUid(absence)));
        event.getProperties().add(generateAttendee(absence));

        if (method == CANCELLED) {
            event.getProperties().add(new Sequence(1));
        }

        event.getProperties().add(new Organizer(URI.create("mailto:" + calendarProperties.getOrganizer())));

        return Optional.of(event);
    }

    private Attendee generateAttendee(Absence absence) {
        final Attendee attendee = new Attendee(URI.create("mailto:" + absence.getPerson().getEmail()));
        attendee.getParameters().add(REQ_PARTICIPANT);
        attendee.getParameters().add(new Cn(absence.getPerson().getNiceName()));

        return attendee;
    }

    private boolean isSameDay(ZonedDateTime startDateTime, ZonedDateTime endDate) {
        return startDateTime.toLocalDate().isEqual(endDate.toLocalDate().minusDays(1));
    }

    private String generateUid(Absence absence) {
        final String data = absence.getStartDate() + "" + absence.getEndDate() + "" + absence.getPerson();
        return DigestUtils.md5Hex(data).toUpperCase();
    }

    private File generateCalenderFile(String title) {
        final File file;
        try {
            file = File.createTempFile("calendar-", ".ics");
        } catch (IOException e) {
            throw new CalendarException("Could not generate temp file for " + title + " calendar", e);
        }
        return file;
    }

    private File writeCalenderIntoFile(Calendar calendar, File file) {
        try (final FileWriter calendarFileWriter = new FileWriter(file)) {
            final CalendarOutputter calendarOutputter = new CalendarOutputter();
            calendarOutputter.output(calendar, calendarFileWriter);
        } catch (ValidationException | IOException e) {
            throw new CalendarException("iCal calendar could not be written to file", e);
        }
        return file;
    }
}

@@ -1358,58 +1358,8 @@
 om;%0A
-import static java.util.stream.Collectors.toList;%0A
 impo
@@ -3798,45 +3798,8 @@
     
- final List%3CVEvent%3E absencesVEvents =
  abs
@@ -3963,34 +3963,24 @@
    .
-collect(toList());%0A       
+forEach(event -%3E
  cal
@@ -4008,27 +4008,15 @@
 .add
-All(absencesVE
+(e
 vent
-s
+)
 );%0A%0A

Replace collect with forEach