package at.procon.dip.domain.ted.service;

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.entity.Document;
import at.procon.dip.domain.document.entity.DocumentRelation;
import at.procon.dip.domain.document.entity.DocumentTextRepresentation;
import at.procon.dip.domain.document.repository.DocumentRelationRepository;
import at.procon.dip.domain.document.repository.DocumentRepository;
import at.procon.dip.domain.document.repository.DocumentTextRepresentationRepository;
import at.procon.dip.domain.document.service.DocumentRepresentationService;
import at.procon.dip.domain.document.service.command.AddDocumentTextRepresentationCommand;
import at.procon.dip.domain.ted.config.TedProjectionProperties;
import at.procon.dip.domain.ted.entity.TedNoticeLot;
import at.procon.dip.domain.ted.entity.TedNoticeProjection;
import at.procon.dip.domain.ted.repository.TedNoticeLotRepository;
import at.procon.dip.embedding.config.EmbeddingProperties;
import at.procon.dip.embedding.registry.EmbeddingModelRegistry;
import at.procon.dip.embedding.service.RepresentationEmbeddingOrchestrator;
import at.procon.dip.search.service.DocumentLexicalIndexService;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
@Slf4j
public class TedLotDocumentMaterializationService {

    public static final String BUILDER_KEY = "ted-lot-clustering-text-v1";

    private final TedProjectionProperties properties;
    private final TedNoticeLotRepository lotRepository;
    private final DocumentRepository documentRepository;
    private final DocumentRelationRepository relationRepository;
    private final DocumentTextRepresentationRepository representationRepository;
    private final DocumentRepresentationService documentRepresentationService;
    private final DocumentLexicalIndexService lexicalIndexService;
    private final RepresentationEmbeddingOrchestrator embeddingOrchestrator;
    private final EmbeddingProperties embeddingProperties;
    private final EmbeddingModelRegistry modelRegistry;

    @Transactional
    public int materializeProjectionLots(UUID projectionId) {
        if (!properties.getLotDocuments().isEnabled()) {
            return 0;
        }
        List<TedNoticeLot> lots = lotRepository.findByNoticeProjection_Id(projectionId);
        int count = 0;
        for (TedNoticeLot lot : lots) {
            materializeLot(lot);
            count++;
        }
        return count;
    }

    @Transactional
    public void materializeLots(TedNoticeProjection projection, List<TedNoticeLot> lots) {
        if (!properties.getLotDocuments().isEnabled() || lots == null || lots.isEmpty()) {
            return;
        }
        for (TedNoticeLot lot : lots) {
            materializeLot(lot);
        }
    }

    private DocumentTextRepresentation materializeLot(TedNoticeLot lot) {
        TedNoticeProjection projection = lot.getNoticeProjection();
        Document parent = projection.getDocument();
        String semanticText = buildSemanticText(projection, lot);
        if (!StringUtils.hasText(semanticText)) {
            log.debug("Skipping TED lot document for lot {} / projection {} because semantic text is blank", lot.getLotId(), projection.getId());
            return null;
        }

        String businessKey = buildLotBusinessKey(projection, lot);
        Document lotDocument = documentRepository.findByBusinessKey(businessKey)
                .orElseGet(() -> newLotDocument(parent, businessKey));
        lotDocument.setDocumentType(DocumentType.TED_NOTICE_LOT);
        lotDocument.setDocumentFamily(DocumentFamily.PROCUREMENT);
        lotDocument.setStatus(DocumentStatus.REPRESENTED);
        lotDocument.setTitle(firstNonBlank(lot.getTitle(), projection.getProjectTitle(), businessKey));
        lotDocument.setSummary(firstNonBlank(lot.getDescription(), projection.getProjectDescription()));
        lotDocument.setLanguageCode(firstNonBlank(projection.getLanguageCode(), parent.getLanguageCode()));
        lotDocument.setMimeType("application/x-ted-notice-lot");
        lotDocument = documentRepository.save(lotDocument);

        ensureRelation(parent, lotDocument, lot);
        return upsertSemanticRepresentation(lotDocument, semanticText, lotDocument.getLanguageCode());
    }

    private Document newLotDocument(Document parent, String businessKey) {
        Document document = new Document();
        document.setOwnerTenant(parent.getOwnerTenant());
        document.setVisibility(parent.getVisibility());
        document.setBusinessKey(businessKey);
        return document;
    }

    private void ensureRelation(Document parent, Document child, TedNoticeLot lot) {
        if (!relationRepository.existsByParentDocument_IdAndChildDocument_IdAndRelationType(parent.getId(), child.getId(), RelationType.CONTAINS)) {
            relationRepository.save(DocumentRelation.builder()
                    .parentDocument(parent)
                    .childDocument(child)
                    .relationType(RelationType.CONTAINS)
                    .sortOrder(resolveSortOrder(lot))
                    .relationMetadata("{\"source\":\"ted-lot-materialization\",\"lotId\":\"" + escapeJson(firstNonBlank(lot.getLotId(), lot.getInternalId(), lot.getId().toString())) + "\"}")
                    .build());
        }
    }

    private DocumentTextRepresentation upsertSemanticRepresentation(Document document, String semanticText, String languageCode) {
        Optional<DocumentTextRepresentation> existing = representationRepository
                .findByDocument_IdAndRepresentationType(document.getId(), RepresentationType.SEMANTIC_TEXT)
                .stream()
                .filter(r -> BUILDER_KEY.equals(r.getBuilderKey()) || r.isPrimaryRepresentation())
                .findFirst();

        boolean changed = existing.isEmpty()
                || !semanticText.equals(existing.get().getTextBody())
                || !equalsNullable(languageCode, existing.get().getLanguageCode())
                || !BUILDER_KEY.equals(existing.get().getBuilderKey());

        DocumentTextRepresentation semantic = existing
                .map(found -> changed ? updateRepresentation(found, semanticText, languageCode) : found)
                .orElseGet(() -> documentRepresentationService.addRepresentation(new AddDocumentTextRepresentationCommand(
                        document.getId(),
                        null,
                        RepresentationType.SEMANTIC_TEXT,
                        BUILDER_KEY,
                        languageCode,
                        null,
                        null,
                        null,
                        null,
                        true,
                        semanticText,
                        false
                )));

        if (changed && shouldQueueEmbeddings()) {
            String modelKey = modelRegistry.getRequiredDefaultDocumentModelKey();
            embeddingOrchestrator.enqueueRepresentation(document.getId(), semantic.getId(), modelKey);
        }
        return semantic;
    }

    private DocumentTextRepresentation updateRepresentation(DocumentTextRepresentation existing, String semanticText, String languageCode) {
        existing.setBuilderKey(BUILDER_KEY);
        existing.setLanguageCode(languageCode);
        existing.setPrimaryRepresentation(true);
        existing.setTextBody(semanticText);
        existing.setCharCount(semanticText.length());
        DocumentTextRepresentation saved = representationRepository.saveAndFlush(existing);
        lexicalIndexService.indexRepresentation(saved.getId());
        return saved;
    }

    private boolean shouldQueueEmbeddings() {
        return properties.getLotDocuments().isQueueEmbeddings()
                && embeddingProperties.isEnabled()
                && StringUtils.hasText(embeddingProperties.getDefaultDocumentModel());
    }

    private String buildSemanticText(TedNoticeProjection projection, TedNoticeLot lot) {
        StringBuilder sb = new StringBuilder(1024);
        append(sb, "Entity type", "Procurement lot");
        append(sb, "Procurement scope", joinedLines(lot.getTitle(), lot.getDescription()));
        append(sb, "Procurement category", joined(firstNonEmpty(lot.getCpvCodes(), projection.getCpvCodes())));
        append(sb, "Relevant domain", projection.getBuyerActivityType());
        append(sb, "Contract type", projection.getContractNature() == null ? null : projection.getContractNature().name());
        append(sb, "Geographic context", joined(firstNonEmpty(lot.getNutsCodes(), projection.getNutsCodes()), projection.getBuyerCountryCode()));

        if (!StringUtils.hasText(lot.getDescription())) {
            append(sb, "Parent notice context", joinedLines(projection.getProjectTitle(), projection.getProjectDescription()));
        } else if (properties.getLotDocuments().isIncludeProjectDescription()) {
            append(sb, "Parent notice context", joinedLines(projection.getProjectTitle(), projection.getProjectDescription()));
        } else {
            append(sb, "Parent notice context", projection.getProjectTitle());
        }
        return sb.toString().trim();
    }

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

    private String buildLotBusinessKey(TedNoticeProjection projection, TedNoticeLot lot) {
        String noticeKey = firstNonBlank(projection.getPublicationId(), projection.getNoticeId(), projection.getDocument().getId().toString());
        String lotKey = firstNonBlank(lot.getLotId(), lot.getInternalId(), lot.getId().toString());
        return "TED_NOTICE_LOT:" + sanitizeKey(noticeKey) + ":" + sanitizeKey(lotKey);
    }

    private int resolveSortOrder(TedNoticeLot lot) {
        String value = firstNonBlank(lot.getLotId(), lot.getInternalId());
        if (value != null) {
            String digits = value.replaceAll("\\D+", "");
            if (StringUtils.hasText(digits)) {
                try {
                    return Integer.parseInt(digits);
                } catch (NumberFormatException ignored) {
                    // use default below
                }
            }
        }
        return 0;
    }

    private String[] firstNonEmpty(String[] first, String[] fallback) {
        return first != null && first.length > 0 ? first : fallback;
    }

    private String joined(String[] values) {
        if (values == null || values.length == 0) {
            return null;
        }
        return Arrays.stream(values)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .collect(Collectors.joining(", "));
    }

    private String joined(String[] values, String fallback) {
        String joined = joined(values);
        if (StringUtils.hasText(joined)) {
            return joined;
        }
        return fallback;
    }

    private String joinedLines(String... values) {
        String joined = Arrays.stream(values)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .collect(Collectors.joining("\n"));
        return StringUtils.hasText(joined) ? joined : null;
    }

    private String sanitizeKey(String value) {
        return value == null ? "unknown" : value.trim().replaceAll("\\s+", "_");
    }

    private String escapeJson(String value) {
        return value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"");
    }

    private boolean equalsNullable(String left, String right) {
        return left == null ? right == null : left.equals(right);
    }

    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (StringUtils.hasText(value)) {
                return value.trim();
            }
        }
        return null;
    }
}
