(function () {
  /**
   * Normalise numeric input.
   */
  function normalizeNumericInput(raw) {
    if (raw == null) return '';
    return String(raw).trim().replace(/\s+/g, '');
  }

  /**
   * Parse loose number.
   */
  function parseLooseNumber(raw) {
    const normalized = normalizeNumericInput(raw).replace(',', '.');
    if (!normalized) return NaN;
    const parsed = Number(normalized);
    return Number.isFinite(parsed) ? parsed : NaN;
  }

  /**
   * Formate french amount.
   */
  function formatFrenchAmount(value, digits) {
    const number = Number(value);
    if (!Number.isFinite(number)) return '0.00';
    return number.toLocaleString('fr-FR', {
      minimumFractionDigits: digits,
      maximumFractionDigits: digits
    });
  }

  /**
   * Synchronise floating labels.
   */
  function syncFloatingLabels(root) {
    const scope = root && root.querySelectorAll ? root : document;
    const labels = scope.querySelectorAll('.rc-field-label[for]');
    labels.forEach(function (label) {
      const fieldId = label.getAttribute('for');
      if (!fieldId) return;
      const field = document.getElementById(fieldId);
      if (!field) return;

      const inputField = label.closest('.input-field');
      const container = inputField || label.parentElement;
      if (container) {
        container.classList.add('rc-has-floating-label');
      }

      label.classList.add('rc-floating-label');
      if (label.dataset.rcFloatBound === 'true') {
        // Déjà lié: on force juste un resync visuel.
        const hasValue = String(field.value || '').trim() !== '';
        label.classList.toggle('active', hasValue || document.activeElement === field);
        return;
      }

      const hasPrefix = Boolean(inputField && inputField.querySelector('.prefix'));
      label.style.left = hasPrefix ? '3rem' : '0.75rem';

      const syncState = function () {
        const hasValue = String(field.value || '').trim() !== '';
        label.classList.toggle('active', hasValue || document.activeElement === field);
      };

      field.addEventListener('focus', syncState);
      field.addEventListener('blur', syncState);
      field.addEventListener('input', syncState);
      field.addEventListener('change', syncState);
      label.dataset.rcFloatBound = 'true';
      syncState();
    });

    if (window.M && typeof window.M.updateTextFields === 'function') {
      window.M.updateTextFields();
    }
  }

  /**
   * Securise error slot.
   */
  function ensureErrorSlot(field, customErrorId) {
    if (!field) return null;

    if (customErrorId) {
      const found = document.getElementById(customErrorId);
      if (found) return found;
    }

    if (field.id) {
      const byConvention = document.getElementById(field.id + '-error');
      if (byConvention) return byConvention;
    }

    if (field.dataset && field.dataset.errorTarget) {
      const byData = document.getElementById(field.dataset.errorTarget);
      if (byData) return byData;
    }

    let sibling = field.parentElement ? field.parentElement.querySelector('.rc-inline-error') : null;
    if (sibling) return sibling;

    sibling = document.createElement('span');
    sibling.className = 'helper-text red-text rc-inline-error';
    sibling.style.display = 'none';
    field.insertAdjacentElement('afterend', sibling);
    return sibling;
  }

  /**
   * Gere pick field label.
   */
  function pickFieldLabel(field, fallbackLabel) {
    if (fallbackLabel) return fallbackLabel;
    if (!field) return 'Champ';

    if (field.dataset && field.dataset.rcLabel) return field.dataset.rcLabel;

    const directLabel = field.id ? document.querySelector('label[for="' + field.id + '"]') : null;
    if (directLabel && directLabel.textContent.trim()) return directLabel.textContent.trim();

    const cardLabel = field.closest('.row, .input-field, td, .card-content')?.querySelector('.rc-field-label');
    if (cardLabel && cardLabel.textContent.trim()) return cardLabel.textContent.trim();

    const th = field.closest('td')?.parentElement?.querySelector('th');
    if (th && th.textContent.trim()) return th.textContent.trim();

    return field.name || field.id || 'Champ';
  }

  /**
   * Met a jour field error.
   */
  function setFieldError(field, errorSlot, message) {
    if (!errorSlot) return;
    errorSlot.textContent = message || '';
    errorSlot.style.display = message ? 'block' : 'none';

    if (field) {
      if (message) {
        field.classList.add('invalid');
      } else {
        field.classList.remove('invalid');
      }
    }
  }

  /**
   * Verifie forbidden numeric chars.
   */
  function hasForbiddenNumericChars(value) {
    return /[A-Za-z€$£¥]/.test(value);
  }

  /**
   * Gere validate numeric.
   */
  function validateNumeric(value, options) {
    const raw = normalizeNumericInput(value);
    const decimals = Number.isInteger(options.decimals) ? options.decimals : 2;
    const label = options.label || 'Champ';
    const required = Boolean(options.required);

    if (!raw) {
      if (required) {
        return { valid: false, message: label + ' est obligatoire.' };
      }
      return { valid: true };
    }

    if (hasForbiddenNumericChars(raw)) {
      return { valid: false, message: label + ' doit contenir uniquement des chiffres, avec virgule ou point décimal.' };
    }

    if (/[^0-9.,-]/.test(raw)) {
      return { valid: false, message: label + ' contient des caractères interdits.' };
    }

    if ((raw.match(/,/g) || []).length > 1 || (raw.match(/\./g) || []).length > 1) {
      return { valid: false, message: label + ' a un format invalide.' };
    }

    if (raw.includes('-') && raw.indexOf('-') !== 0) {
      return { valid: false, message: label + ' a un format invalide.' };
    }

    const decimalRegex = new RegExp('^-?\\d+(?:[.,]\\d{1,' + decimals + '})?$');
    if (!decimalRegex.test(raw)) {
      return { valid: false, message: label + ' doit être un nombre valide (max ' + decimals + ' décimales).' };
    }

    const number = parseLooseNumber(raw);
    if (!Number.isFinite(number)) {
      return { valid: false, message: label + ' doit être un nombre valide.' };
    }

    if (typeof options.min === 'number' && number < options.min) {
      return { valid: false, message: label + ' doit être supérieur ou égal à ' + options.min + '.' };
    }

    if (typeof options.max === 'number' && number > options.max) {
      return { valid: false, message: label + ' doit être inférieur ou égal à ' + options.max + '.' };
    }

    if (options.positive && number <= 0) {
      return { valid: false, message: label + ' doit être strictement supérieur à 0.' };
    }

    return { valid: true, normalized: raw.replace(',', '.') };
  }

  /**
   * Gere validate integer.
   */
  function validateInteger(value, options) {
    const raw = normalizeNumericInput(value);
    const label = options.label || 'Champ';
    const required = Boolean(options.required);

    if (!raw) {
      if (required) {
        return { valid: false, message: label + ' est obligatoire.' };
      }
      return { valid: true };
    }

    if (!/^\d+$/.test(raw)) {
      return { valid: false, message: label + ' doit contenir uniquement des chiffres entiers.' };
    }

    const number = Number(raw);
    if (typeof options.min === 'number' && number < options.min) {
      return { valid: false, message: label + ' doit être supérieur ou égal à ' + options.min + '.' };
    }
    if (typeof options.max === 'number' && number > options.max) {
      return { valid: false, message: label + ' doit être inférieur ou égal à ' + options.max + '.' };
    }

    return { valid: true };
  }

  /**
   * Gere validate immat.
   */
  function validateImmat(value, options) {
    const raw = String(value || '').trim().toUpperCase();
    const label = options.label || 'Immatriculation';

    if (!raw) {
      if (options.required) {
        return { valid: false, message: label + ' est obligatoire.' };
      }
      return { valid: true, normalized: '' };
    }

    if (!/^[A-Z0-9-]+$/.test(raw)) {
      return { valid: false, message: label + ' doit contenir uniquement des lettres majuscules, chiffres et tirets.' };
    }

    return { valid: true, normalized: raw };
  }

  /**
   * Gere validate text safe.
   */
  function validateTextSafe(value, options) {
    const raw = String(value || '').trim();
    const label = options.label || 'Champ';

    if (!raw) {
      if (options.required) {
        return { valid: false, message: label + ' est obligatoire.' };
      }
      return { valid: true };
    }

    if (/[<>;&"]/g.test(raw)) {
      return { valid: false, message: label + ' contient des caractères interdits (<, >, &, ;, ").' };
    }

    const pattern = options.pattern || /^[A-Za-zÀ-ÖØ-öø-ÿ0-9\s'.,:/()\-_]+$/;
    if (!pattern.test(raw)) {
      return { valid: false, message: label + ' contient des caractères non autorisés.' };
    }

    return { valid: true };
  }

  /**
   * Gere validate number or consult.
   */
  function validateNumberOrConsult(raw, options) {
    const value = String(raw || '').trim();
    const lower = value.toLowerCase();

    if (!value) {
      if (options.required) {
        return { valid: false, message: (options.label || 'Champ') + ' est obligatoire.' };
      }
      return { valid: true };
    }

    if (lower === 'nous consulter') {
      return { valid: true };
    }

    return validateNumeric(value, options);
  }

  /**
   * Gere evaluate rule.
   */
  function evaluateRule(field, config) {
    const profile = config.profile;
    const label = pickFieldLabel(field, config.label);
    const required = typeof config.requiredWhen === 'function' ? Boolean(config.requiredWhen(field)) : Boolean(config.required);

    let result;
    switch (profile) {
      case 'numeric':
        result = validateNumeric(field.value, {
          label: label,
          required: required,
          decimals: config.decimals,
          min: config.min,
          max: config.max,
          positive: config.positive
        });
        break;
      case 'integer':
        result = validateInteger(field.value, {
          label: label,
          required: required,
          min: config.min,
          max: config.max
        });
        break;
      case 'immat':
        result = validateImmat(field.value, {
          label: label,
          required: required
        });
        break;
      case 'text':
        result = validateTextSafe(field.value, {
          label: label,
          required: required,
          pattern: config.pattern
        });
        break;
      case 'number_or_consulter':
        result = validateNumberOrConsult(field.value, {
          label: label,
          required: required,
          decimals: config.decimals,
          min: config.min,
          max: config.max,
          positive: config.positive
        });
        break;
      default:
        result = { valid: true };
    }

    if (result.normalized != null && config.normalize !== false && field.value !== result.normalized) {
      field.value = result.normalized;
    }

    return {
      valid: result.valid,
      message: result.message || '',
      label: label
    };
  }

  /**
   * Gere create summary element.
   */
  function createSummaryElement(summaryId) {
    if (!summaryId) return null;
    return document.getElementById(summaryId);
  }

  /**
   * Gere create guard.
   */
  function createGuard(options) {
    const summary = createSummaryElement(options.summaryId);
    const summaryTitle = String(options.summaryTitle || 'Impossible de continuer car :');
    const state = {
      fieldRules: new Map(),
      fieldErrors: new Map(),
      externalChecks: new Map(),
      targetButtons: [],
      onChange: typeof options.onChange === 'function' ? options.onChange : null
    };

    const targetSelectors = Array.isArray(options.blockTargets) ? options.blockTargets : [];
    targetSelectors.forEach(function (selector) {
      document.querySelectorAll(selector).forEach(function (button) {
        if (!button.dataset.rcOriginalDisabled) {
          button.dataset.rcOriginalDisabled = button.disabled ? 'true' : 'false';
        }
        state.targetButtons.push(button);
      });
    });

    /**
     * Gere apply summary.
     */
    function applySummary(messages) {
      if (!summary) return;
      if (!messages.length) {
        summary.style.display = 'none';
        summary.innerHTML = '';
        return;
      }

      const uniqueMessages = Array.from(new Set(messages));
      const list = uniqueMessages.map(function (msg) {
        return '<li>' + msg + '</li>';
      }).join('');

      summary.innerHTML = [
        '<div class="rc-blocking-title">' + summaryTitle + '</div>',
        '<ul class="rc-blocking-list">',
        list,
        '</ul>'
      ].join('');
      summary.style.display = 'block';
    }

    /**
     * Gere apply blocking.
     */
    function applyBlocking(hasErrors) {
      state.targetButtons.forEach(function (button) {
        if (!button) return;
        if (hasErrors) {
          button.disabled = true;
          button.dataset.rcInvalid = 'true';
          return;
        }

        if (button.dataset.rcInvalid === 'true') {
          button.dataset.rcInvalid = 'false';
          button.disabled = button.dataset.rcOriginalDisabled === 'true';
        }
      });
    }

    /**
     * Gere recompute.
     */
    function recompute() {
      const messages = [];

      state.fieldErrors.forEach(function (error) {
        if (error && error.message) {
          messages.push(error.message);
        }
      });

      state.externalChecks.forEach(function (entry, key) {
        try {
          const checkResult = typeof entry.fn === 'function' ? entry.fn() : { valid: true };
          if (!checkResult || checkResult.valid === false) {
            const message = checkResult && checkResult.message ? checkResult.message : entry.message || key;
            messages.push(message);
          }
        } catch (error) {
          console.warn('[RC guard] Check externe en erreur:', key, error);
          messages.push(entry.message || ('Validation en erreur: ' + key));
        }
      });

      applySummary(messages);
      applyBlocking(messages.length > 0);

      if (state.onChange) {
        state.onChange(messages);
      }

      return messages;
    }

    /**
     * Gere validate field.
     */
    function validateField(field) {
      try {
        const config = state.fieldRules.get(field);
        if (!config) return;

        if (!field || !field.isConnected || field.disabled) {
          if (config.errorSlot) setFieldError(field, config.errorSlot, '');
          state.fieldErrors.delete(config.key);
          recompute();
          return;
        }

        if (typeof config.activeWhen === 'function' && !config.activeWhen(field)) {
          setFieldError(field, config.errorSlot, '');
          state.fieldErrors.delete(config.key);
          recompute();
          return;
        }

        // Sécurité défaut: sans activeWhen explicite, on ignore un champ masqué.
        if (typeof config.activeWhen !== 'function' && field.type !== 'hidden' && field.offsetParent === null) {
          setFieldError(field, config.errorSlot, '');
          state.fieldErrors.delete(config.key);
          recompute();
          return;
        }

        const result = evaluateRule(field, config);
        setFieldError(field, config.errorSlot, result.valid ? '' : result.message);

        if (result.valid) {
          state.fieldErrors.delete(config.key);
        } else {
          state.fieldErrors.set(config.key, {
            message: result.message
          });
        }

        recompute();
      } catch (error) {
        const config = state.fieldRules.get(field);
        const key = config && config.key ? config.key : 'champ-inconnu';
        console.warn('[RC guard] Validation en erreur:', key, error);
        if (config && config.errorSlot) {
          setFieldError(field, config.errorSlot, 'Erreur de validation, rechargez la page.');
        }
        state.fieldErrors.set(key, {
          message: 'Erreur de validation sur un champ du formulaire.'
        });
        recompute();
      }
    }

    /**
     * Gere attach field.
     */
    function attachField(field, config) {
      if (!field) return;

      const key = config.key || (field.id ? field.id : (field.name ? field.name : ('field_' + state.fieldRules.size)));
      if (field.dataset && field.dataset.rcGuardAttached === 'true' && state.fieldRules.has(field)) return;

      const fieldConfig = Object.assign({}, config, {
        key: key,
        errorSlot: ensureErrorSlot(field, config.errorId)
      });

      state.fieldRules.set(field, fieldConfig);
      if (field.dataset) {
        field.dataset.rcGuardAttached = 'true';
      }

      const handler = function () {
        validateField(field);
      };

      field.addEventListener('input', handler);
      field.addEventListener('change', handler);
      field.addEventListener('blur', handler);

      validateField(field);
    }

    /**
     * Enregistre field.
     */
    function registerField(selectorOrElement, config) {
      if (!selectorOrElement || !config || !config.profile) return;
      if (typeof selectorOrElement === 'string') {
        document.querySelectorAll(selectorOrElement).forEach(function (element) {
          attachField(element, config);
        });
        return;
      }
      attachField(selectorOrElement, config);
    }

    /**
     * Gere observe.
     */
    function observe(selector, config) {
      if (!selector || !config || !config.profile) return;

      registerField(selector, config);

      const observer = new MutationObserver(function () {
        registerField(selector, config);
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true
      });

      return observer;
    }

    /**
     * Enregistre external.
     */
    function registerExternal(key, fn, fallbackMessage) {
      if (!key || typeof fn !== 'function') return;
      state.externalChecks.set(key, {
        fn: fn,
        message: fallbackMessage || ''
      });
      recompute();
    }

    /**
     * Gere refresh.
     */
    function refresh() {
      state.fieldRules.forEach(function (_cfg, field) {
        validateField(field);
      });
      return recompute();
    }

    return {
      registerField: registerField,
      observe: observe,
      registerExternal: registerExternal,
      refresh: refresh,
      formatFrenchAmount: formatFrenchAmount,
      parseLooseNumber: parseLooseNumber
    };
  }

  window.RCValidationUtils = {
    createGuard: createGuard,
    parseLooseNumber: parseLooseNumber,
    formatFrenchAmount: formatFrenchAmount,
    syncFloatingLabels: syncFloatingLabels
  };
})();
