package at.procon.ted.service;

import at.procon.ted.config.TedProcessorProperties;
import at.procon.ted.model.entity.ProcurementDocument;
import at.procon.ted.repository.ProcurementDocumentRepository;
import at.procon.ted.service.attachment.PdfExtractionService;
import at.procon.ted.service.attachment.AttachmentExtractor.ExtractionResult;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Service for similarity search on TED procurement documents.
 * Uses vector embeddings and cosine similarity for semantic matching.
 *
 * @author Martin.Schweitzer@procon.co.at and claude.ai
 */
@Service
@ConditionalOnRuntimeMode(RuntimeMode.LEGACY)
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class SimilaritySearchService {

    private final VectorizationService vectorizationService;
    private final ProcurementDocumentRepository documentRepository;
    private final PdfExtractionService pdfExtractionService;
    private final TedProcessorProperties properties;

    private static final int DEFAULT_TOP_K = 20;
    private static final double DEFAULT_THRESHOLD = 0.5;

    /**
     * Search for similar documents using text query.
     *
     * @param queryText  the text to search for similar documents
     * @param topK       number of top results to return (default 20)
     * @param threshold  minimum similarity threshold (default 0.5)
     * @return list of similar documents with similarity scores
     */
    public SimilaritySearchResponse searchByText(String queryText, Integer topK, Double threshold) {
        if (queryText == null || queryText.isBlank()) {
            throw new IllegalArgumentException("Query text cannot be empty");
        }

        if (!vectorizationService.isAvailable()) {
            throw new IllegalStateException("Vectorization service is not available");
        }

        int limit = topK != null && topK > 0 ? Math.min(topK, 100) : DEFAULT_TOP_K;
        double similarityThreshold = threshold != null ? threshold : DEFAULT_THRESHOLD;

        log.info("Similarity search: query='{}...', topK={}, threshold={}",
                queryText.substring(0, Math.min(50, queryText.length())), limit, similarityThreshold);

        try {
            // Generate query embedding
            long startTime = System.currentTimeMillis();
            float[] queryEmbedding = vectorizationService.generateQueryEmbedding(queryText);
            String vectorStr = vectorizationService.floatArrayToVectorString(queryEmbedding);
            long embeddingTime = System.currentTimeMillis() - startTime;

            log.debug("Query embedding generated in {}ms ({} dimensions)", embeddingTime, queryEmbedding.length);

            // Execute similarity search (simple query without filters)
            startTime = System.currentTimeMillis();
            List<Object[]> results = documentRepository.findBySimilarity(
                    vectorStr,
                    similarityThreshold,
                    limit
            );
            long searchTime = System.currentTimeMillis() - startTime;

            log.info("Similarity search completed: {} results in {}ms", results.size(), searchTime);

            // Map results to response
            List<SimilarDocument> documents = new ArrayList<>();
            for (Object[] row : results) {
                SimilarDocument doc = mapToSimilarDocument(row);
                if (doc != null) {
                    documents.add(doc);
                }
            }

            return SimilaritySearchResponse.builder()
                    .query(truncateText(queryText, 200))
                    .results(documents)
                    .resultCount(documents.size())
                    .threshold(similarityThreshold)
                    .embeddingTimeMs(embeddingTime)
                    .searchTimeMs(searchTime)
                    .build();

        } catch (Exception e) {
            log.error("Similarity search failed: {}", e.getMessage(), e);
            throw new RuntimeException("Similarity search failed: " + e.getMessage(), e);
        }
    }

    /**
     * Search for similar documents using PDF content.
     *
     * @param pdfData    the PDF file content
     * @param filename   original filename
     * @param topK       number of top results to return (default 20)
     * @param threshold  minimum similarity threshold (default 0.5)
     * @return list of similar documents with similarity scores
     */
    public SimilaritySearchResponse searchByPdf(byte[] pdfData, String filename, Integer topK, Double threshold) {
        if (pdfData == null || pdfData.length == 0) {
            throw new IllegalArgumentException("PDF data cannot be empty");
        }

        log.info("Extracting text from PDF: {} ({} bytes)", filename, pdfData.length);

        // Extract text from PDF
        long startTime = System.currentTimeMillis();
        ExtractionResult extractionResult = pdfExtractionService.extract(pdfData, filename, "application/pdf");
        long extractionTime = System.currentTimeMillis() - startTime;

        if (!extractionResult.success()) {
            throw new RuntimeException("PDF text extraction failed: " + extractionResult.errorMessage());
        }

        String extractedText = extractionResult.extractedText();
        if (extractedText == null || extractedText.isBlank()) {
            throw new RuntimeException("No text content could be extracted from PDF");
        }

        log.info("PDF text extracted in {}ms: {} characters", extractionTime, extractedText.length());

        // Search using extracted text
        SimilaritySearchResponse response = searchByText(extractedText, topK, threshold);

        // Add PDF extraction info to response
        return SimilaritySearchResponse.builder()
                .query("PDF: " + filename + " (" + extractedText.length() + " chars extracted)")
                .results(response.getResults())
                .resultCount(response.getResultCount())
                .threshold(response.getThreshold())
                .embeddingTimeMs(response.getEmbeddingTimeMs())
                .searchTimeMs(response.getSearchTimeMs())
                .pdfExtractionTimeMs(extractionTime)
                .extractedTextLength(extractedText.length())
                .build();
    }

    /**
     * Map database result row to SimilarDocument DTO.
     * Result format: [id (UUID), similarity (Double)]
     */
    private SimilarDocument mapToSimilarDocument(Object[] row) {
        if (row == null || row.length < 2) {
            return null;
        }

        try {
            UUID id = (UUID) row[0];
            Double similarity = row[1] != null ? ((Number) row[1]).doubleValue() : null;

            // Fetch full document for detailed mapping
            return documentRepository.findById(id)
                    .map(doc -> SimilarDocument.builder()
                            .id(doc.getId())
                            .publicationId(doc.getPublicationId())
                            .noticeId(doc.getNoticeId())
                            .noticeUrl(doc.getNoticeUrl())
                            .noticeType(doc.getNoticeType() != null ? doc.getNoticeType().name() : null)
                            .projectTitle(doc.getProjectTitle())
                            .projectDescription(truncateText(doc.getProjectDescription(), 500))
                            .buyerName(doc.getBuyerName())
                            .buyerCountryCode(doc.getBuyerCountryCode())
                            .buyerCity(doc.getBuyerCity())
                            .contractNature(doc.getContractNature() != null ? doc.getContractNature().name() : null)
                            .procedureType(doc.getProcedureType() != null ? doc.getProcedureType().name() : null)
                            .publicationDate(doc.getPublicationDate())
                            .submissionDeadline(doc.getSubmissionDeadline())
                            .cpvCodes(doc.getCpvCodes() != null ? List.of(doc.getCpvCodes()) : List.of())
                            .estimatedValue(doc.getEstimatedValue())
                            .estimatedValueCurrency(doc.getEstimatedValueCurrency())
                            .similarity(similarity)
                            .similarityPercent(similarity != null ? Math.round(similarity * 100) : null)
                            .build())
                    .orElse(null);

        } catch (Exception e) {
            log.warn("Failed to map search result: {}", e.getMessage());
            return null;
        }
    }

    /**
     * Truncate text to specified length.
     */
    private String truncateText(String text, int maxLength) {
        if (text == null) return null;
        if (text.length() <= maxLength) return text;
        return text.substring(0, maxLength - 3) + "...";
    }

    /**
     * Response DTO for similarity search.
     */
    @lombok.Data
    @lombok.Builder
    @lombok.NoArgsConstructor
    @lombok.AllArgsConstructor
    public static class SimilaritySearchResponse {
        private String query;
        private List<SimilarDocument> results;
        private int resultCount;
        private double threshold;
        private long embeddingTimeMs;
        private long searchTimeMs;
        private Long pdfExtractionTimeMs;
        private Integer extractedTextLength;
    }

    /**
     * DTO for a similar document result.
     */
    @lombok.Data
    @lombok.Builder
    @lombok.NoArgsConstructor
    @lombok.AllArgsConstructor
    public static class SimilarDocument {
        private UUID id;
        private String publicationId;
        private String noticeId;
        private String noticeUrl;
        private String noticeType;
        private String projectTitle;
        private String projectDescription;
        private String buyerName;
        private String buyerCountryCode;
        private String buyerCity;
        private String contractNature;
        private String procedureType;
        private LocalDate publicationDate;
        private java.time.OffsetDateTime submissionDeadline;
        private List<String> cpvCodes;
        private BigDecimal estimatedValue;
        private String estimatedValueCurrency;
        private Double similarity;
        private Long similarityPercent;
    }
}
