import { NextResponse } from "next/server";
import { z } from "zod";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
import { SortDir } from "@open-mercato/shared/lib/query/types";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import {
  CustomerAddress,
  CustomerDealCompanyLink,
  CustomerDealPersonLink
} from "../../../data/entities.js";
import { buildDealListFilters, dealListQuerySchema } from "../route.js";
import { applyEntityIdRestriction, findMatchingEntityIdsBySearchTokensAcrossSources } from "../../utils.js";
import { createPagedListResponseSchema } from "../../openapi.js";
import {
  resolveDealLocations
} from "../../../lib/dealsMapLocation.js";
import { E } from "../../../../../generated/entities.ids.generated.js";
const metadata = {
  GET: { requireAuth: true, requireFeatures: ["customers.deals.view", "customers.activities.view"] }
};
const mapSortFields = ["createdAt", "updatedAt", "title", "value", "probability", "expectedCloseAt"];
const sortFieldMap = {
  createdAt: "created_at",
  updatedAt: "updated_at",
  title: "title",
  value: "value_amount",
  probability: "probability",
  expectedCloseAt: "expected_close_at"
};
const querySchema = dealListQuerySchema.extend({
  pageSize: z.coerce.number().min(1).max(100).default(100),
  sortField: z.enum(mapSortFields).optional()
});
const dealMapAssociationSchema = z.object({
  id: z.string().uuid(),
  label: z.string().nullable()
});
const dealMapLocationSchema = z.object({
  latitude: z.number(),
  longitude: z.number(),
  city: z.string().nullable(),
  region: z.string().nullable(),
  country: z.string().nullable(),
  source: z.enum(["company", "person"]),
  entityId: z.string().uuid(),
  addressId: z.string().uuid()
});
const dealMapItemSchema = z.object({
  id: z.string().uuid(),
  title: z.string().nullable(),
  status: z.string().nullable(),
  pipelineId: z.string().uuid().nullable(),
  pipelineStageId: z.string().uuid().nullable(),
  pipelineStage: z.string().nullable(),
  valueAmount: z.number().nullable(),
  valueCurrency: z.string().nullable(),
  probability: z.number().nullable(),
  expectedCloseAt: z.string().nullable(),
  ownerUserId: z.string().uuid().nullable(),
  updatedAt: z.string().nullable(),
  companies: z.array(dealMapAssociationSchema),
  people: z.array(dealMapAssociationSchema),
  location: dealMapLocationSchema.nullable()
});
const mapErrorSchema = z.object({
  error: z.string()
});
const openApi = {
  tag: "Customers",
  summary: "Deals map listing with resolved locations",
  methods: {
    GET: {
      summary: "Paginated deals that have a resolvable map location",
      description: "Returns a page of deals that have a coordinate-bearing linked company/person address, each enriched with one resolved location (company primary first, then earliest created; person addresses as fallback). Deals with no coordinate-bearing address are excluded entirely, so every item carries a non-null location in normal operation; the schema keeps location nullable only for the rare case where the address is deleted between the located-deal resolution and the page fetch.",
      query: querySchema,
      responses: [
        { status: 200, description: "Paged located deals", schema: createPagedListResponseSchema(dealMapItemSchema) }
      ],
      errors: [
        { status: 400, description: "Invalid query parameters", schema: mapErrorSchema },
        { status: 401, description: "Unauthorized", schema: mapErrorSchema },
        { status: 403, description: "Missing required features", schema: mapErrorSchema }
      ]
    }
  }
};
function readEntityRef(ref) {
  if (typeof ref === "string") return { id: ref, record: null };
  if (ref && typeof ref === "object") {
    const record = ref;
    return { id: typeof record.id === "string" ? record.id : null, record };
  }
  return { id: null, record: null };
}
function readLinkRow(link, linkedRef) {
  const { id: dealId } = readEntityRef(link.deal);
  if (!dealId) return null;
  const { id: entityId, record } = readEntityRef(linkedRef);
  if (!entityId) return null;
  const label = record && typeof record.displayName === "string" ? record.displayName : "";
  return { dealId, entityId, label, createdAt: link.createdAt ?? null };
}
function groupAssociations(rows) {
  const byDeal = /* @__PURE__ */ new Map();
  for (const row of rows) {
    const bucket = byDeal.get(row.dealId) ?? [];
    if (!bucket.some((entry) => entry.id === row.entityId)) {
      bucket.push({ id: row.entityId, label: row.label });
      byDeal.set(row.dealId, bucket);
    }
  }
  return byDeal;
}
function toStringOrNull(value) {
  return typeof value === "string" && value.length > 0 ? value : null;
}
function toFiniteNumberOrNull(value) {
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
  if (typeof value === "string" && value.trim().length > 0) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}
function toIsoStringOrNull(value) {
  if (value instanceof Date) return value.toISOString();
  return toStringOrNull(value);
}
function readArrayParam(searchParams, key) {
  const all = searchParams.getAll(key);
  if (!all.length) return null;
  const trimmed = all.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
  return trimmed.length ? trimmed : null;
}
function collectRefIds(rows, key) {
  const ids = /* @__PURE__ */ new Set();
  for (const row of rows) {
    if (!row || typeof row !== "object") continue;
    const { id } = readEntityRef(row[key]);
    if (id) ids.add(id);
  }
  return Array.from(ids);
}
function readFilterIdAllowlist(filters) {
  const idFilter = filters.id;
  if (!idFilter || typeof idFilter !== "object" || Array.isArray(idFilter)) return null;
  const record = idFilter;
  if (typeof record.$eq === "string") return [record.$eq];
  if (Array.isArray(record.$in)) return record.$in.filter((value) => typeof value === "string");
  return null;
}
function emptyMapResponse(page, pageSize) {
  return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 });
}
async function GET(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth?.tenantId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const url = new URL(req.url);
  const params = url.searchParams;
  const parsed = querySchema.safeParse({
    page: params.get("page") ?? void 0,
    pageSize: params.get("pageSize") ?? void 0,
    search: params.get("search") ?? void 0,
    status: readArrayParam(params, "status") ?? void 0,
    pipelineId: readArrayParam(params, "pipelineId") ?? void 0,
    pipelineStageId: params.get("pipelineStageId") ?? void 0,
    ownerUserId: readArrayParam(params, "ownerUserId") ?? void 0,
    personId: readArrayParam(params, "personId") ?? void 0,
    companyId: readArrayParam(params, "companyId") ?? void 0,
    valueCurrency: readArrayParam(params, "valueCurrency") ?? void 0,
    expectedCloseAtFrom: params.get("expectedCloseAtFrom") ?? void 0,
    expectedCloseAtTo: params.get("expectedCloseAtTo") ?? void 0,
    isStuck: params.get("isStuck") ?? void 0,
    isOverdue: params.get("isOverdue") ?? void 0,
    sortField: params.get("sortField") ?? void 0,
    sortDir: params.get("sortDir") ?? void 0
  });
  if (!parsed.success) {
    return NextResponse.json({ error: "Invalid query parameters" }, { status: 400 });
  }
  const query = parsed.data;
  const container = await createRequestContainer();
  const em = container.resolve("em");
  const queryEngine = container.resolve("queryEngine");
  const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
  const effectiveTenantId = scope.tenantId ?? auth.tenantId;
  const orgScopeIds = scope.filterIds === null ? null : Array.isArray(scope.filterIds) && scope.filterIds.length > 0 ? Array.from(new Set(scope.filterIds.filter((id) => typeof id === "string" && id.length > 0))) : auth.orgId ? [auth.orgId] : [];
  if (!effectiveTenantId || Array.isArray(orgScopeIds) && orgScopeIds.length === 0) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const orgScopeFilter = orgScopeIds ? { organizationId: { $in: orgScopeIds } } : {};
  const fallbackOrgId = orgScopeIds && orgScopeIds.length > 0 ? orgScopeIds[0] : void 0;
  const decryptionScope = { tenantId: effectiveTenantId, organizationId: fallbackOrgId };
  const coordinateEntityRows = await findWithDecryption(
    em,
    CustomerAddress,
    {
      latitude: { $ne: null },
      longitude: { $ne: null },
      tenantId: effectiveTenantId,
      ...orgScopeFilter
    },
    { fields: ["entity"] },
    decryptionScope
  );
  const locatedEntityIds = collectRefIds(coordinateEntityRows, "entity");
  if (locatedEntityIds.length === 0) {
    return emptyMapResponse(query.page, query.pageSize);
  }
  const [locatedCompanyLinkRows, locatedPersonLinkRows] = await Promise.all([
    findWithDecryption(
      em,
      CustomerDealCompanyLink,
      { company: { $in: locatedEntityIds } },
      { fields: ["deal"] },
      decryptionScope
    ),
    findWithDecryption(
      em,
      CustomerDealPersonLink,
      { person: { $in: locatedEntityIds } },
      { fields: ["deal"] },
      decryptionScope
    )
  ]);
  const locatedDealIds = Array.from(
    /* @__PURE__ */ new Set([
      ...collectRefIds(locatedCompanyLinkRows, "deal"),
      ...collectRefIds(locatedPersonLinkRows, "deal")
    ])
  );
  if (locatedDealIds.length === 0) {
    return emptyMapResponse(query.page, query.pageSize);
  }
  const ctx = {
    container,
    auth,
    organizationScope: scope,
    selectedOrganizationId: fallbackOrgId ?? null,
    organizationIds: orgScopeIds,
    request: req
  };
  const filters = await buildDealListFilters(query, ctx);
  const existingAllowlist = readFilterIdAllowlist(filters);
  let searchAllowlist = existingAllowlist;
  if (query.search && locatedEntityIds.length > 0) {
    const nameMatchedEntityIds = await findMatchingEntityIdsBySearchTokensAcrossSources({
      ctx,
      query: query.search,
      sources: [
        {
          entityType: E.customers.customer_entity,
          fields: ["display_name", "primary_email", "primary_phone", "description"]
        }
      ]
    });
    const locatedEntitySet = new Set(locatedEntityIds);
    const locatedMatchedEntityIds = (nameMatchedEntityIds ?? []).filter((id) => locatedEntitySet.has(id));
    if (locatedMatchedEntityIds.length > 0) {
      const [companyNameLinkRows, personNameLinkRows] = await Promise.all([
        findWithDecryption(
          em,
          CustomerDealCompanyLink,
          { company: { $in: locatedMatchedEntityIds } },
          { fields: ["deal"] },
          decryptionScope
        ),
        findWithDecryption(
          em,
          CustomerDealPersonLink,
          { person: { $in: locatedMatchedEntityIds } },
          { fields: ["deal"] },
          decryptionScope
        )
      ]);
      let nameMatchedDealIds = [
        ...collectRefIds(companyNameLinkRows, "deal"),
        ...collectRefIds(personNameLinkRows, "deal")
      ];
      if (nameMatchedDealIds.length > 0) {
        const nonSearchAllowlist = readFilterIdAllowlist(
          await buildDealListFilters({ ...query, search: void 0 }, ctx)
        );
        if (nonSearchAllowlist) {
          const nonSearchLookup = new Set(nonSearchAllowlist);
          nameMatchedDealIds = nameMatchedDealIds.filter((id) => nonSearchLookup.has(id));
        }
      }
      if (nameMatchedDealIds.length > 0) {
        searchAllowlist = Array.from(/* @__PURE__ */ new Set([...existingAllowlist ?? [], ...nameMatchedDealIds]));
      }
    }
  }
  const allowlistLookup = searchAllowlist ? new Set(searchAllowlist) : null;
  const restrictedDealIds = allowlistLookup ? locatedDealIds.filter((id) => allowlistLookup.has(id)) : locatedDealIds;
  delete filters.id;
  applyEntityIdRestriction(filters, restrictedDealIds);
  const sortColumn = query.sortField ? sortFieldMap[query.sortField] : "id";
  const sortDir = query.sortDir === "desc" ? SortDir.Desc : SortDir.Asc;
  const sort = sortColumn === "id" ? [{ field: "id", dir: sortDir }] : [{ field: sortColumn, dir: sortDir }, { field: "id", dir: SortDir.Asc }];
  const res = await queryEngine.query(E.customers.customer_deal, {
    fields: [
      "id",
      // `organization_id` is projected (never exposed in the response) so the query engine can decrypt
      // each row's encrypted fields (e.g. `title`) with the row's OWN org. Without it, an unrestricted
      // "All organizations" scope has no single fallback org to decrypt with and titles come back as
      // ciphertext — the List route projects it for the same reason (#3481).
      "organization_id",
      "title",
      "status",
      "pipeline_id",
      "pipeline_stage_id",
      "pipeline_stage",
      "value_amount",
      "value_currency",
      "probability",
      "expected_close_at",
      "owner_user_id",
      "updated_at"
    ],
    sort,
    page: { page: query.page, pageSize: query.pageSize },
    filters,
    tenantId: effectiveTenantId,
    organizationId: fallbackOrgId,
    organizationIds: orgScopeIds ?? void 0
  });
  const rows = (Array.isArray(res.items) ? res.items : []).filter(
    (row) => !!row && typeof row === "object"
  );
  const dealIds = rows.map((row) => typeof row.id === "string" && row.id.trim().length ? row.id : null).filter((value) => value !== null);
  const [companyLinks, personLinks] = dealIds.length ? await Promise.all([
    findWithDecryption(
      em,
      CustomerDealCompanyLink,
      { deal: { $in: dealIds } },
      { populate: ["company"] },
      decryptionScope
    ),
    findWithDecryption(
      em,
      CustomerDealPersonLink,
      { deal: { $in: dealIds } },
      { populate: ["person"] },
      decryptionScope
    )
  ]) : [[], []];
  const companyRows = companyLinks.map((link) => readLinkRow(link, link.company)).filter((row) => row !== null);
  const personRows = personLinks.map((link) => readLinkRow(link, link.person)).filter((row) => row !== null);
  const companiesByDeal = groupAssociations(companyRows);
  const peopleByDeal = groupAssociations(personRows);
  const linkedEntityIds = Array.from(
    new Set([...companyRows, ...personRows].map((row) => row.entityId))
  );
  const addresses = linkedEntityIds.length ? await findWithDecryption(
    em,
    CustomerAddress,
    {
      entity: { $in: linkedEntityIds },
      latitude: { $ne: null },
      longitude: { $ne: null },
      tenantId: effectiveTenantId,
      ...orgScopeFilter
    },
    {},
    decryptionScope
  ) : [];
  const addressRows = [];
  for (const address of addresses) {
    const { id: entityId } = readEntityRef(address.entity);
    if (!entityId) continue;
    addressRows.push({
      id: address.id,
      entityId,
      isPrimary: address.isPrimary === true,
      latitude: address.latitude ?? null,
      longitude: address.longitude ?? null,
      city: address.city ?? null,
      region: address.region ?? null,
      country: address.country ?? null,
      createdAt: address.createdAt ?? null
    });
  }
  const toLink = (row) => ({
    dealId: row.dealId,
    entityId: row.entityId,
    createdAt: row.createdAt
  });
  const locationByDeal = resolveDealLocations(
    dealIds,
    companyRows.map(toLink),
    personRows.map(toLink),
    addressRows
  );
  const items = rows.map((row) => {
    const id = typeof row.id === "string" && row.id.trim().length ? row.id : null;
    if (!id) return null;
    return {
      id,
      title: toStringOrNull(row.title),
      status: toStringOrNull(row.status),
      pipelineId: toStringOrNull(row.pipeline_id),
      pipelineStageId: toStringOrNull(row.pipeline_stage_id),
      pipelineStage: toStringOrNull(row.pipeline_stage),
      valueAmount: toFiniteNumberOrNull(row.value_amount),
      valueCurrency: toStringOrNull(row.value_currency),
      probability: toFiniteNumberOrNull(row.probability),
      expectedCloseAt: toIsoStringOrNull(row.expected_close_at),
      ownerUserId: toStringOrNull(row.owner_user_id),
      updatedAt: toIsoStringOrNull(row.updated_at),
      companies: companiesByDeal.get(id) ?? [],
      people: peopleByDeal.get(id) ?? [],
      location: locationByDeal.get(id) ?? null
    };
  }).filter((item) => item !== null);
  return NextResponse.json({
    items,
    total: res.total,
    page: query.page,
    pageSize: query.pageSize,
    totalPages: Math.ceil(res.total / (query.pageSize || 1))
  });
}
export {
  GET,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
