package at.procon.dip.migration.service;

import at.procon.dip.classification.spi.DetectionResult;
import at.procon.dip.domain.document.ContentRole;
import at.procon.dip.domain.document.DocumentFamily;
import at.procon.dip.domain.document.DocumentStatus;
import at.procon.dip.domain.document.DocumentType;
import at.procon.dip.domain.document.RelationType;
import at.procon.dip.domain.document.RepresentationType;
import at.procon.dip.domain.document.SourceType;
import at.procon.dip.domain.document.StorageType;
import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.document.entity.DocumentContent;
import at.procon.dip.domain.document.entity.DocumentSource;
import at.procon.dip.domain.document.entity.DocumentTextRepresentation;
import at.procon.dip.domain.document.repository.DocumentContentRepository;
import at.procon.dip.domain.document.repository.DocumentSourceRepository;
import at.procon.dip.domain.document.repository.DocumentTextRepresentationRepository;
import at.procon.dip.domain.document.service.DocumentContentService;
import at.procon.dip.domain.document.service.DocumentRelationService;
import at.procon.dip.domain.document.service.DocumentRepresentationService;
import at.procon.dip.domain.document.service.DocumentService;
import at.procon.dip.domain.document.service.command.AddDocumentContentCommand;
import at.procon.dip.domain.document.service.command.AddDocumentSourceCommand;
import at.procon.dip.domain.document.service.command.AddDocumentTextRepresentationCommand;
import at.procon.dip.domain.document.service.command.CreateDocumentRelationCommand;
import at.procon.dip.domain.ted.service.TedGenericDocumentRootService;
import at.procon.dip.domain.ted.service.TedNoticeProjectionService;
import at.procon.dip.domain.ted.service.TedPackageDocumentService;
import at.procon.dip.migration.config.LegacyTedBackfillProperties;
import at.procon.dip.embedding.config.EmbeddingProperties;
import at.procon.dip.embedding.service.EmbeddingPersistenceService;
import at.procon.dip.embedding.service.RepresentationEmbeddingOrchestrator;
import at.procon.dip.extraction.spi.ExtractedStructuredPayload;
import at.procon.dip.extraction.spi.ExtractionResult;
import at.procon.dip.ingestion.spi.OriginalContentStoragePolicy;
import at.procon.dip.ingestion.spi.SourceDescriptor;
import at.procon.dip.normalization.service.TextRepresentationBuildService;
import at.procon.dip.normalization.spi.RepresentationBuildRequest;
import at.procon.dip.normalization.spi.TextRepresentationDraft;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import at.procon.dip.search.service.DocumentLexicalIndexService;
import at.procon.ted.model.entity.ProcurementDocument;
import at.procon.ted.repository.LegacyTedEmbeddingSnapshot;
import at.procon.ted.repository.ProcurementDocumentRepository;
import at.procon.ted.util.HashUtils;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@RequiredArgsConstructor
@Slf4j
public class LegacyTedBackfillWorker {

    private static final String XML_MIME_TYPE = "application/xml";
    private static final String TEXT_MIME_TYPE = "text/plain";
    private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();

    private final ProcurementDocumentRepository procurementDocumentRepository;
    private final TedGenericDocumentRootService tedGenericDocumentRootService;
    private final TedPackageDocumentService tedPackageDocumentService;
    private final TedNoticeProjectionService tedNoticeProjectionService;
    private final DocumentService documentService;
    private final DocumentSourceRepository sourceRepository;
    private final at.procon.dip.domain.document.service.DocumentSourceService sourceService;
    private final DocumentContentRepository contentRepository;
    private final DocumentContentService documentContentService;
    private final DocumentTextRepresentationRepository representationRepository;
    private final DocumentRepresentationService documentRepresentationService;
    private final DocumentLexicalIndexService lexicalIndexService;
    private final DocumentRelationService documentRelationService;
    private final TextRepresentationBuildService textRepresentationBuildService;
    private final RepresentationEmbeddingOrchestrator embeddingOrchestrator;
    private final EmbeddingPersistenceService embeddingPersistenceService;
    private final EmbeddingProperties embeddingProperties;
    private final LegacyTedBackfillProperties backfillProperties;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public BackfillOutcome backfill(UUID legacyProcurementDocumentId, String importBatchId, boolean queueEmbeddings) {
        ProcurementDocument legacyDocument = procurementDocumentRepository.findById(legacyProcurementDocumentId)
                .orElseThrow(() -> new IllegalArgumentException("Unknown legacy TED procurement document id: " + legacyProcurementDocumentId));

        Document document = tedGenericDocumentRootService.ensureGenericTedDocument(legacyDocument);
        ensureSource(document, legacyDocument, importBatchId);
        DocumentContent originalContent = ensureOriginalXmlContent(document, legacyDocument);
        DocumentContent normalizedTextContent = ensureNormalizedTextContent(document, legacyDocument);

        List<TextRepresentationDraft> drafts = buildDrafts(legacyDocument);
        List<DocumentTextRepresentation> savedRepresentations = ensureRepresentations(document, originalContent, normalizedTextContent, drafts);

        tedPackageDocumentService.ensurePackageDocumentForLegacyNotice(legacyDocument)
                .ifPresent(packageDocument -> documentRelationService.ensureRelation(new CreateDocumentRelationCommand(
                        packageDocument.getId(),
                        document.getId(),
                        RelationType.CONTAINS,
                        null,
                        legacyDocument.getSourcePath()
                )));

        UUID projectionId = tedNoticeProjectionService.registerOrRefreshProjection(legacyDocument, document.getId());
        documentService.updateStatus(document.getId(), DocumentStatus.REPRESENTED);

        Set<UUID> migratedRepresentationIds = backfillProperties.isMigrateEmbeddings()
                ? migrateLegacyEmbeddings(legacyDocument, savedRepresentations)
                : Set.of();

        if (queueEmbeddings) {
            queueEmbeddings(document.getId(), savedRepresentations, migratedRepresentationIds);
        }

        return new BackfillOutcome(document.getId(), projectionId);
    }

    private void ensureSource(Document document, ProcurementDocument legacyDocument, String importBatchId) {
        String externalSourceId = legacyDocument.getId().toString();
        DocumentSource existing = sourceRepository.findByDocument_Id(document.getId()).stream()
                .filter(candidate -> candidate.getSourceType() == SourceType.MIGRATION || Objects.equals(candidate.getExternalSourceId(), externalSourceId))
                .findFirst()
                .orElse(null);
        if (existing == null) {
            sourceService.addSource(new AddDocumentSourceCommand(
                    document.getId(),
                    SourceType.MIGRATION,
                    externalSourceId,
                    legacyDocument.getSourcePath(),
                    legacyDocument.getSourceFilename(),
                    null,
                    importBatchId,
                    legacyDocument.getCreatedAt() != null ? legacyDocument.getCreatedAt() : OffsetDateTime.now()
            ));
            return;
        }

        existing.setSourceType(SourceType.MIGRATION);
        existing.setExternalSourceId(externalSourceId);
        existing.setSourceUri(legacyDocument.getSourcePath());
        existing.setSourceFilename(legacyDocument.getSourceFilename());
        existing.setImportBatchId(importBatchId);
        existing.setReceivedAt(legacyDocument.getCreatedAt() != null ? legacyDocument.getCreatedAt() : existing.getReceivedAt());
        sourceRepository.save(existing);
    }

    private DocumentContent ensureOriginalXmlContent(Document document, ProcurementDocument legacyDocument) {
        String xml = legacyDocument.getXmlDocument();
        long sizeBytes = legacyDocument.getFileSizeBytes() != null
                ? legacyDocument.getFileSizeBytes()
                : (xml == null ? 0L : (long) xml.getBytes(StandardCharsets.UTF_8).length);
        return ensureContent(document, ContentRole.ORIGINAL, XML_MIME_TYPE, xml, legacyDocument.getDocumentHash(), sizeBytes);
    }

    private DocumentContent ensureNormalizedTextContent(Document document, ProcurementDocument legacyDocument) {
        String normalizedText = resolveNormalizedText(legacyDocument);
        String contentHash = HashUtils.computeSha256(legacyDocument.getDocumentHash() + ":NORMALIZED_TEXT:" + normalizedText);
        return ensureContent(document, ContentRole.NORMALIZED_TEXT, TEXT_MIME_TYPE, normalizedText, contentHash, (long) normalizedText.length());
    }

    private DocumentContent ensureContent(Document document,
                                          ContentRole role,
                                          String mimeType,
                                          String text,
                                          String contentHash,
                                          Long sizeBytes) {
        List<DocumentContent> existing = contentRepository.findByDocument_IdAndContentRole(document.getId(), role);
        if (!existing.isEmpty()) {
            DocumentContent content = existing.get(0);
            content.setStorageType(StorageType.DB_TEXT);
            content.setMimeType(mimeType);
            content.setCharsetName(CHARSET_UTF8);
            content.setTextContent(text);
            content.setBinaryContent(null);
            content.setBinaryRef(null);
            content.setContentHash(contentHash);
            content.setSizeBytes(sizeBytes);
            return contentRepository.save(content);
        }

        return documentContentService.addContent(new AddDocumentContentCommand(
                document.getId(),
                role,
                StorageType.DB_TEXT,
                mimeType,
                CHARSET_UTF8,
                text,
                null,
                null,
                contentHash,
                sizeBytes
        ));
    }

    private List<TextRepresentationDraft> buildDrafts(ProcurementDocument legacyDocument) {
        String normalizedText = resolveNormalizedText(legacyDocument);

        Map<ContentRole, String> derivedText = new LinkedHashMap<>();
        derivedText.put(ContentRole.NORMALIZED_TEXT, normalizedText);

        SourceDescriptor sourceDescriptor = new SourceDescriptor(
                null,
                SourceType.MIGRATION,
                legacyDocument.getId().toString(),
                legacyDocument.getSourcePath(),
                legacyDocument.getSourceFilename(),
                XML_MIME_TYPE,
                null,
                legacyDocument.getXmlDocument(),
                legacyDocument.getCreatedAt(),
                OriginalContentStoragePolicy.DEFAULT,
                Map.of("legacyProcurementDocumentId", legacyDocument.getId().toString())
        );

        DetectionResult detectionResult = new DetectionResult(
                DocumentType.TED_NOTICE,
                DocumentFamily.PROCUREMENT,
                XML_MIME_TYPE,
                legacyDocument.getLanguageCode(),
                Map.of()
        );

        ExtractionResult extractionResult = new ExtractionResult(
                derivedText,
                List.of(new ExtractedStructuredPayload("ted-notice", buildStructuredAttributes(legacyDocument))),
                List.of()
        );

        return textRepresentationBuildService.build(new RepresentationBuildRequest(
                sourceDescriptor,
                detectionResult,
                extractionResult
        ));
    }

    private List<DocumentTextRepresentation> ensureRepresentations(Document document,
                                                                   DocumentContent originalContent,
                                                                   DocumentContent normalizedTextContent,
                                                                   List<TextRepresentationDraft> drafts) {
        if (drafts == null || drafts.isEmpty()) {
            return List.of();
        }

        List<DocumentTextRepresentation> existing = new ArrayList<>(representationRepository.findByDocument_Id(document.getId()));
        existing.sort(Comparator.comparing(DocumentTextRepresentation::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())));

        List<DocumentTextRepresentation> saved = new ArrayList<>();
        for (TextRepresentationDraft draft : drafts) {
            if (!StringUtils.hasText(draft.textBody())) {
                continue;
            }

            DocumentTextRepresentation representation = findMatchingRepresentation(existing, draft)
                    .orElseGet(DocumentTextRepresentation::new);

            DocumentContent linkedContent = draft.sourceContentRole() == ContentRole.NORMALIZED_TEXT
                    ? normalizedTextContent
                    : originalContent;

            boolean isNew = representation.getId() == null;
            representation.setDocument(document);
            representation.setContent(linkedContent);
            representation.setRepresentationType(draft.representationType());
            representation.setBuilderKey(draft.builderKey());
            representation.setLanguageCode(draft.languageCode());
            representation.setTokenCount(draft.textBody().length());
            representation.setCharCount(draft.textBody().length());
            representation.setChunkIndex(draft.chunkIndex());
            representation.setChunkStartOffset(draft.chunkStartOffset());
            representation.setChunkEndOffset(draft.chunkEndOffset());
            representation.setPrimaryRepresentation(draft.primary());
            representation.setTextBody(draft.textBody());

            DocumentTextRepresentation savedRepresentation;
            if (isNew) {
                savedRepresentation = documentRepresentationService.addRepresentation(new AddDocumentTextRepresentationCommand(
                        document.getId(),
                        linkedContent == null ? null : linkedContent.getId(),
                        draft.representationType(),
                        draft.builderKey(),
                        draft.languageCode(),
                        draft.textBody().length(),
                        draft.chunkIndex(),
                        draft.chunkStartOffset(),
                        draft.chunkEndOffset(),
                        draft.primary(),
                        draft.textBody(),
                        false
                ));
                existing.add(savedRepresentation);
            } else {
                savedRepresentation = representationRepository.save(representation);
                lexicalIndexService.indexRepresentation(savedRepresentation.getId());
            }
            saved.add(savedRepresentation);
        }
        return saved;
    }

    private java.util.Optional<DocumentTextRepresentation> findMatchingRepresentation(List<DocumentTextRepresentation> existing,
                                                                                      TextRepresentationDraft draft) {
        return existing.stream()
                .filter(candidate -> candidate.getRepresentationType() == draft.representationType())
                .filter(candidate -> Objects.equals(candidate.getChunkIndex(), draft.chunkIndex()))
                .filter(candidate -> Objects.equals(candidate.isPrimaryRepresentation(), draft.primary()))
                .findFirst();
    }

    private Set<UUID> migrateLegacyEmbeddings(ProcurementDocument legacyDocument,
                                              List<DocumentTextRepresentation> savedRepresentations) {
        LegacyTedEmbeddingSnapshot embeddingSnapshot = procurementDocumentRepository
                .findEmbeddingSnapshotByIdForMigration(legacyDocument.getId())
                .orElse(null);
        if (embeddingSnapshot == null || !StringUtils.hasText(embeddingSnapshot.getVectorText())) {
            return Set.of();
        }
        if (!embeddingProperties.isEnabled() || !StringUtils.hasText(embeddingProperties.getDefaultDocumentModel())) {
            log.warn("Skipping legacy embedding migration for TED notice {} because no default document model is configured", legacyDocument.getId());
            return Set.of();
        }

        DocumentTextRepresentation targetRepresentation = selectPrimaryEmbeddingTarget(savedRepresentations)
                .orElseThrow(() -> new IllegalStateException("No suitable representation found for migrated embedding of legacy TED notice " + legacyDocument.getId()));

        float[] vector = parseVectorText(embeddingSnapshot.getVectorText());
        if (vector.length == 0) {
            log.warn("Skipping legacy embedding migration for TED notice {} because the legacy vector is empty", legacyDocument.getId());
            return Set.of();
        }

        var embedding = embeddingPersistenceService.ensurePending(targetRepresentation.getId(), embeddingProperties.getDefaultDocumentModel());
        embeddingPersistenceService.saveCompleted(embedding.getId(), vector, embeddingSnapshot.getTokenCount());
        log.debug("Migrated legacy embedding for TED notice {} to representation {} (dimension={}, tokenCount={})",
                legacyDocument.getId(), targetRepresentation.getId(), vector.length, embeddingSnapshot.getTokenCount());
        return Set.of(targetRepresentation.getId());
    }

    private Optional<DocumentTextRepresentation> selectPrimaryEmbeddingTarget(List<DocumentTextRepresentation> representations) {
        if (representations == null || representations.isEmpty()) {
            return Optional.empty();
        }
        return representations.stream()
                .filter(rep -> rep.getRepresentationType() == RepresentationType.SEMANTIC_TEXT && rep.isPrimaryRepresentation())
                .findFirst()
                .or(() -> representations.stream().filter(DocumentTextRepresentation::isPrimaryRepresentation).findFirst())
                .or(() -> representations.stream().filter(rep -> rep.getRepresentationType() == RepresentationType.SEMANTIC_TEXT).findFirst())
                .or(() -> Optional.of(representations.get(0)));
    }

    private float[] parseVectorText(String vectorText) {
        if (!StringUtils.hasText(vectorText)) {
            return new float[0];
        }
        String trimmed = vectorText.trim();
        if (trimmed.startsWith("[")) {
            trimmed = trimmed.substring(1);
        }
        if (trimmed.endsWith("]")) {
            trimmed = trimmed.substring(0, trimmed.length() - 1);
        }
        if (!StringUtils.hasText(trimmed)) {
            return new float[0];
        }
        String[] parts = trimmed.split(",");
        float[] result = new float[parts.length];
        for (int i = 0; i < parts.length; i++) {
            result[i] = Float.parseFloat(parts[i].trim());
        }
        return result;
    }

    private void queueEmbeddings(UUID documentId,
                                 List<DocumentTextRepresentation> representations,
                                 Set<UUID> migratedRepresentationIds) {
        if (!embeddingProperties.isEnabled() || !StringUtils.hasText(embeddingProperties.getDefaultDocumentModel())) {
            log.debug("Skipping embedding queue for migrated document {} because no default document model is configured", documentId);
            return;
        }

        Set<UUID> skippedIds = migratedRepresentationIds == null ? Set.of() : new LinkedHashSet<>(migratedRepresentationIds);
        for (DocumentTextRepresentation representation : representations) {
            if (representation.getId() != null && skippedIds.contains(representation.getId())) {
                continue;
            }
            RepresentationType type = representation.getRepresentationType();
            boolean queue = switch (type) {
                case SEMANTIC_TEXT -> true;
                case CHUNK -> true;
                default -> false;
            };
            if (!queue) {
                continue;
            }
            embeddingOrchestrator.enqueueRepresentation(documentId, representation.getId(), embeddingProperties.getDefaultDocumentModel());
        }
    }

    private String resolveNormalizedText(ProcurementDocument legacyDocument) {
        if (StringUtils.hasText(legacyDocument.getTextContent())) {
            return legacyDocument.getTextContent().trim();
        }

        StringBuilder sb = new StringBuilder();
        appendLine(sb, legacyDocument.getProjectTitle());
        appendBlank(sb, legacyDocument.getProjectDescription());
        appendLine(sb, legacyDocument.getBuyerName());
        appendLine(sb, joinArray(legacyDocument.getCpvCodes()));
        appendLine(sb, joinArray(legacyDocument.getNutsCodes()));
        String fallback = sb.toString().trim();
        return StringUtils.hasText(fallback) ? fallback : legacyDocument.getDocumentHash();
    }

    private Map<String, Object> buildStructuredAttributes(ProcurementDocument legacyDocument) {
        Map<String, Object> attrs = new LinkedHashMap<>();
        putIfText(attrs, "title", legacyDocument.getProjectTitle());
        putIfText(attrs, "description", legacyDocument.getProjectDescription());
        putIfText(attrs, "buyerName", legacyDocument.getBuyerName());
        putIfText(attrs, "cpvCodes", joinArray(legacyDocument.getCpvCodes()));
        putIfText(attrs, "nutsCodes", joinArray(legacyDocument.getNutsCodes()));
        putIfText(attrs, "publicationId", legacyDocument.getPublicationId());
        return attrs;
    }

    private void putIfText(Map<String, Object> target, String key, String value) {
        if (StringUtils.hasText(value)) {
            target.put(key, value);
        }
    }

    private String joinArray(String[] values) {
        if (values == null || values.length == 0) {
            return null;
        }
        return String.join(", ", values);
    }

    private void appendLine(StringBuilder sb, String value) {
        if (StringUtils.hasText(value)) {
            if (sb.length() > 0) {
                sb.append('\n');
            }
            sb.append(value.trim());
        }
    }

    private void appendBlank(StringBuilder sb, String value) {
        if (StringUtils.hasText(value)) {
            if (sb.length() > 0) {
                sb.append("\n\n");
            }
            sb.append(value.trim());
        }
    }

    public record BackfillOutcome(UUID documentId, UUID projectionId) {
    }

}
