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

  const LOG = "[FIVA]";

  const FIELD_MAP = Object.freeze({
    country: ["country/region you are applying visa from", "country region you are applying visa from", "applying visa from", "country/region"],
    mission: ["indian mission/office", "indian mission office", "indian mission", "mission office"],
    nationality: ["nationality/region", "nationality region", "present nationality", "nationality"],
    dateOfBirth: ["date of birth", "birth date", "dob"],
    email: ["email id", "email address", "e-mail", "email"],
    reEnterEmail: ["re-enter email id", "re enter email id", "confirm email", "re-enter email", "re enter email"],
    expectedArrivalDate: ["expected date of arrival", "expected date of journey", "date of journey", "date of arrival", "arrival date"],
    visaType: ["visa service", "visa type", "type of visa required", "type of visa", "visa category"],
    purpose: ["purpose of visit", "visa purpose", "purpose"],
    surname: ["surname", "last name", "family name"],
    givenNames: ["given name", "given names", "first name"],
    fullName: ["full name", "applicant name", "name of applicant", "name as in passport"],
    gender: ["sex", "gender"],
    cityOfBirth: ["city of birth", "place of birth", "birth place", "birth city"],
    countryOfBirth: ["country of birth", "birth country"],
    religion: ["religion"],
    education: ["educational qualification", "education qualification", "education"],
    passportNumber: ["passport number", "passport no"],
    passportIssueDate: ["date of issue", "passport issue date", "issue date"],
    passportExpiryDate: ["date of expiry", "passport expiry date", "expiry date"]
  });

  const COUNTRY_NATIONALITY = Object.freeze({
    bangladesh: { country: "Bangladesh", nationality: "Bangladeshi" },
    bangladeshi: { country: "Bangladesh", nationality: "Bangladeshi" },
    bgd: { country: "Bangladesh", nationality: "Bangladeshi" },
    "p bgd": { country: "Bangladesh", nationality: "Bangladeshi" },
    india: { country: "India", nationality: "Indian" },
    indian: { country: "India", nationality: "Indian" },
    pakistan: { country: "Pakistan", nationality: "Pakistani" },
    pakistani: { country: "Pakistan", nationality: "Pakistani" },
    nepal: { country: "Nepal", nationality: "Nepalese" },
    nepalese: { country: "Nepal", nationality: "Nepalese" },
    "sri lanka": { country: "Sri Lanka", nationality: "Sri Lankan" },
    "sri lankan": { country: "Sri Lanka", nationality: "Sri Lankan" },
    "united states": { country: "United States", nationality: "American" },
    usa: { country: "United States", nationality: "American" },
    american: { country: "United States", nationality: "American" },
    "united kingdom": { country: "United Kingdom", nationality: "British" },
    uk: { country: "United Kingdom", nationality: "British" },
    british: { country: "United Kingdom", nationality: "British" },
    canada: { country: "Canada", nationality: "Canadian" },
    canadian: { country: "Canada", nationality: "Canadian" },
    australia: { country: "Australia", nationality: "Australian" },
    australian: { country: "Australia", nationality: "Australian" }
  });

  const META_CONFIDENCE = Object.freeze({
    structured: 96,
    inferred: 82
  });

  function compact(value = "") {
    return String(value || "")
      .replace(/\u0000/g, " ")
      .replace(/[“”]/g, "\"")
      .replace(/[‘’]/g, "'")
      .replace(/[ \t]+/g, " ")
      .replace(/\n[ \t]+/g, "\n")
      .replace(/[ \t]+\n/g, "\n")
      .trim();
  }

  function normalize(value = "") {
    return compact(value)
      .toLowerCase()
      .replace(/&/g, " and ")
      .replace(/[^a-z0-9]+/g, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

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

  function meta(confidence = META_CONFIDENCE.structured, source = "structured", patch = {}) {
    return { confidence, source, verified: confidence >= 90, ...patch };
  }

  function displayParts(display = "") {
    const [day, month, year] = String(display || "").split("/");
    if (!day || !month || !year) return null;
    return {
      day: String(day).padStart(2, "0"),
      month: String(month).padStart(2, "0"),
      year: String(year).padStart(4, "0")
    };
  }

  function candidateDates(value = "") {
    const source = compact(value).toUpperCase().replace(/[.]/g, "-");
    if (!source) return [];
    const candidates = [];
    [
      /\b\d{1,2}[-/\s][A-Z]{3,9}[-/\s]\d{4}\b/g,
      /\b\d{4}[-/]\d{1,2}[-/]\d{1,2}\b/g,
      /\b\d{1,2}[-/]\d{1,2}[-/]\d{4}\b/g
    ].forEach((pattern) => {
      let match;
      while ((match = pattern.exec(source)) !== null) {
        candidates.push({ value: match[0], index: match.index });
      }
    });
    return candidates.sort((left, right) => left.index - right.index);
  }

  function dateParts(value = "", field = "") {
    const scoped = fieldScopedValue(field, value);
    const strict = global.FIVA?.StrictDobValidator?.parseDate?.(scoped);
    if (strict?.display) {
      return displayParts(strict.display);
    }

    const strictCandidate = candidateDates(scoped)
      .map((candidate) => global.FIVA?.StrictDobValidator?.parseDate?.(candidate.value))
      .find((candidate) => candidate?.display);
    if (strictCandidate?.display) {
      return displayParts(strictCandidate.display);
    }

    const source = compact(scoped);
    const date = source.match(/(\d{1,4})[\/\-. ](\d{1,2})[\/\-. ](\d{4})/);
    if (!date) return null;

    let year;
    let month;
    let day;

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

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

  function normalizeDate(value = "", field = "") {
    const parsed = dateParts(value, field);
    return parsed ? `${parsed.year}-${parsed.month}-${parsed.day}` : "";
  }

  function cleanValue(value = "") {
    return stripFieldHints(value)
      .replace(/^[*:;\-\s]+/, "")
      .replace(/\s+\*$/g, "")
      .replace(/\b(page|registration|details)\b\s*$/i, "")
      .trim();
  }

  function stripFieldHints(value = "") {
    return compact(value)
      .replace(/\(\s*as\s+in\s+passport\s*\)/gi, " ")
      .replace(/\(\s*dd\s*\/\s*mm\s*\/\s*yyyy\s*\)/gi, " ")
      .replace(/\bAs\s+in\s+Passport\b/gi, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

  function countryMeta(value = "") {
    const source = normalize(value);
    if (!source) return null;
    return COUNTRY_NATIONALITY[source]
      || Object.entries(COUNTRY_NATIONALITY).find(([key]) => source.includes(key) || key.includes(source))?.[1]
      || null;
  }

  function normalizeCountry(value = "") {
    const alias = global.FIVA?.NormalizeOptionText?.nationalityAliasResolver?.(value);
    if (alias) return alias;
    const source = cleanValue(value);
    const known = countryMeta(source)?.country;
    return known || titleCase(source);
  }

  function normalizeNationality(value = "") {
    const alias = global.FIVA?.NormalizeOptionText?.nationalityAliasResolver?.(value);
    if (alias) return alias;
    const source = cleanValue(value);
    const known = countryMeta(source)?.nationality;
    return known || titleCase(source);
  }

  function normalizeMission(value = "") {
    const source = normalize(value);
    if (!source) return "";
    if (source.includes("dhaka")) return "BANGLADESH-DHAKA";
    if (source.includes("chittagong") || source.includes("chattogram")) return "BANGLADESH-CHITTAGONG";
    if (source.includes("rajshahi")) return "BANGLADESH-RAJSHAHI";
    if (source.includes("sylhet")) return "BANGLADESH-SYLHET";
    return cleanValue(value).toUpperCase().replace(/\s*-\s*/g, "-").replace(/\s+/g, "-");
  }

  function normalizeVisaType(value = "") {
    const source = normalize(value);
    if (!source) return "";
    if (source.includes("tourist")) return "e-Tourist Visa";
    if (source.includes("business")) return "e-Business Visa";
    if (source.includes("medical attendant")) return "Medical Attendant Visa";
    if (source.includes("medical")) return "Medical Visa";
    if (source.includes("student")) return "Student Visa";
    if (source.includes("entry")) return "Entry Visa";
    return titleCase(value);
  }

  function normalizePurpose(value = "") {
    const source = normalize(value);
    if (!source) return "";
    if (source.includes("business")) return "Business";
    if (source.includes("medical attendant")) return "Medical Attendant";
    if (source.includes("medical")) return "Medical Treatment";
    if (/touris|travel|sightseeing|visit/.test(source)) return "Tourism";
    if (source.includes("student") || source.includes("study")) return "Study";
    return titleCase(value);
  }

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

  function normalizePassportNumber(value = "") {
    const exact = cleanValue(value).match(/\b[A-Z]{1,3}[0-9]{5,10}\b/i);
    return exact?.[0]?.toUpperCase().replace(/[^A-Z0-9]/g, "") || "";
  }

  function normalizeFieldValue(field, value = "") {
    const source = cleanValue(fieldScopedValue(field, value));
    if (!source) return "";
    if (field === "email" || field === "reEnterEmail") return source.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i)?.[0]?.toLowerCase() || "";
    if (field === "dateOfBirth" || field === "expectedArrivalDate" || field === "passportIssueDate" || field === "passportExpiryDate") return normalizeDate(source, field);
    if (field === "gender") return normalizeGender(source);
    if (field === "passportNumber") return normalizePassportNumber(source);
    if (field === "country" || field === "countryOfBirth") return normalizeCountry(source);
    if (field === "nationality") return normalizeNationality(source);
    if (field === "mission") return normalizeMission(source);
    if (field === "visaType") return normalizeVisaType(source);
    if (field === "purpose") return normalizePurpose(source);
    if (field === "surname" || field === "givenNames" || field === "fullName" || field === "cityOfBirth") return titleCase(source);
    return titleCase(source);
  }

  function allLabels() {
    return Object.entries(FIELD_MAP)
      .flatMap(([field, aliases]) => aliases.map((alias) => ({ field, alias, normalized: normalize(alias) })))
      .sort((left, right) => right.normalized.length - left.normalized.length);
  }

  function labelRegex(alias) {
    const tokens = normalize(alias).split(" ").filter(Boolean).map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
    return new RegExp(`\\b${tokens.join("[\\s/:()\\-]*")}\\b`, "gi");
  }

  function findLabelMatches(text = "") {
    const matches = [];
    allLabels().forEach(({ field, alias }) => {
      const regex = labelRegex(alias);
      let match;
      while ((match = regex.exec(text)) !== null) {
        matches.push({
          field,
          index: match.index,
          end: match.index + match[0].length,
          text: match[0],
          aliasLength: normalize(alias).length
        });
      }
    });

    return matches
      .sort((left, right) => left.index - right.index || right.aliasLength - left.aliasLength)
      .filter((match, index, list) => {
        const previous = list[index - 1];
        return !previous || match.index >= previous.end || match.field !== previous.field;
      });
  }

  function fieldScopedValue(field = "", value = "") {
    const source = cleanValue(value);
    if (!source) return "";
    const boundary = findLabelMatches(source)
      .filter((match) => match.index > 0 && match.field !== field)
      .sort((left, right) => left.index - right.index)[0];
    return boundary ? cleanValue(source.slice(0, boundary.index)) : source;
  }

  function trimAtObviousBoundary(value = "") {
    return cleanValue(value)
      .split(/\n{2,}/)[0]
      .replace(/\s{3,}/g, " ")
      .trim();
  }

  function valueFromSegment(text, match, next) {
    const maxEnd = Math.min(text.length, match.end + 220);
    const end = next ? Math.min(next.index, maxEnd) : maxEnd;
    return trimAtObviousBoundary(text.slice(match.end, end));
  }

  function addField(result, field, value, confidence = META_CONFIDENCE.structured) {
    const scopedValue = fieldScopedValue(field, value);
    const normalizedValue = normalizeFieldValue(field, scopedValue);
    if (!normalizedValue || result.fields[field]) return;
    result.fields[field] = normalizedValue;
    result.meta[field] = meta(confidence, "structured", {
      sourceValue: cleanValue(scopedValue),
      rawValue: cleanValue(value),
      mixedLabels: cleanValue(scopedValue) !== cleanValue(value)
    });
  }

  function firstMatch(source = "", patterns = []) {
    for (const pattern of patterns) {
      const match = source.match(pattern);
      if (match?.[1]) return cleanValue(match[1]);
    }
    return "";
  }

  function addPdfField(result, field, value, confidence = 98) {
    addField(result, field, value, confidence);
    if (result.meta[field]) {
      result.meta[field] = {
        ...result.meta[field],
        confidence,
        source: "pdf_text",
        verified: confidence >= 90,
        sourceValue: result.meta[field].sourceValue || cleanValue(value)
      };
    }
  }

  function parseIndianVisaApplication(text = "") {
    const result = { fields: {}, meta: {} };
    const source = stripFieldHints(text);
    if (!/HIGH\s+COMMISSION\s+OF\s+INDIA|Visa\s+Application\s+Form/i.test(source)) return result;

    const missionHeader = source.match(/HIGH\s+COMMISSION\s+OF\s+INDIA\s+([A-Z\s]+)\(\s*([A-Z\s]+)\s*\)/i);
    if (missionHeader?.[1] && missionHeader?.[2]) {
      addPdfField(result, "country", missionHeader[2], 98);
      addPdfField(result, "mission", `${missionHeader[2]}-${missionHeader[1]}`, 98);
    }

    addPdfField(result, "surname", firstMatch(source, [
      /Surname\s+([A-Z][A-Z\s.'-]*?)(?=\s+Given\s+Name\b)/i
    ]), 98);
    addPdfField(result, "givenNames", firstMatch(source, [
      /Given\s+Name\s+([A-Z][A-Z\s.'-]*?)(?=\s+Previous\/other\s+Name|\s+Previous\s+Name|\s+Gender\b)/i
    ]), 98);
    if (result.fields.givenNames && result.fields.surname) {
      addPdfField(result, "fullName", `${result.fields.givenNames} ${result.fields.surname}`, 98);
    }
    addPdfField(result, "gender", firstMatch(source, [
      /Gender\s+([A-Z]+)\s+Marital\s+Status/i
    ]), 98);
    addPdfField(result, "dateOfBirth", firstMatch(source, [
      /Date\s+of\s+Birth\s+([0-9]{1,2}[-/\s][A-Z]{3,9}[-/\s][0-9]{4}|[0-9]{1,2}[-/][0-9]{1,2}[-/][0-9]{4}|[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2})(?=\s+Religion\b)/i
    ]), 98);
    addPdfField(result, "countryOfBirth", firstMatch(source, [
      /Country\s+of\s+Birth\s+([A-Z\s]+?)(?=\s+Citizenship\b)/i
    ]), 96);
    addPdfField(result, "nationality", firstMatch(source, [
      /Current\s+Nationality\s+([A-Z\s]+?)(?=\s+Nationality\s+by\s+Birth\b)/i
    ]), 98);
    addPdfField(result, "passportNumber", firstMatch(source, [
      /Passport\s+No\.?\s+([A-Z0-9]{6,16})(?=\s+Date\s+of\s+Issue\b)/i
    ]), 98);
    addPdfField(result, "passportIssueDate", firstMatch(source, [
      /Date\s+of\s+Issue\s+([0-9]{1,2}[-/\s][A-Z]{3,9}[-/\s][0-9]{4}|[0-9]{1,2}[-/][0-9]{1,2}[-/][0-9]{4}|[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2})(?=\s+Place\s+of\s+Issue\b)/i
    ]), 98);
    addPdfField(result, "passportExpiryDate", firstMatch(source, [
      /Date\s+of\s+Expiry\s+([0-9]{1,2}[-/\s][A-Z]{3,9}[-/\s][0-9]{4}|[0-9]{1,2}[-/][0-9]{1,2}[-/][0-9]{4}|[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2})(?=\s+Any\s+other\s+Passport\b)/i
    ]), 98);
    addPdfField(result, "email", firstMatch(source, [
      /Email\s+address\s+([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})(?=\s+Permanent\b|\s+[A-Z]\.\s+Family\b)/i
    ]), 98);
    addPdfField(result, "visaType", firstMatch(source, [
      /Type\s+Of\s+Visa\s+Required\s+([A-Z\s-]+?)(?=\s+No\s+of\s+Entries\b)/i
    ]), 98);
    addPdfField(result, "expectedArrivalDate", firstMatch(source, [
      /Expected\s+Date\s+of\s+Journey\s+([0-9]{1,2}[-/\s][A-Z]{3,9}[-/\s][0-9]{4}|[0-9]{1,2}[-/][0-9]{1,2}[-/][0-9]{4}|[0-9]{4}[-/][0-9]{1,2}[-/][0-9]{1,2})(?=\s+Port\s+Of\s+Arrival\b)/i
    ]), 98);
    addPdfField(result, "purpose", firstMatch(source, [
      /Purpose\s+of\s+Visit\s*:\s*(.+?)(?=\s+F\.\s+Previous\s+Visit\b|\s+Have\s+You\s+Ever\b)/i
    ]), 96);

    return result;
  }

  function parseByLabelPositions(text = "") {
    const result = { fields: {}, meta: {} };
    const source = compact(text).replace(/\r/g, "\n");
    const matches = findLabelMatches(source);

    matches.forEach((match, index) => {
      const value = valueFromSegment(source, match, matches[index + 1]);
      addField(result, match.field, value);
    });

    return result;
  }

  function parseLinePairs(text = "") {
    const result = { fields: {}, meta: {} };
    const lines = compact(text).split(/\n+/).map(cleanValue).filter(Boolean);
    const labels = allLabels();

    lines.forEach((line, index) => {
      labels.forEach(({ field, alias }) => {
        if (result.fields[field]) return;
        const regex = labelRegex(alias);
        const match = regex.exec(line);
        if (!match) return;
        const inlineValue = cleanValue(line.slice(match.index + match[0].length));
        const nextLine = lines[index + 1] && !findLabelMatches(lines[index + 1]).length ? lines[index + 1] : "";
        addField(result, field, inlineValue || nextLine);
      });
    });

    return result;
  }

  function parseLooseValues(text = "") {
    const result = { fields: {}, meta: {} };
    const source = compact(text);
    addField(result, "email", source.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i)?.[0] || "", 90);
    addField(result, "passportNumber", source.match(/\b[A-Z]{1,3}[0-9]{5,10}\b/i)?.[0] || "", 78);

    return result;
  }

  function mergeParsed(...parts) {
    const fields = {};
    const metaFields = {};
    parts.forEach((part) => {
      Object.entries(part?.fields || part || {}).forEach(([key, value]) => {
        if (key.startsWith("__") || key === "sourceConfidence" || fields[key] || !value) return;
        fields[key] = value;
      });
      Object.entries(part?.meta || part?.__fieldMeta || {}).forEach(([key, value]) => {
        if (!metaFields[key]) metaFields[key] = value;
      });
    });
    if (fields.email && !fields.reEnterEmail) {
      fields.reEnterEmail = fields.email;
      metaFields.reEnterEmail = meta(META_CONFIDENCE.inferred, "inferred");
    }
    if (fields.dateOfBirth && !fields.dob) fields.dob = fields.dateOfBirth;
    if (fields.expectedArrivalDate && !fields.arrivalDate) fields.arrivalDate = fields.expectedArrivalDate;
    if (fields.givenNames && !fields.givenName) fields.givenName = fields.givenNames;
    return {
      ...fields,
      __fieldMeta: metaFields,
      sourceConfidence: Object.fromEntries(Object.entries(metaFields).map(([key, value]) => [key, {
        value: fields[key] || "",
        confidence: Math.max(0, Math.min(1, Number(value?.confidence || 0) / 100)),
        source: value?.source || "structured"
      }]))
    };
  }

  function parseStructured(text = "") {
    const fields = mergeParsed(
      parseIndianVisaApplication(text),
      parseLinePairs(text),
      parseByLabelPositions(text),
      parseLooseValues(text)
    );
    if (Object.keys(fields).filter((key) => !key.startsWith("__")).length >= 3) {
      console.log(LOG, "Structured PDF parsed successfully");
    }
    return fields;
  }

  function mergeFields(primary = {}, fallback = {}) {
    return mergeParsed(primary, fallback);
  }

  const FivaFieldMap = Object.freeze({
    COUNTRY_NATIONALITY,
    FIELD_MAP,
    fieldScopedValue,
    mergeFields,
    normalizeDate,
    normalizeFieldValue,
    parseIndianVisaApplication,
    parseStructured
  });

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