package org.raddatz.familienarchiv.document;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Heuristic overwrite test for the one-time title backfill (#726, FR-004): decides whether a
 * STORED title is a machine-generated auto-title (and so may be rebuilt from the row's current
 * state) versus hand-written prose (left untouched). Used ONLY by the backfill — save-time
 * regeneration uses an exact old-vs-new comparison instead, with no heuristic.
 *
 * <p>A stored title is overwritable iff, after stripping the literal {@code index} prefix:
 * <ol>
 *   <li>it is exactly {@code {index}}, or</li>
 *   <li>{@code {index} – {dateLabel}} with an optional trailing {@code  – {location}} segment
 *       (any location — a present, valid date label is itself strong evidence of a machine
 *       title), or</li>
 *   <li>{@code {index} – {location}} where the segment equals the document's current location
 *       (no date label, so the segment must match the known location to be distinguished from
 *       prose).</li>
 * </ol>
 *
 * <p>Security: the {@code index} is compared <em>literally</em> via {@link String#startsWith}
 * (never compiled into a regex) because {@code originalFilename} is user-controlled and may carry
 * regex metacharacters — an unquoted pattern would be a ReDoS / regex-injection vector
 * (CWE-1333 / CWE-625). The date-label sub-patterns use only bounded, non-nested quantifiers over
 * short tokens, so there is no catastrophic backtracking. Fail-closed: any null/blank index or
 * structural surprise returns {@code false}.
 */
final class DocumentTitleBackfillMatcher {

    private static final String SEPARATOR = " – ";

    // German month tokens derived from the SAME Locale.GERMAN formatters DocumentTitleFormatter
    // uses, so the matcher's accepted spellings cannot drift from what the factory emits (full
    // names "Januar"…"Dezember"; abbreviations "Jan."…"Dez." — note May/June/July/März carry no
    // period). Pattern.quote each so a "." in an abbreviation is literal, never a wildcard.
    private static final String FULL_MONTH = monthAlternation("MMMM");
    private static final String ABBR_MONTH = monthAlternation("MMM");
    private static final String SEASON = "(?:Frühling|Sommer|Herbst|Winter)";
    private static final String YEAR = "\\d{1,4}";
    private static final String DAY_NUM = "\\d{1,2}";

    // One complete date label, anchored, optionally followed by a free-form trailing location
    // segment. Only bounded/non-nested quantifiers over short tokens plus a single trailing
    // ".+" → linear, no catastrophic backtracking (FR-004 ReDoS guard).
    private static final Pattern DATE_LABEL_WITH_OPTIONAL_LOCATION = Pattern.compile(
            "^(?:" + String.join("|",
                    YEAR,                                                                    // 1916
                    "ca\\. " + YEAR,                                                         // ca. 1920
                    FULL_MONTH + " " + YEAR,                                                 // Juni 1916
                    DAY_NUM + "\\. " + FULL_MONTH + " " + YEAR,                              // 24. Dezember 1943
                    SEASON + " " + YEAR,                                                     // Sommer 1916
                    "Datum unbekannt",
                    DAY_NUM + "\\.–" + DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR,           // 10.–11. Jan. 1917
                    DAY_NUM + "\\. " + ABBR_MONTH + " – " + DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR,        // 30. Jan. – 2. Feb. 1917
                    DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR + " – " + DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR, // 30. Dez. 1916 – 2. Jan. 1917
                    DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR,                              // 10. Jan. 1917 (range end == start)
                    "ab " + DAY_NUM + "\\. " + ABBR_MONTH + " " + YEAR)                      // ab 10. Jan. 1917
                    + ")(?: – .+)?$");

    private DocumentTitleBackfillMatcher() {
    }

    static boolean isOverwritable(String title, String index, String location) {
        if (title == null || index == null || index.isBlank()) {
            return false; // fail closed
        }
        if (!title.startsWith(index)) {
            return false; // index is matched LITERALLY, never as a regex
        }
        String tail = title.substring(index.length());
        if (tail.isEmpty()) {
            return true; // exactly {index}
        }
        if (!tail.startsWith(SEPARATOR)) {
            return false;
        }
        String body = tail.substring(SEPARATOR.length());
        if (DATE_LABEL_WITH_OPTIONAL_LOCATION.matcher(body).matches()) {
            return true; // {dateLabel} (+ optional trailing location)
        }
        // No date label: the lone segment must equal the document's current location to be
        // distinguished from hand-written prose.
        return location != null && !location.isBlank() && body.equals(location);
    }

    private static String monthAlternation(String pattern) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.GERMAN);
        Set<String> tokens = new LinkedHashSet<>();
        for (int month = 1; month <= 12; month++) {
            tokens.add(formatter.format(LocalDate.of(2000, month, 15)));
        }
        return tokens.stream().map(Pattern::quote).collect(Collectors.joining("|", "(?:", ")"));
    }
}
