import { z } from "zod";
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import {
  CustomerDealPersonLink,
  CustomerEntity,
  CustomerPersonCompanyLink,
  CustomerPersonProfile
} from "../../data/entities.js";
import { E } from "../../../../generated/entities.ids.generated.js";
import { personCreateSchema, personUpdateSchema } from "../../data/validators.js";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import {
  applyEntityIdExclusion,
  applyEntityIdRestriction,
  findMatchingEntityIdsWithQueryEngine,
  findMatchingEntityIdsBySearchTokensAcrossSources,
  withScopedPayload
} from "../utils.js";
import { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from "@open-mercato/shared/lib/crud/custom-fields";
import { buildIlikeTerm } from "@open-mercato/shared/lib/db/buildIlikeTerm";
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
import {
  createCustomersCrudOpenApi,
  createPagedListResponseSchema,
  defaultOkResponseSchema
} from "../openapi.js";
import {
  filterActivePersonCompanyLinks,
  withActiveCustomerPersonCompanyLinkFilter,
  withCustomerPersonCompanyLinkScope,
  withScopedCustomerDealLinkWhere
} from "../../lib/personCompanyLinkTable.js";
import { normalizeProfilePayload } from "./payload.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("customers");
const rawBodySchema = z.object({}).passthrough();
const listSchema = z.object({
  page: z.coerce.number().min(1).default(1),
  pageSize: z.coerce.number().min(1).max(100).default(50),
  search: z.string().optional(),
  email: z.string().optional(),
  emailStartsWith: z.string().optional(),
  emailContains: z.string().optional(),
  status: z.string().optional(),
  lifecycleStage: z.string().optional(),
  source: z.string().optional(),
  hasEmail: z.string().optional(),
  hasPhone: z.string().optional(),
  hasNextInteraction: z.string().optional(),
  createdFrom: z.string().optional(),
  createdTo: z.string().optional(),
  sortField: z.string().optional(),
  sortDir: z.enum(["asc", "desc"]).optional(),
  id: z.string().uuid().optional(),
  tagIds: z.string().optional(),
  tagIdsEmpty: z.string().optional(),
  excludeIds: z.string().optional(),
  excludeLinkedCompanyId: z.string().uuid().optional(),
  excludeLinkedDealId: z.string().uuid().optional()
}).passthrough();
const routeMetadata = {
  GET: { requireAuth: true, requireFeatures: ["customers.people.view"] },
  POST: { requireAuth: true, requireFeatures: ["customers.people.manage"] },
  PUT: { requireAuth: true, requireFeatures: ["customers.people.manage"] },
  DELETE: { requireAuth: true, requireFeatures: ["customers.people.manage"] }
};
const metadata = routeMetadata;
const crud = makeCrudRoute({
  metadata: routeMetadata,
  orm: {
    entity: CustomerEntity,
    idField: "id",
    orgField: "organizationId",
    tenantField: "tenantId",
    softDeleteField: "deletedAt"
  },
  enrichers: { entityId: "customers.person" },
  indexer: { entityType: E.customers.customer_entity },
  list: {
    schema: listSchema,
    entityId: E.customers.customer_entity,
    fields: [
      "id",
      "display_name",
      "description",
      "owner_user_id",
      "primary_email",
      "primary_phone",
      "status",
      "lifecycle_stage",
      "source",
      "next_interaction_at",
      "next_interaction_name",
      "next_interaction_ref_id",
      "next_interaction_icon",
      "next_interaction_color",
      "organization_id",
      "tenant_id",
      "kind",
      "created_at",
      "updated_at"
    ],
    sortFieldMap: {
      name: "display_name",
      email: "primary_email",
      primaryEmail: "primary_email",
      status: "status",
      lifecycleStage: "lifecycle_stage",
      source: "source",
      nextInteractionAt: "next_interaction_at",
      createdAt: "created_at",
      updatedAt: "updated_at"
    },
    buildFilters: async (query, ctx) => {
      const advancedFilterTree = consumeAdvancedFilterState(query);
      const filters = { kind: { $eq: "person" } };
      if (query.id) filters.id = { $eq: query.id };
      if (query.search) {
        const matchingIds = ctx ? await findMatchingEntityIdsBySearchTokensAcrossSources({
          ctx,
          query: query.search,
          sources: [
            {
              entityType: E.customers.customer_entity,
              fields: [
                "display_name",
                "primary_email",
                "primary_phone",
                "description",
                "status",
                "lifecycle_stage",
                "source",
                "next_interaction_name"
              ]
            },
            {
              entityType: E.customers.customer_person_profile,
              fields: [
                "display_name",
                "primary_email",
                "primary_phone",
                "status",
                "lifecycle_stage",
                "source",
                "first_name",
                "last_name",
                "preferred_name",
                "job_title",
                "department",
                "seniority",
                "timezone",
                "linked_in_url",
                "twitter_url"
              ],
              mapToEntityIds: {
                table: "customer_people",
                targetColumn: "entity_id"
              }
            }
          ]
        }) : null;
        if (matchingIds !== null && matchingIds.length > 0) {
          applyEntityIdRestriction(filters, matchingIds);
        } else {
          const searchPattern = buildIlikeTerm(query.search);
          filters.$or = [
            { display_name: { $ilike: searchPattern } },
            { primary_email: { $ilike: searchPattern } },
            { primary_phone: { $ilike: searchPattern } },
            { description: { $ilike: searchPattern } },
            { next_interaction_name: { $ilike: searchPattern } }
          ];
        }
      }
      const email = typeof query.email === "string" ? query.email.trim().toLowerCase() : "";
      const emailStartsWith = typeof query.emailStartsWith === "string" ? query.emailStartsWith.trim().toLowerCase() : "";
      const emailContains = typeof query.emailContains === "string" ? query.emailContains.trim().toLowerCase() : "";
      if (email) {
        filters.primary_email = { $eq: email };
      } else if (emailStartsWith) {
        filters.primary_email = { $ilike: buildIlikeTerm(emailStartsWith, "startsWith") };
      } else if (emailContains) {
        filters.primary_email = { $ilike: buildIlikeTerm(emailContains) };
      }
      if (query.status) {
        filters.status = { $eq: query.status };
      }
      if (query.lifecycleStage) {
        filters.lifecycle_stage = { $eq: query.lifecycleStage };
      }
      if (query.source) {
        filters.source = { $eq: query.source };
      }
      const tagIdsRaw = typeof query.tagIds === "string" ? query.tagIds : "";
      const tagIds = tagIdsRaw.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
      const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true;
      if (tagIdsEmpty) {
        filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
      } else if (tagIds.length > 0) {
        filters["tag_assignments.tag_id"] = { $in: tagIds };
      }
      const excludedIds = /* @__PURE__ */ new Set();
      const excludeIdsRaw = typeof query.excludeIds === "string" ? query.excludeIds : "";
      excludeIdsRaw.split(",").map((value) => value.trim()).filter((value) => value.length > 0).forEach((value) => excludedIds.add(value));
      if (ctx && query.excludeLinkedCompanyId) {
        try {
          const em = ctx.container.resolve("em");
          const decryptionScope = {
            tenantId: ctx.auth?.tenantId ?? null,
            organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
          };
          const linkWhere = await withActiveCustomerPersonCompanyLinkFilter(
            em,
            withCustomerPersonCompanyLinkScope({ company: query.excludeLinkedCompanyId }, decryptionScope),
            "customers.people.GET"
          );
          const links = filterActivePersonCompanyLinks(
            await findWithDecryption(
              em,
              CustomerPersonCompanyLink,
              linkWhere,
              { populate: ["person"] },
              decryptionScope
            )
          );
          links.forEach((link) => {
            const personId = link.person?.id;
            if (typeof personId === "string" && personId.length > 0) excludedIds.add(personId);
          });
        } catch (err) {
          logger.warn("exclusion lookup failed; falling back to base result set", { component: "people.list", err });
        }
      }
      if (ctx && query.excludeLinkedDealId) {
        try {
          const em = ctx.container.resolve("em");
          const decryptionScope = {
            tenantId: ctx.auth?.tenantId ?? null,
            organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
          };
          const links = await findWithDecryption(
            em,
            CustomerDealPersonLink,
            withScopedCustomerDealLinkWhere(query.excludeLinkedDealId, decryptionScope),
            { populate: ["person"] },
            decryptionScope
          );
          links.forEach((link) => {
            const personId = link.person?.id;
            if (typeof personId === "string" && personId.length > 0) excludedIds.add(personId);
          });
        } catch (err) {
          logger.warn("exclusion lookup failed; falling back to base result set", { component: "people.list", err });
        }
      }
      applyEntityIdExclusion(filters, Array.from(excludedIds));
      const hasEmail = parseBooleanToken(query.hasEmail);
      if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {
        filters.primary_email = { $exists: hasEmail };
      }
      const hasPhone = parseBooleanToken(query.hasPhone);
      if (hasPhone !== null) {
        filters.primary_phone = { $exists: hasPhone };
      }
      const hasNextInteraction = parseBooleanToken(query.hasNextInteraction);
      if (hasNextInteraction !== null) {
        filters.next_interaction_at = { $exists: hasNextInteraction };
      }
      const createdRange = {};
      if (query.createdFrom) {
        const from = new Date(query.createdFrom);
        if (!Number.isNaN(from.getTime())) createdRange.$gte = from;
      }
      if (query.createdTo) {
        const to = new Date(query.createdTo);
        if (!Number.isNaN(to.getTime())) createdRange.$lte = to;
      }
      if (Object.keys(createdRange).length) {
        filters.created_at = createdRange;
      }
      if (ctx) {
        try {
          const em = ctx.container.resolve("em");
          const cfFilters = await buildCustomFieldFiltersFromQuery({
            entityIds: [E.customers.customer_entity, E.customers.customer_person_profile],
            query,
            em,
            tenantId: ctx.auth?.tenantId ?? null
          });
          Object.assign(filters, cfFilters);
        } catch (err) {
          logger.warn("custom field filter resolution failed; falling back to base filters", { component: "people.list", err });
        }
      }
      if (ctx && advancedFilterTree) {
        const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree);
        const matchedIds = await findMatchingEntityIdsWithQueryEngine({
          ctx,
          entityId: E.customers.customer_entity,
          filters: advancedFilters,
          customFieldSources: [
            {
              entityId: E.customers.customer_person_profile,
              table: "customer_people",
              alias: "person_profile",
              recordIdColumn: "id",
              join: { fromField: "id", toField: "entity_id" }
            }
          ],
          joins: [
            {
              alias: "tag_assignments",
              table: "customer_tag_assignments",
              from: { field: "id" },
              to: { field: "entity_id" },
              type: "left"
            }
          ]
        });
        applyEntityIdRestriction(filters, matchedIds);
      }
      return filters;
    },
    customFieldSources: [
      {
        entityId: E.customers.customer_person_profile,
        table: "customer_people",
        alias: "person_profile",
        recordIdColumn: "id",
        join: { fromField: "id", toField: "entity_id" }
      }
    ],
    joins: [
      {
        alias: "tag_assignments",
        table: "customer_tag_assignments",
        from: { field: "id" },
        to: { field: "entity_id" },
        type: "left"
      }
    ],
    transformItem: (item) => {
      if (!item || typeof item !== "object") return item;
      const record = item;
      const normalized = { ...record };
      delete normalized.kind;
      const cfEntries = extractAllCustomFieldEntries(record);
      for (const key of Object.keys(normalized)) {
        if (key.startsWith("cf:")) {
          delete normalized[key];
        }
      }
      return { ...normalized, ...cfEntries };
    }
  },
  actions: {
    create: {
      commandId: "customers.people.create",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        const scoped = withScopedPayload(raw ?? {}, ctx, translate);
        const { base, custom } = splitCustomFieldPayload(scoped);
        const parsed = personCreateSchema.parse(base);
        return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed;
      },
      response: ({ result }) => ({
        id: result?.entityId ?? result?.id ?? null,
        personId: result?.personId ?? null
      }),
      status: 201
    },
    update: {
      commandId: "customers.people.update",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        const scoped = withScopedPayload(raw ?? {}, ctx, translate);
        const normalized = normalizeProfilePayload(scoped, translate);
        const { base, custom } = splitCustomFieldPayload(normalized);
        const parsed = personUpdateSchema.parse(base);
        return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed;
      },
      // Return the freshly-bumped updatedAt so inline-edit detail pages can refresh
      // their optimistic-lock token between sequential saves (#2055).
      response: (arg) => {
        const raw = arg?.updatedAt ?? arg?.result?.updatedAt ?? null;
        return {
          ok: true,
          updatedAt: raw instanceof Date ? raw.toISOString() : typeof raw === "string" ? raw : null
        };
      }
    },
    delete: {
      commandId: "customers.people.delete",
      schema: rawBodySchema,
      mapInput: async ({ parsed, ctx }) => {
        const { translate } = await resolveTranslations();
        const id = parsed?.body?.id ?? parsed?.id ?? parsed?.query?.id ?? (ctx.request ? new URL(ctx.request.url).searchParams.get("id") : null);
        if (!id) throw new CrudHttpError(400, { error: translate("customers.errors.person_required", "Person id is required") });
        return { id };
      },
      response: () => ({ ok: true })
    }
  },
  hooks: {
    afterList: async (payload, ctx) => {
      const items = Array.isArray(payload?.items) ? payload.items : [];
      const ids = items.map((item) => item && typeof item === "object" && typeof item.id === "string" ? item.id : null).filter((id) => typeof id === "string" && id.length > 0);
      if (!ids.length) return;
      const em = ctx.container.resolve("em");
      const decryptionScope = {
        tenantId: ctx.auth?.tenantId ?? null,
        organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
      };
      const profileWhere = {
        entity: { $in: ids },
        tenantId: ctx.auth?.tenantId ?? null
      };
      if (ctx.selectedOrganizationId) {
        profileWhere.organizationId = ctx.selectedOrganizationId;
      }
      const [entities, profiles] = await Promise.all([
        findWithDecryption(
          em,
          CustomerEntity,
          {
            id: { $in: ids },
            deletedAt: null,
            kind: "person"
          },
          void 0,
          decryptionScope
        ),
        findWithDecryption(
          em,
          CustomerPersonProfile,
          profileWhere,
          { populate: ["entity", "company"] },
          decryptionScope
        )
      ]);
      const entitiesById = /* @__PURE__ */ new Map();
      for (const entity of entities) {
        entitiesById.set(entity.id, entity);
      }
      const profilesByEntityId = /* @__PURE__ */ new Map();
      for (const profile of profiles) {
        const profileEntity = profile.entity;
        const entityId = typeof profileEntity?.id === "string" ? profileEntity.id : null;
        if (entityId) profilesByEntityId.set(entityId, profile);
      }
      payload.items = items.map((item) => {
        if (!item || typeof item !== "object") return item;
        const record = item;
        const entity = typeof record.id === "string" ? entitiesById.get(record.id) : void 0;
        const profile = typeof record.id === "string" ? profilesByEntityId.get(record.id) : void 0;
        if (!entity && !profile) return item;
        return {
          ...record,
          display_name: entity?.displayName ?? record.display_name ?? null,
          description: entity?.description ?? record.description ?? null,
          owner_user_id: entity?.ownerUserId ?? record.owner_user_id ?? null,
          primary_email: entity?.primaryEmail ?? record.primary_email ?? null,
          primary_phone: entity?.primaryPhone ?? record.primary_phone ?? null,
          status: entity?.status ?? record.status ?? null,
          lifecycle_stage: entity?.lifecycleStage ?? record.lifecycle_stage ?? null,
          source: entity?.source ?? record.source ?? null,
          next_interaction_at: entity?.nextInteractionAt ? entity.nextInteractionAt.toISOString() : record.next_interaction_at ?? null,
          next_interaction_name: entity?.nextInteractionName ?? record.next_interaction_name ?? null,
          next_interaction_ref_id: entity?.nextInteractionRefId ?? record.next_interaction_ref_id ?? null,
          next_interaction_icon: entity?.nextInteractionIcon ?? record.next_interaction_icon ?? null,
          next_interaction_color: entity?.nextInteractionColor ?? record.next_interaction_color ?? null,
          first_name: profile?.firstName ?? null,
          last_name: profile?.lastName ?? null,
          preferred_name: profile?.preferredName ?? null,
          job_title: profile?.jobTitle ?? null,
          department: profile?.department ?? null,
          seniority: profile?.seniority ?? null,
          timezone: profile?.timezone ?? null,
          linked_in_url: profile?.linkedInUrl ?? null,
          twitter_url: profile?.twitterUrl ?? null,
          company_entity_id: profile?.company && typeof profile.company === "object" ? profile.company.id : profile?.company ?? null
        };
      });
    }
  }
});
const { POST, PUT, DELETE } = crud;
const GET = crud.GET;
const personListItemSchema = z.object({
  id: z.string().uuid(),
  display_name: z.string().optional(),
  description: z.string().nullable().optional(),
  owner_user_id: z.string().uuid().nullable().optional(),
  primary_email: z.string().nullable().optional(),
  primary_phone: z.string().nullable().optional(),
  status: z.string().nullable().optional(),
  lifecycle_stage: z.string().nullable().optional(),
  source: z.string().nullable().optional(),
  next_interaction_at: z.string().nullable().optional(),
  next_interaction_name: z.string().nullable().optional(),
  next_interaction_ref_id: z.string().nullable().optional(),
  next_interaction_icon: z.string().nullable().optional(),
  next_interaction_color: z.string().nullable().optional(),
  organization_id: z.string().uuid().nullable().optional(),
  tenant_id: z.string().uuid().nullable().optional(),
  created_at: z.string().nullable().optional()
});
const personCreateResponseSchema = z.object({
  id: z.string().uuid().nullable(),
  personId: z.string().uuid().nullable()
});
const openApi = createCustomersCrudOpenApi({
  resourceName: "Person",
  pluralName: "People",
  querySchema: listSchema,
  listResponseSchema: createPagedListResponseSchema(personListItemSchema),
  create: {
    schema: personCreateSchema,
    responseSchema: personCreateResponseSchema,
    description: "Creates a person contact using scoped organization and tenant identifiers."
  },
  update: {
    schema: personUpdateSchema,
    responseSchema: defaultOkResponseSchema,
    description: "Updates contact details or custom fields for a person."
  },
  del: {
    schema: z.object({ id: z.string().uuid() }),
    responseSchema: defaultOkResponseSchema,
    description: "Deletes a person by id. Request body or query may provide the identifier.",
    errors: [
      {
        status: 422,
        description: "Person has dependent records (e.g. linked deals); unlink or reassign before delete.",
        schema: z.object({
          error: z.string(),
          code: z.literal("PERSON_HAS_DEPENDENTS")
        })
      }
    ]
  }
});
export {
  DELETE,
  GET,
  POST,
  PUT,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
