package com.unicorn.hgzero.meeting.biz.service;

import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import com.unicorn.hgzero.meeting.biz.domain.Session;
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisWriter;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
import com.unicorn.hgzero.meeting.infra.cache.CacheService;
import com.unicorn.hgzero.meeting.infra.event.dto.MeetingEndedEvent;
import com.unicorn.hgzero.meeting.infra.event.dto.MeetingStartedEvent;
import com.unicorn.hgzero.meeting.infra.event.dto.NotificationRequestEvent;
import com.unicorn.hgzero.meeting.infra.event.publisher.EventPublisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

/**
 * 회의 Service
 * Meeting 관련 모든 UseCase 구현
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MeetingService implements
        CreateMeetingUseCase,
        StartMeetingUseCase,
        EndMeetingUseCase,
        CancelMeetingUseCase,
        GetMeetingUseCase,
        InviteParticipantUseCase {

    private final MeetingReader meetingReader;
    private final MeetingWriter meetingWriter;
    private final SessionReader sessionReader;
    private final SessionWriter sessionWriter;
    private final MinutesReader minutesReader;
    private final MinutesWriter minutesWriter;
    private final MeetingAnalysisReader meetingAnalysisReader;
    private final MeetingAnalysisWriter meetingAnalysisWriter;
    private final CacheService cacheService;
    private final EventPublisher eventPublisher;
    private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter participantWriter;
    private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader participantReader;

    /**
     * 회의 생성
     */
    @Override
    @Transactional
    public Meeting createMeeting(CreateMeetingCommand command) {
        log.info("Creating meeting: {}", command.title());

        // 1. 회의 시간 유효성 검사
        if (command.scheduledAt().isAfter(command.endTime()) ||
            command.scheduledAt().isEqual(command.endTime())) {
            log.warn("Invalid meeting time: start={}, end={}",
                    command.scheduledAt(), command.endTime());
            throw new BusinessException(ErrorCode.INVALID_MEETING_TIME);
        }

        // 2. 중복 회의 체크
        long conflictCount = meetingReader.countConflictingMeetings(
                command.organizerId(),
                command.scheduledAt(),
                command.endTime()
        );

        if (conflictCount > 0) {
            log.warn("Meeting time conflict detected: organizerId={}, count={}",
                    command.organizerId(), conflictCount);
            throw new BusinessException(ErrorCode.MEETING_TIME_CONFLICT);
        }

        // 3. 회의 ID 생성
        String meetingId = UUID.randomUUID().toString();

        // 4. 회의 도메인 객체 생성
        Meeting meeting = Meeting.builder()
                .meetingId(meetingId)
                .title(command.title())
                .purpose(command.purpose())
                .description(command.agenda())
                .location(command.location())
                .scheduledAt(command.scheduledAt())
                .endTime(command.endTime())
                .status("SCHEDULED")
                .organizerId(command.organizerId())
                .participants(command.participants())
                .templateId(command.templateId())
                .build();

        // 5. 회의 저장
        Meeting savedMeeting = meetingWriter.save(meeting);

        // 5-1. 참석자 목록 저장
        if (command.participants() != null && !command.participants().isEmpty()) {
            participantWriter.saveParticipants(meetingId, command.participants());
            log.debug("Participants saved: meetingId={}, count={}", meetingId, command.participants().size());
        }

        // 6. 캐시 저장 (TTL: 10분)
        try {
            cacheService.cacheMeeting(meetingId, savedMeeting, 600);
            log.debug("Meeting cached: meetingId={}", meetingId);
        } catch (Exception e) {
            log.warn("Failed to cache meeting: meetingId={}", meetingId, e);
            // 캐시 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        // 7. 참석자 초대 이벤트 발행 (비동기)
        try {
            eventPublisher.publishMeetingCreated(
                    meetingId,
                    command.title(),
                    command.scheduledAt(),
                    command.location(),
                    command.participants(),
                    command.organizerId(),
                    command.organizerId() // organizerName은 나중에 User 서비스 연동 시 개선
            );
            log.debug("Meeting invitation events published: meetingId={}, participants={}",
                    meetingId, command.participants().size());
        } catch (Exception e) {
            log.error("Failed to publish meeting invitation events: meetingId={}", meetingId, e);
            // 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        log.info("Meeting created successfully: {}", savedMeeting.getMeetingId());
        return savedMeeting;
    }

    /**
     * 회의 시작
     */
    @Override
    @Transactional
    public Session startMeeting(String meetingId) {
        log.info("Starting meeting: {}", meetingId);

        // 1. Redis 캐시 조회
        Meeting meeting = cacheService.getCachedMeeting(meetingId, Meeting.class);

        // 2. 캐시 미스시 DB 조회 및 캐싱
        if (meeting == null) {
            log.debug("Cache miss for meeting: {}", meetingId);
            meeting = meetingReader.findById(meetingId)
                    .orElseThrow(() -> {
                        log.error("Meeting not found: {}", meetingId);
                        return new BusinessException(ErrorCode.ENTITY_NOT_FOUND);
                    });

            // 캐시 저장 (TTL: 10분)
            try {
                cacheService.cacheMeeting(meetingId, meeting, 600);
            } catch (Exception e) {
                log.warn("Failed to cache meeting: {}", meetingId, e);
            }
        } else {
            log.debug("Cache hit for meeting: {}", meetingId);
        }

        // 3. 비즈니스 규칙 검증
        // TODO: 권한 검증 (생성자 또는 참석자) - userId 파라미터 필요

        // 4. 회의 상태 확인 (임시로 SCHEDULED와 IN_PROGRESS 모두 시작 가능)
        if (!"SCHEDULED".equals(meeting.getStatus()) && !"IN_PROGRESS".equals(meeting.getStatus())) {
            log.warn("Meeting is not in SCHEDULED or IN_PROGRESS status: meetingId={}, status={}",
                    meetingId, meeting.getStatus());

            throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE,
                    "회의 상태가 올바르지 않습니다");
        }

        // 5. 회의 세션 생성
        String sessionId = UUID.randomUUID().toString();
        Session session = Session.builder()
                .sessionId(sessionId)
                .meetingId(meetingId)
                .startedBy(meeting.getOrganizerId()) // TODO: 실제 사용자 ID 사용
                .startedAt(LocalDateTime.now())
                .status("ACTIVE")
                .build();

        // 6. 세션 저장
        Session savedSession = sessionWriter.save(session);
        log.debug("Session created: sessionId={}, meetingId={}", sessionId, meetingId);

        // 7. 회의 상태를 IN_PROGRESS로 업데이트
        meeting.start();
        Meeting updatedMeeting = meetingWriter.save(meeting);
        log.debug("Meeting status updated to IN_PROGRESS: {}", meetingId);

        // 8. 캐시 무효화
        try {
            cacheService.evictCache("meeting:", meetingId);
            log.debug("Meeting cache evicted: {}", meetingId);
        } catch (Exception e) {
            log.warn("Failed to evict meeting cache: {}", meetingId, e);
        }

        // 9. 회의록 초안 생성 (빈 회의록)
        String minutesId = UUID.randomUUID().toString();
        Minutes minutesDraft = Minutes.builder()
                .minutesId(minutesId)
                .meetingId(meetingId)
                .title(meeting.getTitle() + " - 회의록")
                .sections(List.of())
                .status("DRAFT")
                .version(1)
                .createdBy(meeting.getOrganizerId()) // TODO: 실제 사용자 ID 사용
                .createdAt(LocalDateTime.now())
                .build();

        Minutes savedMinutes = minutesWriter.save(minutesDraft);
        log.debug("Minutes draft created: minutesId={}, meetingId={}", minutesId, meetingId);

        // 세션에 회의록 ID 연결
        Session updatedSession = Session.builder()
                .sessionId(savedSession.getSessionId())
                .meetingId(savedSession.getMeetingId())
                .minutesId(minutesId)
                .startedBy(savedSession.getStartedBy())
                .startedAt(savedSession.getStartedAt())
                .status(savedSession.getStatus())
                .build();
        sessionWriter.save(updatedSession);

        // 10. 비동기 이벤트 발행
        try {
            MeetingStartedEvent event = MeetingStartedEvent.builder()
                    .meetingId(meetingId)
                    .sessionId(sessionId)
                    .title(meeting.getTitle())
                    .startTime(meeting.getStartedAt())
                    .organizer(meeting.getOrganizerId())
                    .participants(meeting.getParticipants())
                    .minutesId(minutesId)
                    .eventTime(LocalDateTime.now())
                    .build();

            eventPublisher.publishMeetingStarted(event);
            log.debug("MeetingStarted event published: meetingId={}, sessionId={}",
                    meetingId, sessionId);
        } catch (Exception e) {
            log.error("Failed to publish MeetingStarted event: meetingId={}", meetingId, e);
            // 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        log.info("Meeting started successfully: meetingId={}, sessionId={}, minutesId={}",
                meetingId, sessionId, minutesId);
        return updatedSession;
    }

    /**
     * 회의 종료 및 AI 분석
     */
    @Override
    @Transactional
    public MeetingEndDTO endMeeting(String meetingId) {
        log.info("Ending meeting: {}", meetingId);

        // 1. 회의 조회
        log.debug("Searching for meeting with ID: {}", meetingId);
        Meeting meeting = meetingReader.findById(meetingId)
                .orElseThrow(() -> {
                    log.error("Meeting not found: {}", meetingId);
                    return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의를 찾을 수 없습니다: " + meetingId);
                });
        log.debug("Found meeting: {}, status: {}", meeting.getTitle(), meeting.getStatus());

        // 2. 회의 상태 검증 (SCHEDULED 또는 IN_PROGRESS만 종료 가능)
        if (!"SCHEDULED".equals(meeting.getStatus()) && !"IN_PROGRESS".equals(meeting.getStatus())) {
            log.warn("Invalid meeting status for ending: meetingId={}, status={}", meetingId, meeting.getStatus());
            throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, 
                    "회의를 종료할 수 없는 상태입니다. 현재 상태: " + meeting.getStatus());
        }

        // 3. 회의 종료
        meeting.end();
        Meeting updatedMeeting = meetingWriter.save(meeting);

        // 4. 회의록 조회 (SCHEDULED 상태면 빈 회의록 생성, IN_PROGRESS면 기존 회의록 조회)
        Minutes minutes;
        if ("SCHEDULED".equals(meeting.getStatus())) {
            // SCHEDULED 상태에서 종료하는 경우 빈 회의록 생성
            String minutesId = UUID.randomUUID().toString();
            minutes = Minutes.builder()
                    .minutesId(minutesId)
                    .meetingId(meetingId)
                    .title(meeting.getTitle() + " - 회의록")
                    .sections(List.of())
                    .status("DRAFT")
                    .version(1)
                    .createdBy(meeting.getOrganizerId())
                    .createdAt(LocalDateTime.now())
                    .build();
            minutesWriter.save(minutes);
            log.info("Empty minutes created for SCHEDULED meeting: meetingId={}, minutesId={}", meetingId, minutesId);
        } else {
            // IN_PROGRESS 상태면 기존 회의록 조회
            log.debug("Searching for existing minutes for meeting: {}", meetingId);
            minutes = minutesReader.findLatestByMeetingId(meetingId)
                    .orElseThrow(() -> {
                        log.error("Minutes not found for meeting: {}", meetingId);
                        return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의록을 찾을 수 없습니다: " + meetingId);
                    });
            log.debug("Found minutes: {}", minutes.getTitle());
        }

        // 5. 기존 분석 데이터 확인 후 사용 또는 새로 생성
        Optional<MeetingAnalysis> existingAnalysis = meetingAnalysisReader.findLatestByMeetingId(meetingId);
        MeetingAnalysis analysis;
        
        if (existingAnalysis.isPresent()) {
            // 기존 분석 데이터가 있으면 사용
            analysis = existingAnalysis.get();
            log.info("Using existing analysis data for meeting: {}", meetingId);
        } else {
            // 기존 분석 데이터가 없으면 새로 생성
            analysis = createBasicAnalysis(meeting, minutes);
            meetingAnalysisWriter.save(analysis);
            log.info("Created new analysis data for meeting: {}", meetingId);
        }
        
        // 6. AI 서비스에 회의 종료 이벤트 발행
        try {
            MeetingEndedEvent meetingEndedEvent = MeetingEndedEvent.builder()
                    .meetingId(meetingId)
                    .title(meeting.getTitle())
                    .endTime(meeting.getEndedAt())
                    .organizer(meeting.getOrganizerId())
                    .participants(meeting.getParticipants())
                    .minutesId(minutes.getMinutesId())
                    .todoCount(0) // AI 분석으로 추후 업데이트될 예정
                    .eventTime(LocalDateTime.now())
                    .build();

            eventPublisher.publishMeetingEnded(meetingEndedEvent);
            log.info("MeetingEnded event published: meetingId={}, minutesId={}", 
                    meetingId, minutes.getMinutesId());
        } catch (Exception e) {
            log.error("Failed to publish MeetingEnded event: meetingId={}", meetingId, e);
            // 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        // 7. 참석자에게 회의 종료 알림 이벤트 발행
        try {
            for (String participantEmail : meeting.getParticipants()) {
                NotificationRequestEvent notificationEvent = NotificationRequestEvent.builder()
                        .notificationType("MEETING_ENDED")
                        .recipientEmail(participantEmail)
                        .recipientId(participantEmail)
                        .recipientName(participantEmail)
                        .title("회의 종료")
                        .message(String.format("'%s' 회의가 종료되었습니다. 회의록을 확인해 주세요.", meeting.getTitle()))
                        .relatedEntityId(meetingId)
                        .relatedEntityType("MEETING")
                        .requestedBy(meeting.getOrganizerId())
                        .eventTime(LocalDateTime.now())
                        .build();

                eventPublisher.publishNotificationRequest(notificationEvent);
            }
            log.info("Meeting end notification events published: meetingId={}, participants={}", 
                    meetingId, meeting.getParticipants().size());
        } catch (Exception e) {
            log.error("Failed to publish meeting end notification events: meetingId={}", meetingId, e);
            // 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        // 8. 결과 DTO 구성
        MeetingEndDTO result = buildMeetingEndDTO(meeting, analysis);

        log.info("Meeting ended successfully: {}", meetingId);
        return result;
    }

    /**
     * 간단한 회의 분석 수행 (AI 서비스 없이 기본 정보만 처리)
     * AI 분석은 별도 AI 서비스에서 비동기로 처리되어야 함
     */
    private MeetingAnalysis createBasicAnalysis(Meeting meeting, Minutes minutes) {
        log.info("Creating basic analysis for meeting: {}", meeting.getMeetingId());

        // 기본 분석 정보만 생성 (키워드나 상세 분석은 AI 서비스에서 별도 처리)
        return MeetingAnalysis.builder()
                .analysisId(UUID.randomUUID().toString())
                .meetingId(meeting.getMeetingId())
                .minutesId(minutes.getMinutesId())
                .keywords(List.of()) // AI 서비스에서 별도로 채워짐
                .agendaAnalyses(List.of()) // AI 서비스에서 별도로 채워짐
                .status("PENDING") // AI 처리 대기 상태
                .createdAt(LocalDateTime.now())
                .build();
    }

    /**
     * MeetingEndDTO 구성
     */
    private MeetingEndDTO buildMeetingEndDTO(Meeting meeting, MeetingAnalysis analysis) {
        // 회의 시간 및 참석자 수 계산 (실제 데이터 기반)
        int durationMinutes = calculateActualDuration(meeting);
        int participantCount = calculateActualParticipantCount(meeting.getMeetingId());
        
        // 전체 Todo 개수 계산 (AI 분석 완료되기 전까지는 0)
        int totalTodos = analysis.getAgendaAnalyses().stream()
                .mapToInt(agenda -> agenda.getExtractedTodos().size())
                .sum();

        // 안건별 요약 DTO 변환
        List<MeetingEndDTO.AgendaSummaryDTO> agendaSummaries = analysis.getAgendaAnalyses().stream()
                .map(agenda -> MeetingEndDTO.AgendaSummaryDTO.builder()
                        .title(agenda.getTitle())
                        .aiSummaryShort(agenda.getAiSummaryShort())
                        .details(MeetingEndDTO.AgendaDetailsDTO.builder()
                                .discussion(agenda.getDiscussion())
                                .decisions(agenda.getDecisions())
                                .pending(agenda.getPending())
                                .build())
                        .todos(agenda.getExtractedTodos().stream()
                                .map(todo -> MeetingEndDTO.TodoSummaryDTO.builder()
                                        .title(todo)
                                        .build())
                                .toList())
                        .build())
                .toList();

        return MeetingEndDTO.builder()
                .title(meeting.getTitle())
                .participantCount(participantCount)
                .durationMinutes(durationMinutes)
                .agendaCount(analysis.getAgendaAnalyses().size())
                .todoCount(totalTodos)
                .keywords(analysis.getKeywords())
                .agendaSummaries(agendaSummaries)
                .build();
    }

    /**
     * 회의 취소
     */
    @Override
    @Transactional
    public Meeting cancelMeeting(String meetingId) {
        log.info("Canceling meeting: {}", meetingId);

        // 회의 조회
        Meeting meeting = meetingReader.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));

        // 회의 취소 가능 상태 검증
        if ("COMPLETED".equals(meeting.getStatus()) || "CANCELLED".equals(meeting.getStatus())) {
            throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
        }

        // 회의 취소
        meeting.cancel();

        // 저장
        Meeting updatedMeeting = meetingWriter.save(meeting);

        log.info("Meeting cancelled successfully: {}", meetingId);
        return updatedMeeting;
    }

    /**
     * 회의 실제 진행 시간 계산
     */
    private int calculateActualDuration(Meeting meeting) {
        if (meeting.getStartedAt() != null && meeting.getEndedAt() != null) {
            // 실제 시작/종료 시간이 있으면 실제 진행 시간 계산
            Duration duration = Duration.between(meeting.getStartedAt(), meeting.getEndedAt());
            return (int) duration.toMinutes();
        } else if (meeting.getScheduledAt() != null && meeting.getEndTime() != null) {
            // 예정 시간 기준으로 계산
            Duration duration = Duration.between(meeting.getScheduledAt(), meeting.getEndTime());
            return (int) duration.toMinutes();
        } else {
            // 기본값 (1시간)
            return 60;
        }
    }

    /**
     * 실제 참석자 수 계산
     */
    private int calculateActualParticipantCount(String meetingId) {
        // 실제 참석한 참석자 수 계산 (meeting_participants 테이블에서 attended=true인 수)
        // 현재는 기본적으로 주최자 + 참석자 수로 계산
        List<String> participants = meetingReader.findById(meetingId)
                .map(Meeting::getParticipants)
                .orElse(List.of());
        return participants.size() + 1; // 주최자 포함
    }

    /**
     * ID로 회의 조회
     */
    @Override
    @Transactional(readOnly = true)
    public Meeting getMeeting(String meetingId) {
        log.debug("Getting meeting: {}", meetingId);

        return meetingReader.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
    }

    /**
     * 주최자 ID로 회의 목록 조회
     */
    @Override
    @Transactional(readOnly = true)
    public List<Meeting> getMeetingsByOrganizer(String organizerId) {
        log.debug("Getting meetings by organizer: {}", organizerId);

        return meetingReader.findByOrganizerId(organizerId);
    }

    /**
     * 상태로 회의 목록 조회
     */
    @Override
    @Transactional(readOnly = true)
    public List<Meeting> getMeetingsByStatus(String status) {
        log.debug("Getting meetings by status: {}", status);

        return meetingReader.findByStatus(status);
    }

    /**
     * 일정 시간 범위로 회의 목록 조회
     */
    @Override
    @Transactional(readOnly = true)
    public List<Meeting> getMeetingsByScheduledTime(LocalDateTime startTime, LocalDateTime endTime) {
        log.debug("Getting meetings by scheduled time: {} ~ {}", startTime, endTime);

        return meetingReader.findByScheduledTimeBetween(startTime, endTime);
    }

    /**
     * 주최자 ID와 상태로 회의 목록 조회
     */
    @Override
    @Transactional(readOnly = true)
    public List<Meeting> getMeetingsByOrganizerAndStatus(String organizerId, String status) {
        log.debug("Getting meetings by organizer: {} and status: {}", organizerId, status);

        return meetingReader.findByOrganizerIdAndStatus(organizerId, status);
    }

    /**
     * 회의 참석자 초대
     */
    @Override
    @Transactional
    public void inviteParticipant(InviteParticipantCommand command) {
        log.info("Inviting participant to meeting: {}, email: {}", command.meetingId(), command.email());

        // 회의 조회
        Meeting meeting = meetingReader.findById(command.meetingId())
                .orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));

        // 회의 상태 검증 (예약됨 또는 진행중인 회의만 초대 가능)
        if ("COMPLETED".equals(meeting.getStatus()) || "CANCELLED".equals(meeting.getStatus())) {
            throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
        }

        // 이미 참석자로 등록되었는지 확인
        if (participantReader.existsParticipant(command.meetingId(), command.email())) {
            log.warn("Email {} is already a participant of meeting {}", command.email(), command.meetingId());
            throw new BusinessException(ErrorCode.DUPLICATE_RESOURCE);
        }

        // 참석자 저장
        participantWriter.saveParticipant(command.meetingId(), command.email());

        // 회의 초대 알림 이벤트 발행
        try {
            NotificationRequestEvent event = NotificationRequestEvent.builder()
                    .notificationType("MEETING_INVITATION")
                    .recipientEmail(command.email())
                    .recipientId(command.email())
                    .recipientName(command.email())
                    .title("회의 초대")
                    .message(String.format("'%s' 회의에 초대되었습니다. 일시: %s, 장소: %s, 참여 링크: %s",
                            meeting.getTitle(), meeting.getScheduledAt(), meeting.getLocation(), command.frontendUrl()))
                    .relatedEntityId(command.meetingId())
                    .relatedEntityType("MEETING")
                    .requestedBy(meeting.getOrganizerId())
                    .requestedByName(command.inviterName())
                    .eventTime(LocalDateTime.now())
                    .build();

            eventPublisher.publishNotificationRequest(event);
            log.info("Meeting invitation event published for email: {}, meetingId: {}", 
                    command.email(), command.meetingId());
        } catch (Exception e) {
            log.error("Failed to publish meeting invitation event: meetingId={}, email={}", 
                    command.meetingId(), command.email(), e);
            // 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
        }

        log.info("Participant invited successfully: {} to meeting {}", command.email(), command.meetingId());
    }
}
