package org.raddatz.familienarchiv.person;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.springframework.lang.Nullable;

import org.raddatz.familienarchiv.person.PersonNameAliasDTO;
import org.raddatz.familienarchiv.person.PersonSummaryDTO;
import org.raddatz.familienarchiv.person.PersonUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonNameAlias;
import org.raddatz.familienarchiv.person.PersonNameAliasType;
import org.raddatz.familienarchiv.person.PersonType;
import org.raddatz.familienarchiv.person.PersonNameAliasRepository;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class PersonService {

    private final PersonRepository personRepository;
    private final PersonNameAliasRepository aliasRepository;

    public List<PersonSummaryDTO> findAll(String q) {
        if (q == null) {
            return personRepository.findAllWithDocumentCount();
        }
        if (q.isBlank()) {
            return List.of();
        }
        return personRepository.searchWithDocumentCount(q.trim());
    }

    public List<PersonSummaryDTO> findTopByDocumentCount(int limit) {
        return personRepository.findTopByDocumentCount(limit);
    }

    public Person getById(UUID id) {
        return personRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id));
    }

    public long count() {
        return personRepository.count();
    }

    public List<Person> findCorrespondents(UUID personId, String q) {
        if (q != null && !q.isBlank()) {
            return personRepository.findCorrespondentsWithFilter(personId, q);
        }
        return personRepository.findCorrespondents(personId);
    }

    public List<Person> getAllById(List<UUID> ids) {
        return personRepository.findAllById(ids);
    }

    public List<Person> findAllFamilyMembers() {
        return personRepository.findByFamilyMemberTrueOrderByLastNameAscFirstNameAsc();
    }

    @Transactional
    public Person setFamilyMember(UUID personId, boolean familyMember) {
        Person person = getById(personId);
        person.setFamilyMember(familyMember);
        return personRepository.save(person);
    }

    public Optional<Person> findByName(String firstName, String lastName) {
        return personRepository.findByFirstNameIgnoreCaseAndLastNameIgnoreCase(firstName, lastName);
    }

    @Nullable
    @Transactional
    public Person findOrCreateByAlias(String rawName) {
        String alias = rawName.trim();
        PersonType type = PersonTypeClassifier.classify(alias);
        if (type == PersonType.SKIP) return null;

        return personRepository.findByAliasIgnoreCase(alias).orElseGet(() -> {
            if (type == PersonType.INSTITUTION || type == PersonType.GROUP) {
                return personRepository.save(Person.builder()
                        .alias(alias)
                        .lastName(alias)
                        .personType(type)
                        .build());
            }

            PersonNameParser.SplitName split = PersonNameParser.split(alias);
            Person person = personRepository.save(Person.builder()
                    .alias(alias)
                    .firstName(split.firstName())
                    .lastName(split.lastName())
                    .build());
            if (split.maidenName() != null) {
                int nextSortOrder = aliasRepository.findMaxSortOrder(person.getId()) + 1;
                aliasRepository.save(PersonNameAlias.builder()
                        .person(person)
                        .lastName(split.maidenName())
                        .type(PersonNameAliasType.MAIDEN_NAME)
                        .sortOrder(nextSortOrder)
                        .build());
            }
            return person;
        });
    }

    @Transactional
    public Person createPerson(String firstName, String lastName, String alias) {
        Person person = Person.builder()
                .firstName(firstName)
                .lastName(lastName)
                .alias(alias == null || alias.isBlank() ? null : alias.trim())
                .build();
        return personRepository.save(person);
    }

    @Transactional
    public Person createPerson(PersonUpdateDTO dto) {
        if (dto.getPersonType() == PersonType.SKIP) {
            throw DomainException.badRequest(ErrorCode.INVALID_PERSON_TYPE, "SKIP is not a valid person type for manual creation");
        }
        validateYears(dto.getBirthYear(), dto.getDeathYear());
        Person person = Person.builder()
                .personType(dto.getPersonType())
                .title(dto.getTitle() == null || dto.getTitle().isBlank() ? null : dto.getTitle().trim())
                .firstName(dto.getFirstName())
                .lastName(dto.getLastName())
                .alias(dto.getAlias() == null || dto.getAlias().isBlank() ? null : dto.getAlias().trim())
                .notes(dto.getNotes() == null || dto.getNotes().isBlank() ? null : dto.getNotes().trim())
                .birthYear(dto.getBirthYear())
                .deathYear(dto.getDeathYear())
                .build();
        return personRepository.save(person);
    }

    private void validateYears(Integer birthYear, Integer deathYear) {
        if (birthYear != null && birthYear <= 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Geburtsjahr muss eine positive Zahl sein");
        }
        if (deathYear != null && deathYear <= 0) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Todesjahr muss eine positive Zahl sein");
        }
        if (birthYear != null && deathYear != null && birthYear > deathYear) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Geburtsjahr darf nicht nach dem Todesjahr liegen");
        }
    }

    @Transactional
    public Person updatePerson(UUID id, PersonUpdateDTO dto) {
        if (dto.getPersonType() == PersonType.SKIP) {
            throw DomainException.badRequest(ErrorCode.INVALID_PERSON_TYPE, "SKIP is not a valid person type for manual editing");
        }
        validateYears(dto.getBirthYear(), dto.getDeathYear());
        Person person = personRepository.findById(id)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id));
        person.setPersonType(dto.getPersonType());
        person.setTitle(dto.getTitle() == null || dto.getTitle().isBlank() ? null : dto.getTitle().trim());
        person.setFirstName(dto.getFirstName());
        person.setLastName(dto.getLastName());
        person.setAlias(dto.getAlias() == null || dto.getAlias().isBlank() ? null : dto.getAlias().trim());
        person.setNotes(dto.getNotes() == null || dto.getNotes().isBlank() ? null : dto.getNotes().trim());
        person.setBirthYear(dto.getBirthYear());
        person.setDeathYear(dto.getDeathYear());
        return personRepository.save(person);
    }

    @Transactional
    public void mergePersons(UUID sourceId, UUID targetId) {
        if (sourceId.equals(targetId)) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Quelle und Ziel dürfen nicht identisch sein");
        }
        personRepository.findById(sourceId)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Source person not found: " + sourceId));
        personRepository.findById(targetId)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Target person not found: " + targetId));

        // Reassign sender references
        personRepository.reassignSender(sourceId, targetId);

        // Add target as receiver where source is receiver but target is not yet
        personRepository.insertMissingReceiverReference(sourceId, targetId);

        // Remove all remaining source receiver references (duplicates already handled)
        personRepository.deleteReceiverReferences(sourceId);

        personRepository.deleteById(sourceId);
    }

    // ─── Alias management ───────────────────────────────────────────────────

    public List<PersonNameAlias> getAliases(UUID personId) {
        getById(personId);
        return aliasRepository.findByPersonIdOrderBySortOrderAscCreatedAtAsc(personId);
    }

    @Transactional
    public PersonNameAlias addAlias(UUID personId, PersonNameAliasDTO dto) {
        Person person = getById(personId);
        int nextSortOrder = aliasRepository.findMaxSortOrder(personId) + 1;
        PersonNameAlias alias = PersonNameAlias.builder()
                .person(person)
                .lastName(dto.lastName())
                .firstName(dto.firstName())
                .type(dto.type())
                .sortOrder(nextSortOrder)
                .build();
        return aliasRepository.save(alias);
    }

    @Transactional
    public void removeAlias(UUID personId, UUID aliasId) {
        PersonNameAlias alias = aliasRepository.findById(aliasId)
                .orElseThrow(() -> DomainException.notFound(ErrorCode.ALIAS_NOT_FOUND, "Alias not found: " + aliasId));
        if (!alias.getPerson().getId().equals(personId)) {
            throw DomainException.forbidden("Alias does not belong to this person");
        }
        aliasRepository.delete(alias);
    }
}
