(function initFivaPassportParser(global) {
  "use strict";

  const COUNTRY_BY_CODE = Object.freeze({
    BGD: { country: "Bangladesh", nationality: "Bangladeshi" },
    IND: { country: "India", nationality: "Indian" },
    PAK: { country: "Pakistan", nationality: "Pakistani" },
    NPL: { country: "Nepal", nationality: "Nepalese" },
    LKA: { country: "Sri Lanka", nationality: "Sri Lankan" },
    USA: { country: "United States", nationality: "American" },
    GBR: { country: "United Kingdom", nationality: "British" },
    CAN: { country: "Canada", nationality: "Canadian" },
    AUS: { country: "Australia", nationality: "Australian" }
  });

  const FIELD_PATTERNS = Object.freeze({
    fullName: [
      /(?:full\s*name|applicant\s*name|name\s*of\s*applicant)\s*[:\-]\s*([A-Z][A-Z\s.'-]{3,})/i,
      /(?:name)\s*[:\-]\s*([A-Z][A-Z\s.'-]{3,})/i
    ],
    passportNumber: [
      /(?:passport\s*(?:no|number))\s*[:\-]?\s*([A-Z0-9]{6,16})/i,
      /\b([A-Z]{1,3}[0-9]{6,9})\b/i
    ],
    nationality: [
      /nationality\s*[:\-]\s*([A-Z][A-Z\s]{3,})/i
    ],
    country: [
      /(?:country|country\s*of\s*birth|place\s*of\s*birth)\s*[:\-]\s*([A-Z][A-Z\s]{3,})/i
    ],
    mission: [
      /(?:indian\s*mission|mission\s*office|mission|ivac\s*center|application\s*center)\s*[:\-]\s*([A-Z][A-Z\s]{3,})/i
    ],
    dateOfBirth: [
      /(?:date\s*of\s*birth|dob|birth\s*date)\s*[:\-]\s*([0-9]{1,4}[\/\-. ][0-9]{1,2}[\/\-. ][0-9]{4}|[0-9]{1,2}[-\/\s][A-Z]{3,9}[-\/\s][0-9]{4})/i
    ],
    gender: [
      /(?:sex|gender)\s*[:\-]\s*(male|female|m|f)\b/i
    ],
    email: [
      /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
    ],
    expectedArrivalDate: [
      /(?:expected\s*date\s*of\s*arrival|arrival\s*date|date\s*of\s*arrival)\s*[:\-]\s*([0-9]{1,4}[\/\-. ][0-9]{1,2}[\/\-. ][0-9]{4}|[0-9]{1,2}[-\/\s][A-Z]{3,9}[-\/\s][0-9]{4})/i
    ],
    visaType: [
      /(?:visa\s*type|type\s*of\s*visa)\s*[:\-]\s*([A-Z][A-Z\s]{3,})/i
    ],
    purpose: [
      /(?:purpose\s*of\s*visit|purpose)\s*[:\-]\s*([A-Z][A-Z\s]{3,})/i
    ],
    passportExpiryDate: [
      /(?:expiry\s*date|date\s*of\s*expiry|passport\s*expiry)\s*[:\-]\s*([0-9]{1,4}[\/\-. ][0-9]{1,2}[\/\-. ][0-9]{4}|[0-9]{1,2}[-\/\s][A-Z]{3,9}[-\/\s][0-9]{4})/i
    ]
  });

  function cleanText(text = "") {
    return String(text)
      .replace(/\u0000/g, " ")
      .replace(/[|]/g, "I")
      .replace(/[“”]/g, "\"")
      .replace(/[‘’]/g, "'")
      .replace(/\s+/g, " ")
      .trim();
  }

  function titleCase(value = "") {
    return String(value)
      .toLowerCase()
      .replace(/\b[a-z]/g, (letter) => letter.toUpperCase())
      .replace(/\s+/g, " ")
      .trim();
  }

  function cleanName(value = "") {
    const compact = String(value || "").replace(/\s+/g, " ").trim();
    if (!compact) return "";
    return /[a-z]/.test(compact) ? compact : titleCase(compact);
  }

  function stripTrailingLabels(value = "") {
    const labelPattern = /\s+(full\s*name|applicant\s*name|name\s*of\s*applicant|passport\s*(?:no|number)|nationality|country\s*of\s*birth|place\s*of\s*birth|country|indian\s*mission|mission\s*office|mission|ivac\s*center|application\s*center|date\s*of\s*birth|dob|birth\s*date|sex|gender|email|e-mail|expected\s*date\s*of\s*arrival|arrival\s*date|date\s*of\s*arrival|visa\s*type|type\s*of\s*visa|purpose\s*of\s*visit|purpose|expiry\s*date|date\s*of\s*expiry|passport\s*expiry)$/i;
    let next = String(value || "").replace(/\s+/g, " ").trim();
    while (labelPattern.test(next)) {
      next = next.replace(labelPattern, "").trim();
    }
    return next;
  }

  function normalizeDate(value = "", mode = "dob") {
    const strict = global.FIVA?.StrictDobValidator?.parseDate?.(value);
    if (strict?.display) {
      const [day, month, year] = strict.display.split("/");
      return `${year}-${month}-${day}`;
    }
    const source = String(value || "").trim();
    const parts = source.match(/^(\d{1,4})[\/\-. ](\d{1,2})[\/\-. ](\d{4})$/);
    if (!parts) return "";

    let year;
    let month;
    let day;

    if (parts[1].length === 4) {
      year = Number(parts[1]);
      month = Number(parts[2]);
      day = Number(parts[3]);
    } else {
      day = Number(parts[1]);
      month = Number(parts[2]);
      year = Number(parts[3]);
    }

    if (!year || month < 1 || month > 12 || day < 1 || day > 31) return "";
    return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
  }

  function normalizeGender(value = "") {
    const source = String(value || "").toLowerCase();
    if (source === "f" || source.includes("female")) return "Female";
    if (source === "m" || source.includes("male")) return "Male";
    return "";
  }

  function normalizeMrzLine(line = "") {
    return String(line || "")
      .toUpperCase()
      .replace(/\s+/g, "")
      .replace(/[«<‹]/g, "<")
      .replace(/[^A-Z0-9<]/g, "");
  }

  function dateFromMrz(value = "", mode = "dob") {
    if (!/^\d{6}$/.test(value)) return "";
    const year = Number(value.slice(0, 2));
    const month = Number(value.slice(2, 4));
    const day = Number(value.slice(4, 6));
    const currentTwoDigits = Number(String(new Date().getFullYear()).slice(-2));
    let fullYear = mode === "expiry" || year <= currentTwoDigits ? 2000 + year : 1900 + year;
    if (mode === "dob" && fullYear > new Date().getFullYear()) fullYear -= 100;
    if (month < 1 || month > 12 || day < 1 || day > 31) return "";
    return `${String(fullYear).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
  }

  function mrzCharValue(char = "<") {
    if (/[0-9]/.test(char)) return Number(char);
    if (/[A-Z]/.test(char)) return char.charCodeAt(0) - 55;
    return 0;
  }

  function mrzCheckDigit(value = "") {
    const weights = [7, 3, 1];
    return String(value || "")
      .split("")
      .reduce((total, char, index) => total + (mrzCharValue(char) * weights[index % weights.length]), 0) % 10;
  }

  function mrzDigitMatches(value = "", digit = "") {
    return /^\d$/.test(String(digit || "")) && mrzCheckDigit(value) === Number(digit);
  }

  function meta(confidence, source) {
    return { confidence, source, verified: confidence >= 90 };
  }

  function canonicalCountryByCode(code = "") {
    const normalizedCode = String(code || "").toUpperCase().replace(/[^A-Z]/g, "");
    const alias = global.FIVA?.NormalizeOptionText?.nationalityAliasResolver?.(normalizedCode);
    const country = alias || COUNTRY_BY_CODE[normalizedCode]?.country || "";
    return country ? country.toUpperCase() : "";
  }

  function parseMrzCodeHint(text = "") {
    const source = String(text || "")
      .toUpperCase()
      .replace(/\s+/g, "")
      .replace(/[«‹]/g, "<");
    const match = source.match(/P<([A-Z]{3})/);
    const country = canonicalCountryByCode(match?.[1] || "");
    if (!country) return {};
    return {
      country,
      nationality: country,
      __fieldMeta: {
        country: meta(98, "mrz"),
        nationality: meta(98, "mrz")
      }
    };
  }

  function parseMrz(text = "") {
    const lines = String(text)
      .split(/\r?\n/)
      .map(normalizeMrzLine)
      .filter((line) => line.length >= 28 && line.includes("<"));

    for (let index = 0; index < lines.length - 1; index += 1) {
      const first = lines[index];
      const second = lines[index + 1];
      if (!first.startsWith("P<") || second.length < 30) continue;

      const issuingCode = first.slice(2, 5).replace(/</g, "");
      const country = canonicalCountryByCode(issuingCode);
      const countryMeta = COUNTRY_BY_CODE[issuingCode] || {};
      const namePart = first.slice(5).replace(/<+$/g, "");
      const [surnameRaw = "", givenRaw = ""] = namePart.split("<<");
      const surname = titleCase(surnameRaw.replace(/</g, " "));
      const givenNames = titleCase(givenRaw.replace(/</g, " "));
      const passportNumber = second.slice(0, 9).replace(/</g, "");
      const nationalityCode = second.slice(10, 13).replace(/</g, "") || issuingCode;
      const nationality = canonicalCountryByCode(nationalityCode) || country;
      const passportValid = mrzDigitMatches(second.slice(0, 9), second.slice(9, 10));
      const dobValid = mrzDigitMatches(second.slice(13, 19), second.slice(19, 20));
      const expiryValid = mrzDigitMatches(second.slice(21, 27), second.slice(27, 28));

      return {
        surname,
        givenNames,
        fullName: [givenNames, surname].filter(Boolean).join(" "),
        passportNumber,
        nationality: nationality || nationalityCode,
        country: country || countryMeta.country || "",
        dateOfBirth: dateFromMrz(second.slice(13, 19), "dob"),
        gender: normalizeGender(second.slice(20, 21)),
        passportExpiryDate: dateFromMrz(second.slice(21, 27), "expiry"),
        __fieldMeta: {
          surname: meta(92, "mrz"),
          givenNames: meta(92, "mrz"),
          fullName: meta(92, "mrz"),
          passportNumber: meta(passportValid ? 99 : 78, "mrz"),
          nationality: meta(95, "mrz"),
          country: meta(92, "mrz"),
          dateOfBirth: meta(dobValid ? 99 : 78, "mrz"),
          gender: meta(94, "mrz"),
          passportExpiryDate: meta(expiryValid ? 99 : 78, "mrz")
        }
      };
    }

    return {};
  }

  function firstPatternValue(text, patterns = []) {
    for (const pattern of patterns) {
      const match = text.match(pattern);
      if (!match) continue;
      return match[1] || match[0] || "";
    }
    return "";
  }

  function parseLabeledFields(text = "") {
    const cleaned = cleanText(text);
    const fields = {};
    const fieldMeta = {};

    Object.entries(FIELD_PATTERNS).forEach(([field, patterns]) => {
      const value = stripTrailingLabels(firstPatternValue(cleaned, patterns));
      if (!value) return;

      if (field.toLowerCase().includes("date")) fields[field] = normalizeDate(value, field === "passportExpiryDate" ? "expiry" : "dob");
      else if (field === "gender") fields[field] = normalizeGender(value);
      else if (field === "email") fields[field] = value.toLowerCase();
      else if (field === "passportNumber") fields[field] = value.toUpperCase().replace(/[^A-Z0-9]/g, "");
      else fields[field] = cleanName(value);

      fieldMeta[field] = meta(field === "email" ? 98 : 94, "text");
    });

    fields.__fieldMeta = fieldMeta;

    return fields;
  }

  function parse(text = "") {
    const mrz = parseMrz(text);
    const mrzHint = Object.keys(mrz).length ? {} : parseMrzCodeHint(text);
    const labeled = parseLabeledFields(text);
    const mergedMeta = {
      ...(labeled.__fieldMeta || {}),
      ...(mrzHint.__fieldMeta || {}),
      ...(mrz.__fieldMeta || {})
    };
    delete labeled.__fieldMeta;
    delete mrzHint.__fieldMeta;
    delete mrz.__fieldMeta;
    return {
      ...labeled,
      ...Object.fromEntries(Object.entries(mrzHint).filter(([, value]) => Boolean(value))),
      ...Object.fromEntries(Object.entries(mrz).filter(([, value]) => Boolean(value))),
      __fieldMeta: mergedMeta
    };
  }

  const PassportParser = Object.freeze({
    COUNTRY_BY_CODE,
    cleanText,
    mrzCheckDigit,
    mrzDigitMatches,
    parseMrzCodeHint,
    normalizeDate,
    normalizeGender,
    parse,
    parseMrz
  });

  global.FIVA = Object.assign(global.FIVA || {}, { PassportParser });
})(typeof globalThis !== "undefined" ? globalThis : this);
