package org.raddatz.familienarchiv.importing;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonGeneration;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.person.PersonType;
import org.raddatz.familienarchiv.person.PersonUpsertCommand;
import org.raddatz.familienarchiv.person.relationship.RelationType;
import org.raddatz.familienarchiv.person.relationship.RelationshipService;
import org.raddatz.familienarchiv.person.relationship.dto.RelationshipUpsertRequest;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Loads {@code canonical-persons-tree.json} into the person + relationship domains.
 * Tree persons are upserted via {@link PersonService} keyed on the shared
 * {@code personId} slug (which Phase 1 #670 now emits into the tree), so they reconcile
 * with the register rather than duplicating it. Relationships reference persons by the
 * tree's local {@code rowId}; each side is mapped to the upserted person's UUID and
 * created through {@link RelationshipService} (never the relationship repository —
 * layering rule). A duplicate relationship on re-import is swallowed for idempotency.
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class PersonTreeImporter {

    // The tree JSON is a local implementation detail, not a shared API payload, so the
    // importer owns its own mapper rather than depending on the web ObjectMapper bean.
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private final PersonService personService;
    private final RelationshipService relationshipService;

    public int load(File artifact) {
        JsonNode root = readTree(artifact);
        Map<String, UUID> idByRowId = upsertPersons(root.path("persons"));
        int relationships = createRelationships(root.path("relationships"), idByRowId);
        log.info("Imported {} tree persons and {} relationships from {}",
                idByRowId.size(), relationships, artifact.getName());
        return idByRowId.size();
    }

    private JsonNode readTree(File artifact) {
        try {
            return OBJECT_MAPPER.readTree(artifact);
        } catch (Exception e) {
            throw DomainException.badRequest(ErrorCode.IMPORT_ARTIFACT_INVALID,
                    "Unreadable canonical artifact: " + artifact.getName());
        }
    }

    private Map<String, UUID> upsertPersons(JsonNode persons) {
        Map<String, UUID> idByRowId = new HashMap<>();
        for (JsonNode node : persons) {
            String personId = text(node, "personId");
            if (personId.isBlank()) continue;
            Person person = personService.upsertBySourceRef(toCommand(node, personId));
            idByRowId.put(text(node, "rowId"), person.getId());
        }
        return idByRowId;
    }

    private PersonUpsertCommand toCommand(JsonNode node, String personId) {
        return PersonUpsertCommand.builder()
                .sourceRef(personId)
                .lastName(blankToNull(text(node, "lastName")))
                .firstName(blankToNull(text(node, "firstName")))
                .maidenName(blankToNull(text(node, "maidenName")))
                .notes(blankToNull(text(node, "notes")))
                .birthYear(intOrNull(node, "birthYear"))
                .deathYear(intOrNull(node, "deathYear"))
                .generation(generationOrNull(node, personId))
                .familyMember(node.path("familyMember").asBoolean(false))
                .personType(PersonType.PERSON)
                .provisional(false)
                .build();
    }

    /**
     * Returns the JSON {@code generation} value if present and within the
     * {@link PersonGeneration} bounds; null otherwise. Out-of-range values
     * log a WARN but never abort the batch — mirrors the register-importer
     * skip-and-warn policy.
     */
    private static Integer generationOrNull(JsonNode node, String personId) {
        Integer raw = intOrNull(node, "generation");
        if (raw == null) return null;
        if (raw < PersonGeneration.MIN_GENERATION || raw > PersonGeneration.MAX_GENERATION) {
            log.warn("Skipping out-of-range generation '{}' for person {}", raw, personId);
            return null;
        }
        return raw;
    }

    private int createRelationships(JsonNode relationships, Map<String, UUID> idByRowId) {
        int created = 0;
        for (JsonNode node : relationships) {
            // Trap: a relationship node's personId / relatedPersonId fields carry the tree's
            // local rowId (e.g. "row_a"), NOT a person slug. They are resolved through
            // idByRowId to the upserted person's UUID.
            UUID person = idByRowId.get(text(node, "personId"));
            UUID related = idByRowId.get(text(node, "relatedPersonId"));
            if (person == null || related == null) {
                log.warn("Skipping tree relationship with unresolved rowId: {} -> {}",
                        text(node, "personId"), text(node, "relatedPersonId"));
                continue;
            }
            if (addRelationshipIdempotently(person, related, text(node, "type"))) {
                created++;
            }
        }
        return created;
    }

    private boolean addRelationshipIdempotently(UUID person, UUID related, String type) {
        try {
            relationshipService.addRelationship(person,
                    new RelationshipUpsertRequest(related, RelationType.valueOf(type), null, null, null, null, null));
            return true;
        } catch (DomainException e) {
            if (e.getCode() == ErrorCode.DUPLICATE_RELATIONSHIP
                    || e.getCode() == ErrorCode.CIRCULAR_RELATIONSHIP) {
                return false;
            }
            throw e;
        }
    }

    private static String text(JsonNode node, String field) {
        JsonNode value = node.get(field);
        return value == null || value.isNull() ? "" : value.asText();
    }

    private static Integer intOrNull(JsonNode node, String field) {
        JsonNode value = node.get(field);
        return value == null || value.isNull() ? null : value.asInt();
    }

    private static String blankToNull(String s) {
        return (s == null || s.isBlank()) ? null : s;
    }
}
