import { CustomFieldDef, CustomFieldValue } from "@open-mercato/core/modules/entities/data/entities";
import { decryptCustomFieldValue, resolveTenantEncryptionService } from "../encryption/customFieldValues.js";
import { parseBooleanToken } from "../boolean.js";
import { extractCustomFieldEntries } from "./custom-fields-client.js";
import { createLogger } from "../logger/index.js";
import {
  buildCustomFieldDefinitionIndexFromRows,
  normalizeDefinitionKey,
  normalizeFieldsetFilter,
  selectDefinitionForRecord
} from "./custom-field-definition-index.js";
const logger = createLogger("shared").child({ component: "crud" });
function buildCustomFieldSelectorsForEntity(entityId, sets) {
  const keys = Array.from(new Set(
    (sets || []).filter((s) => s.entity === entityId).flatMap((s) => (s.fields || []).map((f) => f.key))
  ));
  const selectors = keys.map((k) => `cf:${k}`);
  const outputKeys = keys.map((k) => `cf_${k}`);
  return { keys, selectors, outputKeys };
}
function normalizeCustomFieldValue(val) {
  if (Array.isArray(val)) return val;
  if (typeof val === "string") {
    const s = val.trim();
    if (s.startsWith("{") && s.endsWith("}")) {
      const inner = s.slice(1, -1).trim();
      if (!inner) return [];
      return inner.split(/[\s,]+/).map((x) => x.trim()).filter(Boolean);
    }
    return s;
  }
  return val;
}
function extractCustomFieldsFromItem(item, keys) {
  const out = {};
  for (const key of keys) {
    const colon = item[`cf:${key}`];
    const snake = item[`cf_${key}`];
    const value = colon !== void 0 ? colon : snake;
    if (value !== void 0) out[`cf_${key}`] = normalizeCustomFieldValue(value);
  }
  return out;
}
function extractAllCustomFieldEntries(item) {
  return extractCustomFieldEntries(item);
}
async function buildCustomFieldFiltersFromQuery(opts) {
  const out = {};
  const entries = Object.entries(opts.query).filter(([k]) => k.startsWith("cf_"));
  if (!entries.length) return out;
  const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length ? opts.entityIds : opts.entityId ? [opts.entityId] : [];
  if (!entityIdList.length) return out;
  const defs = await opts.em.find(CustomFieldDef, {
    entityId: { $in: entityIdList },
    isActive: true,
    $and: [
      { $or: [{ tenantId: opts.tenantId }, { tenantId: null }] }
    ]
  });
  const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset);
  const order = /* @__PURE__ */ new Map();
  entityIdList.map(String).forEach((id, index) => order.set(id, index));
  const byKey = {};
  for (const d of defs) {
    if (fieldsetFilter) {
      const fieldsets = Array.isArray(d.configJson?.fieldsets) ? d.configJson.fieldsets.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [];
      const rawFieldset = typeof d.configJson?.fieldset === "string" ? d.configJson.fieldset.trim() : "";
      const normalizedFieldset = rawFieldset.length ? rawFieldset : null;
      const matches = fieldsets.length > 0 ? fieldsets.some((entry) => fieldsetFilter.has(entry)) : fieldsetFilter.has(normalizedFieldset);
      if (!matches) continue;
    }
    const key = d.key;
    const entityId = String(d.entityId);
    const current = byKey[key];
    const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER;
    if (!current) {
      byKey[key] = { kind: d.kind, multi: Boolean(d.configJson?.multi), entityId };
      continue;
    }
    const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER;
    if (rankNew < rankOld) {
      byKey[key] = { kind: d.kind, multi: Boolean(d.configJson?.multi), entityId };
    }
  }
  const coerce = (kind, v) => {
    if (v == null) return v;
    switch (kind) {
      case "integer":
        return Number.parseInt(String(v), 10);
      case "float":
        return Number.parseFloat(String(v));
      case "boolean":
        return parseBooleanToken(String(v)) === true;
      case "date":
      case "datetime":
        return String(v);
      default:
        return String(v);
    }
  };
  for (const [rawKey, rawVal] of entries) {
    const isIn = rawKey.endsWith("In");
    const key = isIn ? rawKey.replace(/^cf_/, "").replace(/In$/, "") : rawKey.replace(/^cf_/, "");
    const def = byKey[key];
    const fieldId = `cf:${key}`;
    if (!def) continue;
    if (isIn) {
      const list = Array.isArray(rawVal) ? rawVal : String(rawVal).split(",").map((s) => s.trim()).filter(Boolean);
      if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) };
    } else {
      out[fieldId] = coerce(def.kind, rawVal);
    }
  }
  return out;
}
function splitCustomFieldPayload(raw) {
  const base = {};
  const custom = {};
  if (!raw || typeof raw !== "object") return { base, custom };
  for (const [key, value] of Object.entries(raw)) {
    if (key === "customFields") {
      if (Array.isArray(value)) {
        value.forEach((entry) => {
          if (!entry || typeof entry !== "object") return;
          const entryKey = typeof entry.key === "string" ? entry.key.trim() : "";
          if (!entryKey) return;
          custom[entryKey] = entry.value;
        });
        continue;
      }
      if (value && typeof value === "object") {
        for (const [ck, cv] of Object.entries(value)) {
          const normalizedKey = typeof ck === "string" ? ck.trim() : "";
          if (!normalizedKey) continue;
          custom[normalizedKey] = cv;
        }
        continue;
      }
    }
    if (key === "customValues" && value && typeof value === "object" && !Array.isArray(value)) {
      for (const [ck, cv] of Object.entries(value)) {
        custom[String(ck)] = cv;
      }
      continue;
    }
    if (key.startsWith("cf_")) {
      custom[key.slice(3)] = value;
      continue;
    }
    if (key.startsWith("cf:")) {
      custom[key.slice(3)] = value;
      continue;
    }
    base[key] = value;
  }
  return { base, custom };
}
function extractCustomFieldValuesFromPayload(raw) {
  return splitCustomFieldPayload(raw).custom;
}
const CF_DEF_INDEX_CACHE_KEY_PREFIX = "crud:cf-def-index";
const CF_DEF_INDEX_DEFAULT_TTL_MS = 0;
function resolveCfDefIndexCacheTtlMs() {
  const raw = process.env.OM_CF_DEF_CACHE_TTL_MS;
  if (raw === void 0) return CF_DEF_INDEX_DEFAULT_TTL_MS;
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) return CF_DEF_INDEX_DEFAULT_TTL_MS;
  return parsed;
}
function buildCfDefIndexCacheKey(opts) {
  const tenant = opts.tenantId ?? "global";
  const entities = opts.entityIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|");
  const orgs = opts.organizationIds.length ? opts.organizationIds.slice().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("|") : "none";
  const fieldset = opts.fieldsetKey ?? "all";
  return `${CF_DEF_INDEX_CACHE_KEY_PREFIX}:${tenant}:${entities}:${orgs}:${fieldset}`;
}
function buildCfDefIndexCacheTags(opts) {
  const tenant = opts.tenantId ?? "global";
  const tagBase = `entities:definitions:${tenant}`;
  const tags = /* @__PURE__ */ new Set([tagBase]);
  for (const entityId of opts.entityIds) {
    tags.add(`${tagBase}:entity:${entityId}`);
  }
  return Array.from(tags);
}
function normalizeFieldsetKey(value) {
  if (!value) return null;
  if (Array.isArray(value)) {
    const cleaned = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
    if (!cleaned.length) return null;
    return cleaned.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(",");
  }
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : null;
}
function serializableIndexFromMap(index) {
  return Array.from(index.entries());
}
function indexMapFromSerializable(value) {
  if (!Array.isArray(value)) return null;
  const map = /* @__PURE__ */ new Map();
  for (const entry of value) {
    if (!Array.isArray(entry) || entry.length !== 2) return null;
    const [key, summaries] = entry;
    if (typeof key !== "string" || !Array.isArray(summaries)) return null;
    map.set(key, summaries);
  }
  return map;
}
const requestScopedCfDefIndexCache = /* @__PURE__ */ new WeakMap();
function getRequestScopedCfDefIndexCache(scope) {
  let bucket = requestScopedCfDefIndexCache.get(scope);
  if (!bucket) {
    bucket = /* @__PURE__ */ new Map();
    requestScopedCfDefIndexCache.set(scope, bucket);
  }
  return bucket;
}
async function loadCustomFieldDefinitionIndexFresh(opts) {
  const { em, entityIds, orgCandidates } = opts;
  const tenantId = opts.tenantId ?? null;
  const scopeClauses = [
    tenantId ? { $or: [{ tenantId }, { tenantId: null }] } : { tenantId: null }
  ];
  if (orgCandidates.length) {
    scopeClauses.push({
      $or: [{ organizationId: { $in: orgCandidates } }, { organizationId: null }]
    });
  } else {
    scopeClauses.push({ organizationId: null });
  }
  const where = {
    entityId: { $in: entityIds },
    deletedAt: null,
    isActive: true,
    $and: scopeClauses
  };
  const defs = await em.find(CustomFieldDef, where);
  const rows = defs.map((def) => ({
    key: def.key,
    entityId: String(def.entityId),
    kind: typeof def.kind === "string" ? def.kind : null,
    configJson: def.configJson,
    organizationId: def.organizationId ?? null,
    tenantId: def.tenantId ?? null,
    deletedAt: def.deletedAt ?? null,
    updatedAt: def.updatedAt ?? null
  }));
  return buildCustomFieldDefinitionIndexFromRows(rows, {
    organizationIds: orgCandidates,
    fieldset: opts.fieldset
  });
}
async function loadCustomFieldDefinitionIndex(opts) {
  const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds];
  const entityIds = list.map((id) => typeof id === "string" ? id.trim() : String(id ?? "")).filter((id) => id.length > 0);
  if (!entityIds.length) return /* @__PURE__ */ new Map();
  const tenantId = opts.tenantId ?? null;
  const orgCandidates = Array.isArray(opts.organizationIds) ? opts.organizationIds.map((id) => typeof id === "string" ? id.trim() : id).filter((id) => typeof id === "string" && id.length > 0) : [];
  const fieldsetKey = normalizeFieldsetKey(opts.fieldset);
  const ttlMs = resolveCfDefIndexCacheTtlMs();
  const cacheKey = buildCfDefIndexCacheKey({
    tenantId,
    entityIds,
    organizationIds: orgCandidates,
    fieldsetKey
  });
  const requestBucket = opts.requestScope ? getRequestScopedCfDefIndexCache(opts.requestScope) : null;
  if (requestBucket) {
    const cached = requestBucket.get(cacheKey);
    if (cached) return cached;
  }
  const sharedCache = ttlMs > 0 ? opts.cache ?? null : null;
  if (sharedCache && typeof sharedCache.get === "function") {
    try {
      const cached = await sharedCache.get(cacheKey);
      const restored = indexMapFromSerializable(cached);
      if (restored) {
        if (requestBucket) requestBucket.set(cacheKey, restored);
        return restored;
      }
    } catch (err) {
      logger.warn("Custom-field definition cache read failed", { err });
    }
  }
  const index = await loadCustomFieldDefinitionIndexFresh({
    ...opts,
    entityIds,
    orgCandidates
  });
  if (sharedCache && typeof sharedCache.set === "function") {
    try {
      await sharedCache.set(cacheKey, serializableIndexFromMap(index), {
        ttl: ttlMs,
        tags: buildCfDefIndexCacheTags({ tenantId, entityIds })
      });
    } catch (err) {
      logger.warn("Custom-field definition cache write failed", { err });
    }
  }
  if (requestBucket) requestBucket.set(cacheKey, index);
  return index;
}
function applyCustomFieldsNormalization(record, decorated, options = {}) {
  const stripPrefixedKeys = options.stripPrefixedKeys === true;
  const base = {};
  for (const [key, value] of Object.entries(record)) {
    if (stripPrefixedKeys && (key.startsWith("cf_") || key.startsWith("cf:"))) continue;
    base[key] = value;
  }
  base.customValues = decorated.customValues;
  base.customFields = decorated.customFields;
  return base;
}
function decorateRecordWithCustomFields(record, definitions, context = {}) {
  const rawEntries = extractAllCustomFieldEntries(record);
  if (!Object.keys(rawEntries).length) {
    return { customValues: null, customFields: [] };
  }
  const values = {};
  const entries = [];
  const organizationId = context.organizationId ?? null;
  const tenantId = context.tenantId ?? null;
  Object.entries(rawEntries).forEach(([prefixedKey, value]) => {
    const bareKey = prefixedKey.replace(/^cf_/, "");
    const normalizedKey = normalizeDefinitionKey(bareKey);
    if (!normalizedKey) return;
    const defsForKey = definitions.get(normalizedKey) ?? [];
    const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId);
    if (!resolvedDef) return;
    values[bareKey] = value;
    const entry = {
      key: bareKey,
      label: resolvedDef.label ?? bareKey,
      value,
      kind: resolvedDef.kind ?? null,
      multi: resolvedDef.multi ?? Array.isArray(value)
    };
    entries.push({
      entry,
      priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,
      updatedAt: resolvedDef.updatedAt ?? 0
    });
  });
  const ordered = entries.sort((a, b) => {
    const priorityDiff = a.priority - b.priority;
    if (priorityDiff !== 0) return priorityDiff;
    const updatedDiff = b.updatedAt - a.updatedAt;
    if (updatedDiff !== 0) return updatedDiff;
    return a.entry.key.localeCompare(b.entry.key);
  }).map((item) => item.entry);
  return {
    customValues: Object.keys(values).length ? values : null,
    customFields: ordered
  };
}
async function loadCustomFieldValues(opts) {
  const { em, entityId, recordIds } = opts;
  if (!Array.isArray(recordIds) || recordIds.length === 0) return {};
  const normalizedRecordIds = recordIds.map((id) => String(id));
  let encryptionService;
  const encryptionCache = /* @__PURE__ */ new Map();
  const getEncryptionService = () => {
    if (encryptionService !== void 0) return encryptionService;
    encryptionService = resolveTenantEncryptionService(em, opts.encryptionService);
    return encryptionService;
  };
  const tenantCandidates = /* @__PURE__ */ new Set();
  tenantCandidates.add(null);
  if (opts.tenantIdByRecord) {
    for (const val of Object.values(opts.tenantIdByRecord)) {
      tenantCandidates.add(val ? String(val) : null);
    }
  }
  if (opts.tenantFallbacks) {
    for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null);
  }
  const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null;
  const tenantList = Array.from(tenantCandidates);
  const tenantNonNull = tenantList.filter((t) => t !== null);
  const tenantFilter = tenantNonNull.length ? { tenantId: { $in: [...tenantNonNull, null] } } : { tenantId: null };
  const cfRows = await em.find(CustomFieldValue, {
    entityId,
    recordId: { $in: normalizedRecordIds },
    deletedAt: null,
    ...tenantList.length ? tenantFilter : {}
  });
  if (!cfRows.length) return {};
  const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))));
  const organizationCandidates = /* @__PURE__ */ new Set();
  organizationCandidates.add(null);
  if (opts.organizationIdByRecord) {
    for (const val of Object.values(opts.organizationIdByRecord)) {
      organizationCandidates.add(val ? String(val) : null);
    }
  }
  for (const row of cfRows) {
    organizationCandidates.add(row.organizationId ? String(row.organizationId) : null);
  }
  const orgList = Array.from(organizationCandidates);
  const defs = allKeys.length ? await em.find(CustomFieldDef, {
    entityId,
    key: { $in: allKeys },
    deletedAt: null,
    isActive: true,
    ...tenantList.length ? { tenantId: tenantFilter.tenantId } : {},
    organizationId: { $in: orgList }
  }) : [];
  const defsByKey = /* @__PURE__ */ new Map();
  for (const def of defs) {
    const list = defsByKey.get(def.key) || [];
    list.push(def);
    defsByKey.set(def.key, list);
  }
  const pickDefinition = (fieldKey, organizationId, tenantId) => {
    const candidates = defsByKey.get(fieldKey);
    if (!candidates || candidates.length === 0) return null;
    const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt);
    const list = active.length ? active : candidates;
    if (organizationId && tenantId) {
      const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId);
      if (exact) return exact;
    }
    if (organizationId) {
      const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId));
      if (orgMatch) return orgMatch;
    }
    if (tenantId) {
      const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId);
      if (tenantMatch) return tenantMatch;
    }
    const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null);
    return global ?? list[0];
  };
  const valueFromRow = (row) => {
    if (row.valueMultiline !== null && row.valueMultiline !== void 0) return row.valueMultiline;
    if (row.valueText !== null && row.valueText !== void 0) return row.valueText;
    if (row.valueInt !== null && row.valueInt !== void 0) return row.valueInt;
    if (row.valueFloat !== null && row.valueFloat !== void 0) return row.valueFloat;
    if (row.valueBool !== null && row.valueBool !== void 0) return row.valueBool;
    return null;
  };
  const buckets = /* @__PURE__ */ new Map();
  const rowInfos = cfRows.map((row) => {
    const recordId = String(row.recordId);
    const key = String(row.fieldKey);
    const bucketKey = `${recordId}::${key}`;
    const orgId = row.organizationId ? String(row.organizationId) : null;
    const tenantId = row.tenantId ? String(row.tenantId) : null;
    const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null);
    const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant);
    const def = pickDefinition(key, resolvedOrgId, resolvedTenantId);
    const encrypted = Boolean(def?.configJson && def.configJson?.encrypted);
    const value = valueFromRow(row);
    return { bucketKey, resolvedOrgId, resolvedTenantId, tenantId, def, encrypted, value };
  });
  const decryptedValues = await Promise.all(
    rowInfos.map(
      (info) => info.encrypted ? decryptCustomFieldValue(
        info.value,
        info.resolvedTenantId ?? info.tenantId ?? null,
        getEncryptionService(),
        encryptionCache,
        { kind: info.def?.kind ?? null }
      ) : info.value
    )
  );
  rowInfos.forEach((info, index) => {
    const decrypted = decryptedValues[index];
    const existing = buckets.get(info.bucketKey);
    if (existing) {
      if (existing.orgId == null && info.resolvedOrgId) existing.orgId = info.resolvedOrgId;
      if (existing.tenantId == null && info.resolvedTenantId) existing.tenantId = info.resolvedTenantId;
      if (existing.def == null && info.def) existing.def = info.def;
      existing.encrypted = existing.encrypted || info.encrypted;
      existing.values.push(decrypted);
    } else {
      buckets.set(info.bucketKey, { orgId: info.resolvedOrgId, tenantId: info.resolvedTenantId, values: [decrypted], def: info.def ?? null, encrypted: info.encrypted });
    }
  });
  const result = {};
  for (const [compoundKey, bucket] of buckets.entries()) {
    const [recordId, fieldKey] = compoundKey.split("::");
    if (!result[recordId]) result[recordId] = {};
    const prefixed = `cf_${fieldKey}`;
    const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null));
    if (def && def.configJson && typeof def.configJson === "object" && def.configJson.multi) {
      const cleaned = bucket.values.filter((v) => v !== void 0 && v !== null);
      result[recordId][prefixed] = cleaned;
    } else if (bucket.values.length > 1) {
      const cleaned = bucket.values.filter((v) => v !== void 0);
      result[recordId][prefixed] = cleaned;
    } else {
      result[recordId][prefixed] = bucket.values[0] ?? null;
    }
  }
  return result;
}
function summarizeCustomFields(record) {
  const entries = extractAllCustomFieldEntries(record);
  const values = Object.fromEntries(
    Object.entries(entries).map(([prefixedKey, value]) => [
      prefixedKey.replace(/^cf_/, ""),
      value
    ])
  );
  const customValues = Object.keys(values).length ? values : null;
  const customFields = Object.entries(values).map(([key, value]) => ({
    key,
    label: key,
    value,
    kind: null,
    multi: Array.isArray(value)
  }));
  return { entries, customValues, customFields };
}
export {
  applyCustomFieldsNormalization,
  buildCustomFieldFiltersFromQuery,
  buildCustomFieldSelectorsForEntity,
  decorateRecordWithCustomFields,
  extractAllCustomFieldEntries,
  extractCustomFieldValuesFromPayload,
  extractCustomFieldsFromItem,
  getRequestScopedCfDefIndexCache,
  loadCustomFieldDefinitionIndex,
  loadCustomFieldValues,
  normalizeCustomFieldValue,
  splitCustomFieldPayload,
  summarizeCustomFields
};
//# sourceMappingURL=custom-fields.js.map
