package de.cannamanage.service;

import de.cannamanage.domain.entity.ClubEvent;
import de.cannamanage.domain.entity.EventRsvp;
import de.cannamanage.domain.entity.TenantContext;
import de.cannamanage.domain.enums.*;
import de.cannamanage.service.repository.ClubEventRepository;
import de.cannamanage.service.repository.EventRsvpRepository;
import de.cannamanage.service.repository.MemberRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
 * Service for club event management — CRUD, RSVP, iCal generation, and recurring event expansion.
 */
@Service
@Transactional
public class EventService {

    private static final Logger log = LoggerFactory.getLogger(EventService.class);
    private static final DateTimeFormatter ICAL_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");

    private final ClubEventRepository eventRepository;
    private final EventRsvpRepository rsvpRepository;
    private final MemberRepository memberRepository;
    private final NotificationService notificationService;
    private final InfoBoardService infoBoardService;
    private final AuditService auditService;

    public EventService(ClubEventRepository eventRepository,
                       EventRsvpRepository rsvpRepository,
                       MemberRepository memberRepository,
                       NotificationService notificationService,
                       InfoBoardService infoBoardService,
                       AuditService auditService) {
        this.eventRepository = eventRepository;
        this.rsvpRepository = rsvpRepository;
        this.memberRepository = memberRepository;
        this.notificationService = notificationService;
        this.infoBoardService = infoBoardService;
        this.auditService = auditService;
    }

    /**
     * Create a new club event and notify all members.
     */
    public ClubEvent createEvent(UUID clubId, String title, String description, EventType eventType,
                                  Instant startAt, Instant endAt, String location, Integer maxAttendees,
                                  boolean recurring, RecurrenceRule recurrenceRule, LocalDate recurrenceEndDate,
                                  UUID createdBy, boolean postToInfoBoard) {
        var event = new ClubEvent(clubId, title, description, eventType, startAt, endAt, location, maxAttendees, createdBy);
        event.setRecurring(recurring);
        event.setRecurrenceRule(recurrenceRule);
        event.setRecurrenceEndDate(recurrenceEndDate);

        ClubEvent saved = eventRepository.save(event);
        log.info("Club event created: {} '{}' in club {}", saved.getId(), title, clubId);

        // Notify all club members
        try {
            var members = memberRepository.findByClubId(clubId);
            members.forEach(member -> {
                if (member.getUserId() != null) {
                    notificationService.sendNotification(
                            member.getUserId(),
                            NotificationType.EVENT_CREATED,
                            title,
                            formatEventNotification(saved),
                            "/portal/events/" + saved.getId()
                    );
                }
            });
        } catch (Exception e) {
            log.warn("Failed to send event notifications: {}", e.getMessage());
        }

        // Auto-post to info board
        if (postToInfoBoard) {
            try {
                String infoContent = formatEventInfoBoardContent(saved);
                infoBoardService.createPost(clubId, "📅 " + title, infoContent,
                        InfoBoardCategory.EVENT, false, createdBy);
            } catch (Exception e) {
                log.warn("Failed to auto-post event to info board: {}", e.getMessage());
            }
        }

        // Audit
        auditService.log(AuditEventType.EVENT_CREATED, "ClubEvent", saved.getId().toString(),
                "Event created: " + title);

        return saved;
    }

    /**
     * Update event details.
     */
    public ClubEvent updateEvent(UUID eventId, String title, String description, EventType eventType,
                                  Instant startAt, Instant endAt, String location, Integer maxAttendees,
                                  boolean recurring, RecurrenceRule recurrenceRule, LocalDate recurrenceEndDate) {
        var event = eventRepository.findById(eventId)
                .orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));

        event.setTitle(title);
        event.setDescription(description);
        event.setEventType(eventType);
        event.setStartAt(startAt);
        event.setEndAt(endAt);
        event.setLocation(location);
        event.setMaxAttendees(maxAttendees);
        event.setRecurring(recurring);
        event.setRecurrenceRule(recurrenceRule);
        event.setRecurrenceEndDate(recurrenceEndDate);

        ClubEvent saved = eventRepository.save(event);
        auditService.log(AuditEventType.EVENT_UPDATED, "ClubEvent", saved.getId().toString(),
                "Event updated: " + title);
        return saved;
    }

    /**
     * Cancel (delete) an event and notify attendees.
     */
    public void cancelEvent(UUID eventId) {
        var event = eventRepository.findById(eventId)
                .orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));

        // Notify ACCEPTED and MAYBE attendees
        var attendees = rsvpRepository.findByEventIdAndStatusIn(eventId,
                List.of(RsvpStatus.ACCEPTED, RsvpStatus.MAYBE));
        for (var rsvp : attendees) {
            var member = memberRepository.findById(rsvp.getMemberId());
            member.ifPresent(m -> {
                if (m.getUserId() != null) {
                    notificationService.sendNotification(
                            m.getUserId(),
                            NotificationType.EVENT_CANCELLED,
                            "Veranstaltung abgesagt: " + event.getTitle(),
                            "Die Veranstaltung \"" + event.getTitle() + "\" wurde abgesagt.",
                            null
                    );
                }
            });
        }

        auditService.log(AuditEventType.EVENT_CANCELLED, "ClubEvent", event.getId().toString(),
                "Event cancelled: " + event.getTitle());
        eventRepository.delete(event);
        log.info("Event cancelled and deleted: {}", eventId);
    }

    /**
     * Get a single event by ID.
     */
    @Transactional(readOnly = true)
    public ClubEvent getEvent(UUID eventId) {
        return eventRepository.findById(eventId)
                .orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));
    }

    /**
     * List events in a date range for calendar view.
     * Includes recurring event expansion.
     */
    @Transactional(readOnly = true)
    public List<ClubEvent> listEvents(Instant from, Instant to) {
        UUID tenantId = TenantContext.getCurrentTenant();
        var events = eventRepository.findByTenantIdAndStartAtBetweenOrderByStartAtAsc(tenantId, from, to);

        // Also find recurring events that started before 'from' but may have occurrences in range
        var allEvents = eventRepository.findByTenantIdAndStartAtBetweenOrderByStartAtAsc(
                tenantId, Instant.EPOCH, to);

        List<ClubEvent> result = new ArrayList<>(events);
        for (var event : allEvents) {
            if (event.isRecurring() && event.getStartAt().isBefore(from)) {
                result.addAll(expandRecurring(event, from, to));
            }
        }

        result.sort(Comparator.comparing(ClubEvent::getStartAt));
        return result;
    }

    /**
     * List upcoming events (next 30 days) for portal widget.
     */
    @Transactional(readOnly = true)
    public List<ClubEvent> listUpcomingEvents(int limit) {
        UUID tenantId = TenantContext.getCurrentTenant();
        return eventRepository.findByTenantIdAndStartAtAfterOrderByStartAtAsc(
                tenantId, Instant.now(), PageRequest.of(0, limit));
    }

    /**
     * Create or update an RSVP. Enforces maxAttendees for ACCEPTED status.
     * Returns the RSVP or throws if event is full.
     */
    public EventRsvp rsvp(UUID eventId, UUID memberId, RsvpStatus status) {
        var event = eventRepository.findById(eventId)
                .orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));

        // Enforce max attendees for ACCEPTED
        if (status == RsvpStatus.ACCEPTED && event.getMaxAttendees() != null) {
            long currentAccepted = rsvpRepository.countByEventIdAndStatus(eventId, RsvpStatus.ACCEPTED);
            // Check if this member already had ACCEPTED (update case)
            var existing = rsvpRepository.findByEventIdAndMemberId(eventId, memberId);
            boolean alreadyAccepted = existing.isPresent() && existing.get().getStatus() == RsvpStatus.ACCEPTED;
            if (!alreadyAccepted && currentAccepted >= event.getMaxAttendees()) {
                throw new IllegalStateException("EVENT_FULL");
            }
        }

        // Upsert RSVP
        var rsvp = rsvpRepository.findByEventIdAndMemberId(eventId, memberId)
                .orElseGet(() -> new EventRsvp(event, memberId, status));
        rsvp.setStatus(status);
        rsvp.setRespondedAt(Instant.now());

        return rsvpRepository.save(rsvp);
    }

    /**
     * Get all RSVPs for an event.
     */
    @Transactional(readOnly = true)
    public List<EventRsvp> getAttendees(UUID eventId) {
        return rsvpRepository.findByEventId(eventId);
    }

    /**
     * Get RSVP counts by status for an event.
     */
    @Transactional(readOnly = true)
    public Map<RsvpStatus, Long> getAttendeeCounts(UUID eventId) {
        Map<RsvpStatus, Long> counts = new HashMap<>();
        for (RsvpStatus status : RsvpStatus.values()) {
            counts.put(status, rsvpRepository.countByEventIdAndStatus(eventId, status));
        }
        return counts;
    }

    /**
     * Generate RFC 5545 iCal (.ics) content for an event.
     */
    @Transactional(readOnly = true)
    public String generateIcal(UUID eventId) {
        var event = eventRepository.findById(eventId)
                .orElseThrow(() -> new NoSuchElementException("Event not found: " + eventId));

        var sb = new StringBuilder();
        sb.append("BEGIN:VCALENDAR\r\n");
        sb.append("VERSION:2.0\r\n");
        sb.append("PRODID:-//CannaManage//Events//EN\r\n");
        sb.append("BEGIN:VEVENT\r\n");
        sb.append("UID:").append(event.getId()).append("@cannamanage.de\r\n");
        sb.append("DTSTART:").append(formatIcalDate(event.getStartAt())).append("\r\n");
        if (event.getEndAt() != null) {
            sb.append("DTEND:").append(formatIcalDate(event.getEndAt())).append("\r\n");
        }
        sb.append("SUMMARY:").append(escapeIcal(event.getTitle())).append("\r\n");
        if (event.getDescription() != null) {
            sb.append("DESCRIPTION:").append(escapeIcal(event.getDescription())).append("\r\n");
        }
        if (event.getLocation() != null) {
            sb.append("LOCATION:").append(escapeIcal(event.getLocation())).append("\r\n");
        }
        if (event.isRecurring() && event.getRecurrenceRule() != null) {
            sb.append("RRULE:FREQ=").append(toIcalFreq(event.getRecurrenceRule()));
            if (event.getRecurrenceRule() == RecurrenceRule.BIWEEKLY) {
                sb.append(";INTERVAL=2");
            }
            if (event.getRecurrenceEndDate() != null) {
                sb.append(";UNTIL=").append(event.getRecurrenceEndDate().format(DateTimeFormatter.BASIC_ISO_DATE)).append("T235959Z");
            }
            sb.append("\r\n");
        }
        sb.append("DTSTAMP:").append(formatIcalDate(Instant.now())).append("\r\n");
        sb.append("END:VEVENT\r\n");
        sb.append("END:VCALENDAR\r\n");
        return sb.toString();
    }

    /**
     * Expand a recurring event into virtual occurrences within the given date range.
     */
    List<ClubEvent> expandRecurring(ClubEvent event, Instant from, Instant to) {
        if (!event.isRecurring() || event.getRecurrenceRule() == null) {
            return Collections.emptyList();
        }

        List<ClubEvent> occurrences = new ArrayList<>();
        ZoneId zone = ZoneId.of("Europe/Berlin");
        LocalDateTime baseStart = LocalDateTime.ofInstant(event.getStartAt(), zone);
        Duration duration = event.getEndAt() != null
                ? Duration.between(event.getStartAt(), event.getEndAt())
                : Duration.ZERO;

        LocalDate endDate = event.getRecurrenceEndDate() != null
                ? event.getRecurrenceEndDate()
                : baseStart.toLocalDate().plusYears(1);

        LocalDateTime cursor = baseStart;
        while (true) {
            cursor = advanceCursor(cursor, event.getRecurrenceRule());
            Instant occurrenceStart = cursor.atZone(zone).toInstant();

            if (occurrenceStart.isAfter(to)) break;
            if (cursor.toLocalDate().isAfter(endDate)) break;

            if (occurrenceStart.isAfter(from) || occurrenceStart.equals(from)) {
                // Create a virtual (non-persisted) event representing this occurrence
                ClubEvent virtual = new ClubEvent(
                        event.getClubId(), event.getTitle(), event.getDescription(),
                        event.getEventType(), occurrenceStart,
                        duration.isZero() ? null : occurrenceStart.plus(duration),
                        event.getLocation(), event.getMaxAttendees(), event.getCreatedBy()
                );
                virtual.setId(event.getId()); // Same ID for reference
                virtual.setRecurring(true);
                virtual.setRecurrenceRule(event.getRecurrenceRule());
                occurrences.add(virtual);
            }
        }
        return occurrences;
    }

    // --- Private helpers ---

    private LocalDateTime advanceCursor(LocalDateTime cursor, RecurrenceRule rule) {
        return switch (rule) {
            case WEEKLY -> cursor.plusWeeks(1);
            case BIWEEKLY -> cursor.plusWeeks(2);
            case MONTHLY -> cursor.plusMonths(1);
        };
    }

    private String formatIcalDate(Instant instant) {
        return ICAL_DATE_FORMAT.format(instant.atZone(ZoneOffset.UTC));
    }

    private String escapeIcal(String text) {
        return text.replace("\\", "\\\\")
                   .replace(",", "\\,")
                   .replace(";", "\\;")
                   .replace("\n", "\\n");
    }

    private String toIcalFreq(RecurrenceRule rule) {
        return switch (rule) {
            case WEEKLY, BIWEEKLY -> "WEEKLY";
            case MONTHLY -> "MONTHLY";
        };
    }

    private String formatEventNotification(ClubEvent event) {
        var sb = new StringBuilder("Neue Veranstaltung: ");
        sb.append(event.getTitle());
        if (event.getLocation() != null) {
            sb.append(" — ").append(event.getLocation());
        }
        return sb.toString();
    }

    private String formatEventInfoBoardContent(ClubEvent event) {
        var sb = new StringBuilder();
        sb.append("<p><strong>").append(event.getTitle()).append("</strong></p>");
        sb.append("<p>📅 ").append(formatGermanDate(event.getStartAt()));
        if (event.getEndAt() != null) {
            sb.append(" – ").append(formatGermanDate(event.getEndAt()));
        }
        sb.append("</p>");
        if (event.getLocation() != null) {
            sb.append("<p>📍 ").append(event.getLocation()).append("</p>");
        }
        if (event.getDescription() != null) {
            sb.append("<p>").append(event.getDescription()).append("</p>");
        }
        if (event.getMaxAttendees() != null) {
            sb.append("<p>Max. Teilnehmer: ").append(event.getMaxAttendees()).append("</p>");
        }
        return sb.toString();
    }

    private String formatGermanDate(Instant instant) {
        return DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
                .format(instant.atZone(ZoneId.of("Europe/Berlin")));
    }
}
