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

import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.document.repository.DocumentRepository;
import at.procon.dip.domain.ted.entity.TedNoticeLot;
import at.procon.dip.domain.ted.entity.TedNoticeOrganization;
import at.procon.dip.domain.ted.entity.TedNoticeProjection;
import at.procon.dip.domain.ted.repository.TedNoticeLotRepository;
import at.procon.dip.domain.ted.repository.TedNoticeOrganizationRepository;
import at.procon.dip.domain.ted.repository.TedNoticeProjectionRepository;
import at.procon.dip.domain.ted.config.TedProjectionProperties;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import at.procon.ted.model.entity.Organization;
import at.procon.ted.model.entity.ProcurementDocument;
import at.procon.ted.model.entity.ProcurementLot;

import java.util.*;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

/**
 * Phase 3 service that materializes TED-specific structured projections on top of the generic DOC document root.
 */
@Service
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@RequiredArgsConstructor
@Slf4j
public class TedNoticeProjectionService {

    private final TedProjectionProperties properties;
    private final TedPackageIdentifierResolver packageIdentifierResolver;
    private final TedGenericDocumentRootService tedGenericDocumentRootService;
    private final DocumentRepository documentRepository;
    private final TedNoticeProjectionRepository projectionRepository;
    private final TedNoticeLotRepository lotRepository;
    private final TedNoticeOrganizationRepository organizationRepository;
    private final TedLotDocumentMaterializationService lotDocumentMaterializationService;

    @Transactional
    public UUID registerOrRefreshProjection(ProcurementDocument legacyDocument) {
        if (!properties.isEnabled()) {
            return null;
        }

        UUID genericDocumentId = tedGenericDocumentRootService.ensureGenericTedDocumentRoot(legacyDocument);
        return registerOrRefreshProjection(legacyDocument, genericDocumentId);
    }

    @Transactional
    public UUID registerOrRefreshProjection(ProcurementDocument legacyDocument, UUID genericDocumentId) {
        if (!properties.isEnabled()) {
            return null;
        }

        UUID resolvedDocumentId = genericDocumentId;
        if (resolvedDocumentId == null) {
            resolvedDocumentId = tedGenericDocumentRootService.ensureGenericTedDocumentRoot(legacyDocument);
        }

        UUID finalResolvedDocumentId = resolvedDocumentId;
        Document genericDocument = documentRepository.findById(resolvedDocumentId)
                .orElseThrow(() -> new IllegalArgumentException("Unknown DOC document id: " + finalResolvedDocumentId));

        TedNoticeProjection projection = projectionRepository.findByDocument_Id(genericDocument.getId())
                .orElseGet(TedNoticeProjection::new);

        mapProjection(projection, genericDocument, legacyDocument);
        projection = projectionRepository.save(projection);
        replaceLots(projection, legacyDocument.getLots());
        replaceOrganizations(projection, legacyDocument.getOrganizations());

        log.debug("Phase 3 TED projection ensured for generic doc {} -> projection {} (noticeId={}, publicationId={})",
                genericDocument.getId(), projection.getId(), legacyDocument.getNoticeId(), legacyDocument.getPublicationId());
        return projection.getId();
    }

    private void mapProjection(TedNoticeProjection projection, Document genericDocument, ProcurementDocument legacyDocument) {
        projection.setDocument(genericDocument);
        projection.setLegacyProcurementDocumentId(legacyDocument.getId());
        projection.setPackageIdentifier(packageIdentifierResolver
                .resolveFromSourceMetadata(legacyDocument.getSourcePath(), legacyDocument.getSourceFilename())
                .orElse(null));
        projection.setNoticeId(legacyDocument.getNoticeId());
        projection.setPublicationId(legacyDocument.getPublicationId());
        projection.setNoticeUrl(legacyDocument.getNoticeUrl());
        projection.setOjsId(legacyDocument.getOjsId());
        projection.setContractFolderId(legacyDocument.getContractFolderId());
        projection.setNoticeType(legacyDocument.getNoticeType());
        projection.setNoticeSubtypeCode(legacyDocument.getNoticeSubtypeCode());
        projection.setSdkVersion(legacyDocument.getSdkVersion());
        projection.setUblVersion(legacyDocument.getUblVersion());
        projection.setLanguageCode(legacyDocument.getLanguageCode());
        projection.setIssueDateTime(legacyDocument.getIssueDateTime());
        projection.setPublicationDate(legacyDocument.getPublicationDate());
        projection.setSubmissionDeadline(legacyDocument.getSubmissionDeadline());
        projection.setBuyerName(legacyDocument.getBuyerName());
        projection.setBuyerCountryCode(legacyDocument.getBuyerCountryCode());
        projection.setBuyerCity(legacyDocument.getBuyerCity());
        projection.setBuyerPostalCode(legacyDocument.getBuyerPostalCode());
        projection.setBuyerNutsCode(legacyDocument.getBuyerNutsCode());
        projection.setBuyerActivityType(legacyDocument.getBuyerActivityType());
        projection.setBuyerLegalType(legacyDocument.getBuyerLegalType());
        projection.setProjectTitle(legacyDocument.getProjectTitle());
        projection.setProjectDescription(legacyDocument.getProjectDescription());
        projection.setInternalReference(legacyDocument.getInternalReference());
        projection.setContractNature(legacyDocument.getContractNature());
        projection.setProcedureType(legacyDocument.getProcedureType());
        projection.setCpvCodes(copyArray(legacyDocument.getCpvCodes()));
        projection.setNutsCodes(copyArray(legacyDocument.getNutsCodes()));
        projection.setEstimatedValue(legacyDocument.getEstimatedValue());
        projection.setEstimatedValueCurrency(legacyDocument.getEstimatedValueCurrency());
        projection.setTotalLots(legacyDocument.getTotalLots());
        projection.setMaxLotsAwarded(legacyDocument.getMaxLotsAwarded());
        projection.setMaxLotsSubmitted(legacyDocument.getMaxLotsSubmitted());
        projection.setRegulatoryDomain(legacyDocument.getRegulatoryDomain());
        projection.setEuFunded(legacyDocument.getEuFunded());
    }

    private void replaceLots(TedNoticeProjection projection, List<ProcurementLot> legacyLots) {
        lotRepository.deleteByNoticeProjection_Id(projection.getId());
        lotRepository.flush();

        if (legacyLots == null || legacyLots.isEmpty()) {
            return;
        }

        Map<String, ProcurementLot> byLotId = new LinkedHashMap<>();
        int duplicateCount = 0;
        for (ProcurementLot lot : legacyLots) {
            String key = StringUtils.hasText(lot.getLotId()) ? lot.getLotId().trim() : lot.getInternalId();
            if (!StringUtils.hasText(key)) {
                key = "__row__" + byLotId.size();
            }
            ProcurementLot existing = byLotId.get(key);
            if (existing == null) {
                byLotId.put(key, lot);
            } else {
                duplicateCount++;
                byLotId.put(key, mergeLot(existing, lot));
            }
        }

        if (duplicateCount > 0) {
            log.warn("Collapsing {} duplicate TED lot rows for projection {} before insert", duplicateCount, projection.getId());
        }

        List<TedNoticeLot> projectedLots = new ArrayList<>();
        for (ProcurementLot lot : byLotId.values()) {
            projectedLots.add(TedNoticeLot.builder()
                    .noticeProjection(projection)
                    .lotId(lot.getLotId())
                    .internalId(lot.getInternalId())
                    .title(lot.getTitle())
                    .description(lot.getDescription())
                    .cpvCodes(copyArray(lot.getCpvCodes()))
                    .nutsCodes(copyArray(lot.getNutsCodes()))
                    .estimatedValue(lot.getEstimatedValue())
                    .estimatedValueCurrency(lot.getEstimatedValueCurrency())
                    .durationValue(lot.getDurationValue())
                    .durationUnit(lot.getDurationUnit())
                    .submissionDeadline(lot.getSubmissionDeadline())
                    .euFunded(lot.getEuFunded())
                    .build());
        }
        List<TedNoticeLot> savedLots = lotRepository.saveAll(projectedLots);
        lotDocumentMaterializationService.materializeLots(projection, savedLots);
    }

    private void replaceOrganizations(TedNoticeProjection projection, List<Organization> legacyOrganizations) {
        organizationRepository.deleteByNoticeProjection_Id(projection.getId());
        organizationRepository.flush();
        if (legacyOrganizations == null || legacyOrganizations.isEmpty()) {
            return;
        }

        Map<String, Organization> byReference = new LinkedHashMap<>();
        int duplicateCount = 0;
        for (Organization organization : legacyOrganizations) {
            String key = organizationKey(organization, byReference.size());
            Organization existing = byReference.get(key);
            if (existing == null) {
                byReference.put(key, organization);
            } else {
                duplicateCount++;
                byReference.put(key, mergeOrganization(existing, organization));
            }
        }

        if (duplicateCount > 0) {
            log.warn("Collapsing {} duplicate TED organization rows for projection {} before insert", duplicateCount, projection.getId());
        }

        List<TedNoticeOrganization> projectedOrganizations = new ArrayList<>();
        for (Organization organization : byReference.values()) {
            projectedOrganizations.add(TedNoticeOrganization.builder()
                    .noticeProjection(projection)
                    .orgReference(organization.getOrgReference())
                    .role(organization.getRole())
                    .name(organization.getName())
                    .companyId(organization.getCompanyId())
                    .countryCode(organization.getCountryCode())
                    .city(organization.getCity())
                    .postalCode(organization.getPostalCode())
                    .streetName(organization.getStreetName())
                    .nutsCode(organization.getNutsCode())
                    .websiteUri(organization.getWebsiteUri())
                    .email(organization.getEmail())
                    .phone(organization.getPhone())
                    .build());
        }
        organizationRepository.saveAll(projectedOrganizations);
    }

   private ProcurementLot mergeLot(ProcurementLot left, ProcurementLot right) {
        if (completenessScore(right) > completenessScore(left)) {
            return right;
        }
        return left;
    }

    private Organization mergeOrganization(Organization left, Organization right) {
        if (completenessScore(right) > completenessScore(left)) {
            return right;
        }
        return left;
    }

    private int completenessScore(ProcurementLot lot) {
        int score = 0;
        score += textScore(lot.getLotId());
        score += textScore(lot.getInternalId());
        score += textScore(lot.getTitle());
        score += textScore(lot.getDescription());
        score += arrayScore(lot.getCpvCodes());
        score += arrayScore(lot.getNutsCodes());
        score += lot.getEstimatedValue() != null ? 1 : 0;
        score += textScore(lot.getEstimatedValueCurrency());
        score += lot.getDurationValue() != null ? 1 : 0;
        score += textScore(lot.getDurationUnit());
        score += lot.getSubmissionDeadline() != null ? 1 : 0;
        score += lot.getEuFunded() != null ? 1 : 0;
        return score;
    }

    private int completenessScore(Organization organization) {
        int score = 0;
        score += textScore(organization.getOrgReference());
        score += textScore(organization.getRole());
        score += textScore(organization.getName());
        score += textScore(organization.getCompanyId());
        score += textScore(organization.getCountryCode());
        score += textScore(organization.getCity());
        score += textScore(organization.getPostalCode());
        score += textScore(organization.getStreetName());
        score += textScore(organization.getNutsCode());
        score += textScore(organization.getWebsiteUri());
        score += textScore(organization.getEmail());
        score += textScore(organization.getPhone());
        return score;
    }

    private int textScore(String value) {
        return StringUtils.hasText(value) ? 1 : 0;
    }

    private int arrayScore(String[] values) {
        return values != null && values.length > 0 ? 1 : 0;
    }

    private String organizationKey(Organization organization, int ordinal) {
        if (StringUtils.hasText(organization.getOrgReference())) {
            return organization.getOrgReference().trim();
        }
        if (StringUtils.hasText(organization.getCompanyId())) {
            return "company:" + organization.getCompanyId().trim();
        }
        if (StringUtils.hasText(organization.getName())) {
            return "name:" + organization.getName().trim();
        }
        return "__row__" + ordinal;
    }

    private String[] copyArray(String[] source) {
        return source == null ? null : source.clone();
    }
}
