package org.raddatz.familienarchiv.person.relationship;

import lombok.RequiredArgsConstructor;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.relationship.dto.CreateRelationshipRequest;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipDTO;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipWithPersonDTO;
import org.raddatz.familienarchiv.person.relationship.dto.NetworkDTO;
import org.raddatz.familienarchiv.person.relationship.dto.PersonNodeDTO;
import org.raddatz.familienarchiv.person.relationship.dto.RelationshipDTO;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

/**
 * Owns the {@code person_relationships} table and the family_member flag.
 * Always orchestrates {@link PersonService} for cross-domain access — never
 * touches {@link org.raddatz.familienarchiv.repository.PersonRepository}.
 */
@Service
@RequiredArgsConstructor
public class RelationshipService {

    // Single source of truth for which relationship types are part of the family graph.
    // Consulted by addRelationship (to set family_member on both endpoints) and by
    // getFamilyNetwork (to filter the edges returned). FRIEND/COLLEAGUE/etc. are excluded.
    private static final List<RelationType> FAMILY_RELATION_TYPES =
            List.of(RelationType.PARENT_OF, RelationType.SPOUSE_OF, RelationType.SIBLING_OF);

    private final PersonRelationshipRepository relationshipRepository;
    private final PersonService personService;
    private final RelationshipInferenceService inferenceService;

    public List<RelationshipDTO> getRelationships(UUID personId) {
        personService.getById(personId);
        List<PersonRelationship> rels = relationshipRepository.findAllByPersonIdOrRelatedPersonId(personId);
        return rels.stream().map(RelationshipService::toDTO).toList();
    }

    public List<InferredRelationshipWithPersonDTO> getInferredRelationships(UUID personId) {
        personService.getById(personId);
        return inferenceService.findAllFor(personId);
    }

    public Optional<InferredRelationshipDTO> getRelationshipBetween(UUID a, UUID b) {
        personService.getById(a);
        personService.getById(b);
        return inferenceService.infer(a, b);
    }

    public NetworkDTO getFamilyNetwork() {
        // Two queries: 1 for nodes (family members), 1 for edges (family-graph types).
        List<Person> familyMembers = personService.findAllFamilyMembers();
        Set<UUID> familyIds = new HashSet<>(familyMembers.size());
        List<PersonNodeDTO> nodes = new ArrayList<>(familyMembers.size());
        for (Person p : familyMembers) {
            familyIds.add(p.getId());
            nodes.add(new PersonNodeDTO(
                    p.getId(), p.getDisplayName(), p.getBirthYear(), p.getDeathYear(),
                    p.getGeneration(), true));
        }

        List<PersonRelationship> familyEdges = relationshipRepository.findAllByRelationTypeIn(
                FAMILY_RELATION_TYPES);

        List<RelationshipDTO> edges = new ArrayList<>();
        for (PersonRelationship r : familyEdges) {
            UUID p = r.getPerson().getId();
            UUID rp = r.getRelatedPerson().getId();
            if (familyIds.contains(p) && familyIds.contains(rp)) {
                edges.add(toDTO(r));
            }
        }
        return new NetworkDTO(nodes, edges);
    }

    @Transactional
    public RelationshipDTO addRelationship(UUID personId, CreateRelationshipRequest dto) {
        if (personId.equals(dto.relatedPersonId())) {
            throw DomainException.badRequest(
                    ErrorCode.VALIDATION_ERROR, "Cannot relate a person to themselves");
        }
        Person person = personService.getById(personId);
        Person relatedPerson = personService.getById(dto.relatedPersonId());

        validateYears(dto.fromYear(), dto.toYear());

        if (dto.relationType() == RelationType.PARENT_OF
                && relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
                        relatedPerson.getId(), personId, RelationType.PARENT_OF)) {
            throw DomainException.conflict(
                    ErrorCode.CIRCULAR_RELATIONSHIP,
                    "Reverse PARENT_OF already exists between " + personId + " and " + relatedPerson.getId());
        }

        PersonRelationship rel = PersonRelationship.builder()
                .person(person)
                .relatedPerson(relatedPerson)
                .relationType(dto.relationType())
                .fromYear(dto.fromYear())
                .toYear(dto.toYear())
                .notes(blankToNull(dto.notes()))
                .build();

        PersonRelationship saved;
        try {
            // saveAndFlush so the unique_rel constraint violates synchronously and is
            // caught here, not at commit time outside the @Transactional boundary.
            saved = relationshipRepository.saveAndFlush(rel);
        } catch (DataIntegrityViolationException e) {
            throw DomainException.conflict(
                    ErrorCode.DUPLICATE_RELATIONSHIP,
                    "Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + dto.relationType() + ")");
        }
        // Family-graph edges imply both endpoints are family members. Idempotent: the
        // setter is a no-op when the person is already flagged, so re-imports stay clean.
        if (FAMILY_RELATION_TYPES.contains(dto.relationType())) {
            personService.setFamilyMember(person.getId(), true);
            personService.setFamilyMember(relatedPerson.getId(), true);
        }
        return toDTO(saved);
    }

    @Transactional
    public void deleteRelationship(UUID personId, UUID relId) {
        PersonRelationship rel = relationshipRepository.findById(relId)
                .orElseThrow(() -> DomainException.notFound(
                        ErrorCode.RELATIONSHIP_NOT_FOUND, "Relationship not found: " + relId));

        UUID storageSubject = rel.getPerson().getId();
        UUID storageObject = rel.getRelatedPerson().getId();
        if (!personId.equals(storageSubject) && !personId.equals(storageObject)) {
            throw DomainException.forbidden(
                    "Relationship " + relId + " does not belong to person " + personId);
        }
        relationshipRepository.delete(rel);
    }

    @Transactional
    public Person setFamilyMember(UUID personId, boolean familyMember) {
        return personService.setFamilyMember(personId, familyMember);
    }

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

    private static void validateYears(Integer fromYear, Integer toYear) {
        if (fromYear != null && toYear != null && toYear < fromYear) {
            throw DomainException.badRequest(
                    ErrorCode.VALIDATION_ERROR, "toYear must not be before fromYear");
        }
    }

    private static RelationshipDTO toDTO(PersonRelationship r) {
        Person p = r.getPerson();
        Person rp = r.getRelatedPerson();
        return new RelationshipDTO(
                r.getId(),
                p.getId(),
                rp.getId(),
                p.getDisplayName(),
                p.getBirthYear(),
                p.getDeathYear(),
                rp.getDisplayName(),
                rp.getBirthYear(),
                rp.getDeathYear(),
                r.getRelationType(),
                r.getFromYear(),
                r.getToYear(),
                r.getNotes());
    }
}
