import { NextResponse } from "next/server";
import { z } from "zod";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
import {
  CustomerEntity,
  CustomerPersonProfile,
  CustomerAddress,
  CustomerComment,
  CustomerActivity,
  CustomerTagAssignment,
  CustomerLabelAssignment,
  CustomerDealPersonLink,
  CustomerTodoLink,
  CustomerInteraction
} from "../../../data/entities.js";
import { User } from "@open-mercato/core/modules/auth/data/entities";
import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
import { E } from "../../../../../generated/entities.ids.generated.js";
import { mergePersonCustomFieldValues, resolvePersonCustomFieldRouting } from "../../../lib/customFieldRouting.js";
import { isOpenInteractionStatus, TERMINAL_INTERACTION_STATUS_LIST } from "../../../lib/interactionStatus.js";
import {
  CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,
  EXAMPLE_TODO_SOURCE,
  CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,
  mapInteractionRecordToActivitySummary,
  mapInteractionRecordToTodoSummary
} from "../../../lib/interactionCompatibility.js";
import { resolveCustomerInteractionFeatureFlags } from "../../../lib/interactionFeatureFlags.js";
import { hydrateCanonicalInteractions } from "../../../lib/interactionReadModel.js";
import { findWithDecryption, findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { parseBooleanFromUnknown, parseBooleanToken } from "@open-mercato/shared/lib/boolean";
import { isOrganizationReadAccessAllowed } from "@open-mercato/core/modules/directory/utils/organizationScopeGuard";
import { loadPersonCompanyLinks, summarizePersonCompanies } from "../../../lib/personCompanies.js";
import { normalizeCustomerDetailCustomFields } from "../../detailCustomFields.js";
import { buildEmailVisibilityMikroFilter } from "../../../lib/visibilityFilter.js";
import { resolveCustomerDetailTenantScope } from "../../../lib/detailTenantScope.js";
import { runWithCacheTenant } from "@open-mercato/cache";
import { buildCollectionTags, canonicalizeResourceTag, isCrudCacheEnabled, resolveCrudCache } from "@open-mercato/shared/lib/crud/cache";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("customers");
const metadata = {
  GET: { requireAuth: true, requireFeatures: ["customers.people.view"] }
};
const paramsSchema = z.object({
  id: z.string().uuid()
});
const PERSON_DETAIL_TTL_MS = 6e4;
const PERSON_DETAIL_TAG_RESOURCES = [
  "customers.person",
  "customers.address",
  "customers.tagAssignment",
  "customers.labelAssignment",
  "customers.personCompanyLink",
  "customers.interaction",
  "customers.activity"
];
function buildPersonDetailCacheKey(params) {
  const include = [...params.includeTokens].sort((a, b) => a.localeCompare(b)).join("|") || "none";
  const scoped = [...params.scopedOrganizationIds].sort((a, b) => a.localeCompare(b)).join("|") || "null";
  return [
    "customers:person-detail",
    params.personId,
    `tenant=${params.tenantId ?? "null"}`,
    `org=${params.organizationId ?? "null"}`,
    `caller=${params.callerId ?? "null"}`,
    `selOrg=${params.selectedOrganizationId ?? "null"}`,
    `scope=${scoped}`,
    `mode=${params.interactionMode}`,
    `include=${include}`
  ].join(":");
}
function buildPersonDetailCacheTags(tenantId, organizationId) {
  const tags = [];
  for (const resource of PERSON_DETAIL_TAG_RESOURCES) {
    const canonical = canonicalizeResourceTag(resource) ?? resource;
    tags.push(...buildCollectionTags(canonical, tenantId, [organizationId]));
  }
  return tags;
}
function parseIncludeParams(request) {
  const url = new URL(request.url);
  const raw = url.searchParams.getAll("include");
  const tokens = /* @__PURE__ */ new Set();
  raw.forEach((entry) => {
    if (!entry) return;
    entry.split(",").map((part) => part.trim().toLowerCase()).filter((part) => part.length > 0).forEach((part) => tokens.add(part));
  });
  return tokens;
}
function forbidden(message) {
  return NextResponse.json({ error: message }, { status: 403 });
}
function notFound(message) {
  return NextResponse.json({ error: message }, { status: 404 });
}
function serializeTags(assignments) {
  return assignments.map((assignment) => {
    const tag = assignment.tag;
    if (!tag || typeof tag === "string") return null;
    return {
      id: tag.id,
      label: tag.label,
      color: tag.color ?? null
    };
  }).filter(
    (tag) => tag !== null
  );
}
function extractTodoTitle(record) {
  const candidates = ["title", "subject", "name", "summary", "text", "description"];
  for (const key of candidates) {
    const value = record[key];
    if (typeof value === "string" && value.trim().length > 0) {
      return value.trim();
    }
  }
  return null;
}
function parseNumber(value) {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string") {
    const trimmed = value.trim();
    if (!trimmed) return null;
    const parsed = Number(trimmed);
    if (!Number.isNaN(parsed)) return parsed;
  }
  return null;
}
function parseDateValue(value) {
  if (value instanceof Date) {
    const ts = value.getTime();
    return Number.isNaN(ts) ? null : value.toISOString();
  }
  if (typeof value === "string") {
    const trimmed = value.trim();
    if (!trimmed) return null;
    const candidate = new Date(trimmed);
    if (!Number.isNaN(candidate.getTime())) return candidate.toISOString();
  }
  return null;
}
function readCustomField(record, key) {
  const custom = record.custom ?? record.customFields ?? record.cf;
  if (custom && typeof custom === "object") {
    const bucket = custom;
    if (key in bucket) return bucket[key];
  }
  return void 0;
}
function normalizeProfilerTokens(input) {
  if (!input) return [];
  return input.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token.length > 0);
}
function profilerMatches(scope, tokens) {
  if (!tokens.length) return false;
  const lower = scope.toLowerCase();
  return tokens.some((token) => {
    if (token === "*" || token === "all" || parseBooleanToken(token) === true) return true;
    if (token.endsWith("*")) {
      const prefix = token.slice(0, -1);
      return prefix.length === 0 ? true : lower.startsWith(prefix);
    }
    return token === lower;
  });
}
function isRouteProfilingEnabled(scope) {
  const candidates = [
    process.env.OM_CRUD_PROFILE,
    process.env.NEXT_PUBLIC_OM_CRUD_PROFILE,
    process.env.OM_ROUTE_PROFILE,
    process.env.NEXT_PUBLIC_OM_ROUTE_PROFILE
  ];
  for (const raw of candidates) {
    if (!raw) continue;
    const tokens = normalizeProfilerTokens(raw);
    if (profilerMatches(scope, tokens)) return true;
  }
  return false;
}
function createRouteProfiler(scope) {
  if (!isRouteProfilingEnabled(scope)) {
    return { enabled: false, mark: () => {
    }, end: () => {
    } };
  }
  const marks = [{ label: "start", time: process.hrtime.bigint() }];
  const round = (value) => Math.round(value * 1e3) / 1e3;
  return {
    enabled: true,
    mark(label, extra) {
      marks.push({ label, time: process.hrtime.bigint(), extra });
    },
    end(extra) {
      marks.push({ label: "end", time: process.hrtime.bigint(), extra });
      const segments = [];
      for (let idx = 1; idx < marks.length; idx += 1) {
        const current = marks[idx];
        const previous = marks[idx - 1];
        if (current.label === "end") continue;
        const durationMs = Number(current.time - previous.time) / 1e6;
        segments.push({
          step: current.label,
          durationMs: round(durationMs),
          ...current.extra ? { extra: current.extra } : {}
        });
      }
      const totalNs = marks[marks.length - 1].time - marks[0].time;
      const payload = {
        scope,
        totalMs: round(Number(totalNs) / 1e6),
        steps: segments
      };
      const tail = marks[marks.length - 1];
      if (tail.extra && Object.keys(tail.extra).length > 0) payload.meta = tail.extra;
      else if (extra && Object.keys(extra).length > 0) payload.meta = extra;
      try {
        logger.info("route:profile", { payload });
      } catch {
      }
    }
  };
}
async function resolveTodoDetails(queryEngine, links, tenantId, organizationIds, profiler) {
  const details = /* @__PURE__ */ new Map();
  if (!links.length || !tenantId) return details;
  const scopedOrgIds = organizationIds.filter((value) => typeof value === "string" && value.trim().length > 0);
  const idsBySource = /* @__PURE__ */ new Map();
  for (const link of links) {
    const source = typeof link.todoSource === "string" && link.todoSource.trim().length > 0 ? link.todoSource : EXAMPLE_TODO_SOURCE;
    const id = typeof link.todoId === "string" && link.todoId.trim().length > 0 ? link.todoId : String(link.todoId ?? "");
    if (!id) continue;
    if (!idsBySource.has(source)) idsBySource.set(source, /* @__PURE__ */ new Set());
    idsBySource.get(source).add(id);
  }
  profiler?.mark("todo_links_grouped", { sourceCount: idsBySource.size, linkCount: links.length });
  for (const [source, idSet] of idsBySource.entries()) {
    const ids = Array.from(idSet);
    if (!ids.length) continue;
    profiler?.mark("todo_query_start", { source, count: ids.length });
    try {
      const result = await queryEngine.query(source, {
        tenantId,
        organizationIds: scopedOrgIds.length > 0 ? scopedOrgIds : void 0,
        filters: { id: { $in: ids } },
        includeCustomFields: ["priority", "due_at", "severity", "description"],
        page: { page: 1, pageSize: Math.max(ids.length, 1) }
      });
      const itemCount = Array.isArray(result.items) ? result.items.length : 0;
      profiler?.mark("todo_query_complete", { source, itemCount });
      let enriched = 0;
      for (const item of result.items ?? []) {
        if (!item || typeof item !== "object") continue;
        const record = item;
        const rawId = typeof record.id === "string" && record.id.trim().length > 0 ? record.id : String(record.id ?? "");
        if (!rawId) continue;
        const title = extractTodoTitle(record);
        const isDone = (() => {
          const direct = parseBooleanFromUnknown(record.is_done);
          if (direct !== null) return direct;
          const custom = parseBooleanFromUnknown(readCustomField(record, "is_done"));
          if (custom !== null) return custom;
          const generic = parseBooleanFromUnknown(record.isDone);
          if (generic !== null) return generic;
          return parseBooleanFromUnknown(readCustomField(record, "isDone"));
        })();
        const priority = (() => {
          const candidates = [
            record["cf:priority"],
            record["cf_priority"],
            record.priority,
            readCustomField(record, "priority")
          ];
          for (const candidate of candidates) {
            const parsed = parseNumber(candidate);
            if (parsed !== null) return parsed;
          }
          return null;
        })();
        const severity = (() => {
          const candidates = [
            record["cf:severity"],
            record["cf_severity"],
            readCustomField(record, "severity")
          ];
          for (const candidate of candidates) {
            if (typeof candidate === "string") {
              const trimmed = candidate.trim();
              if (trimmed.length) return trimmed;
            }
          }
          return null;
        })();
        const descriptionValue = (() => {
          const candidates = [
            record.description,
            record["cf:description"],
            record["cf_description"],
            readCustomField(record, "description")
          ];
          for (const candidate of candidates) {
            if (typeof candidate === "string") {
              const trimmed = candidate.trim();
              if (trimmed.length) return trimmed;
            }
          }
          return null;
        })();
        const dueAt = (() => {
          const candidates = [
            record.due_at,
            record.dueAt,
            record["cf:due_at"],
            record["cf_due_at"],
            readCustomField(record, "due_at"),
            readCustomField(record, "dueAt")
          ];
          for (const candidate of candidates) {
            const parsed = parseDateValue(candidate);
            if (parsed) return parsed;
          }
          return null;
        })();
        const organizationId = typeof record.organization_id === "string" && record.organization_id.trim().length > 0 ? record.organization_id : typeof record.organizationId === "string" && record.organizationId.trim().length > 0 ? record.organizationId : null;
        const customValues = {};
        const assignCustomValue = (key, value) => {
          if (typeof key !== "string") return;
          const trimmedKey = key.trim();
          if (!trimmedKey.length) return;
          customValues[trimmedKey] = value === void 0 ? null : value;
        };
        for (const [rawKey, rawValue] of Object.entries(record)) {
          if (rawKey.startsWith("cf:")) {
            assignCustomValue(rawKey.slice(3), rawValue);
          } else if (rawKey.startsWith("cf_")) {
            assignCustomValue(rawKey.slice(3), rawValue);
          }
        }
        const nestedCustom = record.custom ?? record.customFields ?? record.cf;
        if (nestedCustom && typeof nestedCustom === "object") {
          for (const [nestedKey, nestedValue] of Object.entries(nestedCustom)) {
            assignCustomValue(nestedKey, nestedValue);
          }
        }
        details.set(`${source}:${rawId}`, {
          title,
          isDone,
          priority,
          severity,
          description: descriptionValue,
          dueAt,
          organizationId,
          customValues: Object.keys(customValues).length ? customValues : null
        });
        enriched += 1;
      }
      profiler?.mark("todo_items_processed", { source, enriched });
    } catch (err) {
      profiler?.mark("todo_query_failed", { source, error: err instanceof Error ? err.message : String(err) });
      logger.warn("customers.people.detail: failed to resolve todos", { source, err });
    }
  }
  profiler?.mark("todo_details_ready", { enrichedCount: details.size });
  return details;
}
async function GET(_req, ctx) {
  const profiler = createRouteProfiler("customers.people.detail");
  profiler.mark("request_received");
  const includeTokens = parseIncludeParams(_req);
  profiler.mark("includes_parsed", { include: Array.from(includeTokens) });
  const includeActivities = includeTokens.has("activities");
  const includeAddresses = includeTokens.has("addresses");
  const includeComments = includeTokens.has("comments") || includeTokens.has("notes");
  const includeDeals = includeTokens.has("deals");
  const includeInteractions = includeTokens.has("interactions");
  const includeTodos = includeTokens.has("todos") || includeTokens.has("tasks");
  const plannedPreviewLimit = 5;
  let statusCode = 500;
  let profileMeta;
  let addresses = [];
  let comments = [];
  let activities = [];
  let todoLinks = [];
  let todoDetails = /* @__PURE__ */ new Map();
  let deals = [];
  let tagAssignments = [];
  let userMap = /* @__PURE__ */ new Map();
  let customFields = {};
  let viewerUserId = null;
  let profile = null;
  let companies = [];
  try {
    const parse = paramsSchema.safeParse({ id: ctx.params?.id });
    if (!parse.success) {
      statusCode = 400;
      profileMeta = { reason: "invalid_person_id" };
      return NextResponse.json({ error: "Invalid person id" }, { status: 400 });
    }
    profiler.mark("params_resolved", { id: parse.data.id });
    const auth = await getAuthFromRequest(_req);
    if (!auth) {
      profiler.mark("auth_missing");
      statusCode = 401;
      profileMeta = { reason: "unauthorized" };
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }
    profiler.mark("auth_resolved", {
      tenantId: auth.tenantId ?? null,
      organizationId: auth.orgId ?? null,
      userId: auth.sub ?? null,
      isApiKey: !!auth.isApiKey
    });
    viewerUserId = auth.isApiKey ? null : auth.sub ?? null;
    const container = await createRequestContainer();
    profiler.mark("container_resolved");
    const scope = await resolveOrganizationScopeForRequest({ container, auth, request: _req });
    const interactionFlags = await resolveCustomerInteractionFeatureFlags(container, scope?.tenantId ?? auth.tenantId);
    const interactionMode = interactionFlags.unified ? "canonical" : "legacy";
    profiler.mark("scope_resolved", {
      scopedOrganizations: Array.isArray(scope?.filterIds) ? scope.filterIds.length : 0
    });
    const em = container.resolve("em");
    const tenantScope = resolveCustomerDetailTenantScope(parse.data.id, "person", auth);
    if (!tenantScope.allowed) {
      statusCode = 404;
      profileMeta = { reason: "person_tenant_mismatch" };
      return notFound("Person not found");
    }
    const person = await findOneWithDecryption(
      em,
      CustomerEntity,
      tenantScope.where,
      {},
      {
        tenantId: scope?.tenantId ?? auth.tenantId ?? null,
        organizationId: scope?.selectedId ?? auth.orgId ?? null
      }
    );
    profiler.mark("person_loaded", { found: !!person });
    if (!person) {
      statusCode = 404;
      profileMeta = { reason: "person_not_found" };
      return notFound("Person not found");
    }
    if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: person.organizationId })) {
      statusCode = 403;
      profileMeta = { reason: "organization_forbidden" };
      return forbidden("Access denied");
    }
    const personDetailTenantId = person.tenantId ?? auth.tenantId ?? null;
    const personDetailOrganizationId = person.organizationId ?? null;
    const personDetailCache = isCrudCacheEnabled() ? resolveCrudCache(container) : null;
    const personDetailCacheKey = personDetailCache ? buildPersonDetailCacheKey({
      personId: person.id,
      tenantId: personDetailTenantId,
      organizationId: personDetailOrganizationId,
      callerId: auth.sub ?? null,
      selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
      scopedOrganizationIds: Array.isArray(scope?.filterIds) ? scope.filterIds : [],
      interactionMode,
      includeTokens: Array.from(includeTokens)
    }) : null;
    const personDetailCacheTags = personDetailCache ? buildPersonDetailCacheTags(personDetailTenantId, personDetailOrganizationId) : [];
    if (personDetailCache && personDetailCacheKey) {
      const cached = await runWithCacheTenant(personDetailTenantId, () => personDetailCache.get(personDetailCacheKey));
      if (cached) {
        statusCode = 200;
        profileMeta = { cacheHit: true, include: Array.from(includeTokens), interactionMode };
        profiler.mark("cache_hit");
        return NextResponse.json(cached);
      }
    }
    const [profileResult, personCompanyLinks] = await Promise.all([
      findOneWithDecryption(em, CustomerPersonProfile, { entity: person }, { populate: ["company"] }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }),
      loadPersonCompanyLinks(em, person)
    ]);
    profile = profileResult;
    profiler.mark("profile_loaded", { found: !!profile });
    companies = summarizePersonCompanies(profile, personCompanyLinks);
    const emailVisibilityFilter = buildEmailVisibilityMikroFilter({
      currentUserId: viewerUserId,
      userFeatures: void 0
    });
    const personScope = { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null };
    const shouldLoadCanonicalInteractions = includeInteractions || includeActivities || includeTodos;
    const [
      addressesResult,
      tagAssignmentsResult,
      labelAssignments,
      commentsResult,
      canonicalInteractionRows
    ] = await Promise.all([
      includeAddresses ? findWithDecryption(em, CustomerAddress, { entity: person.id }, { orderBy: { isPrimary: "desc", createdAt: "desc" } }, personScope) : Promise.resolve([]),
      findWithDecryption(em, CustomerTagAssignment, { entity: person.id }, { populate: ["tag"] }, personScope),
      findWithDecryption(em, CustomerLabelAssignment, { entity: person.id }, { populate: ["label"] }, personScope),
      includeComments ? findWithDecryption(em, CustomerComment, { entity: person.id }, { orderBy: { createdAt: "desc" }, limit: 50 }, personScope) : Promise.resolve([]),
      shouldLoadCanonicalInteractions ? findWithDecryption(
        em,
        CustomerInteraction,
        interactionFlags.unified ? { entity: person.id, deletedAt: null, ...emailVisibilityFilter } : { entity: person.id, ...emailVisibilityFilter },
        { orderBy: { scheduledAt: "asc", createdAt: "desc" }, limit: 100 },
        personScope
      ) : Promise.resolve([])
    ]);
    addresses = addressesResult;
    tagAssignments = tagAssignmentsResult;
    comments = commentsResult;
    profiler.mark("addresses_loaded", { count: addresses.length });
    profiler.mark("tags_loaded", { count: tagAssignments.length });
    profiler.mark("labels_loaded", { count: labelAssignments.length });
    profiler.mark("comments_loaded", { count: comments.length });
    profiler.mark("canonical_interactions_loaded", { count: canonicalInteractionRows.length });
    const canonicalActiveInteractions = canonicalInteractionRows.filter((interaction) => !interaction.deletedAt);
    const canonicalInteractions = shouldLoadCanonicalInteractions ? await hydrateCanonicalInteractions({
      em,
      container,
      auth,
      selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
      interactions: canonicalActiveInteractions,
      enrich: includeInteractions
    }) : [];
    profiler.mark("canonical_interactions_hydrated", { count: canonicalInteractions.length });
    const plannedPreviewInteractions = shouldLoadCanonicalInteractions ? (() => {
      const sorted = [...canonicalInteractions].filter((interaction) => isOpenInteractionStatus(interaction.status) && interaction.interactionType !== "task").sort((left, right) => {
        const leftTime = new Date(left.scheduledAt ?? left.createdAt).getTime();
        const rightTime = new Date(right.scheduledAt ?? right.createdAt).getTime();
        if (leftTime === rightTime) return left.id.localeCompare(right.id);
        return leftTime - rightTime;
      });
      return sorted.slice(0, plannedPreviewLimit);
    })() : await hydrateCanonicalInteractions({
      em,
      container,
      auth,
      selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
      interactions: await findWithDecryption(
        em,
        CustomerInteraction,
        {
          entity: person.id,
          organizationId: person.organizationId,
          tenantId: person.tenantId,
          deletedAt: null,
          status: { $nin: [...TERMINAL_INTERACTION_STATUS_LIST] },
          interactionType: { $ne: "task" },
          ...emailVisibilityFilter
        },
        { orderBy: { scheduledAt: "ASC", createdAt: "ASC" }, limit: plannedPreviewLimit },
        { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }
      ),
      enrich: true
    });
    if (includeActivities && !interactionFlags.unified) {
      activities = await findWithDecryption(em, CustomerActivity, { entity: person.id }, { orderBy: { occurredAt: "desc", createdAt: "desc" }, limit: 50 }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null });
      profiler.mark("activities_loaded", { count: activities.length });
    }
    if (includeInteractions) {
      profiler.mark("interactions_loaded", { count: canonicalInteractions.length });
    }
    if (includeTodos && !interactionFlags.unified) {
      todoLinks = await findWithDecryption(em, CustomerTodoLink, { entity: person.id }, { orderBy: { createdAt: "desc" }, limit: 50 }, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null });
      profiler.mark("todo_links_loaded", { count: todoLinks.length });
      if (todoLinks.length) {
        const queryEngine = container.resolve("queryEngine");
        try {
          todoDetails = await resolveTodoDetails(
            queryEngine,
            todoLinks,
            person.tenantId ?? auth.tenantId ?? null,
            [person.organizationId ?? null, ...scope?.filterIds ?? []],
            profiler
          );
        } catch (err) {
          logger.warn("customers.people.detail: failed to enrich todo links", { err });
        }
        profiler.mark("todo_details_enriched", { count: todoDetails.size, links: todoLinks.length });
      }
    }
    const canonicalActivityBridgeIds = new Set(
      canonicalInteractionRows.filter((interaction) => interaction.source === CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE).map((interaction) => interaction.id)
    );
    const canonicalTodoBridgeIds = new Set(
      canonicalInteractionRows.filter((interaction) => interaction.source === CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE).map((interaction) => interaction.id)
    );
    const canonicalActivityItems = canonicalInteractions.filter((interaction) => interaction.interactionType !== "task").map((interaction) => mapInteractionRecordToActivitySummary(interaction));
    const canonicalTodoItems = canonicalInteractions.filter((interaction) => interaction.interactionType === "task").map((interaction) => mapInteractionRecordToTodoSummary(interaction));
    const authorIds = /* @__PURE__ */ new Set();
    if (includeActivities) {
      for (const activity of activities) {
        if (activity.authorUserId) authorIds.add(activity.authorUserId);
      }
    }
    if (includeComments) {
      for (const comment of comments) {
        if (comment.authorUserId) authorIds.add(comment.authorUserId);
      }
    }
    if (viewerUserId) authorIds.add(viewerUserId);
    if (authorIds.size) {
      const users = await findWithDecryption(em, User, { id: { $in: Array.from(authorIds) } }, {}, { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null });
      userMap = new Map(
        users.map((user) => [
          user.id,
          {
            name: user.name ?? null,
            email: user.email ?? null
          }
        ])
      );
      profiler.mark("authors_loaded", { count: userMap.size });
    }
    if (includeDeals) {
      const dealLinks = await findWithDecryption(
        em,
        CustomerDealPersonLink,
        { person: person.id },
        { populate: ["deal"] },
        { tenantId: person.tenantId ?? auth.tenantId ?? null, organizationId: person.organizationId ?? auth.orgId ?? null }
      );
      deals = dealLinks.map((link) => link.deal ?? null).filter((deal) => !!deal && typeof deal !== "string");
      profiler.mark("deals_loaded", { count: deals.length });
    }
    const profileId = profile?.id ?? null;
    const [entityCustomFieldValues, profileCustomFieldValues, routing] = await Promise.all([
      loadCustomFieldValues({
        em,
        entityId: E.customers.customer_entity,
        recordIds: [person.id],
        tenantIdByRecord: { [person.id]: person.tenantId ?? null },
        organizationIdByRecord: { [person.id]: person.organizationId ?? null },
        tenantFallbacks: [
          person.tenantId ?? auth.tenantId ?? null
        ].filter((v) => !!v)
      }),
      profileId ? loadCustomFieldValues({
        em,
        entityId: E.customers.customer_person_profile,
        recordIds: [profileId],
        tenantIdByRecord: { [profileId]: profile?.tenantId ?? null },
        organizationIdByRecord: { [profileId]: profile?.organizationId ?? null },
        tenantFallbacks: [
          profile?.tenantId ?? person.tenantId ?? auth.tenantId ?? null
        ].filter((v) => !!v)
      }) : Promise.resolve({}),
      resolvePersonCustomFieldRouting(em, person.tenantId ?? null, person.organizationId ?? null)
    ]);
    profiler.mark("entity_custom_fields_loaded", { keys: Object.keys(entityCustomFieldValues?.[person.id] ?? {}).length });
    if (profileId) {
      profiler.mark("profile_custom_fields_loaded", { keys: Object.keys(profileCustomFieldValues?.[profileId] ?? {}).length });
    }
    profiler.mark("custom_field_routing_resolved", { keys: routing.size });
    customFields = normalizeCustomerDetailCustomFields(
      mergePersonCustomFieldValues(
        routing,
        entityCustomFieldValues?.[person.id] ?? {},
        profileId ? profileCustomFieldValues?.[profileId] ?? {} : {}
      )
    );
    profiler.mark("custom_fields_merged", { keys: Object.keys(customFields).length });
    const viewerUserIdFinal = viewerUserId;
    const [
      commentsCount,
      activitiesCount,
      interactionsCount,
      todosCount,
      addressesCount,
      dealsCount
    ] = await Promise.all([
      includeComments ? Promise.resolve(comments.length) : em.count(CustomerComment, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId
      }),
      em.count(CustomerInteraction, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId,
        deletedAt: null,
        interactionType: { $ne: "task" },
        ...emailVisibilityFilter
      }),
      em.count(CustomerInteraction, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId,
        deletedAt: null,
        ...emailVisibilityFilter
      }),
      interactionFlags.unified ? em.count(CustomerInteraction, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId,
        deletedAt: null,
        interactionType: "task"
      }) : em.count(CustomerTodoLink, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId
      }),
      includeAddresses ? Promise.resolve(addresses.length) : em.count(CustomerAddress, {
        entity: person.id,
        organizationId: person.organizationId,
        tenantId: person.tenantId
      }),
      includeDeals ? Promise.resolve(deals.length) : em.count(CustomerDealPersonLink, {
        person: person.id
      })
    ]);
    const counts = {
      tags: tagAssignments.length + labelAssignments.length,
      comments: commentsCount,
      activities: activitiesCount,
      interactions: interactionsCount,
      todos: todosCount,
      addresses: addressesCount,
      deals: dealsCount,
      companies: companies.length
    };
    const payload = {
      interactionMode,
      person: {
        id: person.id,
        displayName: person.displayName,
        description: person.description,
        ownerUserId: person.ownerUserId,
        primaryEmail: person.primaryEmail,
        primaryPhone: person.primaryPhone,
        status: person.status,
        lifecycleStage: person.lifecycleStage,
        source: person.source,
        temperature: person.temperature,
        renewalQuarter: person.renewalQuarter,
        nextInteractionAt: person.nextInteractionAt ? person.nextInteractionAt.toISOString() : null,
        nextInteractionName: person.nextInteractionName,
        nextInteractionRefId: person.nextInteractionRefId,
        nextInteractionIcon: person.nextInteractionIcon,
        nextInteractionColor: person.nextInteractionColor,
        organizationId: person.organizationId,
        tenantId: person.tenantId,
        isActive: person.isActive,
        createdAt: person.createdAt.toISOString(),
        updatedAt: person.updatedAt.toISOString()
      },
      profile: profile ? {
        id: profile.id,
        firstName: profile.firstName,
        lastName: profile.lastName,
        preferredName: profile.preferredName,
        jobTitle: profile.jobTitle,
        department: profile.department,
        seniority: profile.seniority,
        timezone: profile.timezone,
        linkedInUrl: profile.linkedInUrl,
        twitterUrl: profile.twitterUrl,
        companyEntityId: profile.company ? typeof profile.company === "string" ? profile.company : profile.company.id : null,
        updatedAt: profile.updatedAt.toISOString()
      } : null,
      customFields,
      tags: [
        ...serializeTags(tagAssignments),
        ...labelAssignments.map((a) => {
          const label = a.label;
          if (!label || typeof label === "string") return null;
          return { id: label.id, label: label.label, color: null };
        }).filter((t) => t !== null)
      ],
      addresses: includeAddresses ? addresses.map((address) => ({
        id: address.id,
        name: address.name,
        purpose: address.purpose,
        addressLine1: address.addressLine1,
        addressLine2: address.addressLine2,
        buildingNumber: address.buildingNumber,
        flatNumber: address.flatNumber,
        city: address.city,
        region: address.region,
        postalCode: address.postalCode,
        country: address.country,
        latitude: address.latitude,
        longitude: address.longitude,
        isPrimary: address.isPrimary,
        createdAt: address.createdAt.toISOString()
      })) : [],
      comments: includeComments ? comments.map((comment) => {
        const authorInfo = comment.authorUserId ? userMap.get(comment.authorUserId) : null;
        return {
          id: comment.id,
          body: comment.body,
          authorUserId: comment.authorUserId,
          authorName: authorInfo?.name ?? null,
          authorEmail: authorInfo?.email ?? null,
          dealId: comment.deal ? typeof comment.deal === "string" ? comment.deal : comment.deal.id : null,
          createdAt: comment.createdAt.toISOString(),
          appearanceIcon: comment.appearanceIcon ?? null,
          appearanceColor: comment.appearanceColor ?? null
        };
      }) : [],
      activities: includeActivities ? interactionFlags.unified ? canonicalActivityItems : [
        ...activities.filter((activity) => !canonicalActivityBridgeIds.has(activity.id)).map((activity) => ({
          id: activity.id,
          activityType: activity.activityType,
          subject: activity.subject,
          body: activity.body,
          occurredAt: activity.occurredAt ? activity.occurredAt.toISOString() : null,
          dealId: activity.deal ? typeof activity.deal === "string" ? activity.deal : activity.deal.id : null,
          authorUserId: activity.authorUserId,
          authorName: activity.authorUserId ? userMap.get(activity.authorUserId)?.name ?? null : null,
          authorEmail: activity.authorUserId ? userMap.get(activity.authorUserId)?.email ?? null : null,
          createdAt: activity.createdAt.toISOString(),
          appearanceIcon: activity.appearanceIcon ?? null,
          appearanceColor: activity.appearanceColor ?? null
        })),
        ...canonicalActivityItems.filter(
          (activity) => canonicalInteractions.some(
            (interaction) => interaction.id === activity.id && interaction.source === CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE
          )
        )
      ].sort((left, right) => {
        const leftTime = new Date(left.occurredAt ?? left.createdAt).getTime();
        const rightTime = new Date(right.occurredAt ?? right.createdAt).getTime();
        if (leftTime === rightTime) return right.id.localeCompare(left.id);
        return rightTime - leftTime;
      }).slice(0, 50) : [],
      interactions: includeInteractions ? canonicalInteractions : [],
      deals: includeDeals ? deals.map((deal) => ({
        id: deal.id,
        title: deal.title,
        status: deal.status,
        pipelineStage: deal.pipelineStage,
        pipelineId: deal.pipelineId ?? null,
        pipelineStageId: deal.pipelineStageId ?? null,
        valueAmount: deal.valueAmount,
        valueCurrency: deal.valueCurrency,
        probability: deal.probability,
        expectedCloseAt: deal.expectedCloseAt ? deal.expectedCloseAt.toISOString() : null,
        ownerUserId: deal.ownerUserId,
        source: deal.source,
        closureOutcome: deal.closureOutcome ?? null,
        lossReasonId: deal.lossReasonId ?? null,
        lossNotes: deal.lossNotes ?? null,
        createdAt: deal.createdAt.toISOString(),
        updatedAt: deal.updatedAt.toISOString()
      })) : [],
      todos: includeTodos ? interactionFlags.unified ? canonicalTodoItems : [
        ...todoLinks.filter((link) => !canonicalTodoBridgeIds.has(link.todoId)).map((link) => {
          const source = typeof link.todoSource === "string" && link.todoSource.trim().length > 0 ? link.todoSource : EXAMPLE_TODO_SOURCE;
          const key = `${source}:${link.todoId}`;
          const detail = todoDetails.get(key);
          return {
            id: link.id,
            todoId: link.todoId,
            todoSource: source,
            createdAt: link.createdAt.toISOString(),
            createdByUserId: link.createdByUserId,
            title: detail?.title ?? null,
            isDone: detail?.isDone ?? null,
            priority: detail?.priority ?? null,
            severity: detail?.severity ?? null,
            description: detail?.description ?? null,
            dueAt: detail?.dueAt ?? null,
            todoOrganizationId: detail?.organizationId ?? null,
            customValues: detail?.customValues ?? null
          };
        }),
        ...canonicalTodoItems.filter(
          (todo) => canonicalInteractions.some(
            (interaction) => interaction.id === todo.todoId && interaction.source === CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE
          )
        )
      ].sort((left, right) => {
        const leftTime = new Date(left.createdAt).getTime();
        const rightTime = new Date(right.createdAt).getTime();
        if (leftTime === rightTime) return right.id.localeCompare(left.id);
        return rightTime - leftTime;
      }).slice(0, 50) : [],
      isPrimary: companies.some((entry) => entry.isPrimary),
      companies: companies.map((entry) => ({
        id: entry.companyId,
        displayName: entry.displayName,
        isPrimary: entry.isPrimary
      })),
      company: (() => {
        const primaryCompany = companies.find((entry) => entry.isPrimary) ?? companies[0] ?? null;
        return primaryCompany ? { id: primaryCompany.companyId, displayName: primaryCompany.displayName } : null;
      })(),
      plannedActivitiesPreview: plannedPreviewInteractions,
      counts,
      viewer: {
        userId: viewerUserIdFinal,
        name: viewerUserIdFinal ? userMap.get(viewerUserIdFinal)?.name ?? null : null,
        email: viewerUserIdFinal ? userMap.get(viewerUserIdFinal)?.email ?? auth.email ?? null : auth.email ?? null
      }
    };
    if (personDetailCache && personDetailCacheKey) {
      try {
        await runWithCacheTenant(
          personDetailTenantId,
          () => personDetailCache.set(personDetailCacheKey, payload, {
            ttl: PERSON_DETAIL_TTL_MS,
            tags: personDetailCacheTags
          })
        );
      } catch {
      }
    }
    const response = NextResponse.json(payload);
    statusCode = 200;
    profileMeta = {
      include: Array.from(includeTokens),
      interactionMode,
      counts
    };
    profiler.mark("response_ready", { status: statusCode });
    return response;
  } catch (err) {
    if (statusCode < 400) statusCode = 500;
    const message = err instanceof Error ? err.message : String(err);
    profileMeta = { ...profileMeta ?? {}, error: message };
    throw err;
  } finally {
    profiler.end({ status: statusCode, ...profileMeta ?? {} });
  }
}
const personDetailQuerySchema = z.object({
  include: z.string().optional().describe("Comma-separated list of relations to include (addresses, comments, activities, interactions, deals, todos).")
}).passthrough();
const personDetailResponseSchema = z.object({
  interactionMode: z.enum(["canonical", "legacy"]),
  person: z.object({
    id: z.string().uuid(),
    displayName: z.string().nullable().optional(),
    description: z.string().nullable().optional(),
    ownerUserId: z.string().uuid().nullable().optional(),
    primaryEmail: z.string().nullable().optional(),
    primaryPhone: z.string().nullable().optional(),
    status: z.string().nullable().optional(),
    lifecycleStage: z.string().nullable().optional(),
    source: z.string().nullable().optional(),
    nextInteractionAt: z.string().nullable().optional(),
    nextInteractionName: z.string().nullable().optional(),
    nextInteractionRefId: z.string().nullable().optional(),
    nextInteractionIcon: z.string().nullable().optional(),
    nextInteractionColor: z.string().nullable().optional(),
    organizationId: z.string().uuid().nullable().optional(),
    tenantId: z.string().uuid().nullable().optional(),
    isActive: z.boolean().optional(),
    createdAt: z.string(),
    updatedAt: z.string()
  }),
  profile: z.object({
    id: z.string().uuid(),
    firstName: z.string().nullable().optional(),
    lastName: z.string().nullable().optional(),
    preferredName: z.string().nullable().optional(),
    jobTitle: z.string().nullable().optional(),
    department: z.string().nullable().optional(),
    seniority: z.string().nullable().optional(),
    timezone: z.string().nullable().optional(),
    linkedInUrl: z.string().nullable().optional(),
    twitterUrl: z.string().nullable().optional(),
    companyEntityId: z.string().uuid().nullable().optional(),
    updatedAt: z.string()
  }).nullable(),
  customFields: z.record(z.string(), z.unknown()),
  tags: z.array(
    z.object({
      id: z.string().uuid(),
      label: z.string(),
      color: z.string().nullable().optional()
    })
  ),
  addresses: z.array(
    z.object({
      id: z.string().uuid(),
      name: z.string().nullable().optional(),
      purpose: z.string().nullable().optional(),
      addressLine1: z.string().nullable().optional(),
      addressLine2: z.string().nullable().optional(),
      buildingNumber: z.string().nullable().optional(),
      flatNumber: z.string().nullable().optional(),
      city: z.string().nullable().optional(),
      region: z.string().nullable().optional(),
      postalCode: z.string().nullable().optional(),
      country: z.string().nullable().optional(),
      latitude: z.number().nullable().optional(),
      longitude: z.number().nullable().optional(),
      isPrimary: z.boolean().nullable().optional(),
      createdAt: z.string()
    })
  ),
  comments: z.array(
    z.object({
      id: z.string().uuid(),
      body: z.string().nullable().optional(),
      authorUserId: z.string().uuid().nullable().optional(),
      authorName: z.string().nullable().optional(),
      authorEmail: z.string().nullable().optional(),
      dealId: z.string().uuid().nullable().optional(),
      createdAt: z.string(),
      appearanceIcon: z.string().nullable().optional(),
      appearanceColor: z.string().nullable().optional()
    })
  ),
  activities: z.array(
    z.object({
      id: z.string().uuid(),
      activityType: z.string(),
      subject: z.string().nullable().optional(),
      body: z.string().nullable().optional(),
      occurredAt: z.string().nullable().optional(),
      dealId: z.string().uuid().nullable().optional(),
      authorUserId: z.string().uuid().nullable().optional(),
      authorName: z.string().nullable().optional(),
      authorEmail: z.string().nullable().optional(),
      createdAt: z.string(),
      appearanceIcon: z.string().nullable().optional(),
      appearanceColor: z.string().nullable().optional()
    })
  ),
  interactions: z.array(
    z.object({
      id: z.string().uuid(),
      entityId: z.string().uuid().nullable().optional(),
      interactionType: z.string(),
      title: z.string().nullable().optional(),
      body: z.string().nullable().optional(),
      status: z.string(),
      scheduledAt: z.string().nullable().optional(),
      occurredAt: z.string().nullable().optional(),
      priority: z.number().nullable().optional(),
      authorUserId: z.string().uuid().nullable().optional(),
      ownerUserId: z.string().uuid().nullable().optional(),
      dealId: z.string().uuid().nullable().optional(),
      organizationId: z.string().uuid().nullable().optional(),
      tenantId: z.string().uuid().nullable().optional(),
      authorName: z.string().nullable().optional(),
      authorEmail: z.string().nullable().optional(),
      dealTitle: z.string().nullable().optional(),
      customValues: z.record(z.string(), z.unknown()).nullable().optional(),
      appearanceIcon: z.string().nullable().optional(),
      appearanceColor: z.string().nullable().optional(),
      source: z.string().nullable().optional(),
      _integrations: z.record(z.string(), z.unknown()).optional(),
      createdAt: z.string(),
      updatedAt: z.string()
    })
  ),
  deals: z.array(
    z.object({
      id: z.string().uuid(),
      title: z.string().nullable().optional(),
      status: z.string().nullable().optional(),
      pipelineStage: z.string().nullable().optional(),
      valueAmount: z.number().nullable().optional(),
      valueCurrency: z.string().nullable().optional(),
      probability: z.number().nullable().optional(),
      expectedCloseAt: z.string().nullable().optional(),
      ownerUserId: z.string().uuid().nullable().optional(),
      source: z.string().nullable().optional(),
      closureOutcome: z.string().nullable().optional(),
      lossReasonId: z.string().uuid().nullable().optional(),
      lossNotes: z.string().nullable().optional(),
      createdAt: z.string(),
      updatedAt: z.string()
    })
  ),
  todos: z.array(
    z.object({
      id: z.string().uuid(),
      todoId: z.string().uuid(),
      todoSource: z.string(),
      createdAt: z.string(),
      createdByUserId: z.string().uuid().nullable().optional(),
      title: z.string().nullable().optional(),
      isDone: z.boolean().nullable().optional(),
      priority: z.number().nullable().optional(),
      severity: z.string().nullable().optional(),
      description: z.string().nullable().optional(),
      dueAt: z.string().nullable().optional(),
      todoOrganizationId: z.string().uuid().nullable().optional(),
      customValues: z.record(z.string(), z.unknown()).nullable().optional()
    })
  ),
  isPrimary: z.boolean(),
  companies: z.array(
    z.object({
      id: z.string().uuid(),
      displayName: z.string(),
      isPrimary: z.boolean()
    })
  ),
  viewer: z.object({
    userId: z.string().uuid().nullable(),
    name: z.string().nullable(),
    email: z.string().nullable()
  })
});
const personDetailErrorSchema = z.object({
  error: z.string()
});
const openApi = {
  tag: "Customers",
  summary: "Fetch person detail",
  methods: {
    GET: {
      summary: "Fetch person with related data",
      description: "Returns a person customer record with optional related resources such as addresses, comments, activities, interactions, deals, and todos.",
      query: personDetailQuerySchema,
      responses: [
        { status: 200, description: "Person detail payload", schema: personDetailResponseSchema }
      ],
      errors: [
        { status: 400, description: "Invalid identifier", schema: personDetailErrorSchema },
        { status: 401, description: "Unauthorized", schema: personDetailErrorSchema },
        { status: 403, description: "Forbidden for tenant/organization scope", schema: personDetailErrorSchema },
        { status: 404, description: "Person not found", schema: personDetailErrorSchema }
      ]
    }
  }
};
export {
  GET,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
