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

import at.procon.dip.domain.document.entity.Document;
import at.procon.dip.domain.time.config.TimeDomainProperties;
import at.procon.dip.domain.time.entity.TimeEntry;
import at.procon.dip.domain.time.entity.TimeEntrySearchProjection;
import at.procon.dip.domain.time.entity.TimeSourceSystem;
import at.procon.dip.domain.time.entity.leitstand.*;
import at.procon.dip.domain.time.repository.TimeEntryRepository;
import at.procon.dip.domain.time.repository.TimeEntrySearchProjectionRepository;
import at.procon.dip.domain.time.repository.leitstand.*;
import at.procon.dip.runtime.condition.ConditionalOnRuntimeMode;
import at.procon.dip.runtime.config.RuntimeMode;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@ConditionalOnRuntimeMode(RuntimeMode.NEW)
@ConditionalOnProperty(prefix = "dip.time.leitstand", name = "enabled", havingValue = "true")
@RequiredArgsConstructor
@Slf4j
public class LeitstandTimeProjectionService {

    private final TimeDomainProperties properties;
    private final LeitstandTimeRecordingRepository timeRecordingRepository;
    private final LeitstandTimeRecordingAssignmentRepository timeRecordingAssignmentRepository;
    private final LeitstandPersonTaskAssignmentRepository personTaskAssignmentRepository;
    private final LeitstandTaskRepository taskRepository;
    private final LeitstandCostUnitRepository costUnitRepository;
    private final LeitstandContractRepository contractRepository;
    private final LeitstandContractPositionRepository contractPositionRepository;
    private final LeitstandOrganizationRepository organizationRepository;
    private final LeitstandPersonRepository personRepository;
    private final LeitstandActivityTypeRepository activityTypeRepository;
    private final TimeEntryRepository timeEntryRepository;
    private final TimeEntrySearchProjectionRepository projectionRepository;
    private final TimeEntryRepresentationMaterializationService representationMaterializationService;

    @Transactional
    public void refreshForLeitstandRecordingDbks(Collection<String> recordingDbks) {
        if (recordingDbks == null || recordingDbks.isEmpty()) {
            return;
        }
        List<LeitstandTimeRecording> recordings = timeRecordingRepository.findAllById(recordingDbks).stream()
                .filter(recording -> recording.getTimeEntry() != null)
                .toList();
        if (recordings.isEmpty()) {
            return;
        }
        upsertProjections(recordings);
    }


    @Transactional
    public int refreshForPersonDbk(String personDbk) {
        if (personDbk == null || personDbk.isBlank()) {
            return 0;
        }
        List<LeitstandTimeRecording> recordings = timeRecordingRepository
                .findByPersonDbkAndTimeEntryIsNotNullOrderByRecordedFromAscDbkAsc(personDbk);
        upsertProjections(recordings);
        return recordings.size();
    }

    @Transactional
    public int refreshAll() {
        List<LeitstandTimeRecording> recordings = timeRecordingRepository.findByTimeEntryIsNotNull();
        upsertProjections(recordings);
        return recordings.size();
    }

    private void upsertProjections(List<LeitstandTimeRecording> recordings) {
        for (LeitstandTimeRecording recording : recordings) {
            TimeEntrySearchProjection projection = buildProjection(recording);
            TimeEntrySearchProjection saved = projectionRepository.save(projection);
            if (properties.getLeitstand().isBuildRepresentations()) {
                representationMaterializationService.upsertRepresentations(saved);
            }
        }
    }

    private TimeEntrySearchProjection buildProjection(LeitstandTimeRecording recording) {
        TimeEntry timeEntry = timeEntryRepository.findById(recording.getTimeEntry().getId())
                .orElseThrow(() -> new IllegalArgumentException("Unknown TIME entry id: " + recording.getTimeEntry().getId()));
        Document document = timeEntry.getDocument();

        LeitstandPerson person = recording.getPersonDbk() == null ? null : personRepository.findById(recording.getPersonDbk()).orElse(null);
        LeitstandActivityType activityType = recording.getActivityTypeId() == null ? null : activityTypeRepository.findById(recording.getActivityTypeId()).orElse(null);

        List<LeitstandTimeRecordingAssignment> assignments = timeRecordingAssignmentRepository.findByTimeRecordingDbkOrderByDbkAsc(recording.getDbk());
        List<LeitstandPersonTaskAssignment> personTaskAssignments = personTaskAssignmentRepository.findAllById(assignments.stream()
                .map(LeitstandTimeRecordingAssignment::getPersonTaskAssignmentDbk)
                .filter(Objects::nonNull)
                .distinct()
                .toList());
        Map<String, LeitstandPersonTaskAssignment> ptaByDbk = indexBy(personTaskAssignments, LeitstandPersonTaskAssignment::getDbk);

        Map<String, LeitstandTask> tasksByDbk = indexBy(taskRepository.findAllById(personTaskAssignments.stream()
                .map(LeitstandPersonTaskAssignment::getTaskDbk)
                .filter(Objects::nonNull)
                .distinct()
                .toList()), LeitstandTask::getDbk);

        Map<String, LeitstandCostUnit> costUnitsByDbk = indexBy(costUnitRepository.findAllById(personTaskAssignments.stream()
                .map(LeitstandPersonTaskAssignment::getCostUnitDbk)
                .filter(Objects::nonNull)
                .distinct()
                .toList()), LeitstandCostUnit::getDbk);

        Map<String, LeitstandContract> contractsByDbk = indexBy(contractRepository.findAllById(costUnitsByDbk.values().stream()
                .map(LeitstandCostUnit::getContractDbk)
                .filter(Objects::nonNull)
                .distinct()
                .toList()), LeitstandContract::getDbk);

        Map<String, LeitstandContractPosition> contractPositionsByDbk = indexBy(contractPositionRepository.findAllById(costUnitsByDbk.values().stream()
                .map(LeitstandCostUnit::getContractPositionDbk)
                .filter(Objects::nonNull)
                .distinct()
                .toList()), LeitstandContractPosition::getDbk);

        Set<String> organizationDbks = new LinkedHashSet<>();
        costUnitsByDbk.values().stream().map(LeitstandCostUnit::getOrganizationDbk).filter(Objects::nonNull).forEach(organizationDbks::add);
        contractsByDbk.values().stream().map(LeitstandContract::getOrganizationDbk).filter(Objects::nonNull).forEach(organizationDbks::add);
        if (person != null && person.getOrganizationDbk() != null) {
            organizationDbks.add(person.getOrganizationDbk());
        }
        Map<String, LeitstandOrganization> organizationsByDbk = indexBy(organizationRepository.findAllById(organizationDbks), LeitstandOrganization::getDbk);

        List<LeitstandTask> orderedTasks = assignments.stream()
                .map(a -> ptaByDbk.get(a.getPersonTaskAssignmentDbk()))
                .filter(Objects::nonNull)
                .map(pta -> tasksByDbk.get(pta.getTaskDbk()))
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        List<LeitstandCostUnit> orderedCostUnits = assignments.stream()
                .map(a -> ptaByDbk.get(a.getPersonTaskAssignmentDbk()))
                .filter(Objects::nonNull)
                .map(pta -> costUnitsByDbk.get(pta.getCostUnitDbk()))
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        List<LeitstandContract> orderedContracts = orderedCostUnits.stream()
                .map(cu -> contractsByDbk.get(cu.getContractDbk()))
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        List<LeitstandContractPosition> orderedContractPositions = orderedCostUnits.stream()
                .map(cu -> contractPositionsByDbk.get(cu.getContractPositionDbk()))
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        List<LeitstandOrganization> orderedOrganizations = new ArrayList<>();
        orderedCostUnits.stream().map(cu -> organizationsByDbk.get(cu.getOrganizationDbk())).filter(Objects::nonNull).forEach(org -> { if (!orderedOrganizations.contains(org)) orderedOrganizations.add(org); });
        orderedContracts.stream().map(c -> organizationsByDbk.get(c.getOrganizationDbk())).filter(Objects::nonNull).forEach(org -> { if (!orderedOrganizations.contains(org)) orderedOrganizations.add(org); });
        if (person != null && person.getOrganizationDbk() != null) {
            LeitstandOrganization personOrg = organizationsByDbk.get(person.getOrganizationDbk());
            if (personOrg != null && !orderedOrganizations.contains(personOrg)) orderedOrganizations.add(personOrg);
        }

        LeitstandTask primaryTask = orderedTasks.isEmpty() ? null : orderedTasks.getFirst();
        LeitstandCostUnit primaryCostUnit = orderedCostUnits.isEmpty() ? null : orderedCostUnits.getFirst();
        LeitstandContract primaryContract = orderedContracts.isEmpty() ? null : orderedContracts.getFirst();
        LeitstandContractPosition primaryContractPosition = orderedContractPositions.isEmpty() ? null : orderedContractPositions.getFirst();
        LeitstandOrganization primaryOrganization = orderedOrganizations.isEmpty() ? null : orderedOrganizations.getFirst();

        String summary = buildSummary(recording, primaryTask, primaryCostUnit, primaryOrganization, person);
        String semanticText = buildSemanticText(timeEntry, recording, person, activityType, orderedTasks, orderedCostUnits, orderedContracts, orderedContractPositions, orderedOrganizations);

        TimeEntrySearchProjection projection = projectionRepository.findByTimeEntry_Id(timeEntry.getId())
                .orElseGet(() -> TimeEntrySearchProjection.builder().timeEntry(timeEntry).document(document).build());
        projection.setDocument(document);
        projection.setTimeEntry(timeEntry);
        projection.setSourceSystem(TimeSourceSystem.LEITSTAND);
        projection.setExternalId(timeEntry.getExternalId());
        projection.setLanguageCode(properties.getLeitstand().getRepresentationLanguageCode());
        projection.setEntryStart(timeEntry.getEntryStart());
        projection.setEntryEnd(timeEntry.getEntryEnd());
        projection.setDurationSeconds(timeEntry.getDurationSeconds());
        projection.setPersonExternalId(timeEntry.getPersonExternalId());
        projection.setPersonDisplayName(firstNonBlank(timeEntry.getPersonDisplayName(), formatPerson(person)));
        projection.setActivityTypeId(recording.getActivityTypeId());
        projection.setActivityTypeCode(activityType == null ? null : activityType.getLCode());
        projection.setActivityTypeName(activityType == null ? null : activityType.getBez());
        projection.setRecordType(recording.getRecordType());
        projection.setTimeRecordingDbk(recording.getDbk());
        projection.setTimeRecordingMclId(recording.getMclId());
        projection.setTimeRecordingDesc(recording.getMclDesc());
        projection.setTimeRecordingRemark(recording.getRemark());
        projection.setTimeRecordingUrl(recording.getUrl());
        projection.setPrimaryTaskDbk(primaryTask == null ? null : primaryTask.getDbk());
        projection.setPrimaryTaskMclId(primaryTask == null ? null : primaryTask.getMclId());
        projection.setPrimaryTaskName(primaryTask == null ? null : primaryTask.getMclName());
        projection.setPrimaryTaskDesc(primaryTask == null ? null : primaryTask.getMclDesc());
        projection.setPrimaryCostUnitDbk(primaryCostUnit == null ? null : primaryCostUnit.getDbk());
        projection.setPrimaryCostUnitMclId(primaryCostUnit == null ? null : primaryCostUnit.getMclId());
        projection.setPrimaryCostUnitName(primaryCostUnit == null ? null : primaryCostUnit.getMclName());
        projection.setPrimaryCostUnitDesc(primaryCostUnit == null ? null : primaryCostUnit.getMclDesc());
        projection.setPrimaryContractDbk(primaryContract == null ? null : primaryContract.getDbk());
        projection.setPrimaryContractName(primaryContract == null ? null : primaryContract.getName());
        projection.setPrimaryContractIref(primaryContract == null ? null : primaryContract.getIref());
        projection.setPrimaryContractPositionDbk(primaryContractPosition == null ? null : primaryContractPosition.getDbk());
        projection.setPrimaryContractPositionName(primaryContractPosition == null ? null : primaryContractPosition.getName());
        projection.setPrimaryContractPositionIref(primaryContractPosition == null ? null : primaryContractPosition.getIref());
        projection.setPrimaryOrganizationDbk(primaryOrganization == null ? null : primaryOrganization.getDbk());
        projection.setPrimaryOrganizationName(primaryOrganization == null ? null : primaryOrganization.getName());
        projection.setTaskIdsText(joinDistinct(orderedTasks.stream().map(LeitstandTask::getMclId).toList()));
        projection.setTaskNamesText(joinDistinct(orderedTasks.stream().map(LeitstandTask::getMclName).toList()));
        projection.setTaskDescriptionsText(joinDistinct(orderedTasks.stream().map(LeitstandTask::getMclDesc).toList()));
        projection.setCostUnitIdsText(joinDistinct(orderedCostUnits.stream().map(LeitstandCostUnit::getMclId).toList()));
        projection.setCostUnitNamesText(joinDistinct(orderedCostUnits.stream().map(LeitstandCostUnit::getMclName).toList()));
        projection.setCostUnitDescriptionsText(joinDistinct(orderedCostUnits.stream().map(LeitstandCostUnit::getMclDesc).toList()));
        projection.setContractNamesText(joinDistinct(orderedContracts.stream().map(LeitstandContract::getName).toList()));
        projection.setContractIrefsText(joinDistinct(orderedContracts.stream().map(LeitstandContract::getIref).toList()));
        projection.setContractPositionNamesText(joinDistinct(orderedContractPositions.stream().map(LeitstandContractPosition::getName).toList()));
        projection.setContractPositionIrefsText(joinDistinct(orderedContractPositions.stream().map(LeitstandContractPosition::getIref).toList()));
        projection.setOrganizationNamesText(joinDistinct(orderedOrganizations.stream().map(LeitstandOrganization::getName).toList()));
        projection.setSummaryText(summary);
        projection.setSemanticText(semanticText);
        return projection;
    }

    private String buildSummary(LeitstandTimeRecording recording,
                                LeitstandTask primaryTask,
                                LeitstandCostUnit primaryCostUnit,
                                LeitstandOrganization primaryOrganization,
                                LeitstandPerson person) {
        List<String> parts = new ArrayList<>();
        add(parts, formatPerson(person));
        add(parts, recording.getMclDesc());
        add(parts, primaryTask == null ? null : primaryTask.getMclName());
        add(parts, primaryCostUnit == null ? null : primaryCostUnit.getMclName());
        add(parts, primaryOrganization == null ? null : primaryOrganization.getName());
        add(parts, formatPeriod(recording.getRecordedFrom(), recording.getRecordedTo()));
        return String.join(" | ", parts);
    }

    private String buildSemanticText(TimeEntry timeEntry,
                                     LeitstandTimeRecording recording,
                                     LeitstandPerson person,
                                     LeitstandActivityType activityType,
                                     List<LeitstandTask> tasks,
                                     List<LeitstandCostUnit> costUnits,
                                     List<LeitstandContract> contracts,
                                     List<LeitstandContractPosition> contractPositions,
                                     List<LeitstandOrganization> organizations) {
        StringBuilder sb = new StringBuilder();
        sb.append("Document type: TIME_ENTRY\n");
        sb.append("Source system: LEITSTAND\n");
        sb.append("External ID: ").append(timeEntry.getExternalId()).append('\n');
        addLine(sb, "Person", formatPerson(person));
        addLine(sb, "Activity type", activityType == null ? null : firstNonBlank(activityType.getBez(), activityType.getLCode()));
        addLine(sb, "Record type", recording.getRecordType());
        addLine(sb, "Start", formatTimestamp(recording.getRecordedFrom()));
        addLine(sb, "End", formatTimestamp(recording.getRecordedTo()));
        if (timeEntry.getDurationSeconds() != null) {
            addLine(sb, "Duration seconds", String.valueOf(timeEntry.getDurationSeconds()));
        }
        addLine(sb, "Time recording title", recording.getMclDesc());
        addLine(sb, "Time recording ID", recording.getMclId());
        addLine(sb, "Remark", recording.getRemark());
        addLine(sb, "URL", recording.getUrl());
        addLine(sb, "Task IDs", joinDistinct(tasks.stream().map(LeitstandTask::getMclId).toList()));
        addLine(sb, "Task names", joinDistinct(tasks.stream().map(LeitstandTask::getMclName).toList()));
        addLine(sb, "Task descriptions", joinDistinct(tasks.stream().map(LeitstandTask::getMclDesc).toList()));
        addLine(sb, "Cost unit IDs", joinDistinct(costUnits.stream().map(LeitstandCostUnit::getMclId).toList()));
        addLine(sb, "Cost unit names", joinDistinct(costUnits.stream().map(LeitstandCostUnit::getMclName).toList()));
        addLine(sb, "Cost unit descriptions", joinDistinct(costUnits.stream().map(LeitstandCostUnit::getMclDesc).toList()));
        addLine(sb, "Contracts", joinDistinct(contracts.stream().map(LeitstandContract::getName).toList()));
        addLine(sb, "Contract references", joinDistinct(contracts.stream().map(LeitstandContract::getIref).toList()));
        addLine(sb, "Contract positions", joinDistinct(contractPositions.stream().map(LeitstandContractPosition::getName).toList()));
        addLine(sb, "Contract position references", joinDistinct(contractPositions.stream().map(LeitstandContractPosition::getIref).toList()));
        addLine(sb, "Organizations", joinDistinct(organizations.stream().map(LeitstandOrganization::getName).toList()));
        return sb.toString().trim();
    }

    private <T> Map<String, T> indexBy(Collection<T> rows, Function<T, String> id) {
        return rows.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(id, Function.identity(), (a, b) -> a, LinkedHashMap::new));
    }

    private void addLine(StringBuilder sb, String label, String value) {
        if (value != null && !value.isBlank()) {
            sb.append(label).append(": ").append(value.trim()).append('\n');
        }
    }

    private void add(List<String> parts, String value) {
        if (value != null && !value.isBlank()) {
            parts.add(value.trim());
        }
    }

    private String formatPerson(LeitstandPerson person) {
        if (person == null) {
            return null;
        }
        return firstNonBlank(joinName(person.getFirstName(), person.getLastName()), person.getPersonNumber() == null ? null : String.valueOf(person.getPersonNumber()));
    }

    private String joinName(String firstName, String lastName) {
        String left = firstName == null ? "" : firstName.trim();
        String right = lastName == null ? "" : lastName.trim();
        return (left + (left.isEmpty() || right.isEmpty() ? "" : " ") + right).trim();
    }

    private String formatPeriod(OffsetDateTime from, OffsetDateTime to) {
        if (from == null && to == null) {
            return null;
        }
        if (from != null && to != null) {
            return formatTimestamp(from) + " - " + formatTimestamp(to);
        }
        return formatTimestamp(firstNonNull(from, to));
    }

    private OffsetDateTime firstNonNull(OffsetDateTime a, OffsetDateTime b) {
        return a != null ? a : b;
    }

    private String formatTimestamp(OffsetDateTime timestamp) {
        return timestamp == null ? null : timestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
    }

    private String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return null;
    }

    private String joinDistinct(Collection<String> values) {
        return values.stream()
                .filter(Objects::nonNull)
                .map(String::trim)
                .filter(v -> !v.isEmpty())
                .distinct()
                .collect(Collectors.joining(" | "));
    }
}
