/**
 * Client-side mirror of app/Submission/Validation/Rule_Validator.php — Free rules.
 *
 * Pure, dependency-free ESM so it can be bundled into the frontend AND imported
 * by the Node parity check (tests/validate-vectors.mjs). The server verdict is
 * canonical; this exists only for instant on-blur feedback. Both sides are kept
 * in lockstep by tests/validation-vectors.json.
 *
 * Edition boundary (docs/wporg-free-runtime-separation.md §7): this module
 * implements only the Free rule set. Pro rule twins (custom, creditcard, iban,
 * multiplerules) live in src/frontend-pro/pro-validation-rules.js and register
 * themselves through registerRules(); an unregistered rule id passes (the
 * server has the only verdict).
 */

export const FREE_RULES = [
    'alpha', 'alphaspaces', 'alphanumeric', 'numeric', 'numericspaces',
    'minlength', 'maxlength', 'alphaaccented', 'alphaspacesaccented',
    'alphanumericaccented', 'email', 'validurl', 'ipaddress', 'uszipcode',
    'phonenumber', 'sameas',
];

/**
 * Extension point for edition rule sets. Each impl receives
 * (stringValue, params, submission, checkRule) and returns boolean.
 */
const EXTRA_RULES = {};
const EXTRA_MESSAGES = {};

export function registerRules(impls, messages = {}) {
    Object.assign(EXTRA_RULES, impls);
    Object.assign(EXTRA_MESSAGES, messages);
}

// Codepoint count, matching PHP mb_strlen (not UTF-16 unit count).
function utf8Length(s) {
    return [...s].length;
}

function isEmail(s) {
    // Matches the vectors' expectations of PHP FILTER_VALIDATE_EMAIL:
    // local@domain.tld, rejects "a@b" (no dot) and malformed input.
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}

function isValidUrl(s) {
    const hasScheme = /^https?:\/\//i.test(s);
    const hasWww = /^www\./i.test(s);
    if (!hasScheme && !hasWww) {
        return false;
    }
    const probe = hasScheme ? s : 'https://' + s;
    try {
        // eslint-disable-next-line no-new
        new URL(probe);
        return true;
    } catch (e) {
        return false;
    }
}

function isIpAddress(s) {
    // IPv4 with per-octet range check.
    const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
    if (v4) {
        return v4.slice(1).every((o) => Number(o) <= 255);
    }
    // IPv6 — accepts compressed forms like ::1. Good enough to mirror
    // FILTER_VALIDATE_IP for the shapes the vectors exercise.
    if (s.indexOf(':') === -1) {
        return false;
    }
    return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:(:[0-9a-fA-F]{1,4}){1,7}|::)$/.test(s);
}

/**
 * Run one named rule. Returns true on pass, false on fail. Empty handling and
 * the enabled/rule/message wrapping live in validateAdvanced() below.
 *
 * @param {string} rule
 * @param {*} value
 * @param {object} params
 * @param {object} submission  name => value map (for sameas)
 * @returns {boolean}
 */
export function checkRule(rule, value, params = {}, submission = {}) {
    const s = value == null ? '' : String(value);

    switch (rule) {
        case 'alpha': return /^[A-Za-z]+$/.test(s);
        case 'alphaspaces': return /^[A-Za-z ]+$/.test(s);
        case 'alphanumeric': return /^[A-Za-z0-9]+$/.test(s);
        case 'numeric': return /^[0-9]+$/.test(s);
        case 'numericspaces': return /^[0-9 ]+$/.test(s);
        case 'alphaaccented': return /^\p{L}+$/u.test(s);
        case 'alphaspacesaccented': return /^[\p{L} ]+$/u.test(s);
        case 'alphanumericaccented': return /^[\p{L}\p{N}]+$/u.test(s);
        case 'minlength': {
            const n = parseInt(params.n || 0, 10);
            return n <= 0 || utf8Length(s) >= n;
        }
        case 'maxlength': {
            const n = parseInt(params.n || 0, 10);
            return n <= 0 || utf8Length(s) <= n;
        }
        case 'email': return isEmail(s);
        case 'validurl': return isValidUrl(s);
        case 'ipaddress': return isIpAddress(s);
        case 'uszipcode': return /^\d{5}(-\d{4})?$/.test(s);
        case 'phonenumber': return /^[\d\s\-+().]+$/.test(s);
        case 'sameas': {
            const peer = typeof params._resolved_target_name === 'string' ? params._resolved_target_name : '';
            if (peer === '') {
                return true;
            }
            const peerValue = submission[peer];
            return s === (peerValue == null ? '' : String(peerValue));
        }
        default: {
            const impl = EXTRA_RULES[rule];
            if (impl) {
                return impl(s, params, submission, checkRule);
            }
            return true; // unknown / server-only rule id passes (server decides)
        }
    }
}

export function isEmptyValue(v) {
    if (v == null) return true;
    if (typeof v === 'string') return v.trim() === '';
    if (Array.isArray(v)) return v.length === 0;
    return false;
}

const DEFAULT_MESSAGES = {
    alpha: 'Please use letters only.',
    alphaspaces: 'Please use letters and spaces only.',
    alphanumeric: 'Please use letters and numbers only.',
    numeric: 'Please use numbers only.',
    numericspaces: 'Please use numbers and spaces only.',
    alphaaccented: 'Please use letters only.',
    alphaspacesaccented: 'Please use letters and spaces only.',
    alphanumericaccented: 'Please use letters and numbers only.',
    minlength: 'Please enter at least {n} characters.',
    maxlength: 'Please enter no more than {n} characters.',
    email: 'Please enter a valid email address.',
    validurl: 'Please enter a valid URL.',
    ipaddress: 'Please enter a valid IP address.',
    uszipcode: 'Please enter a valid ZIP code.',
    phonenumber: 'Please enter a valid phone number.',
    sameas: 'The values do not match.',
};

function defaultMessage(rule, params) {
    const tpl = DEFAULT_MESSAGES[rule] || EXTRA_MESSAGES[rule] || 'Invalid value.';
    return tpl.replace('{n}', String(parseInt(params.n || 0, 10)));
}

/**
 * Mirror of Rule_Validator::run(). Returns null on pass, else the error message
 * (admin override wins over the rule default).
 *
 * @param {object} validation  { advanced_enabled, rule, params, message }
 * @param {*} value
 * @param {object} submission
 * @returns {string|null}
 */
export function validateAdvanced(validation, value, submission = {}) {
    if (!validation || !validation.advanced_enabled) {
        return null;
    }
    const rule = typeof validation.rule === 'string' ? validation.rule : '';
    if (rule === '') {
        return null;
    }
    if (isEmptyValue(value)) {
        return null;
    }
    if (checkRule(rule, value, validation.params || {}, submission)) {
        return null;
    }
    const override = typeof validation.message === 'string' ? validation.message.trim() : '';
    return override !== '' ? override : defaultMessage(rule, validation.params || {});
}
