package org.raddatz.familienarchiv.document;

import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.document.DocumentVersionSummary;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.UserService;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentVersion;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.document.DocumentVersionRepository;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class DocumentVersionService {

    private final DocumentVersionRepository versionRepository;
    private final UserService userService;
    private final ObjectMapper objectMapper;

    @Transactional
    public void recordVersion(Document doc) {
        AppUser editor = resolveCurrentUser();
        String editorName = buildEditorName(editor);
        UUID editorId = editor != null ? editor.getId() : null;

        String snapshot = serializeSnapshot(doc);
        List<DocumentVersion> previous = versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId());
        String changedFields = computeChangedFields(doc, previous);

        versionRepository.save(DocumentVersion.builder()
                .documentId(doc.getId())
                .savedAt(LocalDateTime.now())
                .editorId(editorId)
                .editorName(editorName)
                .snapshot(snapshot)
                .changedFields(changedFields)
                .build());
    }

    @Transactional
    public int backfillMissingVersions(List<Document> docs) {
        int count = 0;
        for (Document doc : docs) {
            List<DocumentVersion> existing = versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId());
            if (!existing.isEmpty()) continue;
            LocalDateTime savedAt = doc.getCreatedAt() != null ? doc.getCreatedAt() : LocalDateTime.now();
            versionRepository.save(DocumentVersion.builder()
                    .documentId(doc.getId())
                    .savedAt(savedAt)
                    .editorId(null)
                    .editorName("Datenimport")
                    .snapshot(serializeSnapshot(doc))
                    .changedFields("[]")
                    .build());
            count++;
        }
        return count;
    }

    public List<DocumentVersionSummary> getSummaries(UUID documentId) {
        return versionRepository.findByDocumentIdOrderBySavedAtAsc(documentId).stream()
                .map(v -> new DocumentVersionSummary(
                        v.getId(),
                        v.getSavedAt(),
                        v.getEditorName(),
                        parseChangedFields(v.getChangedFields())))
                .toList();
    }

    public DocumentVersion getVersion(UUID documentId, UUID versionId) {
        return versionRepository.findByIdAndDocumentId(versionId, documentId)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND,
                        "Version not found: " + versionId));
    }

    // ─── private helpers ──────────────────────────────────────────────────────

    private AppUser resolveCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) {
            return null;
        }
        try {
            return userService.findByEmail(auth.getName());
        } catch (Exception e) {
            log.warn("Could not resolve editor for version snapshot: {}", e.getMessage());
            return null;
        }
    }

    private String buildEditorName(AppUser user) {
        if (user == null) return "Unknown";
        String first = user.getFirstName();
        String last = user.getLastName();
        if (first != null && !first.isBlank() && last != null && !last.isBlank()) {
            return first + " " + last;
        }
        return user.getEmail();
    }

    private String serializeSnapshot(Document doc) {
        try {
            return objectMapper.writeValueAsString(doc);
        } catch (Exception e) {
            log.error("Failed to serialize document snapshot for {}", doc.getId(), e);
            return "{}";
        }
    }

    private String computeChangedFields(Document current, List<DocumentVersion> previousVersions) {
        if (previousVersions.isEmpty()) {
            return "[]";
        }
        DocumentVersion last = previousVersions.get(previousVersions.size() - 1);
        try {
            Map<String, Object> previousMap = objectMapper.readValue(
                    last.getSnapshot(), new TypeReference<>() {});
            List<String> changed = new ArrayList<>();

            checkScalar(changed, "title", current.getTitle(), previousMap);
            checkScalar(changed, "documentDate",
                    current.getDocumentDate() != null ? current.getDocumentDate().toString() : null,
                    previousMap);
            checkScalar(changed, "location", current.getLocation(), previousMap);
            checkScalar(changed, "documentLocation", current.getDocumentLocation(), previousMap);
            checkScalar(changed, "transcription", current.getTranscription(), previousMap);
            checkScalar(changed, "summary", current.getSummary(), previousMap);
            checkSender(changed, current, previousMap);
            checkReceivers(changed, current, previousMap);
            checkTags(changed, current, previousMap);

            return objectMapper.writeValueAsString(changed);
        } catch (Exception e) {
            log.warn("Could not compute changedFields for document {}", current.getId(), e);
            return "[]";
        }
    }

    private void checkScalar(List<String> changed, String field, String currentValue,
                              Map<String, Object> previousMap) {
        Object prev = previousMap.get(field);
        String prevStr = prev != null ? prev.toString() : null;
        if (!Objects.equals(currentValue, prevStr)) {
            changed.add(field);
        }
    }

    @SuppressWarnings("unchecked")
    private void checkSender(List<String> changed, Document current, Map<String, Object> previousMap) {
        String currentId = current.getSender() != null
                ? current.getSender().getId().toString() : null;
        Object prevSender = previousMap.get("sender");
        String prevId = null;
        if (prevSender instanceof Map<?, ?> senderMap) {
            Object id = senderMap.get("id");
            prevId = id != null ? id.toString() : null;
        }
        if (!Objects.equals(currentId, prevId)) {
            changed.add("sender");
        }
    }

    @SuppressWarnings("unchecked")
    private void checkReceivers(List<String> changed, Document current, Map<String, Object> previousMap) {
        Set<String> currentIds = current.getReceivers() != null
                ? current.getReceivers().stream().map(p -> p.getId().toString()).collect(Collectors.toSet())
                : Set.of();
        Object prevReceivers = previousMap.get("receivers");
        Set<String> prevIds = Set.of();
        if (prevReceivers instanceof List<?> list) {
            prevIds = list.stream()
                    .filter(r -> r instanceof Map<?, ?>)
                    .map(r -> ((Map<?, ?>) r).get("id"))
                    .filter(Objects::nonNull)
                    .map(Object::toString)
                    .collect(Collectors.toSet());
        }
        if (!currentIds.equals(prevIds)) {
            changed.add("receivers");
        }
    }

    @SuppressWarnings("unchecked")
    private void checkTags(List<String> changed, Document current, Map<String, Object> previousMap) {
        Set<String> currentNames = current.getTags() != null
                ? current.getTags().stream().map(Tag::getName).collect(Collectors.toSet())
                : Set.of();
        Object prevTags = previousMap.get("tags");
        Set<String> prevNames = Set.of();
        if (prevTags instanceof List<?> list) {
            prevNames = list.stream()
                    .filter(t -> t instanceof Map<?, ?>)
                    .map(t -> ((Map<?, ?>) t).get("name"))
                    .filter(Objects::nonNull)
                    .map(Object::toString)
                    .collect(Collectors.toSet());
        }
        if (!currentNames.equals(prevNames)) {
            changed.add("tags");
        }
    }

    private List<String> parseChangedFields(String json) {
        try {
            return objectMapper.readValue(json, new TypeReference<>() {});
        } catch (Exception e) {
            return List.of();
        }
    }
}
