package at.procon.dip.ingestion.adapter;

import at.procon.dip.domain.access.DocumentAccessContext;
import at.procon.dip.domain.access.DocumentVisibility;
import at.procon.dip.domain.document.RelationType;
import at.procon.dip.domain.document.SourceType;
import at.procon.dip.domain.document.entity.DocumentSource;
import at.procon.dip.domain.tenant.TenantRef;
import at.procon.dip.domain.document.repository.DocumentSourceRepository;
import at.procon.dip.domain.document.service.DocumentRelationService;
import at.procon.dip.domain.document.service.command.CreateDocumentRelationCommand;
import at.procon.dip.ingestion.config.DipIngestionProperties;
import at.procon.dip.ingestion.dto.ImportedDocumentResult;
import at.procon.dip.ingestion.mail.MailImportIdentityResolver;
import at.procon.dip.ingestion.mail.MailProviderEnvelope;
import at.procon.dip.ingestion.mail.MailProviderEnvelopeAttributes;
import at.procon.dip.ingestion.service.GenericDocumentImportService;
import at.procon.dip.ingestion.service.MailMessageExtractionService;
import at.procon.dip.ingestion.service.MailMessageExtractionService.MailAttachment;
import at.procon.dip.ingestion.service.MailMessageExtractionService.ParsedMailMessage;
import at.procon.dip.ingestion.service.MailMetadataPersistenceService;
import at.procon.dip.ingestion.spi.DocumentIngestionAdapter;
import at.procon.dip.ingestion.spi.IngestionResult;
import at.procon.dip.ingestion.spi.OriginalContentStoragePolicy;
import at.procon.dip.ingestion.spi.SourceDescriptor;
import at.procon.dip.ingestion.util.DocumentImportSupport;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import at.procon.ted.service.attachment.AttachmentExtractor;
import at.procon.ted.service.attachment.ZipExtractionService;
import at.procon.ted.util.HashUtils;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@RequiredArgsConstructor
@Slf4j
public class MailDocumentIngestionAdapter implements DocumentIngestionAdapter {

    private final DipIngestionProperties properties;
    private final GenericDocumentImportService importService;
    private final MailMessageExtractionService mailExtractionService;
    private final DocumentRelationService relationService;
    private final ZipExtractionService zipExtractionService;
    private final MailImportIdentityResolver mailImportIdentityResolver;
    private final MailMetadataPersistenceService mailMetadataPersistenceService;
    private final DocumentSourceRepository documentSourceRepository;

    @Override
    public boolean supports(SourceDescriptor sourceDescriptor) {
        return sourceDescriptor.sourceType() == SourceType.MAIL
                && properties.isEnabled()
                && properties.isMailAdapterEnabled();
    }

    @Override
    public IngestionResult ingest(SourceDescriptor sourceDescriptor) {
        byte[] rawMime = sourceDescriptor.binaryContent();
        if (rawMime == null || rawMime.length == 0) {
            throw new IllegalArgumentException("Mail adapter requires raw MIME bytes");
        }
        ParsedMailMessage parsed = mailExtractionService.parse(rawMime);
        MailProviderEnvelope providerEnvelope = MailProviderEnvelopeAttributes.fromAttributes(sourceDescriptor.attributes());
        DocumentAccessContext accessContext = sourceDescriptor.accessContext() == null ? defaultMailAccessContext() : sourceDescriptor.accessContext();
        String rootSourceIdentifier = mailImportIdentityResolver.resolveRootSourceIdentifier(parsed, providerEnvelope, rawMime);

        Map<String, String> rootAttributes = MailProviderEnvelopeAttributes.merge(sourceDescriptor.attributes(), providerEnvelope);
        if (parsed.subject() != null) rootAttributes.put("subject", parsed.subject());
        if (parsed.fromRaw() != null) rootAttributes.put("from", parsed.fromRaw());
        List<String> toRecipients = parsed.recipients().stream()
                .filter(recipient -> recipient.recipientType() == at.procon.dip.domain.document.entity.MailRecipientType.TO)
                .map(MailMessageExtractionService.MailRecipient::rawValue)
                .toList();
        if (!toRecipients.isEmpty()) rootAttributes.put("to", String.join(", ", toRecipients));
        rootAttributes.putIfAbsent("title", parsed.subject() != null ? parsed.subject() : sourceDescriptor.fileName());
        rootAttributes.put("attachmentCount", Integer.toString(parsed.attachments().size()));
        rootAttributes.put("importBatchId", properties.getMailImportBatchId());
        if (parsed.messageId() != null) rootAttributes.put("messageId", parsed.messageId());
        if (parsed.inReplyTo() != null) rootAttributes.put("inReplyTo", parsed.inReplyTo());

        ImportedDocumentResult rootResult = importService.importDocument(new SourceDescriptor(
                accessContext,
                SourceType.MAIL,
                rootSourceIdentifier,
                sourceDescriptor.sourceUri(),
                sourceDescriptor.fileName() != null ? sourceDescriptor.fileName() : fallbackMailFileName(parsed),
                "message/rfc822",
                rawMime,
                mailExtractionService.serializeMessage(parsed),
                parsed.receivedAt() == null ? OffsetDateTime.now() : parsed.receivedAt(),
                OriginalContentStoragePolicy.STORE,
                rootAttributes
        ));

        UUIDSource rootSource = resolveSourceId(SourceType.MAIL, rootSourceIdentifier);
        mailMetadataPersistenceService.upsertMessageMetadata(
                rootResult.document().getId(),
                rootSource.sourceId(),
                parsed,
                providerEnvelope,
                HashUtils.computeSha256(rawMime)
        );

        List<at.procon.dip.domain.document.CanonicalDocumentMetadata> documents = new ArrayList<>();
        List<String> warnings = new ArrayList<>(rootResult.warnings());
        documents.add(rootResult.document().toCanonicalMetadata());

        int sortOrder = 0;
        for (MailAttachment attachment : parsed.attachments()) {
            importAttachment(rootResult.document().getId(), rootResult.document().getId(), rootSourceIdentifier, accessContext, sourceDescriptor, attachment, documents, warnings, ++sortOrder, 0);
        }

        return new IngestionResult(documents, warnings);
    }

    private void importAttachment(java.util.UUID mailRootDocumentId,
                                  java.util.UUID parentDocumentId,
                                  String rootSourceIdentifier,
                                  DocumentAccessContext accessContext,
                                  SourceDescriptor parentSource,
                                  MailAttachment attachment,
                                  List<at.procon.dip.domain.document.CanonicalDocumentMetadata> documents,
                                  List<String> warnings,
                                  int sortOrder,
                                  int depth) {
        boolean expandableWrapper = properties.isExpandMailZipAttachments()
                && zipExtractionService.canHandle(attachment.fileName(), attachment.contentType());

        String attachmentSourceIdentifier = mailImportIdentityResolver.resolveAttachmentSourceIdentifier(rootSourceIdentifier, attachment);
        Map<String, String> attachmentAttributes = new LinkedHashMap<>();
        attachmentAttributes.put("title", attachment.fileName());
        attachmentAttributes.put("mailRootSourceIdentifier", rootSourceIdentifier);
        attachmentAttributes.put("importBatchId", properties.getMailImportBatchId());
        if (attachment.partPath() != null) {
            attachmentAttributes.put("mailPartPath", attachment.partPath());
        }
        if (attachment.attachmentIndex() != null) {
            attachmentAttributes.put("mailAttachmentIndex", attachment.attachmentIndex().toString());
        }
        if (attachment.contentId() != null) {
            attachmentAttributes.put("mailContentId", attachment.contentId());
        }
        if (attachment.disposition() != null) {
            attachmentAttributes.put("mailDisposition", attachment.disposition());
        }
        if (expandableWrapper) {
            attachmentAttributes.put("wrapperDocument", Boolean.TRUE.toString());
        }

        ImportedDocumentResult attachmentResult = importService.importDocument(new SourceDescriptor(
                accessContext,
                SourceType.MAIL_ATTACHMENT,
                attachmentSourceIdentifier,
                parentSource.sourceUri(),
                attachment.fileName(),
                DocumentImportSupport.normalizeMediaType(attachment.contentType()),
                attachment.data(),
                previewTextIfLikelyText(attachment),
                parentSource.receivedAt() == null ? OffsetDateTime.now() : parentSource.receivedAt(),
                expandableWrapper ? OriginalContentStoragePolicy.SKIP : OriginalContentStoragePolicy.STORE,
                attachmentAttributes
        ));
        documents.add(attachmentResult.document().toCanonicalMetadata());
        warnings.addAll(attachmentResult.warnings());
        RelationType relationType = depth > 0 || attachment.path() != null ? RelationType.EXTRACTED_FROM : RelationType.ATTACHMENT_OF;
        relationService.ensureRelation(new CreateDocumentRelationCommand(
                parentDocumentId, attachmentResult.document().getId(), relationType, sortOrder, attachment.fileName()));
        mailMetadataPersistenceService.upsertAttachmentMetadata(
                mailRootDocumentId,
                attachmentResult.document().getId(),
                attachment,
                "IMPORTED",
                null
        );

        if (expandableWrapper) {
            AttachmentExtractor.ExtractionResult zipResult = zipExtractionService.extract(attachment.data(), attachment.fileName(), attachment.contentType());
            if (!zipResult.success()) {
                warnings.add("ZIP attachment extraction failed for " + attachment.fileName() + ": " + zipResult.errorMessage());
                return;
            }
            int childSort = 0;
            for (AttachmentExtractor.ChildAttachment child : zipResult.childAttachments()) {
                importAttachment(mailRootDocumentId, attachmentResult.document().getId(), rootSourceIdentifier, accessContext, parentSource,
                        new MailAttachment(
                                child.filename(),
                                child.contentType(),
                                child.data(),
                                Long.valueOf(child.data().length),
                                child.pathInArchive(),
                                attachment.partPath() == null ? null : attachment.partPath() + "/" + child.pathInArchive(),
                                attachment.disposition(),
                                attachment.contentId(),
                                attachment.attachmentIndex() == null ? null : attachment.attachmentIndex() * 1000 + (++childSort)
                        ),
                        documents, warnings, childSort, depth + 1);
            }
        }
    }

    private UUIDSource resolveSourceId(SourceType sourceType, String sourceIdentifier) {
        Optional<DocumentSource> source = documentSourceRepository.findBySourceTypeAndExternalSourceId(sourceType, sourceIdentifier);
        return new UUIDSource(source.map(DocumentSource::getId).orElse(null));
    }

    private String fallbackMailFileName(ParsedMailMessage parsed) {
        String subject = parsed.subject() == null || parsed.subject().isBlank() ? "mail-message" : parsed.subject().replaceAll("[^A-Za-z0-9._-]", "_");
        return subject + ".eml";
    }

    private DocumentAccessContext defaultMailAccessContext() {
        String tenantKey = properties.getMailDefaultOwnerTenantKey();
        if (tenantKey == null || tenantKey.isBlank()) {
            tenantKey = properties.getDefaultOwnerTenantKey();
        }
        DocumentVisibility visibility = properties.getMailDefaultVisibility();
        TenantRef tenant = (tenantKey == null || tenantKey.isBlank()) ? null : new TenantRef(null, tenantKey, tenantKey);
        if (tenant == null && visibility == DocumentVisibility.TENANT) {
            visibility = DocumentVisibility.RESTRICTED;
        }
        return new DocumentAccessContext(tenant, visibility);
    }

    private String previewTextIfLikelyText(MailAttachment attachment) {
        String mime = DocumentImportSupport.normalizeMediaType(attachment.contentType());
        String ext = DocumentImportSupport.extensionOf(attachment.fileName());
        if (DocumentImportSupport.isSpreadsheetMime(mime) || DocumentImportSupport.isSpreadsheetExtension(ext)) {
            return null;
        }
        if (DocumentImportSupport.isLikelyTextMime(mime) || DocumentImportSupport.isLikelyTextExtension(ext)) {
            return attachment.safeTextPreview();
        }
        return null;
    }

    private record UUIDSource(java.util.UUID sourceId) {
    }
}
