package com.unicorn.hgzero.notification.service;

import com.unicorn.hgzero.notification.domain.Notification;
import com.unicorn.hgzero.notification.domain.NotificationRecipient;
import com.unicorn.hgzero.notification.domain.NotificationSetting;
import com.unicorn.hgzero.notification.event.event.MeetingCreatedEvent;
import com.unicorn.hgzero.notification.event.event.NotificationRequestEvent;
import com.unicorn.hgzero.notification.event.event.TodoAssignedEvent;
import com.unicorn.hgzero.notification.repository.NotificationRecipientRepository;
import com.unicorn.hgzero.notification.repository.NotificationRepository;
import com.unicorn.hgzero.notification.repository.NotificationSettingRepository;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * 알림 발송 비즈니스 로직 서비스
 *
 * 회의 초대, Todo 할당 등 다양한 알림 발송 처리
 * 중복 방지, 사용자 설정 확인, 재시도 관리 포함
 *
 * @author 준호 (Backend Developer)
 * @version 1.0
 * @since 2025-10-23
 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class NotificationService {

    private final NotificationRepository notificationRepository;
    private final NotificationRecipientRepository recipientRepository;
    private final NotificationSettingRepository settingRepository;
    private final EmailTemplateService templateService;
    private final EmailClient emailClient;
    private final NotificationRouter notificationRouter;
    private final EmailNotifier emailNotifier;

    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    /**
     * 회의 초대 알림 발송
     *
     * @param event 회의 생성 이벤트
     */
    public void sendMeetingInvitation(MeetingCreatedEvent event) {
        log.info("회의 초대 알림 처리 시작 - MeetingId: {}, EventId: {}",
            event.getMeetingId(), event.getEventId());

        // 1. 중복 발송 방지 체크
        if (notificationRepository.existsByEventId(event.getEventId())) {
            log.warn("이미 처리된 이벤트 - EventId: {}", event.getEventId());
            return;
        }

        // 2. 알림 엔티티 생성
        Notification notification = Notification.builder()
            .eventId(event.getEventId())
            .referenceId(event.getMeetingId())
            .referenceType(Notification.ReferenceType.MEETING)
            .notificationType(Notification.NotificationType.MEETING_INVITATION)
            .title("회의 초대: " + event.getTitle())
            .message(event.getDescription())
            .status(Notification.NotificationStatus.PROCESSING)
            .channel(Notification.NotificationChannel.EMAIL)
            .build();

        notificationRepository.save(notification);

        // 3. 각 참석자에게 알림 발송
        int successCount = 0;
        int failureCount = 0;

        for (MeetingCreatedEvent.ParticipantInfo participant : event.getParticipants()) {
            try {
                // 3-1. 알림 설정 확인
                if (!canSendNotification(participant.getUserId(), Notification.NotificationType.MEETING_INVITATION)) {
                    log.info("알림 설정에 의해 발송 제외 - UserId: {}", participant.getUserId());
                    continue;
                }

                // 3-2. 수신자 엔티티 생성
                NotificationRecipient recipient = NotificationRecipient.builder()
                    .recipientUserId(participant.getUserId())
                    .recipientName(participant.getName())
                    .recipientEmail(participant.getEmail())
                    .status(NotificationRecipient.RecipientStatus.PENDING)
                    .build();

                notification.addRecipient(recipient);

                // 3-3. 이메일 템플릿 렌더링
                Map<String, Object> variables = new HashMap<>();
                variables.put("title", event.getTitle());
                variables.put("description", event.getDescription());
                variables.put("startTime", event.getStartTime().format(DATETIME_FORMATTER));
                variables.put("endTime", event.getEndTime().format(DATETIME_FORMATTER));
                variables.put("location", event.getLocation());
                variables.put("organizerName", event.getOrganizer().getName());
                variables.put("participantName", participant.getName());

                String htmlContent = templateService.renderMeetingInvitation(variables);

                // 3-4. 이메일 발송
                emailClient.sendHtmlEmail(
                    participant.getEmail(),
                    "회의 초대: " + event.getTitle(),
                    htmlContent
                );

                // 3-5. 발송 성공 처리
                recipient.markAsSent();
                notification.incrementSentCount();
                successCount++;

                log.info("회의 초대 알림 발송 성공 - Email: {}", participant.getEmail());

            } catch (MessagingException e) {
                // 3-6. 발송 실패 처리
                NotificationRecipient recipient = notification.getRecipients().stream()
                    .filter(r -> r.getRecipientUserId().equals(participant.getUserId()))
                    .findFirst()
                    .orElse(null);

                if (recipient != null) {
                    recipient.markAsFailed(e.getMessage());
                    notification.incrementFailedCount();
                }

                failureCount++;
                log.error("회의 초대 알림 발송 실패 - Email: {}", participant.getEmail(), e);
            }
        }

        // 4. 알림 상태 업데이트
        if (successCount > 0 && failureCount == 0) {
            notification.updateStatus(Notification.NotificationStatus.SENT);
        } else if (successCount > 0 && failureCount > 0) {
            notification.updateStatus(Notification.NotificationStatus.PARTIAL);
        } else {
            notification.updateStatus(Notification.NotificationStatus.FAILED);
        }

        notificationRepository.save(notification);

        log.info("회의 초대 알림 처리 완료 - 성공: {}, 실패: {}", successCount, failureCount);
    }

    /**
     * Todo 할당 알림 발송
     *
     * @param event Todo 할당 이벤트
     */
    public void sendTodoAssignment(TodoAssignedEvent event) {
        log.info("Todo 할당 알림 처리 시작 - TodoId: {}, EventId: {}",
            event.getTodoId(), event.getEventId());

        // 1. 중복 발송 방지 체크
        if (notificationRepository.existsByEventId(event.getEventId())) {
            log.warn("이미 처리된 이벤트 - EventId: {}", event.getEventId());
            return;
        }

        // 2. 알림 엔티티 생성
        Notification notification = Notification.builder()
            .eventId(event.getEventId())
            .referenceId(event.getTodoId())
            .referenceType(Notification.ReferenceType.TODO)
            .notificationType(Notification.NotificationType.TODO_ASSIGNED)
            .title("Todo 할당: " + event.getTitle())
            .message(event.getDescription())
            .status(Notification.NotificationStatus.PROCESSING)
            .channel(Notification.NotificationChannel.EMAIL)
            .build();

        notificationRepository.save(notification);

        try {
            // 3. 알림 설정 확인
            TodoAssignedEvent.UserInfo assignee = event.getAssignee();

            if (!canSendNotification(assignee.getUserId(), Notification.NotificationType.TODO_ASSIGNED)) {
                log.info("알림 설정에 의해 발송 제외 - UserId: {}", assignee.getUserId());
                notification.updateStatus(Notification.NotificationStatus.SENT);
                return;
            }

            // 4. 수신자 엔티티 생성
            NotificationRecipient recipient = NotificationRecipient.builder()
                .recipientUserId(assignee.getUserId())
                .recipientName(assignee.getName())
                .recipientEmail(assignee.getEmail())
                .status(NotificationRecipient.RecipientStatus.PENDING)
                .build();

            notification.addRecipient(recipient);

            // 5. 이메일 템플릿 렌더링
            Map<String, Object> variables = new HashMap<>();
            variables.put("title", event.getTitle());
            variables.put("description", event.getDescription());
            variables.put("deadline", event.getDeadline().format(DATETIME_FORMATTER));
            variables.put("priority", event.getPriority());
            variables.put("assignedByName", event.getAssignedBy().getName());
            variables.put("assigneeName", assignee.getName());

            // 회의 관련 Todo인 경우 회의 정보 추가
            if (event.getMeetingId() != null) {
                variables.put("meetingId", event.getMeetingId());
            }

            String htmlContent = templateService.renderTodoAssigned(variables);

            // 6. 이메일 발송
            emailClient.sendHtmlEmail(
                assignee.getEmail(),
                "Todo 할당: " + event.getTitle(),
                htmlContent
            );

            // 7. 발송 성공 처리
            recipient.markAsSent();
            notification.incrementSentCount();
            notification.updateStatus(Notification.NotificationStatus.SENT);

            log.info("Todo 할당 알림 발송 성공 - Email: {}", assignee.getEmail());

        } catch (MessagingException e) {
            // 8. 발송 실패 처리
            NotificationRecipient recipient = notification.getRecipients().stream()
                .findFirst()
                .orElse(null);

            if (recipient != null) {
                recipient.markAsFailed(e.getMessage());
            }

            notification.incrementFailedCount();
            notification.updateStatus(Notification.NotificationStatus.FAILED);

            log.error("Todo 할당 알림 발송 실패", e);
        }

        notificationRepository.save(notification);
        log.info("Todo 할당 알림 처리 완료");
    }

    /**
     * 범용 알림 발송 처리
     *
     * NotificationRequest 이벤트를 처리하여 알림 발송
     * 중복 방지, 사용자 설정 확인, 채널 라우팅 포함
     *
     * @param event 알림 발송 요청 이벤트
     */
    public void processNotification(NotificationRequestEvent event) {
        // eventId가 없으면 UUID 생성
        String eventId = event.getEventId();
        if (eventId == null || eventId.isBlank()) {
            eventId = java.util.UUID.randomUUID().toString();
            log.info("EventId가 없어 생성함 - EventId: {}", eventId);
        }

        log.info("알림 발송 처리 시작 - EventId: {}, Type: {}",
            eventId, event.getNotificationType());

        // 1. 중복 발송 방지 체크
        if (notificationRepository.existsByEventId(eventId)) {
            log.warn("이미 처리된 이벤트 - EventId: {}", eventId);
            return;
        }

        // 2. 알림 유형 및 참조 유형 변환
        Notification.NotificationType notificationType = parseNotificationType(event.getNotificationType());

        // referenceType 또는 relatedEntityType 중 하나를 사용
        String referenceTypeStr = event.getReferenceType() != null
            ? event.getReferenceType()
            : event.getRelatedEntityType();
        Notification.ReferenceType referenceType = parseReferenceType(referenceTypeStr);

        // referenceId 또는 relatedEntityId 중 하나를 사용
        String referenceIdStr = event.getReferenceId() != null
            ? event.getReferenceId()
            : event.getRelatedEntityId();

        Notification.NotificationChannel channel = parseChannel(event.getChannel());

        // 3. 알림 엔티티 생성
        Notification notification = Notification.builder()
            .eventId(eventId)
            .referenceId(referenceIdStr)
            .referenceType(referenceType)
            .notificationType(notificationType)
            .title(event.getTitle())
            .message(event.getMessage())
            .status(Notification.NotificationStatus.PROCESSING)
            .channel(channel)
            .build();

        notificationRepository.save(notification);

        // 4. 수신자 리스트 구성 (단일 수신자 또는 다중 수신자)
        java.util.List<NotificationRequestEvent.RecipientInfo> recipientList = new java.util.ArrayList<>();

        // 4-1. 단일 수신자 필드가 있으면 추가
        if (event.getRecipientEmail() != null && !event.getRecipientEmail().isBlank()) {
            NotificationRequestEvent.RecipientInfo singleRecipient = NotificationRequestEvent.RecipientInfo.builder()
                .userId(event.getRecipientId())
                .name(event.getRecipientName())
                .email(event.getRecipientEmail())
                .build();
            recipientList.add(singleRecipient);
            log.info("단일 수신자 처리 - Email: {}", event.getRecipientEmail());
        }

        // 4-2. recipients 리스트가 있으면 추가
        if (event.getRecipients() != null && !event.getRecipients().isEmpty()) {
            recipientList.addAll(event.getRecipients());
            log.info("다중 수신자 처리 - Count: {}", event.getRecipients().size());
        }

        // 4-3. 수신자가 없으면 경고 후 종료
        if (recipientList.isEmpty()) {
            log.warn("수신자가 없습니다 - EventId: {}", event.getEventId());
            notification.updateStatus(Notification.NotificationStatus.FAILED);
            notificationRepository.save(notification);
            return;
        }

        // 5. 각 수신자에게 알림 발송
        int successCount = 0;
        int failureCount = 0;

        for (NotificationRequestEvent.RecipientInfo recipient : recipientList) {
            try {
                // 5-1. 사용자 알림 설정 조회
                String userId = recipient.getUserId();
                Optional<NotificationSetting> preferences = Optional.empty();

                if (userId != null && !userId.isBlank()) {
                    preferences = settingRepository.findByUserId(userId);

                    // 5-2. 알림 설정 확인 (userId가 있는 경우만)
                    if (!canSendNotification(userId, notificationType)) {
                        log.info("알림 설정에 의해 발송 제외 - UserId: {}", userId);
                        continue;
                    }
                }

                // 5-3. 수신자 엔티티 생성
                NotificationRecipient recipientEntity = NotificationRecipient.builder()
                    .recipientUserId(userId)
                    .recipientName(recipient.getName())
                    .recipientEmail(recipient.getEmail())
                    .status(NotificationRecipient.RecipientStatus.PENDING)
                    .build();

                notification.addRecipient(recipientEntity);

                // 5-4. 템플릿 렌더링 (템플릿 ID가 있는 경우)
                String htmlContent;
                if (event.getTemplateId() != null && event.getTemplateData() != null) {
                    htmlContent = emailNotifier.loadAndRenderTemplate(
                        event.getTemplateId(),
                        event.getTemplateData()
                    );
                } else {
                    // 기본 메시지 사용
                    htmlContent = createDefaultHtmlContent(event.getTitle(), event.getMessage());
                }

                // 5-5. 채널 라우팅 및 발송
                boolean sent = notificationRouter.routeNotification(
                    eventId,
                    recipient.getEmail(),
                    recipient.getName(),
                    event.getTitle(),
                    htmlContent,
                    preferences.orElse(null),
                    channel
                );

                // 5-6. 발송 결과 처리
                if (sent) {
                    recipientEntity.markAsSent();
                    notification.incrementSentCount();
                    successCount++;
                    log.info("알림 발송 성공 - Email: {}", recipient.getEmail());
                } else {
                    recipientEntity.markAsFailed("발송 실패");
                    notification.incrementFailedCount();
                    failureCount++;
                    log.error("알림 발송 실패 - Email: {}", recipient.getEmail());
                }

            } catch (Exception e) {
                // 5-7. 예외 처리
                NotificationRecipient recipientEntity = notification.getRecipients().stream()
                    .filter(r -> recipient.getEmail().equals(r.getRecipientEmail()))
                    .findFirst()
                    .orElse(null);

                if (recipientEntity != null) {
                    recipientEntity.markAsFailed(e.getMessage());
                    notification.incrementFailedCount();
                }

                failureCount++;
                log.error("알림 발송 중 오류 발생 - Email: {}", recipient.getEmail(), e);
            }
        }

        // 5. 알림 상태 업데이트
        if (successCount > 0 && failureCount == 0) {
            notification.updateStatus(Notification.NotificationStatus.SENT);
        } else if (successCount > 0 && failureCount > 0) {
            notification.updateStatus(Notification.NotificationStatus.PARTIAL);
        } else {
            notification.updateStatus(Notification.NotificationStatus.FAILED);
        }

        notificationRepository.save(notification);

        log.info("알림 발송 처리 완료 - 성공: {}, 실패: {}", successCount, failureCount);
    }

    /**
     * 알림 유형 문자열을 Enum으로 변환
     *
     * @param typeString 알림 유형 문자열
     * @return 알림 유형 Enum
     */
    private Notification.NotificationType parseNotificationType(String typeString) {
        try {
            return Notification.NotificationType.valueOf(typeString);
        } catch (Exception e) {
            log.warn("알 수 없는 알림 유형: {} - MEETING_INVITATION으로 기본 설정", typeString);
            return Notification.NotificationType.MEETING_INVITATION;
        }
    }

    /**
     * 참조 유형 문자열을 Enum으로 변환
     *
     * @param typeString 참조 유형 문자열
     * @return 참조 유형 Enum
     */
    private Notification.ReferenceType parseReferenceType(String typeString) {
        try {
            return Notification.ReferenceType.valueOf(typeString);
        } catch (Exception e) {
            log.warn("알 수 없는 참조 유형: {} - MEETING으로 기본 설정", typeString);
            return Notification.ReferenceType.MEETING;
        }
    }

    /**
     * 채널 문자열을 Enum으로 변환
     *
     * @param channelString 채널 문자열
     * @return 채널 Enum
     */
    private Notification.NotificationChannel parseChannel(String channelString) {
        try {
            return Notification.NotificationChannel.valueOf(channelString);
        } catch (Exception e) {
            log.warn("알 수 없는 채널: {} - EMAIL로 기본 설정", channelString);
            return Notification.NotificationChannel.EMAIL;
        }
    }

    /**
     * 기본 HTML 콘텐츠 생성
     *
     * @param title 제목
     * @param message 메시지
     * @return HTML 본문
     */
    private String createDefaultHtmlContent(String title, String message) {
        return String.format("""
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="UTF-8">
                <title>%s</title>
            </head>
            <body>
                <h2>%s</h2>
                <p>%s</p>
            </body>
            </html>
            """, title, title, message);
    }

    /**
     * 알림 발송 가능 여부 확인
     *
     * 사용자 알림 설정, 채널, 알림 유형, 방해 금지 시간대 체크
     *
     * @param userId 사용자 ID
     * @param notificationType 알림 유형
     * @return 발송 가능 여부
     */
    private boolean canSendNotification(String userId, Notification.NotificationType notificationType) {
        Optional<NotificationSetting> settingOpt = settingRepository.findByUserId(userId);

        // 설정이 없으면 기본값으로 발송 허용 (이메일, 초대/할당 알림만)
        if (settingOpt.isEmpty()) {
            return notificationType == Notification.NotificationType.MEETING_INVITATION
                || notificationType == Notification.NotificationType.TODO_ASSIGNED;
        }

        NotificationSetting setting = settingOpt.get();

        // 이메일 채널 및 알림 유형 활성화 여부 확인
        return setting.canSendNotification(notificationType, Notification.NotificationChannel.EMAIL);
    }
}
