package de.assecutor.votianlt.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.Message;
import de.assecutor.votianlt.model.MessageContentType;
import de.assecutor.votianlt.model.MessageOrigin;
import de.assecutor.votianlt.model.MessageType;
import de.assecutor.votianlt.dto.ChatMessageInboundPayload;
import de.assecutor.votianlt.dto.ChatMessageOutboundPayload;
import de.assecutor.votianlt.event.MessageReceivedEvent;
import de.assecutor.votianlt.messaging.WebSocketService;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.MessageRepository;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;

@Service
@Slf4j
public class MessageService {

    private final MessageRepository messageRepository;
    private final JobRepository jobRepository;
    private final ApplicationEventPublisher eventPublisher;
    private final WebSocketService webSocketService;
    private final ObjectMapper objectMapper;

    public MessageService(MessageRepository messageRepository, JobRepository jobRepository,
            ApplicationEventPublisher eventPublisher, WebSocketService webSocketService, ObjectMapper objectMapper) {
        this.messageRepository = messageRepository;
        this.jobRepository = jobRepository;
        this.eventPublisher = eventPublisher;
        this.webSocketService = webSocketService;
        this.objectMapper = objectMapper;
    }

    /**
     * Save a message to the database
     */
    public Message saveMessage(Message message) {
        return messageRepository.save(message);
    }

    /**
     * Send a general message to a client via WebSocket
     * 
     * @param content
     *            Message content
     * @param receiver
     *            AppUser ID (clientId)
     */
    public Message sendGeneralMessageToClient(String content, String receiver) {
        return sendGeneralMessageToClient(content, receiver, MessageContentType.TEXT);
    }

    public Message sendGeneralMessageToClient(String content, String receiver, MessageContentType contentType) {
        Message message = new Message(content, receiver, MessageOrigin.SERVER, contentType);
        message = saveMessage(message);
        publishMessage(message, receiver);
        return message;
    }

    /**
     * Send a job-related message to a client via WebSocket
     * 
     * @param content
     *            Message content
     * @param receiver
     *            AppUser ID (clientId)
     * @param jobId
     *            Job ObjectId
     * @param jobNumber
     *            Job number
     */
    public Message sendJobMessageToClient(String content, String receiver, ObjectId jobId, String jobNumber) {
        return sendJobMessageToClient(content, receiver, MessageContentType.TEXT, jobId, jobNumber);
    }

    public Message sendJobMessageToClient(String content, String receiver, MessageContentType contentType,
            ObjectId jobId, String jobNumber) {
        JobContext context = resolveJobContext(jobId, jobNumber);
        Message message = new Message(content, receiver, MessageOrigin.SERVER, contentType, context.jobId(),
                context.jobNumber());
        message = saveMessage(message);
        publishMessage(message, receiver);
        return message;
    }

    /**
     * Handle incoming message from a client
     *
     * @param payload
     *            Inbound message payload where receiver = AppUser ID (clientId)
     */
    public Message receiveMessageFromClient(ChatMessageInboundPayload payload) {
        String clientMessageId = payload.messageId();
        if (clientMessageId != null && !clientMessageId.isBlank()) {
            Optional<Message> existingMessage = messageRepository.findFirstByReceiverAndOriginAndClientMessageId(
                    payload.receiver(), MessageOrigin.CLIENT, clientMessageId);
            if (existingMessage.isPresent()) {
                sendClientMessageAck(payload.receiver(), clientMessageId, existingMessage.get());
                return existingMessage.get();
            }
        }

        Message message;
        MessageContentType contentType = payload.contentType();
        if (payload.hasJobContext()) {
            JobContext context = resolveJobContext(payload.jobId(), payload.jobNumber());
            message = new Message(payload.content(), payload.receiver(), MessageOrigin.CLIENT, contentType,
                    context.jobId(), context.jobNumber());
        } else {
            message = new Message(payload.content(), payload.receiver(), MessageOrigin.CLIENT, contentType);
        }
        message.setClientMessageId(clientMessageId);
        message = saveMessage(message);
        sendClientMessageAck(payload.receiver(), clientMessageId, message);
        eventPublisher.publishEvent(new MessageReceivedEvent(this, message));
        return message;
    }

    private void sendClientMessageAck(String receiver, String clientMessageId, Message message) {
        if (receiver == null || receiver.isBlank() || clientMessageId == null || clientMessageId.isBlank()
                || message == null) {
            return;
        }

        try {
            LinkedHashMap<String, Object> payload = new LinkedHashMap<>();
            payload.put("clientMessageId", clientMessageId);
            payload.put("messageId", message.getIdAsString());
            payload.put("createdAt", message.getCreatedAt());

            byte[] data = objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8);
            webSocketService.sendToClient(receiver, "message_ack", data).exceptionally(ex -> {
                log.debug("[Messaging] Failed to send ACK for client message {} to {}: {}", clientMessageId, receiver,
                        ex.getMessage());
                return null;
            });
        } catch (Exception e) {
            log.error("[Messaging] Error sending ACK for client message {}: {}", clientMessageId, e.getMessage());
        }
    }

    /**
     * Publish message to topic for the receiver. Only sends if client is connected,
     * otherwise keeps NOTSEND status.
     */
    private void publishMessage(Message message, String receiver) {
        try {
            // Check if client is connected before attempting to send
            if (!webSocketService.isClientConnected(receiver)) {
                log.debug("[Messaging] Client {} not connected, message {} saved as NOTSEND", receiver,
                        message.getIdAsString());
                // Message already has NOTSEND status by default
                return;
            }

            ChatMessageOutboundPayload payload = ChatMessageOutboundPayload.fromMessage(message);
            byte[] data = objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8);

            // Use WebSocketService directly to get CompletableFuture for delivery tracking
            webSocketService.sendToClient(receiver, "message", data).thenRun(() -> {
                // Success: mark as sent
                message.markAsSent();
                messageRepository.save(message);
                log.debug("[Messaging] Message {} delivered to client {}, marked as SEND", message.getIdAsString(),
                        receiver);
            }).exceptionally(ex -> {
                // Failed to deliver: keep NOTSEND status
                log.debug("[Messaging] Failed to deliver message {} to client {}: {}", message.getIdAsString(),
                        receiver, ex.getMessage());
                return null;
            });

        } catch (Exception e) {
            log.error("[Messaging] Error publishing message: {}", e.getMessage());
        }
    }

    /**
     * Get all undelivered messages (NOTSEND status) for a receiver
     */
    public List<Message> getUndeliveredMessagesForReceiver(String receiver) {
        return messageRepository.findUndeliveredMessagesForReceiver(receiver);
    }

    /**
     * Send pending messages to a client that just connected. Called after
     * successful authentication.
     *
     * @param receiver
     *            AppUser ID (clientId)
     * @return Number of messages sent
     */
    public int sendPendingMessages(String receiver) {
        if (!webSocketService.isClientConnected(receiver)) {
            log.debug("[Messaging] Cannot send pending messages, client {} not connected", receiver);
            return 0;
        }

        List<Message> pendingMessages = getUndeliveredMessagesForReceiver(receiver);
        if (pendingMessages.isEmpty()) {
            return 0;
        }

        int sentCount = 0;
        for (Message message : pendingMessages) {
            try {
                ChatMessageOutboundPayload payload = ChatMessageOutboundPayload.fromMessage(message);
                byte[] data = objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8);

                webSocketService.sendToClient(receiver, "message", data).thenRun(() -> {
                    message.markAsSent();
                    messageRepository.save(message);
                }).exceptionally(ex -> {
                    log.error("[Messaging] Failed to send pending message {}: {}", message.getIdAsString(),
                            ex.getMessage());
                    return null;
                });
                sentCount++;
            } catch (Exception e) {
                log.error("[Messaging] Failed to send pending message {}: {}", message.getIdAsString(), e.getMessage());
            }
        }

        log.info("[Messaging] Sent {} pending messages to client {}", sentCount, receiver);
        return sentCount;
    }

    /**
     * Get all messages for a specific receiver
     */
    public List<Message> getMessagesForReceiver(String receiver) {
        return messageRepository.findByReceiverOrderByCreatedAtDesc(receiver);
    }

    /**
     * Get all unread messages for a specific receiver
     */
    public List<Message> getUnreadMessagesForReceiver(String receiver) {
        return messageRepository.findByReceiverAndIsReadFalseOrderByCreatedAtDesc(receiver);
    }

    /**
     * Get all messages related to a specific job
     */
    public List<Message> getMessagesForJob(ObjectId jobId) {
        return messageRepository.findByJobIdOrderByCreatedAtDesc(jobId);
    }

    /**
     * Get all messages for a receiver and specific job
     */
    public List<Message> getMessagesForReceiverAndJob(String receiver, ObjectId jobId) {
        return messageRepository.findByReceiverAndJobIdOrderByCreatedAtDesc(receiver, jobId);
    }

    /**
     * Get all messages for a specific AppUser (by receiver field), ordered by
     * creation time ascending (oldest first)
     * 
     * @param appUserId
     *            AppUser ID (clientId)
     */
    public List<Message> getMessagesForAppUserAscending(String appUserId) {
        if (appUserId == null || appUserId.isBlank()) {
            return Collections.emptyList();
        }
        return messageRepository.findByReceiverOrderByCreatedAtAsc(appUserId);
    }

    /**
     * Get all messages for a specific AppUser (by receiver field), ordered by
     * creation time descending
     * 
     * @param appUserId
     *            AppUser ID (clientId)
     */
    public List<Message> getMessagesForAppUserDescending(String appUserId) {
        if (appUserId == null || appUserId.isBlank()) {
            return Collections.emptyList();
        }
        return messageRepository.findByReceiverOrderByCreatedAtDesc(appUserId);
    }

    /**
     * Get all general messages for a receiver
     */
    public List<Message> getGeneralMessagesForReceiver(String receiver) {
        return messageRepository.findByReceiverAndMessageTypeOrderByCreatedAtDesc(receiver, MessageType.GENERAL);
    }

    /**
     * Get all job-related messages for a receiver
     */
    public List<Message> getJobRelatedMessagesForReceiver(String receiver) {
        return messageRepository.findByReceiverAndMessageTypeOrderByCreatedAtDesc(receiver, MessageType.JOB_RELATED);
    }

    /**
     * Get all messages (for admin/overview)
     */
    public List<Message> getAllMessages() {
        return messageRepository.findAllByOrderByCreatedAtDesc();
    }

    /**
     * Get messages by origin
     */
    public List<Message> getMessagesByOrigin(MessageOrigin origin) {
        return messageRepository.findByOriginOrderByCreatedAtDesc(origin);
    }

    /**
     * Mark a message as read
     */
    public void markAsRead(ObjectId messageId) {
        Optional<Message> messageOpt = messageRepository.findById(messageId);
        if (messageOpt.isPresent()) {
            Message message = messageOpt.get();
            message.markAsRead();
            messageRepository.save(message);
        }
    }

    /**
     * Get unread message count for a receiver
     */
    public long getUnreadMessageCount(String receiver) {
        return messageRepository.countByReceiverAndIsReadFalse(receiver);
    }

    /**
     * Get a message by ID
     */
    public Optional<Message> getMessageById(ObjectId messageId) {
        return messageRepository.findById(messageId);
    }

    /**
     * Delete a message
     */
    public void deleteMessage(ObjectId messageId) {
        messageRepository.deleteById(messageId);
    }

    public Optional<Job> findJobByIdentifier(String identifier) {
        if (identifier == null || identifier.isBlank()) {
            return Optional.empty();
        }

        Optional<Job> jobOptional = Optional.empty();
        if (ObjectId.isValid(identifier)) {
            try {
                jobOptional = jobRepository.findById(new ObjectId(identifier));
            } catch (IllegalArgumentException ex) {
                log.debug("Identifier {} could not be parsed as ObjectId: {}", identifier, ex.getMessage());
            }
        }

        if (jobOptional.isPresent()) {
            return jobOptional;
        }

        return lookupJobByNumber(identifier);
    }

    private JobContext resolveJobContext(ObjectId jobId, String jobNumber) {
        ObjectId resolvedJobId = jobId;
        String resolvedJobNumber = jobNumber;

        Optional<Job> jobOptional = lookupJob(jobId, jobNumber);
        if (jobOptional.isPresent()) {
            Job job = jobOptional.get();
            resolvedJobId = job.getId();
            resolvedJobNumber = Optional.ofNullable(job.getJobNumber()).orElse(resolvedJobNumber);
        }

        return new JobContext(resolvedJobId, resolvedJobNumber);
    }

    private Optional<Job> lookupJob(ObjectId jobId, String jobNumber) {
        Optional<Job> jobOptional = Optional.empty();

        if (jobId != null) {
            jobOptional = jobRepository.findById(jobId);
            if (jobOptional.isPresent()) {
                return jobOptional;
            }
        }

        if (jobNumber == null || jobNumber.isBlank()) {
            return jobOptional;
        }

        jobOptional = jobRepository.findByJobNumber(jobNumber);
        if (jobOptional.isPresent()) {
            return jobOptional;
        }

        return lookupJobByNumber(jobNumber);
    }

    private Optional<Job> lookupJobByNumber(String candidate) {
        if (candidate == null || candidate.isBlank()) {
            return Optional.empty();
        }

        String normalizedCandidate = normalizeJobToken(candidate);

        Optional<Job> jobOptional = jobRepository.findByJobNumber(candidate);
        if (jobOptional.isPresent()) {
            return jobOptional;
        }

        String relaxedCandidate = candidate.replace('_', ' ');
        if (!relaxedCandidate.equals(candidate)) {
            jobOptional = jobRepository.findByJobNumber(relaxedCandidate);
            if (jobOptional.isPresent()) {
                return jobOptional;
            }
        }

        List<Job> fuzzyMatches = jobRepository.findByJobNumberContainingIgnoreCase(relaxedCandidate);
        if (fuzzyMatches.isEmpty() && !relaxedCandidate.equals(candidate)) {
            fuzzyMatches = jobRepository.findByJobNumberContainingIgnoreCase(candidate);
        }
        if (fuzzyMatches.isEmpty()) {
            fuzzyMatches = jobRepository.findAll();
        }

        return fuzzyMatches.stream()
                .filter(job -> normalizeJobToken(job.getJobNumber()).equalsIgnoreCase(normalizedCandidate)).findFirst();
    }

    private String normalizeJobToken(String value) {
        return value == null ? "" : value.replaceAll("[^a-zA-Z0-9_-]", "_");
    }

    private record JobContext(ObjectId jobId, String jobNumber) {
    }

}
