package de.assecutor.votianlt.pages.view;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Span;
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.UploadI18N;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteParameters;
import com.vaadin.flow.shared.Registration;
import de.assecutor.votianlt.model.AppUser;
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.pages.service.AppUserService;
import de.assecutor.votianlt.service.MessageBroadcaster;
import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import de.assecutor.votianlt.event.MessageReadStatusChangedEvent;
import org.springframework.context.ApplicationEventPublisher;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

@Route(value = "message-details/:clientId/:conversationId", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed("USER")
@Slf4j
public class MessageDetailsView extends Main implements BeforeEnterObserver, HasDynamicTitle {

    private final AppUserService appUserService;
    private final MessageService messageService;
    private final MessageBroadcaster messageBroadcaster;
    private final ApplicationEventPublisher eventPublisher;

    private String participantKey;
    private String conversationId;
    private boolean jobConversation;
    private ObjectId jobIdContext;
    private String jobNumberContext;
    private final VerticalLayout contentLayout;
    private VerticalLayout messagesContainer; // Container for message components
    private Scroller messagesScroller; // Reference to the scroller component
    private Div scrollAnchor; // Anchor element for scrolling to bottom
    private List<Message> currentMessages; // Current messages being displayed for redrawing
    private Registration broadcasterRegistration; // Track listener registration
    private TextArea messageInput;
    private static final int MAX_IMAGE_FILE_SIZE = 32 * 1024 * 1024; // 32 MB aligns with Spring settings
    private static final int TARGET_IMAGE_WIDTH = 1920;
    private static final float JPEG_COMPRESSION_QUALITY = 0.8f;

    public MessageDetailsView(AppUserService appUserService, MessageService messageService,
            MessageBroadcaster messageBroadcaster, ApplicationEventPublisher eventPublisher) {
        this.appUserService = appUserService;
        this.messageService = messageService;
        this.messageBroadcaster = messageBroadcaster;
        this.eventPublisher = eventPublisher;
        addClassName("message-hub-view");

        // Set height to 100% to prevent page from growing beyond viewport
        setHeightFull();

        // Create main layout with fixed positioning
        contentLayout = new VerticalLayout();
        contentLayout.setPadding(true);
        contentLayout.setSpacing(true);
        contentLayout.setWidthFull();
        contentLayout.setHeightFull();
        contentLayout.getStyle().set("overflow", "hidden");
        contentLayout.addClassName("message-thread-layout");

        add(contentLayout);
    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        // Extract route parameters from URL
        RouteParameters parameters = event.getRouteParameters();

        this.participantKey = parameters.get("clientId").orElse(null);
        this.conversationId = parameters.get("conversationId").orElse(null);

        log.info("MessageDetailsView - participant: {}, conversationId: {}", participantKey, conversationId);

        if (participantKey == null || conversationId == null) {
            log.warn("Missing required route parameters: participantKey={}, conversationId={}", participantKey,
                    conversationId);
            event.rerouteToError(IllegalArgumentException.class, "Missing required parameters");
            return;
        }

        loadMessageDetails();
    }

    private void loadMessageDetails() {
        contentLayout.removeAll();

        AppUser client = resolveParticipant();
        String clientName = client != null
                ? (Optional.ofNullable(client.getVorname()).orElse("") + " "
                        + Optional.ofNullable(client.getNachname()).orElse("")).trim()
                : Optional.ofNullable(participantKey).orElse("Unbekannter Teilnehmer");
        if (clientName.isBlank()) {
            clientName = Optional.ofNullable(client).map(AppUser::getBezeichnung).orElse("Unbekannter Teilnehmer");
        }

        List<Message> allMessages = messageService.getMessagesForAppUserAscending(participantKey);
        List<Message> filteredMessages = filterMessagesForConversation(allMessages, conversationId);

        this.jobConversation = conversationId != null && conversationId.startsWith("job-");
        this.jobIdContext = filteredMessages.stream().map(Message::getJobId).filter(Objects::nonNull).findFirst()
                .orElse(null);
        this.jobNumberContext = filteredMessages.stream().map(Message::getJobNumber)
                .filter(value -> value != null && !value.isBlank()).findFirst().orElse(null);

        ensureJobContextForConversation(filteredMessages);

        String conversationTitle = resolveConversationTitle(filteredMessages, conversationId);

        ViewToolbar headerLayout = createHeaderLayout(clientName, conversationTitle);
        contentLayout.add(headerLayout);

        // Store current messages for rendering and future updates
        this.currentMessages = filteredMessages;

        // Reset scroll anchor for new container
        scrollAnchor = null;

        // Create messages container
        messagesContainer = new VerticalLayout();
        messagesContainer.setPadding(false);
        messagesContainer.setSpacing(false);
        messagesContainer.setWidthFull();
        messagesContainer.addClassName("message-thread");

        // Wrap messages container in scroller for vertical scrolling
        messagesScroller = new Scroller(messagesContainer);
        messagesScroller.setWidthFull();
        messagesScroller.setHeightFull();
        messagesScroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
        messagesScroller.getStyle().set("flex", "1 1 auto");
        messagesScroller.addClassName("message-thread-scroller");

        contentLayout.add(messagesScroller);
        contentLayout.setFlexGrow(1, messagesScroller);

        HorizontalLayout inputLayout = createMessageInputArea();
        contentLayout.add(inputLayout);

        // Render messages using Vaadin components
        renderMessages();

        // Ensure scroll anchor exists and scroll to bottom
        ensureScrollAnchor();
        scrollToBottom();
    }

    /**
     * Render all messages using Vaadin components
     */
    private void renderMessages() {
        if (currentMessages == null || messagesContainer == null) {
            return;
        }

        // Reset anchor so a fresh one can be attached after re-rendering
        scrollAnchor = null;
        messagesContainer.removeAll();

        LocalDate currentDate = null;

        for (Message message : currentMessages) {
            LocalDateTime timestamp = resolveTimestamp(message);
            LocalDate messageDate = timestamp.toLocalDate();

            // Add date separator if date changed
            if (currentDate == null || !currentDate.equals(messageDate)) {
                messagesContainer.add(createDateSeparator(messageDate));
                currentDate = messageDate;
            }

            // Add message bubble
            messagesContainer.add(createMessageBubble(message, timestamp));
        }

        // After rendering, mark any unread messages directed to the current user as
        // read
        markVisibleMessagesAsRead();
    }

    /**
     * Marks all currently visible messages that are addressed to the logged-in user
     * as read. This is triggered after (re)rendering the conversation and will also
     * update the in-memory message objects to keep UI state consistent.
     */
    private void markVisibleMessagesAsRead() {
        try {
            if (currentMessages == null || currentMessages.isEmpty()) {
                return;
            }
            boolean anyMarked = false;
            for (Message msg : currentMessages) {
                if (!msg.isRead() && msg.getId() != null) {
                    // Update persistence
                    messageService.markAsRead(msg.getId());
                    // Update in-memory object so UI reflects read state immediately
                    msg.markAsRead();
                    anyMarked = true;
                }
            }
            // Publish event to update badge in sidebar
            if (anyMarked) {
                eventPublisher.publishEvent(new MessageReadStatusChangedEvent(this));
            }
        } catch (Exception e) {
            log.warn("Failed to mark messages as read: {}", e.getMessage());
        }
    }

    private void openImageUploadDialog() {
        Dialog dialog = DialogStylingHelper.createStyledDialog("Bild anhängen", "560px");
        dialog.setCloseOnEsc(true);
        dialog.setCloseOnOutsideClick(true);

        MemoryBuffer buffer = new MemoryBuffer();
        Upload upload = new Upload(buffer);
        upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/gif", "image/webp");
        upload.setMaxFiles(1);
        upload.setMaxFileSize(MAX_IMAGE_FILE_SIZE);
        upload.setI18n(createGermanUploadI18n());
        upload.setWidthFull();

        Span helper = new Span("Unterstützte Formate: PNG, JPG, GIF, WebP (max. 32 MB)");
        helper.addClassName("inline-caption");

        Image preview = new Image();
        preview.setAlt("Vorschau des ausgewählten Bildes");
        preview.setVisible(false);
        preview.setWidth(null);
        preview.setHeight(null);
        preview.getStyle().set("max-width", "100%").set("max-height", "320px").set("height", "auto")
                .set("border-radius", "12px").set("display", "inline-block");

        Div previewWrapper = new Div(preview);
        previewWrapper.setWidthFull();
        previewWrapper.getStyle().set("text-align", "center").set("margin-top", "10px");

        AtomicReference<String> base64Ref = new AtomicReference<>();

        Button confirmButton = new Button(getTranslation("messagedetails.button.send"));
        confirmButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
        confirmButton.setEnabled(false);

        Button cancelButton = new Button("Abbrechen", event -> dialog.close());
        cancelButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_TERTIARY);

        upload.addSucceededListener(event -> {
            try (InputStream inputStream = buffer.getInputStream()) {
                byte[] rawBytes = inputStream.readAllBytes();
                if (rawBytes.length == 0) {
                    base64Ref.set(null);
                    preview.setVisible(false);
                    confirmButton.setEnabled(false);
                    Notification.show("Die Bilddatei ist leer.", 3000, Notification.Position.MIDDLE)
                            .addThemeVariants(NotificationVariant.LUMO_ERROR);
                    return;
                }

                String processedBase64 = processImageToJpegBase64(rawBytes);
                if (processedBase64 == null) {
                    base64Ref.set(null);
                    preview.setVisible(false);
                    confirmButton.setEnabled(false);
                    Notification.show("Das Bild konnte nicht verarbeitet werden.", 3000, Notification.Position.MIDDLE)
                            .addThemeVariants(NotificationVariant.LUMO_ERROR);
                    return;
                }

                base64Ref.set(processedBase64);
                String dataUrl = "data:image/jpeg;base64," + processedBase64;
                preview.setSrc(dataUrl);
                preview.setVisible(true);
                confirmButton.setEnabled(true);
            } catch (IOException ex) {
                log.error("Fehler beim Verarbeiten der Bilddatei", ex);
                base64Ref.set(null);
                preview.setVisible(false);
                confirmButton.setEnabled(false);
                Notification.show("Das Bild konnte nicht gelesen werden.", 3000, Notification.Position.MIDDLE)
                        .addThemeVariants(NotificationVariant.LUMO_ERROR);
            }
        });

        upload.addFileRejectedListener(event -> {
            base64Ref.set(null);
            preview.setVisible(false);
            confirmButton.setEnabled(false);
            Notification.show(event.getErrorMessage(), 3000, Notification.Position.MIDDLE)
                    .addThemeVariants(NotificationVariant.LUMO_ERROR);
        });

        upload.addFailedListener(event -> {
            base64Ref.set(null);
            preview.setVisible(false);
            confirmButton.setEnabled(false);
            Notification.show("Der Upload ist fehlgeschlagen.", 3000, Notification.Position.MIDDLE)
                    .addThemeVariants(NotificationVariant.LUMO_ERROR);
        });

        upload.addFileRemovedListener(event -> {
            base64Ref.set(null);
            preview.setVisible(false);
            confirmButton.setEnabled(false);
        });

        confirmButton.addClickListener(event -> {
            String base64 = base64Ref.get();
            if (base64 == null || base64.isBlank()) {
                Notification.show("Bitte zuerst ein Bild auswählen.", 2500, Notification.Position.MIDDLE)
                        .addThemeVariants(NotificationVariant.LUMO_ERROR);
                return;
            }
            dialog.close();
            sendMessageToParticipant(base64, MessageContentType.IMAGE);
        });

        VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("440px");
        dialogContent.add(upload, helper, previewWrapper);

        dialog.add(DialogStylingHelper.wrapContent(dialogContent));
        dialog.getFooter().add(cancelButton, confirmButton);

        dialog.open();
    }

    /**
     * Create a date separator component
     */
    private Div createDateSeparator(LocalDate date) {
        Div separator = new Div();
        separator.setWidthFull();
        separator.addClassName("message-date-separator");

        Span dateLabel = new Span(DateTimeFormatUtil.formatDate(date));
        dateLabel.addClassName("message-date-chip");

        separator.add(dateLabel);
        return separator;
    }

    /**
     * Create a message bubble component
     */
    private Div createMessageBubble(Message message, LocalDateTime timestamp) {
        // Determine alignment based on message origin
        // CLIENT origin = client messages (left), SERVER origin = server messages
        // (right)
        boolean isServerMessage = message.getOrigin() == MessageOrigin.SERVER;

        // Container for the message (aligns left or right)
        Div messageWrapper = new Div();
        String alignment = isServerMessage ? "right" : "left";
        messageWrapper.addClassNames("message-wrapper", isServerMessage ? "server" : "client");

        // Message bubble
        Div bubble = new Div();
        bubble.addClassNames("message-bubble", isServerMessage ? "server" : "client");
        bubble.getStyle().set("text-align", alignment);

        // Message content component (text or media)
        Component contentComponent = createContentComponent(message, alignment);

        // Timestamp
        Span timeSpan = new Span(DateTimeFormatUtil.formatTime(timestamp));
        timeSpan.addClassName("message-bubble-time");
        timeSpan.getStyle().set("text-align", alignment);

        bubble.add(contentComponent, timeSpan);
        messageWrapper.add(bubble);

        return messageWrapper;
    }

    private Component createContentComponent(Message message, String alignment) {
        MessageContentType contentType = message.getContentType();
        return switch (contentType) {
        case IMAGE -> createImageContent(message.getContent(), alignment);
        case TEXT -> createTextContent(message.getContent(), alignment);
        };
    }

    private Component createTextContent(String contentValue, String alignment) {
        Div contentDiv = new Div();
        String content = Optional.ofNullable(contentValue).filter(value -> !value.isBlank()).orElse("(kein Inhalt)");
        contentDiv.setText(content);
        contentDiv.addClassName("message-bubble-text");
        contentDiv.getStyle().set("text-align", alignment);
        return contentDiv;
    }

    private Component createImageContent(String base64Value, String alignment) {
        Div wrapper = new Div();
        wrapper.addClassNames("message-image-row", "right".equals(alignment) ? "server" : "client");

        String trimmed = Optional.ofNullable(base64Value).map(String::trim).orElse("");
        if (trimmed.isEmpty()) {
            wrapper.setText("(kein Bildinhalt)");
            return wrapper;
        }

        String dataUrl = createDataUrlFromContent(trimmed, null);
        if (dataUrl == null) {
            wrapper.setText("(Bild konnte nicht geladen werden)");
            return wrapper;
        }

        Image image = new Image(dataUrl, "Nachrichtenbild");
        image.addClassName("message-bubble-image");
        image.getElement().setAttribute("loading", "lazy");

        wrapper.add(image);
        return wrapper;
    }

    private String createDataUrlFromContent(String contentValue, String mimeTypeHint) {
        if (contentValue == null || contentValue.isBlank()) {
            return null;
        }
        String trimmed = contentValue.trim();
        if (trimmed.startsWith("data:")) {
            return trimmed;
        }

        byte[] imageBytes;
        try {
            imageBytes = Base64.getDecoder().decode(trimmed);
        } catch (IllegalArgumentException ex) {
            log.warn("Ungültiger Base64-Inhalt für Bildnachricht", ex);
            return null;
        }

        String mimeType = Optional.ofNullable(mimeTypeHint).filter(value -> !value.isBlank())
                .orElseGet(() -> detectImageMimeType(imageBytes));
        return "data:" + mimeType + ";base64," + trimmed;
    }

    private String detectImageMimeType(byte[] bytes) {
        if (bytes == null || bytes.length < 4) {
            return "image/png";
        }

        if ((bytes[0] & 0xFF) == 0xFF && (bytes[1] & 0xFF) == 0xD8) {
            return "image/jpeg";
        }
        if ((bytes[0] & 0xFF) == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47) {
            return "image/png";
        }
        if (bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x38) {
            return "image/gif";
        }
        if (bytes.length >= 12 && bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46
                && bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50) {
            return "image/webp";
        }
        if (bytes[0] == 0x42 && bytes[1] == 0x4D) {
            return "image/bmp";
        }
        return "image/png";
    }

    private String processImageToJpegBase64(byte[] originalBytes) {
        try {
            BufferedImage original = ImageIO.read(new ByteArrayInputStream(originalBytes));
            if (original == null) {
                log.warn("Bild konnte nicht geladen werden (ImageIO liefert null)");
                return null;
            }

            int originalWidth = original.getWidth();
            int originalHeight = original.getHeight();
            if (originalWidth <= 0 || originalHeight <= 0) {
                log.warn("Ungültige Bildmaße: {}x{}", originalWidth, originalHeight);
                return null;
            }

            int targetWidth = Math.min(originalWidth, TARGET_IMAGE_WIDTH);
            int targetHeight = (int) Math.round((double) targetWidth / originalWidth * originalHeight);
            if (targetHeight <= 0) {
                targetHeight = originalHeight;
            }

            BufferedImage scaled = scaleImage(original, targetWidth, targetHeight);
            byte[] jpegBytes = encodeJpeg(scaled, JPEG_COMPRESSION_QUALITY);
            if (jpegBytes == null || jpegBytes.length == 0) {
                log.warn("JPEG-Kodierung lieferte leeres Ergebnis");
                return null;
            }

            return Base64.getEncoder().encodeToString(jpegBytes);
        } catch (IOException ex) {
            log.error("Fehler beim Skalieren/Konvertieren des Bildes", ex);
            return null;
        }
    }

    private BufferedImage scaleImage(BufferedImage original, int targetWidth, int targetHeight) {
        if (original.getWidth() == targetWidth && original.getHeight() == targetHeight) {
            return ensureRgbFormat(original);
        }

        BufferedImage output = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = output.createGraphics();
        try {
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.drawImage(original, 0, 0, targetWidth, targetHeight, null);
        } finally {
            g2d.dispose();
        }
        return output;
    }

    private BufferedImage ensureRgbFormat(BufferedImage image) {
        if (image.getType() == BufferedImage.TYPE_INT_RGB) {
            return image;
        }
        BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = rgbImage.createGraphics();
        try {
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
            g2d.drawImage(image, 0, 0, null);
        } finally {
            g2d.dispose();
        }
        return rgbImage;
    }

    private byte[] encodeJpeg(BufferedImage image, float quality) throws IOException {
        Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
        if (!writers.hasNext()) {
            throw new IOException("Kein JPEG-Writer verfügbar");
        }
        ImageWriter writer = writers.next();
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) {
            writer.setOutput(ios);

            ImageWriteParam params = writer.getDefaultWriteParam();
            if (params.canWriteCompressed()) {
                params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                params.setCompressionQuality(Math.max(0f, Math.min(1f, quality)));
            }
            writer.write(null, new IIOImage(image, null, null), params);
            return baos.toByteArray();
        } finally {
            writer.dispose();
        }
    }

    private UploadI18N createGermanUploadI18n() {
        UploadI18N i18n = new UploadI18N();

        UploadI18N.AddFiles addFiles = new UploadI18N.AddFiles();
        addFiles.setOne("Bild auswählen");
        addFiles.setMany("Bilder auswählen");
        i18n.setAddFiles(addFiles);

        UploadI18N.DropFiles dropFiles = new UploadI18N.DropFiles();
        dropFiles.setOne("Bild hierher ziehen");
        dropFiles.setMany("Bilder hierher ziehen");
        i18n.setDropFiles(dropFiles);

        UploadI18N.Error error = new UploadI18N.Error();
        error.setFileIsTooBig("Die Datei ist größer als 32 MB.");
        error.setTooManyFiles("Es kann nur ein Bild gleichzeitig hochgeladen werden.");
        i18n.setError(error);

        UploadI18N.Uploading uploading = new UploadI18N.Uploading();
        uploading.setStatus(new UploadI18N.Uploading.Status().setConnecting("Verbindung wird aufgebaut...")
                .setStalled("Upload pausiert").setProcessing("Verarbeitung...").setHeld("Warten auf Upload..."));
        uploading.setRemainingTime(new UploadI18N.Uploading.RemainingTime().setPrefix("Verbleibende Zeit: ")
                .setUnknown("Verbleibende Zeit unbekannt"));
        uploading.setError(new UploadI18N.Uploading.Error().setServerUnavailable("Server nicht erreichbar")
                .setUnexpectedServerError("Unerwarteter Serverfehler").setForbidden("Upload nicht erlaubt"));
        i18n.setUploading(uploading);

        return i18n;
    }

    /**
     * Ensure scroll anchor exists at the bottom of messages container
     */
    private void ensureScrollAnchor() {
        if (messagesContainer == null) {
            return;
        }

        if (scrollAnchor == null) {
            scrollAnchor = new Div();
            scrollAnchor.setId("scroll-anchor");
            scrollAnchor.getStyle().set("height", "1px").set("width", "100%");
        }

        if (scrollAnchor.getParent().isEmpty()) {
            messagesContainer.add(scrollAnchor);
        }
    }

    /**
     * Scroll to the bottom of the messages container
     */
    private void scrollToBottom() {
        if (scrollAnchor != null) {
            scrollAnchor.scrollIntoView();
        }
    }

    private ViewToolbar createHeaderLayout(String clientName, String conversationTitle) {
        // Create title with subtitle in a custom layout
        VerticalLayout titleLayout = new VerticalLayout();
        titleLayout.setPadding(false);
        titleLayout.setSpacing(false);

        H1 title = new H1(clientName);
        title.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.FontSize.XLARGE, com.vaadin.flow.theme.lumo.LumoUtility.Margin.NONE, com.vaadin.flow.theme.lumo.LumoUtility.FontWeight.LIGHT);
        title.addClassName("view-toolbar-title");

        titleLayout.add(title);

        // Use ViewToolbar for consistent header layout with drawer toggle
        ViewToolbar toolbar = new ViewToolbar("");
        toolbar.getContent().removeAll();

        // Build custom header content with drawer toggle
        com.vaadin.flow.component.applayout.DrawerToggle drawerToggle = new com.vaadin.flow.component.applayout.DrawerToggle();
        drawerToggle.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.Margin.NONE);
        drawerToggle.addClassName("view-toolbar-toggle");

        com.vaadin.flow.component.html.Div titleRow = new com.vaadin.flow.component.html.Div(drawerToggle, titleLayout);
        titleRow.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.Display.FLEX, com.vaadin.flow.theme.lumo.LumoUtility.AlignItems.CENTER);
        titleRow.addClassName("view-toolbar-title-row");

        toolbar.getContent().add(titleRow);

        return toolbar;
    }

    private HorizontalLayout createMessageInputArea() {
        messageInput = new TextArea();
        messageInput.setPlaceholder(getTranslation("messagedetails.placeholder"));
        messageInput.setWidthFull();
        messageInput.getStyle().set("min-height", "60px");
        messageInput.getStyle().set("max-height", "120px");

        Button attachButton = new Button(VaadinIcon.PAPERCLIP.create());
        attachButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_TERTIARY,
                com.vaadin.flow.component.button.ButtonVariant.LUMO_ICON);
        attachButton.getStyle().set("height", "60px");
        attachButton.getElement().setProperty("title", "Bild anhängen");
        attachButton.getElement().setAttribute("aria-label", "Bild anhängen");
        attachButton.addClickListener(event -> openImageUploadDialog());

        Button sendButton = new Button(VaadinIcon.PAPERPLANE.create());
        sendButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
        sendButton.getStyle().set("height", "60px");
        sendButton.getStyle().set("min-width", "90px");

        sendButton.addClickListener(e -> {
            String message = messageInput.getValue();
            if (message != null && !message.trim().isEmpty()) {
                sendMessageToParticipant(message, MessageContentType.TEXT);
                messageInput.clear();
            }
        });

        HorizontalLayout layout = new HorizontalLayout(attachButton, messageInput, sendButton);
        layout.setWidthFull();
        layout.setAlignItems(FlexComponent.Alignment.END);
        layout.setSpacing(true);
        layout.expand(messageInput);
        layout.addClassName("message-thread-input");

        return layout;
    }

    private void sendMessageToParticipant(String content, MessageContentType contentType) {
        String payload = Optional.ofNullable(content).orElse("");
        if (contentType == MessageContentType.TEXT) {
            payload = payload.trim();
        }

        if (payload.isEmpty()) {
            Notification.show("Nachricht enthält keinen Inhalt.", 2500, Notification.Position.MIDDLE)
                    .addThemeVariants(NotificationVariant.LUMO_ERROR);
            return;
        }

        ensureJobContextForConversation(currentMessages);

        try {
            Message saved;
            if (jobConversation) {
                // participantKey = AppUser ID (receiver)
                saved = messageService.sendJobMessageToClient(payload, participantKey, contentType, jobIdContext,
                        jobNumberContext);
            } else {
                // participantKey = AppUser ID (receiver)
                saved = messageService.sendGeneralMessageToClient(payload, participantKey, contentType);
            }

            // Mark own outgoing message as read immediately
            if (saved != null && saved.getId() != null) {
                try {
                    messageService.markAsRead(saved.getId());
                    saved.markAsRead(); // keep UI state consistent
                } catch (Exception ignore) {
                    // non-fatal
                }
            }

            Notification.show("Nachricht gesendet", 2000, Notification.Position.BOTTOM_END)
                    .addThemeVariants(NotificationVariant.LUMO_SUCCESS);

            // Refresh conversation to include the new message and redraw canvas
            // loadMessageDetails already scrolls to bottom after rendering
            loadMessageDetails();

        } catch (Exception ex) {
            log.error("Failed to send message to {}: {}", participantKey, ex.getMessage(), ex);
            Notification.show("Nachricht konnte nicht gesendet werden: " + ex.getMessage(), 4000,
                    Notification.Position.MIDDLE).addThemeVariants(NotificationVariant.LUMO_ERROR);
        }
    }

    private AppUser resolveParticipant() {
        if (participantKey == null || participantKey.isBlank()) {
            return null;
        }
        try {
            ObjectId objectId = new ObjectId(participantKey);
            return appUserService.findById(objectId);
        } catch (IllegalArgumentException ex) {
            return appUserService.findAll().stream()
                    .filter(user -> participantKey.equals(user.getEmail()) || participantKey.equals(user.getAppCode()))
                    .findFirst().orElse(null);
        }
    }

    private List<Message> filterMessagesForConversation(List<Message> messages, String conversationId) {
        if (conversationId == null || messages == null) {
            return List.of();
        }
        if ("general".equalsIgnoreCase(conversationId)) {
            return messages.stream().filter(
                    msg -> Optional.ofNullable(msg.getMessageType()).orElse(MessageType.GENERAL) == MessageType.GENERAL)
                    .collect(Collectors.toList());
        }
        if (conversationId.startsWith("job-")) {
            String token = conversationId.substring(4);
            return messages.stream()
                    .filter(msg -> Optional.ofNullable(msg.getMessageType())
                            .orElse(MessageType.GENERAL) == MessageType.JOB_RELATED
                            && matchesJobConversation(msg, token))
                    .collect(Collectors.toList());
        }
        return messages;
    }

    private boolean matchesJobConversation(Message message, String token) {
        if (token == null || token.isBlank() || message == null) {
            return false;
        }
        String normalizedToken = token.toLowerCase();
        String jobNumber = Optional.ofNullable(message.getJobNumber()).orElse("");
        String jobId = Optional.ofNullable(message.getJobIdAsString()).orElse("");

        return sanitize(jobNumber).equalsIgnoreCase(normalizedToken)
                || sanitize(jobId).equalsIgnoreCase(normalizedToken) || jobNumber.equalsIgnoreCase(token)
                || jobId.equalsIgnoreCase(token);
    }

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

    private void ensureJobContextForConversation(List<Message> messages) {
        if (!jobConversation) {
            jobIdContext = null;
            jobNumberContext = null;
            return;
        }

        String token = extractJobConversationToken();
        if (token == null || token.isBlank()) {
            return;
        }

        if (jobIdContext == null && ObjectId.isValid(token)) {
            try {
                jobIdContext = new ObjectId(token);
            } catch (IllegalArgumentException ex) {
                log.debug("Conversation token {} could not be converted to ObjectId", token, ex);
            }
        }

        if (messages != null && (jobNumberContext == null || jobNumberContext.isBlank())) {
            jobNumberContext = messages.stream().map(Message::getJobNumber)
                    .filter(value -> value != null && !value.isBlank()).findFirst().orElse(jobNumberContext);
        }

        if (jobIdContext == null || jobNumberContext == null || jobNumberContext.isBlank()) {
            Optional<Job> jobOptional = messageService.findJobByIdentifier(token);
            if (jobOptional.isEmpty() && jobNumberContext != null && !jobNumberContext.isBlank()) {
                jobOptional = messageService.findJobByIdentifier(jobNumberContext);
            }
            if (jobOptional.isPresent()) {
                Job job = jobOptional.get();
                jobIdContext = Optional.ofNullable(job.getId()).orElse(jobIdContext);
                jobNumberContext = Optional.ofNullable(job.getJobNumber()).filter(value -> !value.isBlank())
                        .orElse(jobNumberContext);
            }
        }

        if (jobNumberContext == null || jobNumberContext.isBlank()) {
            jobNumberContext = token;
        }
    }

    private String extractJobConversationToken() {
        if (!jobConversation || conversationId == null || !conversationId.startsWith("job-")) {
            return null;
        }
        return conversationId.length() > 4 ? conversationId.substring(4) : "";
    }

    private String resolveConversationTitle(List<Message> messages, String conversationId) {
        if (conversationId == null) {
            return "Konversation";
        }
        if ("general".equalsIgnoreCase(conversationId)) {
            return "Allgemeine Unterhaltung";
        }
        if (conversationId.startsWith("job-")) {
            if (messages != null && !messages.isEmpty()) {
                for (Message message : messages) {
                    String jobNumber = Optional.ofNullable(message.getJobNumber()).filter(s -> !s.isBlank())
                            .orElse(null);
                    if (jobNumber != null) {
                        return "Auftrag " + jobNumber;
                    }
                    String jobId = Optional.ofNullable(message.getJobIdAsString()).filter(s -> !s.isBlank())
                            .orElse(null);
                    if (jobId != null) {
                        return "Auftrag " + jobId;
                    }
                }
            }
            return "Auftragsbasierte Unterhaltung";
        }
        return "Konversation";
    }

    private LocalDateTime resolveTimestamp(Message message) {
        return Optional.ofNullable(message.getCreatedAt()).orElse(LocalDateTime.now());
    }

    /**
     * Called when the view is attached to the UI Registers listener for incoming
     * messages
     */
    @Override
    protected void onAttach(AttachEvent attachEvent) {
        super.onAttach(attachEvent);
        UI ui = attachEvent.getUI();

        // Register listener for incoming messages
        broadcasterRegistration = messageBroadcaster.register(message -> {
            handleIncomingMessage(ui, message);
        });

        log.info("MessageDetailsView attached and listener registered for conversation: {}", conversationId);
    }

    /**
     * Called when the view is detached from the UI Unregisters listener to prevent
     * memory leaks
     */
    @Override
    protected void onDetach(DetachEvent detachEvent) {
        if (broadcasterRegistration != null) {
            broadcasterRegistration.remove();
            broadcasterRegistration = null;
            log.info("MessageDetailsView detached and listener unregistered for conversation: {}", conversationId);
        }
        super.onDetach(detachEvent);
    }

    /**
     * Handle incoming message broadcast Filters messages to only show those
     * belonging to the current conversation
     */
    private void handleIncomingMessage(UI ui, Message message) {
        if (message == null || participantKey == null || conversationId == null) {
            return;
        }

        // Check if message belongs to the current participant (receiver = AppUser ID)
        boolean involvesParticipant = participantKey.equals(message.getReceiver());

        if (!involvesParticipant) {
            log.debug("Message does not belong to current participant, ignoring");
            return;
        }

        // Check if message belongs to the current conversation
        boolean belongsToConversation = false;

        if ("general".equalsIgnoreCase(conversationId)) {
            // General conversation: messages without job context
            belongsToConversation = message.getJobId() == null && message.getJobNumber() == null;
        } else if (conversationId != null && conversationId.startsWith("job-")) {
            // Job conversation: check if message matches the job
            String token = conversationId.substring(4);
            belongsToConversation = matchesJobConversation(message, token);
        }

        if (!belongsToConversation) {
            log.debug("Message does not belong to current conversation {}, ignoring", conversationId);
            return;
        }

        log.info("New message belongs to current conversation {}, updating UI", conversationId);

        // Update UI in a thread-safe manner using UI.access()
        ui.access(() -> {
            try {
                if (currentMessages != null) {
                    // Add new message to the list
                    currentMessages.add(message);

                    ensureJobContextForConversation(currentMessages);

                    // Re-render all messages with the new message included
                    renderMessages();

                    // Ensure scroll anchor exists and scroll to show new message
                    ensureScrollAnchor();
                    scrollToBottom();

                    log.info("Messages re-rendered with new message");
                }
            } catch (Exception e) {
                log.error("Error updating messages with new message", e);
            }
        });
    }

    @Override
    public String getPageTitle() {
        return getTranslation("page.title.message.history");
    }
}
