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

import at.procon.dip.domain.document.RepresentationType;
import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.document.entity.DocumentTextRepresentation;
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.time.config.TimeDomainProperties;
import at.procon.dip.domain.time.entity.TimeEntrySearchProjection;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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;

@Service
@RequiredArgsConstructor
@Slf4j
public class TimeEntryRepresentationMaterializationService {

    public static final String BUILDER_KEY = "time-entry-structured-text";

    private final TimeDomainProperties timeDomainProperties;
    private final DocumentRepository documentRepository;
    private final DocumentTextRepresentationRepository representationRepository;
    private final DocumentRepresentationService documentRepresentationService;
    private final DocumentLexicalIndexService lexicalIndexService;
    private final RepresentationEmbeddingOrchestrator embeddingOrchestrator;
    private final EmbeddingProperties embeddingProperties;
    private final EmbeddingModelRegistry modelRegistry;

    public void upsertRepresentations(TimeEntrySearchProjection projection) {
        if (projection == null) {
            return;
        }
        upsertRepresentations(List.of(projection));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void upsertRepresentations(List<TimeEntrySearchProjection> projections) {
        if (projections == null || projections.isEmpty()) {
            return;
        }

        List<TimeEntrySearchProjection> eligible = projections.stream()
                .filter(projection -> documentId(projection) != null)
                .filter(projection -> projection.getSemanticText() != null && !projection.getSemanticText().isBlank())
                .toList();
        if (eligible.isEmpty()) {
            return;
        }

        List<UUID> documentIds = eligible.stream()
                .map(this::documentId)
                .distinct()
                .toList();
        Map<UUID, Document> documentsById = documentRepository.findAllById(documentIds).stream()
                .collect(java.util.stream.Collectors.toMap(Document::getId, java.util.function.Function.identity(), (a, b) -> a, LinkedHashMap::new));
        List<Document> documentsToSave = new ArrayList<>();
        for (TimeEntrySearchProjection projection : eligible) {
            UUID documentId = documentId(projection);
            Document document = documentsById.get(documentId);
            if (document == null || documentsToSave.contains(document)) {
                continue;
            }
            document.setTitle(firstNonBlank(projection.getSummaryText(), projection.getTimeRecordingDesc(), projection.getPrimaryTaskName(), projection.getExternalId()));
            document.setSummary(projection.getSummaryText());
            document.setLanguageCode(firstNonBlank(projection.getLanguageCode(), document.getLanguageCode()));
            if (document.getMimeType() == null || document.getMimeType().isBlank()) {
                document.setMimeType("application/x-time-entry");
            }
            documentsToSave.add(document);
        }
        if (!documentsToSave.isEmpty()) {
            documentRepository.saveAll(documentsToSave);
            documentRepository.flush();
        }

        List<DocumentTextRepresentation> changedExisting = new ArrayList<>();
        List<TimeEntrySearchProjection> newRepresentationProjections = new ArrayList<>();
        List<UUID> changedRepresentationIds = new ArrayList<>();
        List<DocumentTextRepresentation> newlyCreatedRepresentations = new ArrayList<>();

        for (TimeEntrySearchProjection projection : eligible) {
            Document document = documentsById.get(documentId(projection));
            if (document == null) {
                continue;
            }
            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()
                    || !projection.getSemanticText().equals(existing.get().getTextBody())
                    || !equalsNullable(projection.getLanguageCode(), existing.get().getLanguageCode())
                    || !BUILDER_KEY.equals(existing.get().getBuilderKey());

            if (!changed) {
                continue;
            }

            if (existing.isPresent()) {
                DocumentTextRepresentation found = existing.get();
                found.setBuilderKey(BUILDER_KEY);
                found.setLanguageCode(projection.getLanguageCode());
                found.setPrimaryRepresentation(true);
                found.setTextBody(projection.getSemanticText());
                found.setCharCount(projection.getSemanticText().length());
                changedExisting.add(found);
            } else {
                newRepresentationProjections.add(projection);
            }
        }

        if (!changedExisting.isEmpty()) {
            representationRepository.saveAll(changedExisting);
            representationRepository.flush();
            changedExisting.stream().map(DocumentTextRepresentation::getId).forEach(changedRepresentationIds::add);
        }

        for (TimeEntrySearchProjection projection : newRepresentationProjections) {
            Document document = documentsById.get(documentId(projection));
            if (document == null) {
                continue;
            }
            DocumentTextRepresentation created = documentRepresentationService.addRepresentation(new AddDocumentTextRepresentationCommand(
                    document.getId(),
                    null,
                    RepresentationType.SEMANTIC_TEXT,
                    BUILDER_KEY,
                    projection.getLanguageCode(),
                    null,
                    null,
                    null,
                    null,
                    true,
                    projection.getSemanticText(),
                    false
            ));
            newlyCreatedRepresentations.add(created);
            changedRepresentationIds.add(created.getId());
        }

        for (UUID representationId : changedRepresentationIds) {
            lexicalIndexService.indexRepresentation(representationId);
        }

        if (embeddingProperties.isEnabled()
                && timeDomainProperties.getLeitstand().isQueueEmbeddings()
                && embeddingProperties.getDefaultDocumentModel() != null
                && !embeddingProperties.getDefaultDocumentModel().isBlank()) {
            String modelKey = modelRegistry.getRequiredDefaultDocumentModelKey();
            for (DocumentTextRepresentation representation : changedExisting) {
                embeddingOrchestrator.enqueueRepresentation(representation.getDocument().getId(), representation.getId(), modelKey);
            }
            for (DocumentTextRepresentation representation : newlyCreatedRepresentations) {
                embeddingOrchestrator.enqueueRepresentation(representation.getDocument().getId(), representation.getId(), modelKey);
            }
        }
    }

    private UUID documentId(TimeEntrySearchProjection projection) {
        Document document = projection == null ? null : projection.getDocument();
        return document == null ? null : document.getId();
    }

    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 (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return null;
    }
}
