package org.raddatz.familienarchiv.importing;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.person.PersonGeneration;
import org.raddatz.familienarchiv.person.PersonService;
import org.raddatz.familienarchiv.person.PersonType;
import org.raddatz.familienarchiv.person.PersonUpsertCommand;
import org.springframework.stereotype.Component;

import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Loads {@code canonical-persons.xlsx} (the register) into the person domain via
 * {@link PersonService}, upserting each person by the normalizer {@code person_id}
 * (source_ref). Register persons are confident identities, so {@code provisional} is
 * driven by the sheet's already-clean value (normally {@code False}).
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class PersonRegisterImporter {

    static final List<String> REQUIRED_HEADERS = List.of("person_id", "last_name", "first_name", "provisional");

    // Matches a leading optional G then a signed integer. Anchored at the
    // start so noise can't slip in before the number, but tolerant of trailing
    // commentary cells (e.g. "G 2  de Gruyter") since curated rows sometimes
    // carry an inline note. Out-of-range values are caught by the post-parse
    // range guard, not by the regex.
    private static final Pattern GENERATION_PATTERN = Pattern.compile("^\\s*G?\\s*(-?\\d+)");

    private final PersonService personService;

    public int load(File artifact) {
        List<CanonicalSheetReader.Row> rows = CanonicalSheetReader.readRows(artifact, REQUIRED_HEADERS);
        int processed = 0;
        for (CanonicalSheetReader.Row row : rows) {
            String personId = row.get("person_id");
            if (personId.isBlank()) continue;
            personService.upsertBySourceRef(toCommand(row, personId));
            processed++;
        }
        log.info("Imported {} register persons from {}", processed, artifact.getName());
        return processed;
    }

    private PersonUpsertCommand toCommand(CanonicalSheetReader.Row row, String personId) {
        return PersonUpsertCommand.builder()
                .sourceRef(personId)
                .lastName(blankToNull(row.get("last_name")))
                .firstName(blankToNull(row.get("first_name")))
                .maidenName(blankToNull(row.get("maiden_name")))
                .notes(blankToNull(row.get("notes")))
                .birthYear(yearOf(row.get("birth_date")))
                .deathYear(yearOf(row.get("death_date")))
                .generation(parseGeneration(row.get("generation"), personId))
                .personType(PersonType.PERSON)
                .provisional(Boolean.parseBoolean(row.get("provisional")))
                .build();
    }

    /**
     * Parses an optional {@code G n} generation cell. Returns null for blanks,
     * non-matching strings, and any value outside the {@link PersonGeneration}
     * bounds (mirroring the V70 CHECK). Out-of-range values log a WARN but
     * never abort the batch — REQ-IMP-001.
     */
    static Integer parseGeneration(String raw, String personId) {
        if (raw == null || raw.isBlank()) return null;
        Matcher m = GENERATION_PATTERN.matcher(raw);
        if (!m.find()) return null;
        int parsed = Integer.parseInt(m.group(1));
        if (parsed < PersonGeneration.MIN_GENERATION || parsed > PersonGeneration.MAX_GENERATION) {
            log.warn("Skipping out-of-range generation '{}' for row {}", raw, personId);
            return null;
        }
        log.debug("Parsed generation '{}' for person {}", raw, personId);
        return parsed;
    }

    private static Integer yearOf(String isoDate) {
        if (isoDate == null || isoDate.isBlank()) return null;
        try {
            return LocalDate.parse(isoDate.trim()).getYear();
        } catch (DateTimeParseException e) {
            return null;
        }
    }

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