package at.procon.dip.ingestion.service;

import at.procon.dip.domain.document.entity.MailRecipientType;
import at.procon.dip.ingestion.util.DocumentImportSupport;
import jakarta.mail.Address;
import jakarta.mail.BodyPart;
import jakarta.mail.Message;
import jakarta.mail.Multipart;
import jakarta.mail.Part;
import jakarta.mail.Session;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeUtility;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class MailMessageExtractionService {

    public ParsedMailMessage parse(byte[] rawMime) {
        try {
            Session session = Session.getDefaultInstance(new Properties());
            MimeMessage message = new MimeMessage(session, new ByteArrayInputStream(rawMime));
            ParsedAddress fromAddress = firstAddress(message.getFrom());
            ParsedAddress replyToAddress = firstAddress(message.getReplyTo());
            List<MailRecipient> recipients = new ArrayList<>();
            collectRecipients(message, Message.RecipientType.TO, MailRecipientType.TO, recipients);
            collectRecipients(message, Message.RecipientType.CC, MailRecipientType.CC, recipients);
            collectRecipients(message, Message.RecipientType.BCC, MailRecipientType.BCC, recipients);

            StringBuilder text = new StringBuilder();
            StringBuilder html = new StringBuilder();
            List<MailAttachment> attachments = new ArrayList<>();
            AtomicInteger attachmentCounter = new AtomicInteger();
            processPart(message, text, html, attachments, "0", attachmentCounter);
            String normalizedText = text.length() > 0 ? text.toString().trim() : htmlToText(html.toString());
            OffsetDateTime receivedAt = message.getReceivedDate() == null ? OffsetDateTime.now()
                    : message.getReceivedDate().toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
            OffsetDateTime sentAt = message.getSentDate() == null ? null
                    : message.getSentDate().toInstant().atZone(ZoneId.systemDefault()).toOffsetDateTime();
            String subject = message.getSubject();
            return new ParsedMailMessage(
                    subject,
                    normalizeSubject(subject),
                    fromAddress.displayName(),
                    fromAddress.emailAddress(),
                    fromAddress.rawValue(),
                    replyToAddress.rawValue(),
                    readHeader(message, "Message-ID"),
                    readHeader(message, "In-Reply-To"),
                    readJoinedHeader(message, "References"),
                    sentAt,
                    receivedAt,
                    normalizedText,
                    html.toString(),
                    recipients,
                    attachments
            );
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed to parse MIME message", e);
        }
    }

    private void collectRecipients(MimeMessage message,
                                   Message.RecipientType recipientType,
                                   MailRecipientType mappedType,
                                   List<MailRecipient> target) throws Exception {
        Address[] addresses = message.getRecipients(recipientType);
        if (addresses == null) {
            return;
        }
        int sortOrder = target.size();
        for (Address address : addresses) {
            ParsedAddress parsed = parseAddress(address);
            target.add(new MailRecipient(mappedType, parsed.displayName(), parsed.emailAddress(), parsed.rawValue(), sortOrder++));
        }
    }

    private void processPart(Part part,
                             StringBuilder text,
                             StringBuilder html,
                             List<MailAttachment> attachments,
                             String partPath,
                             AtomicInteger attachmentCounter) throws Exception {
        String disposition = part.getDisposition();
        String contentType = part.getContentType() == null ? "application/octet-stream" : part.getContentType();
        if (disposition != null && (Part.ATTACHMENT.equalsIgnoreCase(disposition) || Part.INLINE.equalsIgnoreCase(disposition))
                && part.getFileName() != null) {
            attachments.add(extractAttachment(part, partPath, attachmentCounter.incrementAndGet()));
            return;
        }
        Object content = part.getContent();
        if (content instanceof Multipart multipart) {
            for (int i = 0; i < multipart.getCount(); i++) {
                BodyPart bodyPart = multipart.getBodyPart(i);
                processPart(bodyPart, text, html, attachments, partPath + "." + i, attachmentCounter);
            }
        } else if (contentType.toLowerCase().contains("text/plain")) {
            text.append(content.toString()).append("\n");
        } else if (contentType.toLowerCase().contains("text/html")) {
            html.append(content.toString()).append("\n");
        } else if (part.getFileName() != null) {
            attachments.add(extractAttachment(part, partPath, attachmentCounter.incrementAndGet()));
        }
    }

    private MailAttachment extractAttachment(Part part, String partPath, int attachmentIndex) throws Exception {
        String fileName = part.getFileName();
        if (fileName == null) {
            fileName = "attachment";
        }
        try {
            fileName = MimeUtility.decodeText(fileName);
        } catch (Exception ignored) {
        }
        String contentType = part.getContentType();
        byte[] data;
        try (InputStream in = part.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            in.transferTo(out);
            data = out.toByteArray();
        }
        return new MailAttachment(
                fileName,
                contentType,
                data,
                Long.valueOf(data.length),
                null,
                partPath,
                normalizeDisposition(part.getDisposition()),
                readHeader(part, "Content-ID"),
                attachmentIndex
        );
    }

    private ParsedAddress firstAddress(Address[] addresses) {
        if (addresses == null || addresses.length == 0) {
            return new ParsedAddress(null, null, null);
        }
        return parseAddress(addresses[0]);
    }

    private ParsedAddress parseAddress(Address address) {
        if (address == null) {
            return new ParsedAddress(null, null, null);
        }
        if (address instanceof InternetAddress internetAddress) {
            String displayName = internetAddress.getPersonal();
            String emailAddress = internetAddress.getAddress();
            return new ParsedAddress(displayName, emailAddress, internetAddress.toUnicodeString());
        }
        return new ParsedAddress(null, null, address.toString());
    }

    private String normalizeSubject(String subject) {
        if (subject == null || subject.isBlank()) {
            return null;
        }
        String normalized = subject.trim();
        boolean changed;
        do {
            changed = false;
            String next = normalized.replaceFirst("(?i)^(re|fw|fwd)\\s*:\\s*", "").trim();
            if (!next.equals(normalized)) {
                normalized = next;
                changed = true;
            }
        } while (changed && !normalized.isBlank());
        return normalized.replaceAll("\\s+", " ");
    }

    private String normalizeDisposition(String disposition) {
        if (disposition == null || disposition.isBlank()) {
            return null;
        }
        return disposition.trim().toUpperCase();
    }

    private String readHeader(Part part, String name) {
        try {
            String[] headerValues = part.getHeader(name);
            if (headerValues == null || headerValues.length == 0) {
                return null;
            }
            return headerValues[0];
        } catch (Exception e) {
            return null;
        }
    }

    private String readJoinedHeader(MimeMessage message, String name) {
        try {
            String[] headerValues = message.getHeader(name);
            if (headerValues == null || headerValues.length == 0) {
                return null;
            }
            return String.join(" ", headerValues);
        } catch (Exception e) {
            return null;
        }
    }

    private String htmlToText(String html) {
        if (html == null || html.isBlank()) {
            return "";
        }
        try {
            return Jsoup.parse(html).text().replaceAll("\\s+", " ").trim();
        } catch (Exception e) {
            log.debug("Falling back to naive HTML cleanup: {}", e.getMessage());
            return html.replaceAll("<[^>]+>", " ").replaceAll("\\s+", " ").trim();
        }
    }

    public String serializeMessage(ParsedMailMessage parsed) {
        StringBuilder sb = new StringBuilder();
        if (parsed.subject() != null) sb.append("Subject: ").append(parsed.subject()).append("\n");
        if (parsed.fromRaw() != null) sb.append("From: ").append(parsed.fromRaw()).append("\n");
        List<String> toRecipients = parsed.recipients().stream()
                .filter(recipient -> recipient.recipientType() == MailRecipientType.TO)
                .map(MailRecipient::rawValue)
                .toList();
        if (!toRecipients.isEmpty()) sb.append("To: ").append(String.join(", ", toRecipients)).append("\n");
        sb.append("\n");
        if (parsed.textBody() != null) sb.append(parsed.textBody());
        return sb.toString().trim();
    }

    public record ParsedMailMessage(
            String subject,
            String normalizedSubject,
            String fromDisplayName,
            String fromEmail,
            String fromRaw,
            String replyToRaw,
            String messageId,
            String inReplyTo,
            String referencesHeader,
            OffsetDateTime sentAt,
            OffsetDateTime receivedAt,
            String textBody,
            String htmlBody,
            List<MailRecipient> recipients,
            List<MailAttachment> attachments
    ) {
    }

    public record MailRecipient(
            MailRecipientType recipientType,
            String displayName,
            String emailAddress,
            String rawValue,
            int sortOrder
    ) {
    }

    public record MailAttachment(
            String fileName,
            String contentType,
            byte[] data,
            Long sizeBytes,
            String path,
            String partPath,
            String disposition,
            String contentId,
            Integer attachmentIndex
    ) {
        public MailAttachment(String fileName, String contentType, byte[] data, long sizeBytes, String path) {
            this(fileName, contentType, data, sizeBytes, path, null, null, null, null);
        }

        public String safeTextPreview() {
            String extension = DocumentImportSupport.extensionOf(fileName);
            String mime = DocumentImportSupport.normalizeMediaType(contentType);
            if (DocumentImportSupport.isSpreadsheetMime(mime)
                    || DocumentImportSupport.isSpreadsheetExtension(extension)
                    || (!DocumentImportSupport.isLikelyTextMime(mime) && !DocumentImportSupport.isLikelyTextExtension(extension))
                    || DocumentImportSupport.looksBinary(data)) {
                return null;
            }
            return DocumentImportSupport.normalizeText(new String(data, StandardCharsets.UTF_8));
        }
    }

    private record ParsedAddress(String displayName, String emailAddress, String rawValue) {
    }
}
