import { DOC_TYPE_ALIASES, EVENT_TYPE_OPTIONS, HEADER_EVENT_TYPE_OPTIONS } from '../config/constants.js';
import { $ } from '../core/dom.js';
import { fieldsByKindConfig } from '../config/fields-config.js';
import { normalizeUiText } from '../core/text.js';
import { buildFieldSelectionKey, getOrderedFieldEntries, isFieldDefaultSelected, isWorkHeaderFieldSelected } from './field-selections.js';
import { state } from '../state/app-state.js';
import { HIDDEN_FIELD_KEYS, _buildCiFieldConfig, getConfiguredFieldNameSetForDocType, hasHwrOrOrigData, normalizeFieldKey, prioritizeRecordEventTypeField, pruneFieldsObjectToAllowedSet, shouldKeepNonConfiguredFieldsVisible } from '../ui/events.js';

export function resolveFieldConfigRootKind(kindOverride) {
  const kind = String(
    kindOverride != null && kindOverride !== "" ? kindOverride : getCurrentRootKind()
  ).trim();
  if (!kind) return "BDM";
  const lower = kind.toLowerCase();
  if (lower === "census") return "Census";
  if (lower === "bdm") return "BDM";
  const sp = (state.subProjects || []).find((p) => String(p.name) === kind);
  if (sp && sp.root_kind) {
    return String(sp.root_kind).toLowerCase() === "census" ? "Census" : "BDM";
  }
  return lower.includes("census") ? "Census" : "BDM";
}


export function getWorkHeaderDisplayFields(kindOverride) {
  const kind = kindOverride || getCurrentProjectKind();
  const byDocType = _getEffectiveFieldConfig(kind);
  let rawHeaderFields = Array.isArray(byDocType.__HEADER__) ? byDocType.__HEADER__ : [];
  if (!rawHeaderFields.length) {
    const rootKind = resolveFieldConfigRootKind(kind);
    const fallback = fieldsByKindConfig[rootKind] || {};
    rawHeaderFields = Array.isArray(fallback.__HEADER__) ? fallback.__HEADER__ : [];
  }
  return getOrderedFieldEntries(kind, "__HEADER__", rawHeaderFields)
    .map((entry) => entry.field)
    .filter((f) => f && f.fieldName);
}


export function getWorkHeaderFieldNameSet(kindOverride) {
  return new Set(
    getWorkHeaderDisplayFields(kindOverride).map((f) => String(f.fieldName || "")).filter(Boolean)
  );
}


export function isWorkHeaderFieldName(base, kindOverride) {
  return getWorkHeaderFieldNameSet(kindOverride).has(String(base || ""));
}


export function findDocTypeConfigEntry(byDocType, activeKey) {
  const entries = Object.entries(byDocType || {}).filter(
    ([docTypeName]) => docTypeName !== "__GENERAL__" && docTypeName !== "__HEADER__"
  );
  const exact = entries.find(([docTypeName]) => normalizeDocTypeKey(docTypeName) === activeKey);
  if (exact) return exact;

  // BDM Mixed settings may be stored as keys like "Mixed → Baptism".
  // Allow resolving by event type suffix when a direct key is not present.
  const mixedSuffixMatch = entries.find(([docTypeName]) => {
    const normalized = normalizeDocTypeKey(docTypeName);
    return normalized.startsWith("mixed") && normalized.endsWith(activeKey);
  });
  if (mixedSuffixMatch) return mixedSuffixMatch;

  const aliases = DOC_TYPE_ALIASES[activeKey] || [];
  if (!aliases.length) return null;
  return entries.find(([docTypeName]) => aliases.includes(normalizeDocTypeKey(docTypeName))) || null;
}


export function getDocTypeFieldContext(kindOverride, docTypeOverride, options) {
  const kind = kindOverride || getCurrentProjectKind();
  const byDocType = _getEffectiveFieldConfig(kind);
  const activeDocType = docTypeOverride || (state.work && state.work.form_type) || state.docType || "";
  const activeKey = normalizeDocTypeKey(activeDocType);
  const rootKind = resolveFieldConfigRootKind(kind);
  const resolvedActiveKey =
    rootKind === "Census" && activeKey === "mixed" ? "census" : activeKey;
  const specificEntry = findDocTypeConfigEntry(byDocType, resolvedActiveKey);
  const specificName = specificEntry ? specificEntry[0] : "";
  const rawSpecificFields = specificEntry && Array.isArray(specificEntry[1]) ? specificEntry[1] : [];
  const rawGeneralFields = Array.isArray(byDocType.__GENERAL__) ? byDocType.__GENERAL__ : [];
  const specificFields = getOrderedFieldEntries(kind, specificName || "__GENERAL__", rawSpecificFields)
    .map((entry) => entry.field);
  const generalFields = getOrderedFieldEntries(kind, "__GENERAL__", rawGeneralFields)
    .map((entry) => entry.field);

  const forceMergeGeneral = !!(options && options.forceMergeGeneral);
  const isBdmDataDocType = rootKind === "BDM";
  const isContentOrMixed = activeKey === "content" || activeKey === "mixed";
  const useSpecificOnly =
    !forceMergeGeneral && isBdmDataDocType && !isContentOrMixed && specificFields.length > 0;
  if (useSpecificOnly) {
    return { kind, specificName, specificFields, generalFields, fields: [...specificFields] };
  }
  const merged = [...generalFields];
  specificFields.forEach((f) => {
    if (!merged.some((m) => String(m.fieldName || "") === String(f.fieldName || ""))) {
      merged.push(f);
    }
  });
  return { kind, specificName, specificFields, generalFields, fields: merged };
}


export function normalizeDocTypeKey(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]/g, "");
}


export function isCoverpageDocType(docType) {
  const key = normalizeDocTypeKey(docType);
  return key === "coverpage" || key === "coverage";
}


export function isSinglePanelDocType(docType) {
  const key = normalizeDocTypeKey(docType);
  return key === "coverpage" || key === "coverage" || key === "titleboard";
}


export function isNonDataDocType(docType) {
  const key = normalizeDocTypeKey(docType);
  if (!key) return false;
  const groups = state.batchDocTypeGroups;
  if (groups && Array.isArray(groups.non_data)) {
    return groups.non_data.some((name) => normalizeDocTypeKey(name) === key);
  }
  return key === "unknown";
}


export function isActiveWorkNonDataDocType(docTypeOverride) {
  const docType = String(
    docTypeOverride || (state.work && state.work.form_type) || state.docType || ""
  ).trim();
  return isNonDataDocType(docType);
}


export function installWorkEditableBases(work) {
  if (!work || !work.editable_bases || typeof work.editable_bases !== "object") {
    if (work) {
      work._editableHeaderBases = null;
      work._editableRecordBases = null;
    }
    return;
  }
  work._editableHeaderBases = new Set(
    (work.editable_bases.header || []).map((b) => String(b || "").trim()).filter(Boolean)
  );
  work._editableRecordBases = new Set(
    (work.editable_bases.record || []).map((b) => String(b || "").trim()).filter(Boolean)
  );
}


export function getWorkEditableBases() {
  const w = state.work;
  if (!w) return { header: null, record: null };
  return {
    header: w._editableHeaderBases || null,
    record: w._editableRecordBases || null,
  };
}

/** True when this orig. base can be written to the work DB for the active image. */

export function isOrigBasePersistable(base, recordId) {
  const fieldBase = String(base || "").trim();
  if (!fieldBase) return false;
  const { header, record } = getWorkEditableBases();
  if (recordId != null) {
    if (record) return record.has(fieldBase);
    return !isActiveWorkNonDataDocType();
  }
  if (header) return header.has(fieldBase);
  if (isActiveWorkNonDataDocType() && isWorkHeaderFieldName(fieldBase)) return false;
  return true;
}


export function isOrigInputPersistable(input) {
  if (!input || input.readOnly) return false;
  const col = String(input.dataset.col || "");
  if (!col.endsWith("_orig")) return false;
  const base = col.slice(0, -"_orig".length);
  const row = input.closest("tr.field-table__row");
  const recordIdRaw = row && row.dataset.recordId;
  const recordId =
    recordIdRaw != null && recordIdRaw !== "" && Number.isFinite(parseInt(recordIdRaw, 10))
      ? parseInt(recordIdRaw, 10)
      : null;
  return isOrigBasePersistable(base, recordId);
}


export function filterPersistableAutosaveUpdates(entity, updates) {
  const filtered = {};
  const isRecord = entity && entity.recordId != null;
  Object.entries(updates || {}).forEach(([col, val]) => {
    if (!String(col).endsWith("_orig")) return;
    const base = String(col).slice(0, -"_orig".length);
    if (isOrigBasePersistable(base, isRecord ? entity.recordId : null)) {
      filtered[col] = val;
    }
  });
  return filtered;
}


export function normalizeSearchText(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .trim()
    .toLowerCase();
}


export function getCurrentProjectKind() {
  const batchName = state.batch || "";
  const known = (state.projectKinds || []).find((p) => p.project_name === batchName);
  // Prefer the matched sub-project name so Champs tab syncs to it
  if (known && known.sub_project) return String(known.sub_project);
  if (known && known.kind) return String(known.kind);
  const activeDocType = String((state.work && state.work.form_type) || state.docType || "");
  return activeDocType.toLowerCase().includes("census") ? "Census" : "BDM";
}

/** Returns the root kind ("Census" or "BDM") regardless of sub-project. */

export function getCurrentRootKind() {
  const batchName = state.batch || "";
  const known = (state.projectKinds || []).find((p) => p.project_name === batchName);
  if (known && known.kind) return String(known.kind);
  const activeDocType = String((state.work && state.work.form_type) || state.docType || "");
  return activeDocType.toLowerCase().includes("census") ? "Census" : "BDM";
}


export function isCurrentProjectBdm() {
  return String(getCurrentRootKind() || "").toUpperCase() === "BDM";
}

/** Sub-project assigned to the active lot (manual or auto). */

export function getActiveSubProject() {
  const batchName = String(state.batch || "").trim();
  if (!batchName) return null;
  const proj = (state.projectKinds || []).find((p) => p.project_name === batchName);
  const subName = proj && proj.sub_project ? String(proj.sub_project).trim() : "";
  if (!subName) return null;
  return (state.subProjects || []).find((p) => String(p.name) === subName) || null;
}

/** Seuil Rekey (0–100 %) from the active sub-project profile, keyed by field_name. */

export function resolveFieldRekeyThreshold(base, meta) {
  const sp = getActiveSubProject();
  if (sp && Array.isArray(sp.fields)) {
    const row = sp.fields.find((f) => String(f.field_name || "").trim() === String(base || "").trim());
    if (row && row.rekey_threshold != null && row.rekey_threshold !== "") {
      return row.rekey_threshold;
    }
  }
  if (meta && meta.threshold != null && meta.threshold !== "") return meta.threshold;
  return null;
}


export function applySubProjectThresholdsToFieldsObject(fieldsObj) {
  if (!fieldsObj || typeof fieldsObj !== "object") return;
  Object.keys(fieldsObj).forEach((base) => {
    const meta = fieldsObj[base];
    if (!meta || typeof meta !== "object") return;
    const th = resolveFieldRekeyThreshold(base, meta);
    meta.threshold = th != null ? th : null;
  });
}


export function applySubProjectThresholdsToWork(work) {
  if (!work || typeof work !== "object") return;
  if (Array.isArray(work.headers)) {
    work.headers.forEach((hdr) => {
      if (hdr && hdr.fields) applySubProjectThresholdsToFieldsObject(hdr.fields);
    });
  }
  if (work.header && typeof work.header === "object") {
    applySubProjectThresholdsToFieldsObject(work.header);
  }
  if (Array.isArray(work.records)) {
    work.records.forEach((rec) => {
      if (rec && rec.fields) applySubProjectThresholdsToFieldsObject(rec.fields);
    });
  }
}


export function getActiveDocTypeForUi() {
  const sel = $("docTypeSelect");
  if (sel && sel.value != null && String(sel.value).trim()) {
    return String(sel.value).trim();
  }
  if (state.docType != null && String(state.docType).trim()) {
    return String(state.docType).trim();
  }
  if (state.work && state.work.form_type != null) {
    return String(state.work.form_type).trim();
  }
  return "";
}


export function isBdmMixedDocTypeActive() {
  const rootKind = String(getCurrentRootKind() || "").toUpperCase();
  return (
    rootKind === "BDM"
    && normalizeDocTypeKey(getActiveDocTypeForUi()) === "mixed"
  );
}


export function resolveFieldMeta(baseFieldName, fallbackLabel) {
  function lookupEquivalentLabel(fieldName, kindOverride) {
    const target = String(fieldName || "").trim();
    if (!target) return "";
    const kinds = [];
    if (kindOverride) kinds.push(kindOverride);
    kinds.push(getCurrentProjectKind(), getCurrentRootKind(), "Census", "BDM");
    const seenKinds = new Set();
    for (let i = 0; i < kinds.length; i += 1) {
      const kind = String(kinds[i] || "").trim();
      if (!kind || seenKinds.has(kind)) continue;
      seenKinds.add(kind);
      const byDocType = _getEffectiveFieldConfig(kind);
      const entries = Object.entries(byDocType);
      for (let j = 0; j < entries.length; j += 1) {
        const fields = Array.isArray(entries[j][1]) ? entries[j][1] : [];
        const hit = fields.find((f) => String((f && f.fieldName) || "") === target);
        if (hit && String(hit.fieldLabel || "").trim()) {
          return normalizeUiText(hit.fieldLabel);
        }
      }
    }
    return "";
  }

  if (isWorkHeaderFieldName(baseFieldName, getCurrentProjectKind())) {
    const hd = getWorkHeaderDisplayFields(getCurrentProjectKind()).find(
      (f) => String(f.fieldName) === String(baseFieldName || "")
    );
    return {
      fieldLabel: normalizeUiText(hd ? hd.fieldLabel : fallbackLabel),
      fieldName: hd ? hd.fieldName : String(baseFieldName || ""),
    };
  }
  const ctx = getDocTypeFieldContext();
  const equivalentLabel = lookupEquivalentLabel(baseFieldName, ctx.kind);
  const normalizedFallback = normalizeUiText(fallbackLabel || baseFieldName || "");
  if (!ctx.fields.length) {
    return {
      fieldLabel: equivalentLabel || normalizedFallback,
      fieldName: String(baseFieldName || ""),
    };
  }
  const hit = ctx.fields.find((f) => String(f.fieldName || "") === String(baseFieldName || ""));
  const preferredLabel =
    hit && hit.fieldLabel
      ? normalizeUiText(hit.fieldLabel)
      : equivalentLabel || normalizedFallback;
  return {
    fieldLabel: preferredLabel,
    fieldName: hit && hit.fieldName ? hit.fieldName : String(baseFieldName || ""),
  };
}


export function getConfiguredFieldsForActiveDocType(kindOverride, docTypeOverride) {
  const ctx = getDocTypeFieldContext(kindOverride, docTypeOverride);
  return ctx.fields;
}


export function getConfiguredFieldContextForRecord(record) {
  const kind = getCurrentProjectKind();
  const rootKind = String(getCurrentRootKind() || "").toUpperCase();
  const isBdmMixed =
    rootKind === "BDM"
    && normalizeDocTypeKey((state.work && state.work.form_type) || state.docType || "") === "mixed";
  if (!isBdmMixed) return getDocTypeFieldContext(kind);

  const recordFields = (record && record.fields) || {};
  const recordTypeRaw = recordFields.RecordType && recordFields.RecordType.orig != null
    ? recordFields.RecordType.orig
    : "";
  const normalizedRecordType = normalizeEventTypeValue(recordTypeRaw);
  const eventType = normalizedRecordType || "Birth";
  return getDocTypeFieldContext(kind, eventType, { forceMergeGeneral: true });
}


export function resolveFieldLabel(baseFieldName, fallbackLabel) {
  return resolveFieldMeta(baseFieldName, fallbackLabel).fieldLabel;
}


export function isFieldSelected(baseFieldName, fieldContext) {
  const ctx = fieldContext || getDocTypeFieldContext();
  if (!ctx.fields.length) return false;
  const hit = ctx.fields.find((f) => String(f.fieldName || "") === String(baseFieldName || ""));
  if (!hit) return false;
  const inSpecific = ctx.specificFields.some((f) => String(f.fieldName || "") === String(hit.fieldName || ""));
  const sourceDocType = inSpecific ? ctx.specificName : "__GENERAL__";
  const selectionKey = buildFieldSelectionKey(ctx.kind, sourceDocType, hit.fieldName);
  if (Object.prototype.hasOwnProperty.call(state.fieldSelections, selectionKey)) {
    return !!state.fieldSelections[selectionKey];
  }
  return isFieldDefaultSelected(sourceDocType, hit);
}


export function isHiddenField(baseFieldName, meta, options) {
  const resolved = resolveFieldMeta(baseFieldName, meta && meta.label ? meta.label : "");
  const resolvedLabel = resolved.fieldLabel;
  const keys = [
    normalizeFieldKey(baseFieldName),
    normalizeFieldKey(meta && meta.label ? meta.label : ""),
    normalizeFieldKey(resolvedLabel),
  ];
  if (keys.some((k) => HIDDEN_FIELD_KEYS.has(k))) return true;
  const activeKind = (options && options.fieldContext && options.fieldContext.kind) || getCurrentProjectKind();
  if (!(options && (options.forHeader || options.allowHeaderFields)) && isWorkHeaderFieldName(baseFieldName, activeKind)) {
    return true;
  }
  if (options && options.forHeader && isWorkHeaderFieldName(baseFieldName, activeKind)) {
    return !isWorkHeaderFieldSelected(baseFieldName);
  }
  if (!(options && options.forHeader) && isEventTypeFieldBase(baseFieldName)) {
    return false;
  }
  // Keep non-empty record fields visible even if the current DocType selection
  // would otherwise hide them.
  if (shouldKeepNonConfiguredFieldsVisible(options) && hasHwrOrOrigData(meta)) {
    return false;
  }
  return !isFieldSelected(baseFieldName, options && options.fieldContext);
}


export function sanitizeWorkPayloadForConfiguredFields(rawWork) {
  if (!rawWork || typeof rawWork !== "object") return rawWork;
  const work = rawWork;
  const kind = getCurrentProjectKind();
  const rootKind = String(getCurrentRootKind() || "").toUpperCase();
  const activeDocType = String((work && work.form_type) || state.docType || "");
  const activeDocTypeKey = normalizeDocTypeKey(activeDocType);
  const isBdmMixed = rootKind === "BDM" && activeDocTypeKey === "mixed";
  const isSinglePanel = isSinglePanelDocType(activeDocType);
  const headerAllowSet = isSinglePanel
    ? getConfiguredFieldNameSetForDocType(kind, activeDocType)
    : new Set(getWorkHeaderDisplayFields(kind).map((f) => String(f.fieldName || "").trim()).filter(Boolean));
  const hasHeaderConfig = headerAllowSet && headerAllowSet.size > 0;

  if (Array.isArray(work.headers)) {
    work.headers = work.headers.map((hdr) => {
      const current = hdr && typeof hdr === "object" ? hdr : {};
      const next = { ...current };
      next.fields = hasHeaderConfig
        ? pruneFieldsObjectToAllowedSet(current.fields, headerAllowSet)
        : (current.fields && typeof current.fields === "object" ? { ...current.fields } : {});
      return next;
    });
  }

  if (work.header && typeof work.header === "object") {
    work.header = hasHeaderConfig
      ? pruneFieldsObjectToAllowedSet(work.header, headerAllowSet)
      : { ...work.header };
  }

  if (Array.isArray(work.records)) {
    work.records = work.records.map((rec) => {
      const current = rec && typeof rec === "object" ? rec : {};
      const next = { ...current };
      let fieldContext;
      if (isBdmMixed) {
        const recordFields = current.fields || {};
        const recordTypeRaw = recordFields.RecordType && recordFields.RecordType.orig != null
          ? recordFields.RecordType.orig
          : "";
        const normalizedRecordType = normalizeEventTypeValue(recordTypeRaw);
        const eventType = normalizedRecordType || "Birth";
        fieldContext = getDocTypeFieldContext(kind, eventType, { forceMergeGeneral: true });
      } else {
        fieldContext = getDocTypeFieldContext(kind, activeDocType);
      }
      const allowed = new Set(
        (fieldContext.fields || [])
          .map((f) => String((f && f.fieldName) || "").trim())
          .filter(Boolean)
      );
      if (isBdmMixed) allowed.add("RecordType");
      next.fields = allowed.size > 0
        ? pruneFieldsObjectToAllowedSet(current.fields, allowed, { keepFieldsWithData: true })
        : pruneFieldsObjectToAllowedSet(current.fields, null, { keepFieldsWithData: true });
      return next;
    });
  }

  applySubProjectThresholdsToWork(work);
  return work;
}


export function getVisibleFieldBases(fieldsObj, options) {
  const forHeader = options && options.forHeader;
  const fieldContext = options && options.fieldContext;
  let configured;
  if (forHeader) {
    const kind = (fieldContext && fieldContext.kind) || getCurrentProjectKind();
    configured = getWorkHeaderDisplayFields(kind).map((f) => String(f.fieldName || "").trim()).filter(Boolean);
  } else {
    const ctx = fieldContext || getDocTypeFieldContext();
    configured = ctx.fields.map((f) =>
      String(f.fieldName || "").trim()
    ).filter(Boolean);
  }
  const selectedConfigured = configured.filter((base) => {
    const kind = (fieldContext && fieldContext.kind) || getCurrentProjectKind();
    if (forHeader && isWorkHeaderFieldName(base, kind)) {
      return isWorkHeaderFieldSelected(base);
    }
    return isFieldSelected(base, fieldContext);
  });
  const withData = Object.keys(fieldsObj || {}).filter((base) => hasHwrOrOrigData(fieldsObj[base]));

  const merged = [];
  const add = (base) => {
    if (!base) return;
    const kind = (fieldContext && fieldContext.kind) || getCurrentProjectKind();
    if (!forHeader && !(options && options.allowHeaderFields) && isWorkHeaderFieldName(base, kind)) return;
    if (merged.includes(base)) return;
    merged.push(base);
  };
  selectedConfigured.forEach(add);
  if (shouldKeepNonConfiguredFieldsVisible(options)) {
    withData.forEach((base) => {
      const kind = (fieldContext && fieldContext.kind) || getCurrentProjectKind();
      if (forHeader && !isWorkHeaderFieldName(base, kind)) {
        return;
      }
      add(base);
    });
  }
  if (!forHeader) {
    return prioritizeRecordEventTypeField(merged, fieldsObj);
  }
  return merged;
}


export function _getEffectiveFieldConfig(name) {
  const sp = (state.subProjects || []).find((p) => p.name === name);
  if (sp && Array.isArray(sp.fields) && sp.fields.length) {
    return _buildCiFieldConfig(sp);
  }
  if (fieldsByKindConfig[name]) return fieldsByKindConfig[name];
  if (sp && fieldsByKindConfig[sp.root_kind]) return fieldsByKindConfig[sp.root_kind];
  return {};
}


export function isEventTypeFieldBase(base) {
  return String(base || "").trim().toLowerCase() === "recordtype";
}


export function normalizeEventTypeValue(rawValue) {
  const raw = rawValue == null ? "" : String(rawValue).trim();
  if (!raw) return "";
  const lower = raw.toLowerCase();
  const match = EVENT_TYPE_OPTIONS.find((opt) => opt.toLowerCase() === lower);
  return match || raw;
}


export function normalizeHeaderEventTypeValue(rawValue) {
  const raw = rawValue == null ? "" : String(rawValue).trim();
  if (!raw) return "";
  if (isCoverpageDocType(raw)) return "Coverpage";
  const lower = raw.toLowerCase();
  const match = HEADER_EVENT_TYPE_OPTIONS.find((opt) => opt.toLowerCase() === lower);
  return match || raw;
}
