package org.raddatz.familienarchiv.person.relationship;

import lombok.RequiredArgsConstructor;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipDTO;
import org.raddatz.familienarchiv.person.relationship.dto.InferredRelationshipWithPersonDTO;
import org.raddatz.familienarchiv.person.relationship.dto.PersonNodeDTO;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * Derives indirect family relationships by BFS over the family-graph subset
 * (PARENT_OF, SPOUSE_OF, SIBLING_OF). Time-ignorant: from_year / to_year are
 * not consulted. Siblings are also derived from shared parents — no SIBLING_OF
 * row is required.
 */
@Service
@RequiredArgsConstructor
public class RelationshipInferenceService {

    // 8 hops covers great-grandparents ↔ great-great-grandchildren and second cousins —
    // the practical horizon for a 1899–1950 family archive. Paths longer than this are
    // classified as LABEL_DISTANT and rarely carry meaningful relationship labels.
    static final int MAX_DEPTH = 8;

    /** "distant" is the catch-all label for paths that do not match the LABEL_MAP. */
    static final String LABEL_DISTANT = "distant";

    private static final Map<List<RelationToken>, String> LABEL_MAP = buildLabelMap();

    private final PersonRelationshipRepository relationshipRepository;
    private final PersonService personService;

    private static Map<List<RelationToken>, String> buildLabelMap() {
        Map<List<RelationToken>, String> m = new HashMap<>();
        m.put(List.of(RelationToken.UP), "parent");
        m.put(List.of(RelationToken.DOWN), "child");
        m.put(List.of(RelationToken.SPOUSE), "spouse");
        m.put(List.of(RelationToken.SIBLING), "sibling");
        m.put(List.of(RelationToken.UP, RelationToken.UP), "grandparent");
        m.put(List.of(RelationToken.DOWN, RelationToken.DOWN), "grandchild");
        m.put(List.of(RelationToken.UP, RelationToken.UP, RelationToken.UP), "great_grandparent");
        m.put(List.of(RelationToken.DOWN, RelationToken.DOWN, RelationToken.DOWN), "great_grandchild");
        m.put(List.of(RelationToken.UP, RelationToken.SIBLING), "uncle_aunt");
        m.put(List.of(RelationToken.SIBLING, RelationToken.DOWN), "niece_nephew");
        m.put(List.of(RelationToken.UP, RelationToken.UP, RelationToken.SIBLING), "great_uncle_aunt");
        m.put(List.of(RelationToken.SIBLING, RelationToken.DOWN, RelationToken.DOWN), "great_niece_nephew");
        m.put(List.of(RelationToken.SPOUSE, RelationToken.UP), "inlaw_parent");
        m.put(List.of(RelationToken.DOWN, RelationToken.SPOUSE), "inlaw_child");
        m.put(List.of(RelationToken.SPOUSE, RelationToken.SIBLING), "sibling_inlaw");
        m.put(List.of(RelationToken.SIBLING, RelationToken.SPOUSE), "sibling_inlaw");
        m.put(List.of(RelationToken.UP, RelationToken.SIBLING, RelationToken.DOWN), "cousin_1");
        return Collections.unmodifiableMap(m);
    }

    /**
     * Shortest token path from {@code from} to {@code to}, or empty if unreachable
     * within {@link #MAX_DEPTH} hops. Package-private to permit direct path
     * assertions in unit tests.
     */
    Optional<List<RelationToken>> findShortestPath(UUID from, UUID to) {
        if (from.equals(to)) return Optional.empty();
        Map<UUID, List<Edge>> adj = buildAdjacency();
        return bfs(adj, from, to);
    }

    /** Two-sided label between A and B. {@code labelFromA} reads "B is my <labelFromA>". */
    public Optional<InferredRelationshipDTO> infer(UUID a, UUID b) {
        Optional<List<RelationToken>> aToB = findShortestPath(a, b);
        if (aToB.isEmpty()) return Optional.empty();
        List<RelationToken> path = aToB.get();
        return Optional.of(new InferredRelationshipDTO(
                labelFor(path),
                labelFor(reversePath(path)),
                path.size()));
    }

    /** All persons reachable from {@code personId} within MAX_DEPTH, with their labels. */
    public List<InferredRelationshipWithPersonDTO> findAllFor(UUID personId) {
        Map<UUID, List<Edge>> adj = buildAdjacency();
        Map<UUID, List<RelationToken>> shortestPaths = bfsAll(adj, personId);
        shortestPaths.remove(personId);
        if (shortestPaths.isEmpty()) return List.of();

        List<UUID> ids = new ArrayList<>(shortestPaths.keySet());
        Map<UUID, Person> byId = new HashMap<>();
        for (Person p : personService.getAllById(ids)) {
            byId.put(p.getId(), p);
        }

        List<InferredRelationshipWithPersonDTO> out = new ArrayList<>();
        for (UUID id : ids) {
            Person p = byId.get(id);
            if (p == null) continue;
            List<RelationToken> path = shortestPaths.get(id);
            PersonNodeDTO node = new PersonNodeDTO(
                    p.getId(), p.getDisplayName(),
                    RelationshipService.yearOf(p.getBirthDate()),
                    RelationshipService.yearOf(p.getDeathDate()),
                    p.getGeneration(), p.isFamilyMember());
            out.add(new InferredRelationshipWithPersonDTO(node, labelFor(path), path.size()));
        }
        out.sort(Comparator.comparingInt(InferredRelationshipWithPersonDTO::hops)
                .thenComparing(d -> d.person().displayName()));
        return out;
    }

    static String labelFor(List<RelationToken> path) {
        String specific = LABEL_MAP.get(path);
        return specific != null ? specific : LABEL_DISTANT;
    }

    private static List<RelationToken> reversePath(List<RelationToken> path) {
        List<RelationToken> reversed = new ArrayList<>(path.size());
        for (int i = path.size() - 1; i >= 0; i--) {
            reversed.add(path.get(i).reverse());
        }
        return List.copyOf(reversed);
    }

    private Map<UUID, List<Edge>> buildAdjacency() {
        List<PersonRelationship> edges = relationshipRepository.findAllByRelationTypeIn(
                List.of(RelationType.PARENT_OF, RelationType.SPOUSE_OF, RelationType.SIBLING_OF));
        Map<UUID, List<Edge>> adj = new HashMap<>();
        Map<UUID, List<UUID>> parentToChildren = new HashMap<>();

        for (PersonRelationship e : edges) {
            UUID a = e.getPerson().getId();
            UUID b = e.getRelatedPerson().getId();
            switch (e.getRelationType()) {
                case PARENT_OF -> {
                    addEdge(adj, a, b, RelationToken.DOWN);
                    addEdge(adj, b, a, RelationToken.UP);
                    parentToChildren.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
                }
                case SPOUSE_OF -> {
                    addEdge(adj, a, b, RelationToken.SPOUSE);
                    addEdge(adj, b, a, RelationToken.SPOUSE);
                }
                case SIBLING_OF -> {
                    addEdge(adj, a, b, RelationToken.SIBLING);
                    addEdge(adj, b, a, RelationToken.SIBLING);
                }
                default -> { /* family graph excludes other types */ }
            }
        }

        for (List<UUID> children : parentToChildren.values()) {
            for (int i = 0; i < children.size(); i++) {
                for (int j = i + 1; j < children.size(); j++) {
                    UUID c1 = children.get(i);
                    UUID c2 = children.get(j);
                    addEdge(adj, c1, c2, RelationToken.SIBLING);
                    addEdge(adj, c2, c1, RelationToken.SIBLING);
                }
            }
        }
        return adj;
    }

    private static void addEdge(Map<UUID, List<Edge>> adj, UUID from, UUID to, RelationToken token) {
        adj.computeIfAbsent(from, k -> new ArrayList<>()).add(new Edge(to, token));
    }

    private static Optional<List<RelationToken>> bfs(Map<UUID, List<Edge>> adj, UUID from, UUID to) {
        Map<UUID, List<RelationToken>> shortest = new HashMap<>();
        shortest.put(from, List.of());
        Deque<UUID> queue = new ArrayDeque<>();
        queue.add(from);
        while (!queue.isEmpty()) {
            UUID curr = queue.poll();
            List<RelationToken> currPath = shortest.get(curr);
            if (currPath.size() >= MAX_DEPTH) continue;
            for (Edge e : adj.getOrDefault(curr, List.of())) {
                if (shortest.containsKey(e.target())) continue;
                List<RelationToken> nextPath = append(currPath, e.token());
                shortest.put(e.target(), nextPath);
                if (e.target().equals(to)) return Optional.of(nextPath);
                queue.add(e.target());
            }
        }
        return Optional.empty();
    }

    private static Map<UUID, List<RelationToken>> bfsAll(Map<UUID, List<Edge>> adj, UUID from) {
        Map<UUID, List<RelationToken>> shortest = new HashMap<>();
        shortest.put(from, List.of());
        Deque<UUID> queue = new ArrayDeque<>();
        queue.add(from);
        while (!queue.isEmpty()) {
            UUID curr = queue.poll();
            List<RelationToken> currPath = shortest.get(curr);
            if (currPath.size() >= MAX_DEPTH) continue;
            for (Edge e : adj.getOrDefault(curr, List.of())) {
                if (shortest.containsKey(e.target())) continue;
                List<RelationToken> nextPath = append(currPath, e.token());
                shortest.put(e.target(), nextPath);
                queue.add(e.target());
            }
        }
        return shortest;
    }

    private static List<RelationToken> append(List<RelationToken> prefix, RelationToken next) {
        List<RelationToken> out = new ArrayList<>(prefix.size() + 1);
        out.addAll(prefix);
        out.add(next);
        return List.copyOf(out);
    }

    private record Edge(UUID target, RelationToken token) {}
}
