import { NextResponse } from "next/server";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
import { z } from "zod";
import { sql } from "kysely";
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "../lib/imageUrls.js";
import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from "../lib/partitions.js";
import { Attachment, AttachmentPartition } from "../data/entities.js";
import { extractAttachmentContent } from "../lib/textExtraction.js";
import { requestOcrProcessing } from "../lib/ocrQueue.js";
import { StorageDriverFactory } from "../lib/drivers/index.js";
import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
import { assertAttachmentScopeInvariant } from "../lib/access.js";
import {
  mergeAttachmentMetadata,
  normalizeAttachmentAssignments,
  normalizeAttachmentTags,
  readAttachmentMetadata,
  upsertAssignment
} from "../lib/metadata.js";
import { randomUUID } from "crypto";
import { splitCustomFieldPayload } from "@open-mercato/shared/lib/crud/custom-fields";
import { emitCrudSideEffects, setCustomFieldsIfAny } from "@open-mercato/shared/lib/commands/helpers";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { attachmentCrudEvents, attachmentCrudIndexer } from "../lib/crud.js";
import { E } from "../../../generated/entities.ids.generated.js";
import { resolveDefaultAttachmentOcrEnabled } from "../lib/ocrConfig.js";
import {
  detectAttachmentMimeType,
  hasDangerousExecutableExtension,
  isActiveContentAttachment,
  sanitizeUploadedFileName
} from "../lib/security.js";
import {
  isMultipartRequestWithinUploadLimit,
  resolveAttachmentMaxBytes,
  willExceedAttachmentTenantQuota
} from "../lib/upload-limits.js";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("attachments");
const metadata = {
  GET: { requireAuth: true, requireFeatures: ["attachments.view"] },
  POST: { requireAuth: true, requireFeatures: ["attachments.manage"] },
  DELETE: { requireAuth: true, requireFeatures: ["attachments.manage"] }
};
const attachmentQuerySchema = z.object({
  entityId: z.string().min(1).describe("Entity identifier that owns the attachments"),
  recordId: z.string().min(1).describe("Record identifier within the entity"),
  page: z.coerce.number().min(1).optional(),
  pageSize: z.coerce.number().min(1).max(100).optional()
});
const attachmentAssignmentSchema = z.object({
  type: z.string().describe("Assignment type identifier"),
  id: z.string().describe("Assignment record identifier"),
  href: z.string().nullable().optional().describe("Optional link to the related record"),
  label: z.string().nullable().optional().describe("Optional label for the assignment")
});
const attachmentItemSchema = z.object({
  id: z.string().describe("Attachment identifier"),
  url: z.string().describe("Public path to the stored asset"),
  fileName: z.string().describe("Original filename"),
  fileSize: z.number().int().nonnegative().describe("File size in bytes"),
  createdAt: z.string().describe("Upload timestamp (ISO 8601)"),
  mimeType: z.string().nullable().optional().describe("MIME type of the file"),
  thumbnailUrl: z.string().optional().describe("Helper route that renders a thumbnail"),
  partitionCode: z.string().optional().describe("Partition identifier"),
  tags: z.array(z.string()).optional().describe("Tags assigned to the attachment"),
  content: z.string().nullable().optional().describe("Extracted text or markdown content"),
  assignments: z.array(attachmentAssignmentSchema).optional().describe("Records that reference this attachment")
});
const attachmentListResponseSchema = z.object({
  items: z.array(attachmentItemSchema),
  total: z.number().int().nonnegative().optional(),
  page: z.number().int().min(1).optional(),
  pageSize: z.number().int().min(1).optional(),
  totalPages: z.number().int().min(1).optional()
});
const attachmentUploadBodySchema = z.object({
  entityId: z.string().min(1),
  recordId: z.string().min(1),
  fieldKey: z.string().optional(),
  file: z.string().min(1).describe("Binary file payload; supplied as multipart form-data"),
  customFields: z.string().optional().describe("JSON encoded map of custom field values collected from the upload form.")
});
const attachmentDeleteQuerySchema = z.object({
  id: z.string().uuid()
});
const uploadResponseSchema = z.object({
  ok: z.literal(true),
  item: z.object({
    id: z.string(),
    url: z.string(),
    fileName: z.string(),
    fileSize: z.number().int().nonnegative(),
    thumbnailUrl: z.string().optional(),
    content: z.string().nullable().optional(),
    tags: z.array(z.string()).optional(),
    assignments: z.array(attachmentAssignmentSchema).optional(),
    customFields: z.record(z.string(), z.unknown()).optional()
  })
});
const errorSchema = z.object({
  error: z.string()
});
const LIBRARY_ENTITY_ID = "attachments:library";
function parseCustomFieldsEntry(value) {
  if (!value) return {};
  if (typeof value === "string") {
    const trimmed = value.trim();
    if (!trimmed) return {};
    try {
      const parsed = JSON.parse(trimmed);
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        return parsed;
      }
    } catch {
      return {};
    }
  }
  if (typeof value === "object" && !Array.isArray(value) && !(value instanceof File)) {
    return { ...value };
  }
  return {};
}
function buildFormPayload(form) {
  const payload = {};
  form.forEach((value, key) => {
    if (key === "customFields") {
      payload.customFields = parseCustomFieldsEntry(value);
      return;
    }
    payload[key] = value;
  });
  return payload;
}
function parseFormTags(value) {
  if (!value) return [];
  if (typeof value === "string") {
    const trimmed = value.trim();
    if (!trimmed) return [];
    try {
      const parsed = JSON.parse(trimmed);
      return normalizeAttachmentTags(parsed);
    } catch {
      return normalizeAttachmentTags(value);
    }
  }
  return [];
}
function parseFormAssignments(value) {
  if (!value) return [];
  if (typeof value !== "string") return [];
  const trimmed = value.trim();
  if (!trimmed) return [];
  try {
    const parsed = JSON.parse(trimmed);
    return normalizeAttachmentAssignments(parsed);
  } catch {
    return [];
  }
}
async function GET(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  const url = new URL(req.url);
  const parsedQuery = attachmentQuerySchema.safeParse({
    entityId: url.searchParams.get("entityId") || "",
    recordId: url.searchParams.get("recordId") || "",
    page: url.searchParams.get("page") ?? void 0,
    pageSize: url.searchParams.get("pageSize") ?? void 0
  });
  if (!parsedQuery.success) {
    return NextResponse.json({ error: "entityId and recordId are required" }, { status: 400 });
  }
  const { entityId, recordId, page, pageSize } = parsedQuery.data;
  const { resolve } = await createRequestContainer();
  const em = resolve("em");
  const filter = { entityId, recordId, tenantId: auth.tenantId };
  if (auth.orgId) filter.organizationId = auth.orgId;
  const orderBy = { createdAt: "DESC" };
  const usePaging = typeof page === "number" && typeof pageSize === "number";
  const total = usePaging ? await em.count(Attachment, filter) : null;
  const currentPage = usePaging ? Math.max(1, page) : null;
  const currentPageSize = usePaging ? pageSize : null;
  const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize)) : null;
  const pageOffset = usePaging ? (Math.min(currentPage, totalPages) - 1) * currentPageSize : void 0;
  const items = await findWithDecryption(
    em,
    Attachment,
    filter,
    {
      orderBy,
      ...usePaging ? {
        limit: currentPageSize,
        offset: pageOffset
      } : {}
    },
    {
      tenantId: auth.tenantId ?? null,
      organizationId: auth.orgId ?? null
    }
  );
  return NextResponse.json({
    items: items.map((a) => {
      const metadata2 = readAttachmentMetadata(a.storageMetadata);
      return {
        id: a.id,
        url: a.url,
        fileName: a.fileName,
        fileSize: a.fileSize,
        createdAt: a.createdAt,
        mimeType: a.mimeType ?? null,
        partitionCode: a.partitionCode,
        content: a.content ?? null,
        thumbnailUrl: buildAttachmentImageUrl(a.id, {
          width: 320,
          height: 320,
          slug: slugifyAttachmentFileName(a.fileName)
        }),
        tags: metadata2.tags ?? [],
        assignments: metadata2.assignments ?? []
      };
    }),
    ...usePaging ? {
      total,
      page: Math.min(currentPage, totalPages),
      pageSize: currentPageSize,
      totalPages
    } : {}
  });
}
async function POST(req) {
  const { t } = await resolveTranslations();
  const auth = await getAuthFromRequest(req);
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  if (!auth.orgId) {
    return NextResponse.json({
      error: t("attachments.errors.selectOrganization", "Select a specific organization before uploading an attachment.")
    }, { status: 400 });
  }
  const tenantId = auth.tenantId;
  const orgId = auth.orgId;
  const contentType = req.headers.get("content-type") || "";
  if (!contentType.toLowerCase().includes("multipart/form-data")) {
    return NextResponse.json({ error: "Expected multipart/form-data" }, { status: 400 });
  }
  if (!isMultipartRequestWithinUploadLimit(req.headers.get("content-length"))) {
    return NextResponse.json({ error: "Attachment exceeds the maximum upload size." }, { status: 413 });
  }
  const form = await req.formData();
  const formPayload = buildFormPayload(form);
  const customFieldValues = splitCustomFieldPayload(formPayload).custom;
  const entityId = String(form.get("entityId") || "");
  const recordId = String(form.get("recordId") || "");
  const fieldKey = String(form.get("fieldKey") || "");
  const file = form.get("file");
  if (!entityId || !recordId || !file) return NextResponse.json({ error: "entityId, recordId and file are required" }, { status: 400 });
  const partitionOverrideRaw = form.get("partitionCode");
  const partitionOverride = typeof partitionOverrideRaw === "string" && partitionOverrideRaw.trim().length > 0 ? sanitizePartitionCode(partitionOverrideRaw) : null;
  const tags = parseFormTags(form.get("tags"));
  const assignmentsFromForm = parseFormAssignments(form.get("assignments"));
  const { resolve } = await createRequestContainer();
  const em = resolve("em");
  const dataEngine = resolve("dataEngine");
  const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
  await ensureDefaultPartitions(em);
  let partitionFromField = null;
  let fieldMaxAttachmentSizeMb = null;
  if (fieldKey) {
    try {
      const { CustomFieldDef } = await import("@open-mercato/core/modules/entities/data/entities");
      const def = await em.findOne(CustomFieldDef, {
        entityId,
        key: fieldKey,
        $and: [
          { $or: [{ tenantId: auth.tenantId }, { tenantId: null }] }
        ],
        isActive: true
      });
      const cfg = def?.configJson || {};
      const ext = (file.name || "").split(".").pop()?.toLowerCase() || "";
      if (Array.isArray(cfg.acceptExtensions) && cfg.acceptExtensions.length) {
        const allowed = new Set(cfg.acceptExtensions.map((x) => String(x).toLowerCase().replace(/^\./, "")));
        if (!allowed.has(ext)) return NextResponse.json({ error: "File type not allowed" }, { status: 400 });
      }
      if (typeof cfg.maxAttachmentSizeMb === "number" && cfg.maxAttachmentSizeMb > 0) {
        fieldMaxAttachmentSizeMb = cfg.maxAttachmentSizeMb;
      }
      if (typeof cfg.partitionCode === "string" && cfg.partitionCode.trim().length > 0) {
        partitionFromField = sanitizePartitionCode(cfg.partitionCode);
      }
    } catch {
    }
  }
  if (hasDangerousExecutableExtension(file.name)) {
    return NextResponse.json({
      error: t("attachments.errors.dangerousExecutable", "Executable file types are not allowed as attachments.")
    }, { status: 400 });
  }
  const effectiveMaxBytes = resolveAttachmentMaxBytes(fieldMaxAttachmentSizeMb);
  if (file.size > effectiveMaxBytes) {
    return NextResponse.json({
      error: t("attachments.errors.maxUploadSize", "Attachment exceeds the maximum upload size.")
    }, { status: 413 });
  }
  const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId);
  if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {
    return NextResponse.json({
      error: t("attachments.errors.quotaExceeded", "Attachment storage quota exceeded for this tenant.")
    }, { status: 413 });
  }
  const buf = Buffer.from(await file.arrayBuffer());
  const safeName = sanitizeUploadedFileName(file.name);
  const fileMimeType = detectAttachmentMimeType(buf, safeName, file.type);
  if (isActiveContentAttachment(buf, safeName, fileMimeType)) {
    return NextResponse.json({ error: t("attachments.errors.activeContentBlocked", "Active content uploads are not allowed.") }, { status: 400 });
  }
  const defaultPartitionCode = resolveDefaultPartitionCode(entityId);
  const resolvedPartitionCode = partitionOverride ?? partitionFromField ?? defaultPartitionCode;
  const partitionCodeCandidates = Array.from(
    new Set(
      [partitionOverride, partitionFromField, resolvedPartitionCode].filter(
        (code) => typeof code === "string" && code.length > 0
      )
    )
  );
  let partition = null;
  for (const code of partitionCodeCandidates) {
    const record = await em.findOne(AttachmentPartition, { code });
    if (record) {
      partition = record;
      break;
    }
  }
  if (!partition) {
    partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode });
  }
  if (!partition) {
    return NextResponse.json({ error: "Storage partition is not configured." }, { status: 400 });
  }
  const requestedPublicOverride = typeof partitionOverride === "string" && partitionOverride.length > 0 && partition.code === partitionOverride && partition.isPublic === true && partition.code !== defaultPartitionCode && partition.code !== partitionFromField;
  if (requestedPublicOverride) {
    return NextResponse.json({ error: t("attachments.errors.publicPartitionBlocked", "Public storage partitions cannot be selected explicitly for this upload.") }, { status: 403 });
  }
  const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId });
  let storedPath;
  try {
    const stored = await uploadDriver.store({
      partitionCode: partition.code,
      orgId,
      tenantId,
      fileName: safeName,
      buffer: buf
    });
    storedPath = stored.storagePath;
  } catch (error) {
    logger.error("Failed to persist file", { err: error });
    return NextResponse.json({ error: "Failed to persist attachment." }, { status: 500 });
  }
  const requiresOcr = typeof partition.requiresOcr === "boolean" ? Boolean(partition.requiresOcr) : resolveDefaultAttachmentOcrEnabled();
  let extractedContent = null;
  const wantsLlmOcr = requiresOcr && shouldUseLlmOcr(fileMimeType, safeName);
  const ocrService = wantsLlmOcr ? new OcrService() : null;
  const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available);
  if (requiresOcr && !useLlmOcr) {
    const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath);
    try {
      extractedContent = await extractAttachmentContent({
        filePath: localPath,
        mimeType: fileMimeType
      });
    } catch (error) {
      logger.error("Failed to extract attachment content", { err: error });
    } finally {
      await cleanup().catch(() => {
      });
    }
  }
  let assignments = assignmentsFromForm.slice();
  if (entityId !== LIBRARY_ENTITY_ID) {
    assignments = upsertAssignment(assignments, { type: entityId, id: recordId });
  }
  const metadata2 = mergeAttachmentMetadata(null, { assignments, tags });
  const attachmentId = randomUUID();
  assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: auth.orgId });
  const att = em.create(Attachment, {
    id: attachmentId,
    entityId,
    recordId,
    organizationId: auth.orgId,
    tenantId: auth.tenantId,
    fileName: safeName,
    mimeType: fileMimeType,
    fileSize: buf.length,
    partitionCode: partition.code,
    storageDriver: partition.storageDriver || "local",
    storagePath: storedPath,
    url: buildAttachmentFileUrl(attachmentId),
    content: extractedContent,
    storageMetadata: metadata2
  });
  try {
    await em.transactional(async (tx) => {
      await tx.persist(att).flush();
      if (dataEngine) {
        await setCustomFieldsIfAny({
          dataEngine,
          entityId: E.attachments.attachment,
          recordId: attachmentId,
          tenantId,
          organizationId: orgId,
          values: customFieldValues
        });
      }
    });
  } catch (error) {
    logger.error("Failed to persist attachment with custom attributes", { err: error });
    return NextResponse.json({ error: "Failed to save attachment attributes." }, { status: 500 });
  }
  if (useLlmOcr) {
    requestOcrProcessing(em, att, uploadDriver, storedPath).catch((error) => {
      logger.error("Failed to queue OCR processing", { err: error });
    });
  } else if (wantsLlmOcr) {
    logger.warn("OCR requested but OPENAI_API_KEY not configured, falling back to text extraction when available");
  }
  if (dataEngine) {
    await emitCrudSideEffects({
      dataEngine,
      action: "created",
      entity: att,
      identifiers: {
        id: att.id,
        organizationId: att.organizationId ?? null,
        tenantId: att.tenantId ?? null
      },
      events: attachmentCrudEvents,
      indexer: attachmentCrudIndexer
    });
    await dataEngine.flushOrmEntityChanges();
  }
  return NextResponse.json({
    ok: true,
    item: {
      id: attachmentId,
      url: att.url,
      fileName: safeName,
      fileSize: buf.length,
      partitionCode: partition.code,
      thumbnailUrl: buildAttachmentImageUrl(attachmentId, {
        width: 320,
        height: 320,
        slug: slugifyAttachmentFileName(safeName)
      }),
      content: extractedContent ?? null,
      tags: metadata2.tags ?? [],
      assignments: metadata2.assignments ?? [],
      customFields: Object.keys(customFieldValues).length ? customFieldValues : void 0
    }
  });
}
async function readTenantAttachmentUsageBytes(em, tenantId) {
  try {
    const db = em.getKysely();
    const row = await db.selectFrom("attachments").select(sql`sum(file_size)`.as("total_size")).where("tenant_id", "=", tenantId).executeTakeFirst();
    const total = row?.total_size;
    if (typeof total === "number") return Number.isFinite(total) ? total : 0;
    if (typeof total === "string") {
      const parsed = Number(total);
      return Number.isFinite(parsed) ? parsed : 0;
    }
    return 0;
  } catch {
    return 0;
  }
}
async function DELETE(req) {
  const auth = await getAuthFromRequest(req);
  if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  const url = new URL(req.url);
  const id = url.searchParams.get("id") || "";
  if (!id) return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
  const { resolve } = await createRequestContainer();
  const em = resolve("em");
  const dataEngine = resolve("dataEngine");
  const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
  const deleteFilter = { id, tenantId: auth.tenantId };
  if (auth.orgId) deleteFilter.organizationId = auth.orgId;
  const record = await em.findOne(Attachment, deleteFilter);
  if (!record) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
  await em.remove(record).flush();
  await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {
    logger.error("Failed to cleanup cached thumbnails", { err: error });
  });
  if (record.storagePath) {
    const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
      tenantId: record.tenantId ?? auth.tenantId,
      organizationId: record.organizationId ?? auth.orgId ?? ""
    });
    await delDriver.delete(record.partitionCode, record.storagePath);
  }
  if (dataEngine) {
    await emitCrudSideEffects({
      dataEngine,
      action: "deleted",
      entity: record,
      identifiers: {
        id: record.id,
        organizationId: record.organizationId ?? null,
        tenantId: record.tenantId ?? null
      },
      events: attachmentCrudEvents,
      indexer: attachmentCrudIndexer
    });
    await dataEngine.flushOrmEntityChanges();
  }
  return NextResponse.json({ ok: true });
}
const openApi = {
  summary: "Manage entity attachments",
  description: "Upload and list attachments associated with module entities and records.",
  methods: {
    GET: {
      summary: "List attachments for a record",
      description: "Returns uploaded attachments for the given entity record, ordered by newest first.",
      query: attachmentQuerySchema,
      responses: [
        { status: 200, description: "Attachments found for the record", schema: attachmentListResponseSchema }
      ],
      errors: [
        { status: 400, description: "Missing entity or record identifiers", schema: errorSchema },
        { status: 401, description: "Unauthorized", schema: errorSchema }
      ]
    },
    POST: {
      summary: "Upload attachment",
      description: "Uploads a new attachment using multipart form-data and stores metadata for later retrieval.",
      requestBody: {
        contentType: "multipart/form-data",
        schema: attachmentUploadBodySchema
      },
      responses: [
        { status: 200, description: "Attachment stored successfully", schema: uploadResponseSchema }
      ],
      errors: [
        { status: 400, description: "Payload validation error", schema: errorSchema },
        { status: 401, description: "Unauthorized", schema: errorSchema },
        { status: 403, description: "Attachment violates field constraints", schema: errorSchema }
      ]
    },
    DELETE: {
      summary: "Delete attachment",
      description: "Removes an uploaded attachment and deletes the stored asset.",
      query: attachmentDeleteQuerySchema,
      responses: [
        { status: 200, description: "Attachment deleted", schema: z.object({ ok: z.literal(true) }) },
        { status: 404, description: "Attachment not found", schema: errorSchema }
      ],
      errors: [
        { status: 400, description: "Missing attachment identifier", schema: errorSchema },
        { status: 401, description: "Unauthorized", schema: errorSchema }
      ]
    }
  }
};
export {
  DELETE,
  GET,
  POST,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
