package com.rit.ems.controller;

import com.rit.ems.model.Event;
import com.rit.ems.model.Registration;
import com.rit.ems.model.User;
import com.rit.ems.repository.EventRepository;
import com.rit.ems.repository.RegistrationRepository;
import com.rit.ems.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;

@RestController
@RequestMapping("/api/events")
public class EventController {

    @Autowired
    private EventRepository eventRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RegistrationRepository registrationRepository;

    @GetMapping
    public List<Event> getAllEvents() {
        List<Event> events = eventRepository.findAll();
        List<Registration> regs = registrationRepository.findAll();

        for (Event event : events) {
            if (event.getStartDate() != null) {
                String sd = event.getStartDate().replace(" ", "T");
                if (!sd.endsWith("Z")) sd += "Z";
                event.setStartDate(sd);
            }
            if (event.getEndDate() != null) {
                String ed = event.getEndDate().replace(" ", "T");
                if (!ed.endsWith("Z")) ed += "Z";
                event.setEndDate(ed);
            }

            int count = (int) regs.stream()
                    .filter(r -> r.getEventId() != null && r.getEventId().equals(event.getId()))
                    .count();
            event.setCurrentParticipants(count);
            
            // Re-populate conflict messages if needed
            if (!Arrays.asList("APPROVED", "COMPLETED", "CANCELLED").contains(event.getStatus())) {
                String conflictMsg = getConflictMessage(event, event.getId());
                if (conflictMsg != null) {
                    event.setConflictMessage(conflictMsg);
                }
            }
        }
        return events;
    }

    @PostMapping("/propose")
    public ResponseEntity<?> proposeEvent(@RequestBody Map<String, Object> payload) {
        Object userIdObj = payload.get("userId");
        if (userIdObj == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "userId is required"));
        }

        Long userId = Long.parseLong(userIdObj.toString());
        Optional<User> userOpt = userRepository.findById(userId);
        if (userOpt.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "Proposer user not found"));
        }

        User proposer = userOpt.get();
        String startDateStr = (String) payload.get("startDate");
        String endDateStr = (String) payload.get("endDate");

        Instant start = parseDate(startDateStr);
        Instant end = parseDate(endDateStr);

        if (start.isBefore(Instant.now())) {
            return ResponseEntity.badRequest().body(Map.of("message", "Cannot schedule events in the past"));
        }
        if (end.isBefore(start) || end.equals(start)) {
            return ResponseEntity.badRequest().body(Map.of("message", "End date must be after start date"));
        }

        // Temporarily build target Event object to check conflicts
        Event tempEvent = new Event();
        tempEvent.setStartDate(startDateStr);
        tempEvent.setEndDate(endDateStr);
        tempEvent.setLocation((String) payload.get("venue"));
        tempEvent.setInstitution((String) payload.get("institution"));
        tempEvent.setGroupRequestId((String) payload.get("groupRequestId"));

        String conflictMsg = getConflictMessage(tempEvent, null);
        if (conflictMsg != null) {
            String category = (String) payload.get("category");
            Boolean cancelConflicting = (Boolean) payload.get("cancelConflicting");

            if ("PLACEMENT".equals(category) && Boolean.TRUE.equals(cancelConflicting)) {
                // Cancel conflicting events
                List<Event> allEvents = eventRepository.findAll();
                String location = tempEvent.getLocation().trim();
                String inst = tempEvent.getInstitution() != null ? tempEvent.getInstitution().trim() : "RIT";
                
                for (Event other : allEvents) {
                    if ("CANCELLED".equals(other.getStatus())) continue;
                    if (tempEvent.getGroupRequestId() != null && tempEvent.getGroupRequestId().equals(other.getGroupRequestId())) continue;

                    Instant oStart = parseDate(other.getStartDate());
                    Instant oEnd = parseDate(other.getEndDate());
                    String oLoc = other.getLocation() != null ? other.getLocation().trim() : "";
                    String oInst = other.getInstitution() != null ? other.getInstitution().trim() : "RIT";

                    if (oInst.equalsIgnoreCase(inst) && oLoc.equalsIgnoreCase(location) && start.isBefore(oEnd) && end.isAfter(oStart)) {
                        other.setStatus("CANCELLED");
                        other.setRejectionReason("This event is cancelled due to the placement activity happening at the venue at this timing.");
                        eventRepository.save(other);
                    }
                }
            } else {
                // Return conflicts list
                List<Event> conflicts = new ArrayList<>();
                List<Event> allEvents = eventRepository.findAll();
                String location = tempEvent.getLocation().trim();
                String inst = tempEvent.getInstitution() != null ? tempEvent.getInstitution().trim() : "RIT";

                for (Event other : allEvents) {
                    if ("CANCELLED".equals(other.getStatus())) continue;
                    Instant oStart = parseDate(other.getStartDate());
                    Instant oEnd = parseDate(other.getEndDate());
                    String oLoc = other.getLocation() != null ? other.getLocation().trim() : "";
                    String oInst = other.getInstitution() != null ? other.getInstitution().trim() : "RIT";

                    if (oInst.equalsIgnoreCase(inst) && oLoc.equalsIgnoreCase(location) && start.isBefore(oEnd) && end.isAfter(oStart)) {
                        conflicts.add(other);
                    }
                }
                return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
                        "message", conflictMsg,
                        "conflicts", conflicts,
                        "canOverride", "PLACEMENT".equals(category)
                ));
            }
        }

        // Propose logic status
        String status = "REQUESTED";
        if ("PRINCIPAL".equals(proposer.getRole())) {
            status = "APPROVED";
        } else if ("HOD".equals(proposer.getRole()) ||
                "CLUB".equals(payload.get("category")) ||
                "PLACEMENT".equals(payload.get("category")) ||
                "INSTITUTIONAL".equals(payload.get("category")) ||
                "PLACEMENT".equals(proposer.getRole()) ||
                Boolean.TRUE.equals(proposer.getIsPlacementStaff()) ||
                "Placement Department".equals(proposer.getDepartment())) {
            status = "PENDING_PR";
        }

        Event event = new Event();
        event.setId(System.currentTimeMillis() + new Random().nextInt(1000));
        event.setTitle((String) payload.get("eventName"));
        event.setDescription((String) payload.get("description"));
        event.setStartDate(start.toString());
        event.setEndDate(end.toString());
        event.setLocation((String) payload.get("venue"));
        event.setCategory((String) payload.get("category"));
        event.setType((String) payload.get("eventType"));
        event.setInstitution((String) payload.get("institution"));
        event.setDepartment((String) payload.get("department"));
        event.setAcademicYears((List<String>) payload.get("academicYears"));
        event.setStatus(status);
        event.setGuestName((String) payload.get("guestName"));
        event.setGuestSocialProfile((String) payload.get("socialProfile"));
        event.setRequirements((List<String>) payload.get("requirements"));
        event.setTargetedSections((List<String>) payload.get("targetedSections"));
        event.setTargetDepartments((List<String>) payload.get("targetDepartments"));
        event.setGroupRequestId((String) payload.get("groupRequestId"));
        event.setSponsors((List<String>) payload.get("sponsors"));

        event.setBudget(payload.get("budget") != null ? Double.parseDouble(payload.get("budget").toString()) : 0.0);
        event.setHasRegistrationFee(Boolean.TRUE.equals(payload.get("hasRegistrationFee")));
        event.setRegistrationFee(payload.get("registrationFee") != null ? Double.parseDouble(payload.get("registrationFee").toString()) : 0.0);
        event.setPaymentLink((String) payload.get("paymentLink"));
        event.setCentreName((String) payload.get("centreName"));
        event.setIsPublicEvent(Boolean.TRUE.equals(payload.get("isPublicEvent")));
        event.setImage((String) payload.get("image"));
        event.setTargetedBatch((String) payload.get("targetedBatch"));
        event.setOpenToAll(Boolean.TRUE.equals(payload.get("openToAll")));

        Integer maxParticipants = payload.get("maxParticipants") != null && !payload.get("maxParticipants").toString().isEmpty() 
                                  ? Integer.parseInt(payload.get("maxParticipants").toString()) : null;
        event.setMaxParticipants(maxParticipants);

        Event.ProposerInfo proposerInfo = new Event.ProposerInfo(
                proposer.getId(),
                proposer.getFullName(),
                proposer.getEmail(),
                proposer.getRole(),
                proposer.getDepartment()
        );
        event.setProposer(proposerInfo);

        event.setDayConfigs((List<Object>) payload.get("dayConfigs"));
        event.setDeptLimits((Map<String, Object>) payload.get("deptLimits"));
        event.setDeptSectionLimits((Map<String, Object>) payload.get("deptSectionLimits"));

        event.setRefreshmentExpense(payload.get("refreshment_expense") != null ? Double.parseDouble(payload.get("refreshment_expense").toString()) : 0.0);
        event.setTransportationExpense(payload.get("transportation_expense") != null ? Double.parseDouble(payload.get("transportation_expense").toString()) : 0.0);
        event.setSessionCoverageFee(payload.get("session_coverage_fee") != null ? Double.parseDouble(payload.get("session_coverage_fee").toString()) : 0.0);
        event.setTotalExpense(payload.get("total_expense") != null ? Double.parseDouble(payload.get("total_expense").toString()) : 0.0);
        event.setDurationDays(payload.get("durationDays") != null ? Integer.parseInt(payload.get("durationDays").toString()) : 1);

        eventRepository.save(event);
        return ResponseEntity.ok(Map.of("message", "Event proposed successfully", "id", event.getId()));
    }

    @PostMapping("/batch-create")
    public ResponseEntity<?> batchCreateEvents(@RequestBody List<Map<String, Object>> batchList) {
        List<Event> eventsToSave = new ArrayList<>();

        for (Map<String, Object> payload : batchList) {
            String title = (String) payload.get("title");
            String dateObj = payload.containsKey("startDate") ? (String) payload.get("startDate") : (String) payload.get("finalDate");
            if (dateObj == null) {
                return ResponseEntity.badRequest().body(Map.of("message", "Missing date for event: " + title));
            }

            Instant start = parseDate(dateObj);
            Instant end;
            if (payload.containsKey("startDate") && payload.containsKey("endDate")) {
                try {
                    Instant origStart = parseDate((String) payload.get("startDate"));
                    Instant origEnd = parseDate((String) payload.get("endDate"));
                    long durationMs = origEnd.toEpochMilli() - origStart.toEpochMilli();
                    end = Instant.ofEpochMilli(start.toEpochMilli() + durationMs);
                } catch (Exception e) {
                    end = Instant.ofEpochMilli(start.toEpochMilli() + 2 * 60 * 60 * 1000);
                }
            } else {
                end = Instant.ofEpochMilli(start.toEpochMilli() + 2 * 60 * 60 * 1000);
            }

            if (end.isBefore(start) || end.equals(start)) {
                return ResponseEntity.badRequest().body(Map.of("message", "End date must be after start date for event: " + title));
            }

            String dept = payload.containsKey("targetDepartment") ? (String) payload.get("targetDepartment") :
                         (payload.containsKey("department") ? (String) payload.get("department") : "General");
            List<String> ay = payload.containsKey("targetBatch") ? Collections.singletonList((String) payload.get("targetBatch")) :
                             (List<String>) payload.get("academicYears");
            String venue = payload.containsKey("venue") ? (String) payload.get("venue") :
                           (payload.containsKey("location") ? (String) payload.get("location") : "TBD");

            Event.ProposerInfo proposer = new Event.ProposerInfo(1L, "Admin", "admin@rit.edu", "ADMIN", "ADMIN");

            Event event = new Event();
            event.setId(System.currentTimeMillis() + new Random().nextInt(1000));
            event.setTitle(title);
            event.setDescription((String) payload.get("description"));
            event.setStartDate(start.toString());
            event.setEndDate(end.toString());
            event.setLocation(venue.trim());
            event.setCategory((String) payload.get("category"));
            event.setType((String) payload.get("type"));
            event.setInstitution((String) payload.get("institution"));
            event.setDepartment(dept);
            event.setAcademicYears(ay);
            event.setStatus(payload.containsKey("status") ? (String) payload.get("status") : "APPROVED");
            event.setRequirements((List<String>) payload.get("requirements"));
            event.setImage((String) payload.get("image"));
            event.setProposer(proposer);
            event.setOpenToAll(Boolean.TRUE.equals(payload.get("openToAll")));

            String conflictMsg = getConflictMessage(event, null);
            if (conflictMsg != null) {
                return ResponseEntity.badRequest().body(Map.of("message", String.format("Conflict in batch item '%s': %s", title, conflictMsg)));
            }
            eventsToSave.add(event);
        }

        // Check intra-batch conflicts
        for (int i = 0; i < eventsToSave.size(); i++) {
            Event current = eventsToSave.get(i);
            Instant currentStart = parseDate(current.getStartDate());
            Instant currentEnd = parseDate(current.getEndDate());
            String currentLoc = current.getLocation().toLowerCase();

            for (int j = i + 1; j < eventsToSave.size(); j++) {
                Event other = eventsToSave.get(j);
                Instant otherStart = parseDate(other.getStartDate());
                Instant otherEnd = parseDate(other.getEndDate());
                String otherLoc = other.getLocation().toLowerCase();

                if (current.getInstitution().equalsIgnoreCase(other.getInstitution()) &&
                    currentLoc.equals(otherLoc) &&
                    currentStart.isBefore(otherEnd) &&
                    currentEnd.isAfter(otherStart)) {
                    return ResponseEntity.badRequest().body(Map.of("message", String.format("Conflict between imported events '%s' and '%s' at %s",
                            current.getTitle(), other.getTitle(), current.getLocation())));
                }
            }
        }

        eventRepository.saveAll(eventsToSave);
        return ResponseEntity.ok(Map.of("message", "Batch events created successfully", "count", eventsToSave.size()));
    }

    @PostMapping("/{id}/approve")
    public ResponseEntity<?> approveEvent(@PathVariable Long id,
                                          @RequestParam Long userId,
                                          @RequestParam(required = false) String newVenue) {
        Optional<User> userOpt = userRepository.findById(userId);
        Optional<Event> eventOpt = eventRepository.findById(id);

        if (userOpt.isEmpty() || eventOpt.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "User or Event not found"));
        }

        User user = userOpt.get();
        Event event = eventOpt.get();

        if (newVenue != null) {
            event.setLocation(newVenue);
        }

        String conflictMsg = getConflictMessage(event, id);
        if (conflictMsg != null) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", conflictMsg));
        }

        String newStatus = event.getStatus();
        if ("HOD".equals(user.getRole())) {
            newStatus = "PENDING_PR";
        } else if ("PRINCIPAL".equals(user.getRole())) {
            newStatus = "APPROVED";
        } else {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("message", "Only HoD or Principal can approve events"));
        }

        event.setStatus(newStatus);
        eventRepository.save(event);
        return ResponseEntity.ok(Map.of("message", "Event action completed successfully", "status", newStatus));
    }

    @PostMapping("/{id}/reject")
    public ResponseEntity<?> rejectEvent(@PathVariable Long id,
                                         @RequestParam Long userId,
                                         @RequestBody Map<String, String> body) {
        String reason = body.get("reason");
        if (reason == null || reason.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "Rejection reason is mandatory"));
        }

        Optional<User> userOpt = userRepository.findById(userId);
        Optional<Event> eventOpt = eventRepository.findById(id);

        if (userOpt.isEmpty() || eventOpt.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "User or Event not found"));
        }

        User user = userOpt.get();
        Event event = eventOpt.get();
        String newStatus = event.getStatus();

        if ("HOD".equals(user.getRole())) {
            newStatus = "HOD_REJECTED";
        } else if ("PRINCIPAL".equals(user.getRole())) {
            newStatus = "PRINCIPAL_REJECTED";
        } else {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("message", "Only HoD or Principal can reject events"));
        }

        event.setStatus(newStatus);
        event.setRejectionReason(reason);
        eventRepository.save(event);
        return ResponseEntity.ok(Map.of("message", "Event rejected successfully", "status", newStatus));
    }

    @PutMapping("/{id}")
    public ResponseEntity<?> updateEvent(@PathVariable Long id, @RequestBody Map<String, Object> payload) {
        Optional<Event> eventOpt = eventRepository.findById(id);
        if (eventOpt.isEmpty()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", "Event not found"));
        }

        Event event = eventOpt.get();
        Object userIdObj = payload.get("userId");
        if (userIdObj == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "User ID is required to update event"));
        }

        Long userId = Long.parseLong(userIdObj.toString());
        Optional<User> userOpt = userRepository.findById(userId);
        if (userOpt.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "User not found"));
        }

        User user = userOpt.get();
        boolean isAdmin = "ADMIN".equals(user.getRole());

        if (!isAdmin) {
            if (event.getProposer() == null || !event.getProposer().getId().equals(userId)) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("message", "Only the proposer can edit this event"));
            }
            if (!"REQUESTED".equals(event.getStatus())) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("message", "Event cannot be edited once it moves past the initial request stage"));
            }
        }

        String startStr = payload.containsKey("startDate") ? (String) payload.get("startDate") : event.getStartDate();
        String endStr = payload.containsKey("endDate") ? (String) payload.get("endDate") : event.getEndDate();

        Instant start = parseDate(startStr);
        Instant end = parseDate(endStr);

        if (end.isBefore(start) || end.equals(start)) {
            return ResponseEntity.badRequest().body(Map.of("message", "End date must be after start date"));
        }

        // Apply fields selectively
        if (payload.containsKey("title")) event.setTitle((String) payload.get("title"));
        event.setStartDate(start.toString());
        event.setEndDate(end.toString());
        if (payload.containsKey("eventType")) event.setType((String) payload.get("eventType"));
        if (payload.containsKey("institution")) event.setInstitution((String) payload.get("institution"));
        if (payload.containsKey("department")) event.setDepartment((String) payload.get("department"));
        if (payload.containsKey("venue")) event.setLocation((String) payload.get("venue"));
        if (payload.containsKey("category")) event.setCategory((String) payload.get("category"));

        if (payload.containsKey("maxParticipants")) {
            String val = payload.get("maxParticipants").toString();
            event.setMaxParticipants(val.isEmpty() ? null : Integer.parseInt(val));
        }

        if (payload.containsKey("guestName")) event.setGuestName((String) payload.get("guestName"));
        if (payload.containsKey("socialProfile")) event.setGuestSocialProfile((String) payload.get("socialProfile"));
        if (payload.containsKey("academicYears")) event.setAcademicYears((List<String>) payload.get("academicYears"));
        if (payload.containsKey("targetedSections")) event.setTargetedSections((List<String>) payload.get("targetedSections"));
        if (payload.containsKey("requirements")) event.setRequirements((List<String>) payload.get("requirements"));
        if (payload.containsKey("sponsors")) event.setSponsors((List<String>) payload.get("sponsors"));
        if (payload.containsKey("description")) event.setDescription((String) payload.get("description"));

        if (payload.containsKey("budget")) {
            String b = payload.get("budget").toString();
            event.setBudget(b.isEmpty() ? 0.0 : Double.parseDouble(b));
        }
        if (payload.containsKey("hasRegistrationFee")) {
            event.setHasRegistrationFee((Boolean) payload.get("hasRegistrationFee"));
        }
        if (payload.containsKey("registrationFee")) {
            String f = payload.get("registrationFee").toString();
            event.setRegistrationFee(f.isEmpty() ? 0.0 : Double.parseDouble(f));
        }

        if (payload.containsKey("centreName")) event.setCentreName((String) payload.get("centreName"));
        if (payload.containsKey("isPublicEvent")) event.setIsPublicEvent((Boolean) payload.get("isPublicEvent"));
        if (payload.containsKey("image")) event.setImage((String) payload.get("image"));
        if (payload.containsKey("openToAll")) event.setOpenToAll((Boolean) payload.get("openToAll"));
        if (payload.containsKey("targetedBatch")) event.setTargetedBatch((String) payload.get("targetedBatch"));
        if (payload.containsKey("dayConfigs")) event.setDayConfigs((List<Object>) payload.get("dayConfigs"));
        if (payload.containsKey("deptLimits")) event.setDeptLimits((Map<String, Object>) payload.get("deptLimits"));
        if (payload.containsKey("deptSectionLimits")) event.setDeptSectionLimits((Map<String, Object>) payload.get("deptSectionLimits"));

        if (payload.containsKey("refreshment_expense")) event.setRefreshmentExpense(Double.parseDouble(payload.get("refreshment_expense").toString()));
        if (payload.containsKey("transportation_expense")) event.setTransportationExpense(Double.parseDouble(payload.get("transportation_expense").toString()));
        if (payload.containsKey("session_coverage_fee")) event.setSessionCoverageFee(Double.parseDouble(payload.get("session_coverage_fee").toString()));
        if (payload.containsKey("total_expense")) event.setTotalExpense(Double.parseDouble(payload.get("total_expense").toString()));
        if (payload.containsKey("durationDays")) event.setDurationDays(Integer.parseInt(payload.get("durationDays").toString()));

        if (payload.containsKey("status") && isAdmin) {
            event.setStatus((String) payload.get("status"));
        }

        String conflictMsg = getConflictMessage(event, id);
        if (conflictMsg != null) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", conflictMsg));
        }

        eventRepository.save(event);
        return ResponseEntity.ok(Map.of("message", "Event updated successfully"));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteEvent(@PathVariable Long id) {
        eventRepository.deleteById(id);
        return ResponseEntity.ok(Map.of("message", "Event deleted successfully"));
    }

    private Instant parseDate(String val) {
        if (val == null || val.trim().isEmpty()) {
            return Instant.now();
        }
        String clean = val.trim().replace(" ", "T");
        if (clean.length() == 10) {
            clean += "T00:00:00";
        }
        if (clean.length() == 16) {
            clean += ":00";
        }
        if (!clean.contains("Z") && !clean.contains("+") && !clean.contains("-")) {
            clean += "Z";
        }
        try {
            return Instant.parse(clean);
        } catch (Exception e) {
            try {
                return java.time.LocalDateTime.parse(clean.substring(0, 19))
                        .toInstant(java.time.ZoneOffset.UTC);
            } catch (Exception ex) {
                return Instant.now();
            }
        }
    }

    private String getConflictMessage(Event event, Long ignoreId) {
        if (event.getLocation() == null || event.getStartDate() == null || event.getEndDate() == null) {
            return null;
        }
        String location = event.getLocation().trim();
        String institution = event.getInstitution() != null ? event.getInstitution().trim() : "RIT";
        Instant start = parseDate(event.getStartDate());
        Instant end = parseDate(event.getEndDate());

        List<Event> existingEvents = eventRepository.findAll();
        for (Event other : existingEvents) {
            if ("CANCELLED".equals(other.getStatus())) continue;
            if (ignoreId != null && other.getId().equals(ignoreId)) continue;
            if (event.getId() != null && other.getId().equals(event.getId())) continue;
            if (event.getGroupRequestId() != null && event.getGroupRequestId().equals(other.getGroupRequestId())) continue;

            Instant oStart = parseDate(other.getStartDate());
            Instant oEnd = parseDate(other.getEndDate());
            String oLoc = other.getLocation() != null ? other.getLocation().trim() : "";
            String oInst = other.getInstitution() != null ? other.getInstitution().trim() : "RIT";

            if (oInst.equalsIgnoreCase(institution) &&
                oLoc.equalsIgnoreCase(location) &&
                start.isBefore(oEnd) &&
                end.isAfter(oStart)) {
                
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm")
                        .withZone(java.time.ZoneOffset.UTC);
                return String.format("Venue Conflict: '%s' is already booked for '%s' from %s to %s",
                        location, other.getTitle(), formatter.format(oStart), formatter.format(oEnd));
            }
        }
        return null;
    }
}
