import { CustomerDeal, CustomerEntity, CustomerTag, CustomerTagAssignment, CustomerDictionaryEntry } from "../data/entities.js";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { ensureSameScope } from "@open-mercato/shared/lib/commands/scope";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("customers");
import { ensureOrganizationScope as ensureOrganizationScope2, ensureSameScope as ensureSameScope2, ensureTenantScope } from "@open-mercato/shared/lib/commands/scope";
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
function normalizeDictionaryColor(input) {
  if (typeof input !== "string") return null;
  const trimmed = input.trim();
  if (!trimmed) return null;
  const hexMatch = /^#([0-9a-fA-F]{6})$/.exec(trimmed);
  if (!hexMatch) return null;
  return `#${hexMatch[1].toLowerCase()}`;
}
function normalizeDictionaryIcon(input) {
  if (typeof input !== "string") return null;
  const trimmed = input.trim();
  if (!trimmed) return null;
  return trimmed.slice(0, 48);
}
import { assertFound } from "@open-mercato/shared/lib/crud/errors";
async function requireCustomerEntity(em, id, scope, kind, message = "Customer entity not found") {
  const entity = await em.findOne(CustomerEntity, {
    id,
    deletedAt: null,
    tenantId: scope.tenantId,
    organizationId: scope.organizationId
  });
  if (!entity) throw new CrudHttpError(404, { error: message });
  if (kind && entity.kind !== kind) {
    throw new CrudHttpError(400, { error: "Invalid entity type" });
  }
  return entity;
}
async function requireTimelineParentEntity(em, id, scope) {
  const entity = await em.findOne(CustomerEntity, {
    id,
    deletedAt: null,
    tenantId: scope.tenantId,
    organizationId: scope.organizationId
  });
  if (entity) {
    if (entity.kind !== "person" && entity.kind !== "company") {
      throw new CrudHttpError(422, { error: "entityId must reference a person or company" });
    }
    return entity;
  }
  const deal = await em.findOne(CustomerDeal, {
    id,
    deletedAt: null,
    tenantId: scope.tenantId,
    organizationId: scope.organizationId
  });
  if (deal) {
    throw new CrudHttpError(422, { error: "entityId must reference a person or company, not a deal" });
  }
  throw new CrudHttpError(404, { error: "Customer not found" });
}
async function syncEntityTags(em, entity, tags) {
  if (tags === void 0) return;
  const desired = Array.from(new Set((tags ?? []).filter((id) => typeof id === "string")));
  const existing = await loadEntityTagIds(em, entity);
  const toRemove = existing.filter((id) => !desired.includes(id));
  if (toRemove.length) {
    await em.nativeDelete(CustomerTagAssignment, { entity, tag: { $in: toRemove } });
  }
  const toAdd = desired.filter((id) => !existing.includes(id));
  if (!toAdd.length) return;
  const tagsInScope = await em.find(CustomerTag, {
    id: { $in: toAdd },
    organizationId: entity.organizationId,
    tenantId: entity.tenantId
  });
  if (tagsInScope.length !== toAdd.length) {
    throw new CrudHttpError(400, { error: "One or more tags not found for this scope" });
  }
  for (const tag of toAdd) {
    const assignment = em.create(CustomerTagAssignment, {
      tenantId: entity.tenantId,
      organizationId: entity.organizationId,
      tag: em.getReference(CustomerTag, tag),
      entity
    });
    em.persist(assignment);
  }
}
async function loadEntityTagIds(em, entity) {
  const assignments = await findWithDecryption(
    em,
    CustomerTagAssignment,
    { entity },
    { populate: ["tag"] },
    { tenantId: entity.tenantId, organizationId: entity.organizationId }
  );
  return assignments.map(
    (assignment) => typeof assignment.tag === "string" ? assignment.tag : assignment.tag.id
  );
}
async function requireDealInScope(em, dealId, tenantId, organizationId) {
  if (!dealId) return null;
  const deal = await em.findOne(CustomerDeal, { id: dealId, deletedAt: null, tenantId, organizationId });
  if (!deal) throw new CrudHttpError(400, { error: "Deal not found" });
  ensureSameScope(deal, organizationId, tenantId);
  return deal;
}
const DICTIONARY_KINDS = /* @__PURE__ */ new Set([
  "status",
  "source",
  "lifecycle_stage",
  "address_type",
  "activity_type",
  "deal_status",
  "pipeline_stage",
  "job_title",
  "industry",
  "temperature",
  "renewal_quarter",
  "person_company_role",
  "interaction_status"
]);
const CUSTOM_DICTIONARY_KIND_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
async function ensureDictionaryEntry(em, params) {
  const trimmed = params.value?.trim();
  if (!trimmed) return null;
  if (!DICTIONARY_KINDS.has(params.kind) && !CUSTOM_DICTIONARY_KIND_PATTERN.test(params.kind)) {
    throw new CrudHttpError(400, { error: "Unsupported dictionary kind" });
  }
  const normalized = trimmed.toLowerCase();
  const color = params.color === void 0 ? void 0 : normalizeDictionaryColor(params.color);
  const icon = params.icon === void 0 ? void 0 : normalizeDictionaryIcon(params.icon);
  const existing = await em.findOne(CustomerDictionaryEntry, {
    tenantId: params.tenantId,
    organizationId: params.organizationId,
    kind: params.kind,
    normalizedValue: normalized
  });
  if (existing) {
    let changed = false;
    if (color !== void 0) {
      if (existing.color !== color) {
        existing.color = color ?? null;
        changed = true;
      }
    }
    if (icon !== void 0) {
      if (existing.icon !== icon) {
        existing.icon = icon ?? null;
        changed = true;
      }
    }
    if (changed) {
      existing.updatedAt = /* @__PURE__ */ new Date();
      em.persist(existing);
    }
    return existing;
  }
  const entry = em.create(CustomerDictionaryEntry, {
    tenantId: params.tenantId,
    organizationId: params.organizationId,
    kind: params.kind,
    value: trimmed,
    label: params.label?.trim() || trimmed,
    normalizedValue: normalized,
    color: color ?? null,
    icon: icon ?? null
  });
  em.persist(entry);
  return entry;
}
function resolveParentResourceKind(entityKind) {
  if (entityKind === "company") return "customers.company";
  if (entityKind === "person") return "customers.person";
  return null;
}
function normalizeEventEntries(entries) {
  const map = /* @__PURE__ */ new Map();
  for (const entry of entries) {
    if (!entry || typeof entry !== "object") continue;
    const entityType = String(entry.entityType ?? "");
    const recordId = String(entry.recordId ?? "");
    if (!entityType || !recordId) continue;
    const key = [
      entityType,
      recordId,
      entry.organizationId ?? "__org__",
      entry.tenantId ?? "__tenant__"
    ].join("|");
    if (!map.has(key)) {
      map.set(key, {
        entityType,
        recordId,
        organizationId: entry.organizationId ?? null,
        tenantId: entry.tenantId ?? null
      });
    }
  }
  return Array.from(map.values());
}
async function emitQueryIndexEvents(ctx, entries, kind) {
  const normalized = normalizeEventEntries(entries);
  if (!normalized.length) return;
  let bus = null;
  try {
    bus = ctx.container.resolve("eventBus");
  } catch (err) {
    logger.warn("eventBus resolve failed; skipping query index events", { component: "commands.shared", err });
    bus = null;
  }
  if (!bus) return;
  const eventName = kind === "delete" ? "query_index.delete_one" : "query_index.upsert_one";
  const crudAction = kind === "delete" ? "deleted" : "updated";
  await Promise.all(
    normalized.map(
      (entry) => bus.emitEvent(
        eventName,
        {
          entityType: entry.entityType,
          recordId: entry.recordId,
          organizationId: entry.organizationId ?? null,
          tenantId: entry.tenantId ?? null,
          crudAction
        },
        {
          tenantId: entry.tenantId ?? null,
          organizationId: entry.organizationId ?? null
        }
      ).catch((err) => {
        logger.warn("Query index emitEvent failed", { component: "commands.shared", entry, err });
        return void 0;
      })
    )
  );
}
async function emitQueryIndexDeleteEvents(ctx, entries) {
  await emitQueryIndexEvents(ctx, entries, "delete");
}
async function emitQueryIndexUpsertEvents(ctx, entries) {
  await emitQueryIndexEvents(ctx, entries, "upsert");
}
export {
  assertFound,
  emitQueryIndexDeleteEvents,
  emitQueryIndexUpsertEvents,
  ensureDictionaryEntry,
  ensureOrganizationScope2 as ensureOrganizationScope,
  ensureSameScope2 as ensureSameScope,
  ensureTenantScope,
  extractUndoPayload,
  loadEntityTagIds,
  normalizeDictionaryColor,
  normalizeDictionaryIcon,
  requireCustomerEntity,
  requireDealInScope,
  requireTimelineParentEntity,
  resolveParentResourceKind,
  syncEntityTags
};
//# sourceMappingURL=shared.js.map
