package at.procon.dip.migration.audit.service;

import at.procon.dip.migration.audit.config.LegacyTedAuditProperties;
import at.procon.dip.migration.audit.entity.LegacyTedAuditFinding;
import at.procon.dip.migration.audit.entity.LegacyTedAuditFindingType;
import at.procon.dip.migration.audit.entity.LegacyTedAuditRun;
import at.procon.dip.migration.audit.entity.LegacyTedAuditRunStatus;
import at.procon.dip.migration.audit.entity.LegacyTedAuditSeverity;
import at.procon.dip.migration.audit.repository.LegacyTedAuditFindingRepository;
import at.procon.dip.migration.audit.repository.LegacyTedAuditRunRepository;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import at.procon.ted.model.entity.ProcurementDocument;
import at.procon.ted.model.entity.TedDailyPackage;
import at.procon.ted.repository.ProcurementDocumentRepository;
import at.procon.ted.repository.TedDailyPackageRepository;
import java.time.OffsetDateTime;
import java.time.Year;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
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.Sort;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@RequiredArgsConstructor
@Slf4j
public class LegacyTedAuditService {

    private final LegacyTedAuditProperties properties;
    private final TedDailyPackageRepository tedDailyPackageRepository;
    private final ProcurementDocumentRepository procurementDocumentRepository;
    private final LegacyTedAuditRunRepository runRepository;
    private final LegacyTedAuditFindingRepository findingRepository;
    private final JdbcTemplate jdbcTemplate;

    public LegacyTedAuditRun executeAudit() {
        return executeAudit(properties.getStartupRunLimit());
    }

    public LegacyTedAuditRun executeAudit(int requestedLimit) {
        if (!properties.isEnabled()) {
            throw new IllegalStateException("Legacy TED audit is disabled by configuration");
        }

        Integer effectiveLimit = requestedLimit > 0 ? requestedLimit : null;
        int pageSize = properties.getPageSize();
        AuditAccumulator accumulator = new AuditAccumulator();

        LegacyTedAuditRun run = LegacyTedAuditRun.builder()
                .status(LegacyTedAuditRunStatus.RUNNING)
                .requestedLimit(effectiveLimit)
                .pageSize(pageSize)
                .startedAt(OffsetDateTime.now())
                .build();
        run = runRepository.save(run);

        try {
            int scannedPackages = auditPackages(run, accumulator);
            auditGlobalDuplicates(run, accumulator);
            int scannedLegacyDocuments = 0;//auditLegacyDocuments(run, accumulator, effectiveLimit, pageSize);

            run.setStatus(LegacyTedAuditRunStatus.COMPLETED);
            run.setCompletedAt(OffsetDateTime.now());
            run.setScannedPackages(scannedPackages);
            run.setScannedLegacyDocuments(scannedLegacyDocuments);
            run.setFindingCount(accumulator.totalFindings());
            run.setInfoCount(accumulator.infoCount());
            run.setWarningCount(accumulator.warningCount());
            run.setErrorCount(accumulator.errorCount());
            run.setSummaryText(buildSummary(scannedPackages, scannedLegacyDocuments, accumulator));
            run.setFailureMessage(null);
            run = runRepository.save(run);

            log.info("Wave 1 / Milestone A legacy-only audit completed: runId={}, packages={}, documents={}, findings={}, warnings={}, errors={}",
                    run.getId(), scannedPackages, scannedLegacyDocuments, accumulator.totalFindings(),
                    accumulator.warningCount(), accumulator.errorCount());
            return run;
        } catch (RuntimeException ex) {
            run.setStatus(LegacyTedAuditRunStatus.FAILED);
            run.setCompletedAt(OffsetDateTime.now());
            run.setScannedPackages(accumulator.scannedPackages());
            run.setScannedLegacyDocuments(accumulator.scannedLegacyDocuments());
            run.setFindingCount(accumulator.totalFindings());
            run.setInfoCount(accumulator.infoCount());
            run.setWarningCount(accumulator.warningCount());
            run.setErrorCount(accumulator.errorCount());
            run.setFailureMessage(ex.getMessage());
            run.setSummaryText(buildSummary(accumulator.scannedPackages(), accumulator.scannedLegacyDocuments(), accumulator));
            runRepository.save(run);
            log.error("Wave 1 / Milestone A legacy-only audit failed: runId={}", run.getId(), ex);
            throw ex;
        }
    }

    private int auditPackages(LegacyTedAuditRun run, AuditAccumulator accumulator) {
        List<TedDailyPackage> packages = tedDailyPackageRepository.findAll(Sort.by(Sort.Direction.ASC, "year", "serialNumber"));
        if (packages.isEmpty()) {
            return 0;
        }

        Map<Integer, List<TedDailyPackage>> packagesByYear = new TreeMap<>();
        for (TedDailyPackage dailyPackage : packages) {
            packagesByYear.computeIfAbsent(dailyPackage.getYear(), ignored -> new ArrayList<>()).add(dailyPackage);
        }

        int firstYear = packagesByYear.keySet().iterator().next();
        int currentYear = Year.now().getValue();

        for (int year = firstYear; year <= currentYear; year++) {
            List<TedDailyPackage> yearPackages = packagesByYear.get(year);
            if (yearPackages == null || yearPackages.isEmpty()) {
                recordFinding(run, accumulator,
                        LegacyTedAuditSeverity.WARNING,
                        LegacyTedAuditFindingType.PACKAGE_SEQUENCE_GAP,
                        null,
                        null,
                        null,
                        null,
                        "year:" + year,
                        "No TED package rows exist for this year inside the audited interval",
                        "year=" + year + ", intervalStartYear=" + firstYear + ", intervalEndYear=" + currentYear);
                continue;
            }

            auditYearPackageSequence(run, accumulator, year, yearPackages);

            for (TedDailyPackage dailyPackage : yearPackages) {
                accumulator.incrementScannedPackages();
                auditSinglePackage(run, accumulator, dailyPackage);
            }
        }

        return packages.size();
    }

    private void auditYearPackageSequence(LegacyTedAuditRun run,
                                          AuditAccumulator accumulator,
                                          int year,
                                          List<TedDailyPackage> yearPackages) {
        yearPackages.sort((left, right) -> Integer.compare(safeInt(left.getSerialNumber()), safeInt(right.getSerialNumber())));

        int firstSerial = safeInt(yearPackages.getFirst().getSerialNumber());
        if (firstSerial > 1) {
            recordMissingPackageRange(run, accumulator, year, 1, firstSerial - 1,
                    "TED package year starts after serial 1");
        }

        for (int i = 1; i < yearPackages.size(); i++) {
            int previousSerial = safeInt(yearPackages.get(i - 1).getSerialNumber());
            int currentSerial = safeInt(yearPackages.get(i).getSerialNumber());
            if (currentSerial > previousSerial + 1) {
                recordMissingPackageRange(run, accumulator, year, previousSerial + 1, currentSerial - 1,
                        "TED package sequence gap detected");
            }
        }
    }

    private void recordMissingPackageRange(LegacyTedAuditRun run,
                                           AuditAccumulator accumulator,
                                           int year,
                                           int startSerial,
                                           int endSerial,
                                           String message) {
        String startPackageId = formatPackageIdentifier(year, startSerial);
        String endPackageId = formatPackageIdentifier(year, endSerial);
        String referenceKey = startSerial == endSerial ? startPackageId : startPackageId + "-" + endPackageId;

        recordFinding(run, accumulator,
                LegacyTedAuditSeverity.WARNING,
                LegacyTedAuditFindingType.PACKAGE_SEQUENCE_GAP,
                startSerial == endSerial ? startPackageId : null,
                null,
                null,
                null,
                referenceKey,
                message,
                "year=" + year + ", missingStartSerial=" + startSerial + ", missingEndSerial=" + endSerial);
    }

    private void auditSinglePackage(LegacyTedAuditRun run,
                                    AuditAccumulator accumulator,
                                    TedDailyPackage dailyPackage) {
        String packageIdentifier = dailyPackage.getPackageIdentifier();
        int processedCount = safeInt(dailyPackage.getProcessedCount());
        int failedCount = safeInt(dailyPackage.getFailedCount());
        int accountedDocuments = processedCount + failedCount;

        if (dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.COMPLETED
                && dailyPackage.getProcessedAt() == null) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.PACKAGE_COMPLETED_WITHOUT_PROCESSED_AT,
                    packageIdentifier,
                    null,
                    null,
                    null,
                    packageIdentifier,
                    "TED package is marked COMPLETED but processedAt is null",
                    null);
        }

        if (dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.COMPLETED
                && dailyPackage.getXmlFileCount() == null) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.PACKAGE_MISSING_XML_FILE_COUNT,
                    packageIdentifier,
                    null,
                    null,
                    null,
                    packageIdentifier,
                    "TED package is marked COMPLETED but xmlFileCount is null",
                    null);
        }

        if ((dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.DOWNLOADED
                || dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.PROCESSING
                || dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.COMPLETED)
                && !StringUtils.hasText(dailyPackage.getFileHash())) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.PACKAGE_MISSING_FILE_HASH,
                    packageIdentifier,
                    null,
                    null,
                    null,
                    packageIdentifier,
                    "TED package has no file hash recorded",
                    "downloadStatus=" + dailyPackage.getDownloadStatus());
        }

        if (dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.FAILED
                && !StringUtils.hasText(dailyPackage.getErrorMessage())) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.PACKAGE_FAILED_WITHOUT_ERROR_MESSAGE,
                    packageIdentifier,
                    null,
                    null,
                    null,
                    packageIdentifier,
                    "TED package is marked FAILED but has no error message",
                    null);
        }

        if (dailyPackage.getXmlFileCount() != null) {
            if (accountedDocuments > dailyPackage.getXmlFileCount()) {
                recordFinding(run, accumulator,
                        LegacyTedAuditSeverity.ERROR,
                        LegacyTedAuditFindingType.PACKAGE_COMPLETED_COUNT_MISMATCH,
                        packageIdentifier,
                        null,
                        null,
                        null,
                        packageIdentifier,
                        "TED package accounting exceeds xmlFileCount",
                        "xmlFileCount=" + dailyPackage.getXmlFileCount()
                                + ", processedCount=" + processedCount
                                + ", failedCount=" + failedCount);
            } else if (dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.COMPLETED
                    && accountedDocuments < dailyPackage.getXmlFileCount()) {
                recordFinding(run, accumulator,
                        LegacyTedAuditSeverity.WARNING,
                        LegacyTedAuditFindingType.PACKAGE_COMPLETED_COUNT_MISMATCH,
                        packageIdentifier,
                        null,
                        null,
                        null,
                        packageIdentifier,
                        "TED package accounting is below xmlFileCount",
                        "xmlFileCount=" + dailyPackage.getXmlFileCount()
                                + ", processedCount=" + processedCount
                                + ", failedCount=" + failedCount);
            }
        }

        if (isPackageIncompleteForReimport(dailyPackage, processedCount, failedCount, accountedDocuments)) {
            recordFinding(run, accumulator,
                    dailyPackage.getDownloadStatus() == TedDailyPackage.DownloadStatus.FAILED
                            ? LegacyTedAuditSeverity.ERROR
                            : LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.PACKAGE_INCOMPLETE,
                    packageIdentifier,
                    null,
                    null,
                    null,
                    packageIdentifier,
                    "TED package is not fully imported and should be considered for re-import",
                    buildIncompletePackageDetails(dailyPackage, processedCount, failedCount, accountedDocuments));
        }
    }

    private boolean isPackageIncompleteForReimport(TedDailyPackage dailyPackage,
                                                   int processedCount,
                                                   int failedCount,
                                                   int accountedDocuments) {
        TedDailyPackage.DownloadStatus status = dailyPackage.getDownloadStatus();
        if (status == null) {
            return true;
        }
        if (status == TedDailyPackage.DownloadStatus.NOT_FOUND) {
            return false;
        }
        if (status == TedDailyPackage.DownloadStatus.PENDING
                || status == TedDailyPackage.DownloadStatus.DOWNLOADING
                || status == TedDailyPackage.DownloadStatus.DOWNLOADED
                || status == TedDailyPackage.DownloadStatus.PROCESSING
                || status == TedDailyPackage.DownloadStatus.FAILED) {
            return true;
        }
        if (status != TedDailyPackage.DownloadStatus.COMPLETED) {
            return true;
        }
        if (dailyPackage.getXmlFileCount() == null) {
            return true;
        }
        if (failedCount > 0) {
            return true;
        }
        return processedCount < dailyPackage.getXmlFileCount()
                || accountedDocuments != dailyPackage.getXmlFileCount();
    }

    private String buildIncompletePackageDetails(TedDailyPackage dailyPackage,
                                                 int processedCount,
                                                 int failedCount,
                                                 int accountedDocuments) {
        return "status=" + dailyPackage.getDownloadStatus()
                + ", xmlFileCount=" + dailyPackage.getXmlFileCount()
                + ", processedCount=" + processedCount
                + ", failedCount=" + failedCount
                + ", accountedDocuments=" + accountedDocuments;
    }

    private void auditGlobalDuplicates(LegacyTedAuditRun run, AuditAccumulator accumulator) {
        int limit = properties.getMaxDuplicateSamples();

        jdbcTemplate.query(
                """
                SELECT publication_id, COUNT(*) AS duplicate_count
                FROM ted.procurement_document
                WHERE publication_id IS NOT NULL AND publication_id <> ''
                GROUP BY publication_id
                HAVING COUNT(*) > 1
                ORDER BY duplicate_count DESC, publication_id ASC
                LIMIT ?
                """,
                ps -> ps.setInt(1, limit),
                (rs, rowNum) -> {
                    String publicationId = rs.getString("publication_id");
                    long duplicateCount = rs.getLong("duplicate_count");
                    recordFinding(run, accumulator,
                            LegacyTedAuditSeverity.ERROR,
                            LegacyTedAuditFindingType.LEGACY_PUBLICATION_ID_DUPLICATE,
                            null,
                            null,
                            null,
                            null,
                            publicationId,
                            "Legacy TED publicationId appears multiple times",
                            "publicationId=" + publicationId + ", duplicateCount=" + duplicateCount);
                    return null;
                });
    }

    private int auditLegacyDocuments(LegacyTedAuditRun run,
                                     AuditAccumulator accumulator,
                                     Integer requestedLimit,
                                     int pageSize) {
        int processed = 0;
        int pageNumber = 0;

        while (requestedLimit == null || processed < requestedLimit) {
            Page<ProcurementDocument> page = procurementDocumentRepository.findAll(
                    PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.ASC, "createdAt", "id")));

            if (page.isEmpty()) {
                break;
            }

            for (ProcurementDocument legacyDocument : page.getContent()) {
                auditSingleLegacyDocument(run, accumulator, legacyDocument);
                accumulator.incrementScannedLegacyDocuments();
                processed++;
                if (requestedLimit != null && processed >= requestedLimit) {
                    return processed;
                }
            }

            if (!page.hasNext()) {
                break;
            }
            pageNumber++;
        }

        return processed;
    }

    private void auditSingleLegacyDocument(LegacyTedAuditRun run,
                                           AuditAccumulator accumulator,
                                           ProcurementDocument legacyDocument) {
        UUID legacyDocumentId = legacyDocument.getId();
        String referenceKey = buildReferenceKey(legacyDocument);
        String documentHash = legacyDocument.getDocumentHash();

        if (!StringUtils.hasText(documentHash)) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.ERROR,
                    LegacyTedAuditFindingType.LEGACY_DOCUMENT_MISSING_HASH,
                    null,
                    legacyDocumentId,
                    null,
                    null,
                    referenceKey,
                    "Legacy TED document has no documentHash",
                    null);
            return;
        }

        if (!StringUtils.hasText(legacyDocument.getXmlDocument())) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.ERROR,
                    LegacyTedAuditFindingType.LEGACY_DOCUMENT_MISSING_XML,
                    null,
                    legacyDocumentId,
                    null,
                    null,
                    referenceKey,
                    "Legacy TED document has no xmlDocument payload",
                    "documentHash=" + documentHash);
        }

        if (!StringUtils.hasText(legacyDocument.getTextContent())) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.LEGACY_DOCUMENT_MISSING_TEXT,
                    null,
                    legacyDocumentId,
                    null,
                    null,
                    referenceKey,
                    "Legacy TED document has no normalized textContent",
                    "documentHash=" + documentHash);
        }

        if (!StringUtils.hasText(legacyDocument.getPublicationId())) {
            recordFinding(run, accumulator,
                    LegacyTedAuditSeverity.WARNING,
                    LegacyTedAuditFindingType.LEGACY_DOCUMENT_MISSING_PUBLICATION_ID,
                    null,
                    legacyDocumentId,
                    null,
                    null,
                    referenceKey,
                    "Legacy TED document has no publicationId",
                    "documentHash=" + documentHash);
        }
    }

    private void recordFinding(LegacyTedAuditRun run,
                               AuditAccumulator accumulator,
                               LegacyTedAuditSeverity severity,
                               LegacyTedAuditFindingType findingType,
                               String packageIdentifier,
                               UUID legacyProcurementDocumentId,
                               UUID genericDocumentId,
                               UUID tedProjectionId,
                               String referenceKey,
                               String message,
                               String detailsText) {
        if (accumulator.totalFindings() >= properties.getMaxFindingsPerRun()) {
            accumulator.markTruncated();
            if (!accumulator.truncationRecorded()) {
                LegacyTedAuditFinding truncatedFinding = LegacyTedAuditFinding.builder()
                        .run(run)
                        .severity(LegacyTedAuditSeverity.INFO)
                        .findingType(LegacyTedAuditFindingType.FINDINGS_TRUNCATED)
                        .referenceKey(referenceKey != null ? referenceKey : "max-findings-per-run")
                        .message("Legacy TED audit finding limit reached; additional findings were suppressed")
                        .detailsText("maxFindingsPerRun=" + properties.getMaxFindingsPerRun())
                        .build();
                findingRepository.save(truncatedFinding);
                accumulator.recordFinding(LegacyTedAuditSeverity.INFO, true);
            }
            return;
        }

        LegacyTedAuditFinding finding = LegacyTedAuditFinding.builder()
                .run(run)
                .severity(severity)
                .findingType(findingType)
                .packageIdentifier(packageIdentifier)
                .legacyProcurementDocumentId(legacyProcurementDocumentId)
                .documentId(genericDocumentId)
                .tedNoticeProjectionId(tedProjectionId)
                .referenceKey(referenceKey)
                .message(message)
                .detailsText(detailsText)
                .build();
        findingRepository.save(finding);
        accumulator.recordFinding(severity, false);
    }

    private String buildReferenceKey(ProcurementDocument legacyDocument) {
        if (StringUtils.hasText(legacyDocument.getPublicationId())) {
            return legacyDocument.getPublicationId();
        }
        if (StringUtils.hasText(legacyDocument.getNoticeId())) {
            return legacyDocument.getNoticeId();
        }
        if (StringUtils.hasText(legacyDocument.getSourceFilename())) {
            return legacyDocument.getSourceFilename();
        }
        return String.valueOf(legacyDocument.getId());
    }

    private int safeInt(Integer value) {
        return value != null ? value : 0;
    }

    private String formatPackageIdentifier(int year, int serialNumber) {
        return "%04d%05d".formatted(year, serialNumber);
    }

    private String buildSummary(int scannedPackages,
                                int scannedLegacyDocuments,
                                AuditAccumulator accumulator) {
        return "packages=" + scannedPackages
                + ", legacyDocuments=" + scannedLegacyDocuments
                + ", findings=" + accumulator.totalFindings()
                + ", warnings=" + accumulator.warningCount()
                + ", errors=" + accumulator.errorCount()
                + (accumulator.truncated() ? ", truncated=true" : "");
    }

    private static final class AuditAccumulator {
        private int scannedPackages;
        private int scannedLegacyDocuments;
        private int infoCount;
        private int warningCount;
        private int errorCount;
        private boolean truncated;
        private boolean truncationRecorded;

        void incrementScannedPackages() {
            scannedPackages++;
        }

        void incrementScannedLegacyDocuments() {
            scannedLegacyDocuments++;
        }

        void recordFinding(LegacyTedAuditSeverity severity, boolean truncationFindingRecordedNow) {
            switch (severity) {
                case INFO -> infoCount++;
                case WARNING -> warningCount++;
                case ERROR -> errorCount++;
            }
            if (truncationFindingRecordedNow) {
                truncationRecorded = true;
            }
        }

        void markTruncated() {
            truncated = true;
        }

        int totalFindings() {
            return infoCount + warningCount + errorCount;
        }

        int infoCount() {
            return infoCount;
        }

        int warningCount() {
            return warningCount;
        }

        int errorCount() {
            return errorCount;
        }

        int scannedPackages() {
            return scannedPackages;
        }

        int scannedLegacyDocuments() {
            return scannedLegacyDocuments;
        }

        boolean truncated() {
            return truncated;
        }

        boolean truncationRecorded() {
            return truncationRecorded;
        }
    }
}
