package at.procon.ted.service;

import at.procon.ted.config.TedProcessorProperties;
import at.procon.ted.model.dto.DocumentDtos.*;
import at.procon.ted.model.entity.*;
import at.procon.ted.repository.ProcurementDocumentRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.*;
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.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Service for searching procurement documents.
 * 
 * Supports:
 * - Structured search with filters (country, type, CPV codes, dates, etc.)
 * - Semantic search using vector similarity
 * - Combined structured + semantic search
 * 
 * @author Martin.Schweitzer@procon.co.at and claude.ai
 */
@Service
@ConditionalOnRuntimeMode(RuntimeMode.LEGACY)
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class SearchService {

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

    @PersistenceContext
    private EntityManager entityManager;

    /**
     * Search documents with combined structured and semantic filters.
     */
    public SearchResponse search(SearchRequest request) {
        // Normalize pagination parameters
        int page = request.getPage() != null ? request.getPage() : 0;
        int size = Math.min(
                request.getSize() != null ? request.getSize() : properties.getSearch().getDefaultPageSize(),
                properties.getSearch().getMaxPageSize()
        );

        // Check if semantic search is requested
        boolean hasSemanticQuery = request.getSemanticQuery() != null && !request.getSemanticQuery().isBlank();

        if (hasSemanticQuery && vectorizationService.isAvailable()) {
            return semanticSearch(request, page, size);
        } else {
            return structuredSearch(request, page, size);
        }
    }

    /**
     * Perform structured search using JPA Specifications.
     */
    private SearchResponse structuredSearch(SearchRequest request, int page, int size) {
        Specification<ProcurementDocument> spec = buildSpecification(request);

        Sort sort = buildSort(request.getSortBy(), request.getSortDirection());
        Pageable pageable = PageRequest.of(page, size, sort);

        Page<ProcurementDocument> result = documentRepository.findAll(spec, pageable);

        List<DocumentSummary> summaries = result.getContent().stream()
                .map(this::toSummary)
                .collect(Collectors.toList());

        return SearchResponse.builder()
                .documents(summaries)
                .page(page)
                .size(size)
                .totalElements(result.getTotalElements())
                .totalPages(result.getTotalPages())
                .hasNext(result.hasNext())
                .hasPrevious(result.hasPrevious())
                .build();
    }

    /**
     * Perform semantic search with vector similarity.
     */
    private SearchResponse semanticSearch(SearchRequest request, int page, int size) {
        try {
            // Generate query embedding
            float[] queryEmbedding = vectorizationService.generateQueryEmbedding(request.getSemanticQuery());
            String vectorStr = vectorizationService.floatArrayToVectorString(queryEmbedding);

            double threshold = request.getSimilarityThreshold() != null 
                    ? request.getSimilarityThreshold() 
                    : properties.getSearch().getSimilarityThreshold();

            // Execute native query for vector search
            List<Object[]> results = documentRepository.findBySemanticSearch(
                    vectorStr,
                    threshold,
                    request.getCountryCode(),
                    request.getNoticeType() != null ? request.getNoticeType().name() : null,
                    request.getContractNature() != null ? request.getContractNature().name() : null,
                    request.getCpvPrefix(),
                    request.getPublicationDateFrom(),
                    request.getPublicationDateTo(),
                    size,
                    page * size
            );

            // Count total for pagination
            long totalElements = documentRepository.countBySemanticSearch(
                    vectorStr,
                    threshold,
                    request.getCountryCode(),
                    request.getNoticeType() != null ? request.getNoticeType().name() : null,
                    request.getContractNature() != null ? request.getContractNature().name() : null
            );

            // Map results to summaries
            List<DocumentSummary> summaries = results.stream()
                    .map(this::mapSemanticResult)
                    .collect(Collectors.toList());

            int totalPages = (int) Math.ceil((double) totalElements / size);

            return SearchResponse.builder()
                    .documents(summaries)
                    .page(page)
                    .size(size)
                    .totalElements(totalElements)
                    .totalPages(totalPages)
                    .hasNext(page < totalPages - 1)
                    .hasPrevious(page > 0)
                    .build();

        } catch (Exception e) {
            log.error("Semantic search failed, falling back to structured search: {}", e.getMessage());
            // Fallback to structured search without semantic component
            return structuredSearch(request, page, size);
        }
    }

    /**
     * Build JPA Specification from search request.
     */
    private Specification<ProcurementDocument> buildSpecification(SearchRequest request) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            // Country filter
            if (request.getCountryCode() != null && !request.getCountryCode().isBlank()) {
                predicates.add(cb.equal(root.get("buyerCountryCode"), request.getCountryCode()));
            }

            // Multiple countries filter
            if (request.getCountryCodes() != null && !request.getCountryCodes().isEmpty()) {
                predicates.add(root.get("buyerCountryCode").in(request.getCountryCodes()));
            }

            // Notice type filter
            if (request.getNoticeType() != null) {
                predicates.add(cb.equal(root.get("noticeType"), request.getNoticeType()));
            }

            // Contract nature filter
            if (request.getContractNature() != null) {
                predicates.add(cb.equal(root.get("contractNature"), request.getContractNature()));
            }

            // Procedure type filter
            if (request.getProcedureType() != null) {
                predicates.add(cb.equal(root.get("procedureType"), request.getProcedureType()));
            }

            // Publication date range
            if (request.getPublicationDateFrom() != null) {
                predicates.add(cb.greaterThanOrEqualTo(root.get("publicationDate"), request.getPublicationDateFrom()));
            }
            if (request.getPublicationDateTo() != null) {
                predicates.add(cb.lessThanOrEqualTo(root.get("publicationDate"), request.getPublicationDateTo()));
            }

            // Submission deadline filter
            if (request.getSubmissionDeadlineAfter() != null) {
                predicates.add(cb.greaterThan(root.get("submissionDeadline"), request.getSubmissionDeadlineAfter()));
            }

            // EU funded filter
            if (request.getEuFunded() != null) {
                predicates.add(cb.equal(root.get("euFunded"), request.getEuFunded()));
            }

            // Text search on buyer name
            if (request.getBuyerNameContains() != null && !request.getBuyerNameContains().isBlank()) {
                predicates.add(cb.like(cb.lower(root.get("buyerName")), 
                        "%" + request.getBuyerNameContains().toLowerCase() + "%"));
            }

            // Text search on project title
            if (request.getProjectTitleContains() != null && !request.getProjectTitleContains().isBlank()) {
                predicates.add(cb.like(cb.lower(root.get("projectTitle")), 
                        "%" + request.getProjectTitleContains().toLowerCase() + "%"));
            }

            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    /**
     * Build Sort from request parameters.
     */
    private Sort buildSort(String sortBy, String sortDirection) {
        String field = sortBy != null ? sortBy : "publicationDate";
        Sort.Direction direction = "asc".equalsIgnoreCase(sortDirection) 
                ? Sort.Direction.ASC 
                : Sort.Direction.DESC;
        return Sort.by(direction, field);
    }

    /**
     * Convert entity to summary DTO.
     */
    private DocumentSummary toSummary(ProcurementDocument doc) {
        return DocumentSummary.builder()
                .id(doc.getId())
                .publicationId(doc.getPublicationId())
                .noticeId(doc.getNoticeId())
                .noticeType(doc.getNoticeType())
                .projectTitle(doc.getProjectTitle())
                .buyerName(doc.getBuyerName())
                .buyerCountryCode(doc.getBuyerCountryCode())
                .buyerCity(doc.getBuyerCity())
                .contractNature(doc.getContractNature())
                .procedureType(doc.getProcedureType())
                .publicationDate(doc.getPublicationDate())
                .submissionDeadline(doc.getSubmissionDeadline())
                .cpvCodes(doc.getCpvCodes() != null ? Arrays.asList(doc.getCpvCodes()) : List.of())
                .totalLots(doc.getTotalLots())
                .estimatedValue(doc.getEstimatedValue())
                .estimatedValueCurrency(doc.getEstimatedValueCurrency())
                .build();
    }

    /**
     * Map semantic search result array to summary DTO.
     */
    private DocumentSummary mapSemanticResult(Object[] row) {
        // Results from native query: id, publication_id, project_title, buyer_name, buyer_country_code, publication_date, similarity
        UUID id = (UUID) row[0];
        
        return documentRepository.findById(id)
                .map(doc -> {
                    DocumentSummary summary = toSummary(doc);
                    // Set similarity score from query result
                    if (row.length > 6 && row[6] != null) {
                        summary.setSimilarity(((Number) row[6]).doubleValue());
                    }
                    return summary;
                })
                .orElse(DocumentSummary.builder()
                        .id(id)
                        .publicationId(row[1] != null ? row[1].toString() : null)
                        .projectTitle(row[2] != null ? row[2].toString() : null)
                        .buyerName(row[3] != null ? row[3].toString() : null)
                        .buyerCountryCode(row[4] != null ? row[4].toString() : null)
                        .publicationDate(row[5] != null ? (LocalDate) row[5] : null)
                        .similarity(row.length > 6 && row[6] != null ? ((Number) row[6]).doubleValue() : null)
                        .build());
    }

    /**
     * Get document by ID with full details.
     */
    public Optional<DocumentDetail> getDocumentDetail(UUID id) {
        return documentRepository.findById(id).map(this::toDetail);
    }

    /**
     * Get document by publication ID.
     */
    public Optional<DocumentDetail> getDocumentByPublicationId(String publicationId) {
        return documentRepository.findByPublicationId(publicationId).map(this::toDetail);
    }

    /**
     * Convert entity to detail DTO.
     */
    private DocumentDetail toDetail(ProcurementDocument doc) {
        return DocumentDetail.builder()
                .id(doc.getId())
                .documentHash(doc.getDocumentHash())
                .publicationId(doc.getPublicationId())
                .noticeId(doc.getNoticeId())
                .ojsId(doc.getOjsId())
                .contractFolderId(doc.getContractFolderId())
                .noticeType(doc.getNoticeType())
                .noticeSubtypeCode(doc.getNoticeSubtypeCode())
                .sdkVersion(doc.getSdkVersion())
                .ublVersion(doc.getUblVersion())
                .languageCode(doc.getLanguageCode())
                .issueDateTime(doc.getIssueDateTime())
                .publicationDate(doc.getPublicationDate())
                .submissionDeadline(doc.getSubmissionDeadline())
                .buyerName(doc.getBuyerName())
                .buyerCountryCode(doc.getBuyerCountryCode())
                .buyerCity(doc.getBuyerCity())
                .buyerPostalCode(doc.getBuyerPostalCode())
                .buyerNutsCode(doc.getBuyerNutsCode())
                .buyerActivityType(doc.getBuyerActivityType())
                .buyerLegalType(doc.getBuyerLegalType())
                .projectTitle(doc.getProjectTitle())
                .projectDescription(doc.getProjectDescription())
                .internalReference(doc.getInternalReference())
                .contractNature(doc.getContractNature())
                .procedureType(doc.getProcedureType())
                .cpvCodes(doc.getCpvCodes() != null ? Arrays.asList(doc.getCpvCodes()) : List.of())
                .nutsCodes(doc.getNutsCodes() != null ? Arrays.asList(doc.getNutsCodes()) : List.of())
                .estimatedValue(doc.getEstimatedValue())
                .estimatedValueCurrency(doc.getEstimatedValueCurrency())
                .totalLots(doc.getTotalLots())
                .maxLotsAwarded(doc.getMaxLotsAwarded())
                .maxLotsSubmitted(doc.getMaxLotsSubmitted())
                .lots(doc.getLots().stream().map(this::toLotSummary).collect(Collectors.toList()))
                .organizations(doc.getOrganizations().stream().map(this::toOrgSummary).collect(Collectors.toList()))
                .regulatoryDomain(doc.getRegulatoryDomain())
                .euFunded(doc.getEuFunded())
                .vectorizationStatus(doc.getVectorizationStatus())
                .vectorizedAt(doc.getVectorizedAt())
                .sourceFilename(doc.getSourceFilename())
                .fileSizeBytes(doc.getFileSizeBytes())
                .createdAt(doc.getCreatedAt())
                .updatedAt(doc.getUpdatedAt())
                .build();
    }

    private LotSummary toLotSummary(ProcurementLot lot) {
        return LotSummary.builder()
                .id(lot.getId())
                .lotId(lot.getLotId())
                .internalId(lot.getInternalId())
                .title(lot.getTitle())
                .description(lot.getDescription())
                .cpvCodes(lot.getCpvCodes() != null ? Arrays.asList(lot.getCpvCodes()) : List.of())
                .nutsCodes(lot.getNutsCodes() != null ? Arrays.asList(lot.getNutsCodes()) : List.of())
                .estimatedValue(lot.getEstimatedValue())
                .estimatedValueCurrency(lot.getEstimatedValueCurrency())
                .durationValue(lot.getDurationValue())
                .durationUnit(lot.getDurationUnit())
                .submissionDeadline(lot.getSubmissionDeadline())
                .euFunded(lot.getEuFunded())
                .build();
    }

    private OrganizationSummary toOrgSummary(Organization org) {
        return OrganizationSummary.builder()
                .id(org.getId())
                .orgReference(org.getOrgReference())
                .role(org.getRole())
                .name(org.getName())
                .companyId(org.getCompanyId())
                .countryCode(org.getCountryCode())
                .city(org.getCity())
                .postalCode(org.getPostalCode())
                .nutsCode(org.getNutsCode())
                .websiteUri(org.getWebsiteUri())
                .email(org.getEmail())
                .phone(org.getPhone())
                .build();
    }

    /**
     * Get statistics about the document collection.
     */
    public StatisticsResponse getStatistics() {
        // Get vectorization stats
        List<Object[]> vectorStats = documentRepository.countByVectorizationStatus();
        Map<VectorizationStatus, Long> vectorCounts = vectorStats.stream()
                .collect(Collectors.toMap(
                        row -> (VectorizationStatus) row[0],
                        row -> (Long) row[1]
                ));

        // Get country stats
        List<Object[]> countryStats = documentRepository.countByCountry();
        List<CountryStats> countries = countryStats.stream()
                .map(row -> CountryStats.builder()
                        .countryCode((String) row[0])
                        .documentCount((Long) row[1])
                        .build())
                .collect(Collectors.toList());

        // Get notice type stats
        List<Object[]> noticeTypeStats = documentRepository.countByNoticeType();
        List<NoticeTypeStats> noticeTypes = noticeTypeStats.stream()
                .map(row -> NoticeTypeStats.builder()
                        .noticeType((NoticeType) row[0])
                        .documentCount((Long) row[1])
                        .build())
                .collect(Collectors.toList());

        long total = documentRepository.count();

        return StatisticsResponse.builder()
                .totalDocuments(total)
                .vectorizedDocuments(vectorCounts.getOrDefault(VectorizationStatus.COMPLETED, 0L))
                .pendingVectorization(vectorCounts.getOrDefault(VectorizationStatus.PENDING, 0L))
                .failedVectorization(vectorCounts.getOrDefault(VectorizationStatus.FAILED, 0L))
                .uniqueCountries(countries.size())
                .countryStatistics(countries)
                .noticeTypeStatistics(noticeTypes)
                .build();
    }

    /**
     * Get documents with upcoming deadlines.
     */
    public List<DocumentSummary> getUpcomingDeadlines(int limit) {
        Page<ProcurementDocument> page = documentRepository.findUpcomingDeadlines(
                PageRequest.of(0, limit));
        return page.getContent().stream()
                .map(this::toSummary)
                .collect(Collectors.toList());
    }

    /**
     * Get distinct countries in the database.
     */
    public List<String> getDistinctCountries() {
        return documentRepository.countByCountry().stream()
                .map(row -> (String) row[0])
                .filter(Objects::nonNull)
                .sorted()
                .collect(Collectors.toList());
    }

    /**
     * Get distinct CPV codes (main classification).
     */
    public List<String> getDistinctCpvCodes() {
        // This would require a native query to unnest the array
        // For now, return empty list - could be implemented with @Query
        return List.of();
    }
}
