package org.raddatz.familienarchiv.person;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parses name strings from the ODS import into structured Person data.
 *
 * All methods are pure/static so they can be tested without a Spring context.
 */
public class PersonNameParser {

    // Known last names in this archive, longest first to avoid partial matches
    // (e.g. "de Gruyter" must be checked before any single-word name)
    static final List<String> KNOWN_LAST_NAMES = List.of(
            "von der Heide", "von Massenbach", "von Geldern", "von Gelden", "von Staa",
            "de Gruyter", "Dieckmann", "Gruber", "Müller", "Wolff", "Cram");

    private static final Pattern GEB_PATTERN = Pattern.compile(",?\\s*geb\\.?\\s+(.+)$");
    private static final Pattern PAREN_LAST_NAME = Pattern.compile("\\(([^)]+)\\)\\s*$");
    private static final Pattern MULTI_SEPARATOR = Pattern.compile("\\s+(?:und|u)\\s+");
    private static final Pattern SLASH_SEPARATOR = Pattern.compile("//");

    public record SplitName(
            String title,
            String firstName,
            String lastName,
            String maidenName,
            String annotation
    ) {}

    /**
     * Parses the "An" field from the ODS into individual normalised name strings.
     *
     * Handles:
     *   "Walter und Eugenie de Gruyter"  → ["Walter de Gruyter", "Eugenie de Gruyter"]
     *   "Herbert und Clara Cram"         → ["Herbert Cram", "Clara Cram"]
     *   "Hedi und Tutu (Gruber)"         → ["Hedi Gruber", "Tutu Gruber"]
     *   "Clara Cram u Ellen B-M"         → ["Clara Cram", "Ellen B-M"]
     *   "Clara u Familie"                → ["Clara"]
     *   "Walter und Eugenie"             → ["Walter", "Eugenie"]
     *   "Eugenie de Gruyter geb. Müller" → ["Eugenie de Gruyter"]
     */
    public static List<String> parseReceivers(String raw) {
        if (raw == null || raw.isBlank()) return List.of();

        // 0. Pre-split on "//" — each segment is an independent name entry
        String[] slashParts = SLASH_SEPARATOR.split(raw, -1);
        if (slashParts.length > 1) {
            return Arrays.stream(slashParts)
                    .map(String::trim)
                    .filter(s -> !s.isBlank())
                    .flatMap(segment -> parseReceivers(segment).stream())
                    .toList();
        }

        // 1. Strip "geb. Xxx" maiden-name annotations
        String cleaned = GEB_PATTERN.matcher(raw).replaceAll("").trim();

        // 2. If no multi-separator present, this is a single person — leave parens
        //    intact for split()'s annotation extraction
        if (!MULTI_SEPARATOR.matcher(cleaned).find()) {
            return List.of(cleaned);
        }

        // 3. Extract parenthesised last name override, e.g. "(Gruber)"
        //    Only applies to multi-person entries like "Hedi und Tutu (Gruber)"
        String sharedLastName = null;
        Matcher parenMatcher = PAREN_LAST_NAME.matcher(cleaned);
        if (parenMatcher.find()) {
            sharedLastName = parenMatcher.group(1).trim();
            cleaned = cleaned.substring(0, parenMatcher.start()).trim();
        }

        // 4. Split on " und " / " u "
        String[] parts = MULTI_SEPARATOR.split(cleaned);

        // 5. Filter out "Familie" (not a person)
        List<String> nameParts = new ArrayList<>();
        for (String part : parts) {
            if (!part.trim().equalsIgnoreCase("Familie")) {
                nameParts.add(part.trim());
            }
        }

        if (nameParts.isEmpty()) return List.of();
        if (nameParts.size() == 1) return List.of(nameParts.get(0));

        // 6. If parenthesised last name was found, apply to all single-token parts
        if (sharedLastName != null) {
            String ln = sharedLastName;
            return nameParts.stream()
                    .map(p -> p.contains(" ") ? p : p + " " + ln)
                    .toList();
        }

        // 7. Try to detect a shared last name from the final segment and distribute it
        //    to earlier segments that are single first-name tokens
        String lastSegment = nameParts.get(nameParts.size() - 1);
        String detectedLastName = findKnownLastName(lastSegment);

        if (detectedLastName != null) {
            List<String> result = new ArrayList<>();
            for (int i = 0; i < nameParts.size() - 1; i++) {
                String part = nameParts.get(i);
                // Distribute only if the part has no last name of its own
                if (!part.contains(" ") && findKnownLastName(part) == null) {
                    result.add(part + " " + detectedLastName);
                } else {
                    result.add(part);
                }
            }
            result.add(lastSegment);
            return result;
        }

        // 8. No shared last name found — return parts as-is
        return nameParts;
    }

    // --- Pipeline result records (package-private for testing) ---

    public record MaidenNameResult(String cleaned, String maidenName) {}
    public record AnnotationResult(String cleaned, String annotation) {}
    public record TitleResult(String cleaned, String title) {}
    record NameParts(String firstName, String lastName) {}

    /**
     * Splits a single full name string into a structured SplitName.
     * Pipeline: stripMaidenName → normalizeDotCompressed → stripAnnotation → stripTitle → splitByKnownLastNameOrFallback
     */
    public static SplitName split(String rawName) {
        if (rawName == null || rawName.isBlank()) {
            return new SplitName(null, "?", "?", null, null);
        }

        MaidenNameResult maiden = stripMaidenName(rawName);
        String cleaned = maiden.cleaned();

        cleaned = normalizeDotCompressed(cleaned);

        AnnotationResult paren = stripAnnotation(cleaned);
        cleaned = paren.cleaned();

        TitleResult title = stripTitle(cleaned);
        cleaned = title.cleaned();

        NameParts parts = splitByKnownLastNameOrFallback(cleaned);

        String firstName = parts.firstName();
        String lastName = parts.lastName();

        // When a title was stripped and no first name could be extracted, the
        // remaining text is the lastName. "Tante Molly" -> title=Tante, lastName=Molly.
        if (title.title() != null) {
            if ("?".equals(lastName) && !cleaned.contains(" ")) {
                lastName = firstName;
                firstName = null;
            } else if (Objects.equals(firstName, lastName)) {
                firstName = null;
            }
        }

        return new SplitName(
                title.title(), firstName, lastName,
                maiden.maidenName(), paren.annotation()
        );
    }

    /** Strips geb annotations and extracts the maiden name. */
    public static MaidenNameResult stripMaidenName(String input) {
        Matcher m = GEB_PATTERN.matcher(input);
        if (m.find()) {
            String cleaned = input.substring(0, m.start()).trim();
            String maidenName = m.group(1).trim();
            return new MaidenNameResult(cleaned, maidenName);
        }
        return new MaidenNameResult(input, null);
    }

    /** Normalizes dot-compressed names: "Dr.Fr.Zarncke" → "Dr. Fr. Zarncke" */
    static String normalizeDotCompressed(String input) {
        if (!input.contains(" ") && input.contains(".")) {
            return input.replace(".", ". ").trim();
        }
        return input;
    }

    private static final Pattern PAREN_ANNOTATION = Pattern.compile("\\s*\\(([^)]*)\\)\\s*$");
    private static final Pattern UNCERTAIN_NAME = Pattern.compile("^(\\S+)\\s+\\?\\s*$");

    /** Strips trailing parenthesized annotations and extracts the content. */
    public static AnnotationResult stripAnnotation(String input) {
        Matcher m = PAREN_ANNOTATION.matcher(input);
        if (!m.find()) {
            return new AnnotationResult(input, null);
        }
        String cleaned = input.substring(0, m.start()).trim();
        String rawAnnotation = m.group(1).trim();

        Matcher uncertainMatcher = UNCERTAIN_NAME.matcher(rawAnnotation);
        if (uncertainMatcher.matches()) {
            String nameFromAnnotation = uncertainMatcher.group(1);
            cleaned = (cleaned + " " + nameFromAnnotation).trim();
            return new AnnotationResult(cleaned, "?");
        }

        return new AnnotationResult(cleaned, rawAnnotation);
    }

    private static final List<String> DOT_PREFIXES = List.of("Dr.", "Prof.");

    private static final List<String> WORD_PREFIXES = List.of(
            "Frau", "Herr", "Freifrau", "Freiherr",
            "Tante", "Onkel", "Schwester", "Bruder",
            "Cousine", "Cousin", "Freundin", "Freund",
            "Mutter", "Vater", "Pastor");

    /** Strips known title/relationship prefixes, looping for stacked titles. */
    public static TitleResult stripTitle(String input) {
        String remaining = input;
        StringBuilder titleBuilder = new StringBuilder();
        boolean found = true;

        while (found) {
            found = false;

            for (String prefix : DOT_PREFIXES) {
                if (remaining.toLowerCase().startsWith(prefix.toLowerCase())) {
                    titleBuilder.append(titleBuilder.isEmpty() ? "" : " ").append(prefix);
                    remaining = remaining.substring(prefix.length()).trim();
                    found = true;
                    break;
                }
            }
            if (found) continue;

            for (String prefix : WORD_PREFIXES) {
                String lower = remaining.toLowerCase();
                if (lower.startsWith(prefix.toLowerCase() + " ") || lower.equals(prefix.toLowerCase())) {
                    titleBuilder.append(titleBuilder.isEmpty() ? "" : " ").append(prefix);
                    remaining = remaining.length() > prefix.length()
                            ? remaining.substring(prefix.length() + 1).trim()
                            : "";
                    found = true;
                    break;
                }
            }
        }

        if (titleBuilder.isEmpty()) {
            return new TitleResult(input, null);
        }
        return new TitleResult(remaining, titleBuilder.toString());
    }

    /** Splits a cleaned name into firstName/lastName using known last names or last-space fallback. */
    static NameParts splitByKnownLastNameOrFallback(String cleaned) {
        String lastName = findKnownLastName(cleaned);
        if (lastName != null) {
            String firstName = cleaned.substring(0, cleaned.length() - lastName.length()).trim();
            if (firstName.isBlank()) firstName = cleaned;
            return new NameParts(firstName, lastName);
        }

        int lastSpace = cleaned.lastIndexOf(' ');
        if (lastSpace > 0) {
            return new NameParts(cleaned.substring(0, lastSpace).trim(), cleaned.substring(lastSpace + 1).trim());
        }

        return new NameParts(cleaned, "?");
    }

    /** Returns the known last name that the given string ends with, or null. */
    static String findKnownLastName(String name) {
        String lower = name.toLowerCase();
        for (String lastName : KNOWN_LAST_NAMES) {
            if (lower.endsWith(lastName.toLowerCase())) {
                return lastName;
            }
        }
        return null;
    }
}
