Source: module.js

const FORM_DATA_KEY = "formData";
const REGISTRATIONS_KEY = "registrations";


/**
 * Calculate a person's age in years.
 * 
 * @param {Date} birthDate - The birth date of the person.
 * @returns {Number} - The age in years of the person.
 */
function calculateAge(birthDate) {
    if (!birthDate) {
        throw new Error("missing param birthDate");
    }
    if (!(birthDate instanceof Date)) {
        throw new Error("birthDate is not a Date");
    }
    if (isNaN(birthDate.getTime())) {
        throw new Error("birthDate is not a valid Date");
    }
    let dateDiff = new Date(Date.now() - birthDate.getTime());
    let age = Math.abs(dateDiff.getUTCFullYear() - 1970);
    return age;
}

/**
 * Validate the name string.
 * 
 * @param {String} name - The name string to validate.
 * @returns {String} - The validated name string.
 */
function validateName(name) {
    if (!name) {
        throw new Error("name is required");
    }
    if (typeof name !== 'string') {
        throw new Error("name must be a string");
    }
    const trimmedName = name.trim();
    if (!trimmedName.match(/^[\p{L}]+(?:[-' ][\p{L}]+)*$/u)) {
        throw new Error("name must be a valid name");
    }
    return trimmedName;
}

/**
 * Validate the prenom string.
 * 
 * @param {String} prenom - The prenom string to validate.
 * @returns {String} - The validated prenom string.
 */
function validatePrenom(prenom) {
    if (!prenom) {
        throw new Error("prenom is required");
    }
    if (typeof prenom !== 'string') {
        throw new Error("prenom must be a string");
    }
    const trimmedPrenom = prenom.trim();
    if (!trimmedPrenom.match(/^[\p{L}]+(?:[-' ][\p{L}]+)*$/u)) {
        throw new Error("prenom must be a valid prenom");
    }
    return trimmedPrenom;
}

/**
 * Validate the email string.
 * 
 * @param {String} email - The email string to validate.
 * @returns {String} - The validated email string.
 */
function validateEmail(email) {
    if (!email) {
        throw new Error("email is required");
    }
    if (typeof email !== 'string') {
        throw new Error("email must be a string");
    }
    const trimmedEmail = email.trim();
    if (!trimmedEmail.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
        throw new Error("email must be a valid email address");
    }
    return trimmedEmail;
}

/**
 * Validate the date of birth string.
 * 
 * @param {String} dateOfBirth - The date of birth string to validate.
 * @returns {String} - The validated date of birth string.
 */
function validateDateOfBirth(dateOfBirth) {
    if (!dateOfBirth) {
        throw new Error("date of birth is required");
    }
    if (typeof dateOfBirth !== 'string') {
        throw new Error("date of birth must be a string");
    }
    const trimmedDateOfBirth = dateOfBirth.trim();
    const parsedDate = parseDateOfBirth(trimmedDateOfBirth);
    if (!parsedDate) {
        throw new Error("date of birth must be a valid date");
    }
    if (!isAdult(parsedDate)) {
        throw new Error("date of birth must be at least 18 years old");
    }
    return trimmedDateOfBirth;
}

/**
 * Validate the ville string.
 * 
 * @param {String} ville - The ville string to validate.
 * @returns {String} - The validated ville string.
 */
function validateVille(ville) {
    if (!ville) {
        throw new Error("ville is required");
    }
    if (typeof ville !== 'string') {
        throw new Error("ville must be a string");
    }
    const trimmedVille = ville.trim();
    if (!trimmedVille.match(/^[\p{L}]+(?:[-' ][\p{L}]+)*$/u)) {
        throw new Error("ville must be a valid ville");
    }
    return trimmedVille;
}

/**
 * Validate the code postal string.
 * 
 * @param {String} codePostal - The code postal string to validate.
 * @returns {String} - The validated code postal string.
 */
function validateCodePostal(codePostal) {
    if (!codePostal) {
        throw new Error("code postal is required");
    }
    if (typeof codePostal !== 'string') {
        throw new Error("code postal must be a string");
    }
    const trimmedCodePostal = codePostal.trim();
    if (!isFrenchCodePostal(trimmedCodePostal)) {
        throw new Error("code postal must be a valid code postal");
    }
    return trimmedCodePostal;
}

/**
 * Validate the form data object.
 * 
 * @param {FormData} formData - The form data object to validate.
 * @returns {Object} - The validated form data object.
 */
function validateFormData(formData) {
    if (!formData) {
        throw new Error("form data is required");
    }
    if (!(formData instanceof FormData)) {
        throw new Error("form data must be a FormData");
    }
    const data = Object.fromEntries(formData);
    return {
        nom: validateName(data.nom),
        prenom: validatePrenom(data.prenom),
        email: validateEmail(data.email),
        dateOfBirth: validateDateOfBirth(data.dateOfBirth),
        ville: validateVille(data.ville),
        codePostal: validateCodePostal(data.codePostal)
    };
}

/**
 * Check if the date of birth string is an adult.
 * 
 * @param {Date} dateOfBirth - The date of birth to check.
 * @returns {Boolean} - True if the date of birth string is an adult, false otherwise.
 */
function isAdult(dateOfBirth) {
    const age = calculateAge(dateOfBirth);
    return age >= 18;
}

/**
 * Parse date of birth from supported formats:
 * YYYY-MM-DD, YYYY/MM/DD, DD/MM/YYYY, DD-MM-YYYY.
 *
 * @param {String} rawDateOfBirth - The date string to parse.
 * @returns {Date|null} - Parsed date if valid, otherwise null.
 */
function parseDateOfBirth(rawDateOfBirth) {
    const dateOfBirth = rawDateOfBirth.trim();

    const yearFirstMatch = dateOfBirth.match(/^(\d{4})[-/](\d{2})[-/](\d{2})$/);
    if (yearFirstMatch) {
        return buildValidDate(
            Number(yearFirstMatch[1]),
            Number(yearFirstMatch[2]),
            Number(yearFirstMatch[3])
        );
    }

    const dayFirstMatch = dateOfBirth.match(/^(\d{2})[-/](\d{2})[-/](\d{4})$/);
    if (dayFirstMatch) {
        return buildValidDate(
            Number(dayFirstMatch[3]),
            Number(dayFirstMatch[2]),
            Number(dayFirstMatch[1])
        );
    }

    return null;
}

/**
 * Build a date and ensure day/month/year are real.
 *
 * @param {Number} year - Year component.
 * @param {Number} month - Month component (1-12).
 * @param {Number} day - Day component (1-31).
 * @returns {Date|null} - Date when valid, otherwise null.
 */
function buildValidDate(year, month, day) {
    const date = new Date(year, month - 1, day);
    if (
        date.getFullYear() !== year ||
        date.getMonth() !== month - 1 ||
        date.getDate() !== day
    ) {
        return null;
    }
    return date;
}

/**
 * Check if the code postal is a valid French code postal.
 * 
 * @param {String} codePostal - The code postal string to check.
 * @returns {Boolean} - True if the code postal is a valid French code postal, false otherwise.
 */
function isFrenchCodePostal(codePostal) {
    if (typeof codePostal !== 'string') {
        return false;
    }

    const trimmedCodePostal = codePostal.trim();

    // Metropolitan France (01-95), including Corsica postal codes (20xxx).
    const metropolitanPattern = /^(?:0[1-9]|[1-8]\d|9[0-5])\d{3}$/;
    // Overseas departments/collectivities (971-978, 984-989).
    const overseasPattern = /^(?:97[1-8]|98[4-9])\d{2}$/;

    return metropolitanPattern.test(trimmedCodePostal) || overseasPattern.test(trimmedCodePostal);
}

/**
 * Save the form data object to localStorage.
 * 
 * @param {Object} formData - The form data object to save.
 */
function saveFormData(formData) {
    try {
        localStorage.setItem(FORM_DATA_KEY, JSON.stringify(formData));
    } catch (error) {
        throw new Error("unable to save form data");
    }
}

/**
 * Get the form data object from localStorage.
 * 
 * @returns {Object} - The form data object.
 */
function getFormData() {
    const rawFormData = localStorage.getItem(FORM_DATA_KEY);
    if (!rawFormData) {
        return null;
    }
    try {
        return JSON.parse(rawFormData);
    } catch (error) {
        throw new Error("unable to parse saved form data");
    }
}

/**
 * Clear the form data object from localStorage.
 */
function clearFormData() {
    localStorage.removeItem(FORM_DATA_KEY);
}

/**
 * Save registrations list to localStorage.
 *
 * @param {Object[]} registrations - Registrations list.
 */
function saveRegistrations(registrations) {
    try {
        localStorage.setItem(REGISTRATIONS_KEY, JSON.stringify(registrations));
    } catch (error) {
        throw new Error("unable to save registrations");
    }
}

/**
 * Get registrations list from localStorage.
 *
 * @returns {Object[]} - Registrations list.
 */
function getRegistrations() {
    const rawRegistrations = localStorage.getItem(REGISTRATIONS_KEY);
    if (!rawRegistrations) {
        return [];
    }
    try {
        const parsedRegistrations = JSON.parse(rawRegistrations);
        if (!Array.isArray(parsedRegistrations)) {
            throw new Error("invalid registrations format");
        }
        return parsedRegistrations;
    } catch (error) {
        throw new Error("unable to parse saved registrations");
    }
}

/**
 * Append a validated registration to localStorage.
 *
 * @param {Object} registration - Validated registration data.
 * @returns {Object} - Saved registration data.
 */
function appendRegistration(registration) {
    const registrations = getRegistrations();
    const nextRegistrations = [...registrations, registration];
    saveRegistrations(nextRegistrations);
    return registration;
}

/**
 * Handle the form submission event.
 * 
 * @param {Event} e - The form submission event.
 */
function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    const validatedData = validateFormData(formData);
    appendRegistration(validatedData);
    return validatedData;
}

export { calculateAge, validateName, validatePrenom, validateEmail, validateDateOfBirth, validateVille, validateCodePostal, validateFormData, isAdult, isFrenchCodePostal, saveFormData, getFormData, clearFormData, saveRegistrations, getRegistrations, appendRegistration, handleSubmit };