import { AccessLog } from "@open-mercato/core/modules/audit_logs/data/entities";
import {
  accessLogCreateSchema,
  accessLogListSchema
} from "@open-mercato/core/modules/audit_logs/data/validators";
import { resolveTenantEncryptionService } from "@open-mercato/shared/lib/encryption/customFieldValues";
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
import { E } from "../../../generated/entities.ids.generated.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("audit_logs").child({ component: "access-log-service" });
const CORE_RESOURCE_KINDS = /* @__PURE__ */ new Set(["auth.user", "auth.role"]);
function toPositiveNumber(value, fallback) {
  if (!value) return fallback;
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
  return parsed;
}
function toNonNegativeNumber(value, fallback) {
  if (value === void 0 || value === "") return fallback;
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) return fallback;
  return parsed;
}
const CORE_RETENTION_DAYS = toPositiveNumber(process.env.AUDIT_LOGS_CORE_RETENTION_DAYS, 7);
const NON_CORE_RETENTION_HOURS = toPositiveNumber(process.env.AUDIT_LOGS_NON_CORE_RETENTION_HOURS, 8);
const CORE_RETENTION_MS = CORE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
const NON_CORE_RETENTION_MS = NON_CORE_RETENTION_HOURS * 60 * 60 * 1e3;
const ROTATE_INTERVAL_MS = toNonNegativeNumber(process.env.AUDIT_LOGS_ROTATE_INTERVAL_MS, 6e4);
let lastRotatedAt = null;
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
const MAX_BATCH_ROWS = 500;
let validationWarningLogged = false;
let runtimeValidationAvailable = null;
const pendingAccessLogWrites = /* @__PURE__ */ new Set();
function trackPendingAccessLogWrite(promise) {
  pendingAccessLogWrites.add(promise);
  promise.catch(() => void 0).finally(() => {
    pendingAccessLogWrites.delete(promise);
  });
  return promise;
}
async function flushAccessLog() {
  while (pendingAccessLogWrites.size > 0) {
    const snapshot = Array.from(pendingAccessLogWrites);
    await Promise.allSettled(snapshot);
  }
}
const isZodRuntimeMissing = (err) => err instanceof TypeError && typeof err.message === "string" && err.message.includes("_zod");
function serializeJsonColumn(value) {
  if (value === null || value === void 0) return null;
  if (typeof value === "string") {
    try {
      JSON.parse(value);
      return value;
    } catch {
      return JSON.stringify(value);
    }
  }
  try {
    return JSON.stringify(value);
  } catch {
    return null;
  }
}
class AccessLogService {
  constructor(em) {
    this.em = em;
  }
  async log(input) {
    const promise = this.logInternal(input);
    return trackPendingAccessLogWrite(promise);
  }
  async logMany(inputs) {
    if (!Array.isArray(inputs) || inputs.length === 0) return 0;
    const promise = this.logManyInternal(inputs);
    return trackPendingAccessLogWrite(promise);
  }
  flush() {
    return flushAccessLog();
  }
  async logManyInternal(inputs) {
    const parsedResults = await Promise.all(inputs.map((input) => this.parseInput(input)));
    const normalized = [];
    for (const parsed of parsedResults) {
      if (parsed) normalized.push(parsed);
    }
    if (!normalized.length) return 0;
    let written = 0;
    for (let offset = 0; offset < normalized.length; offset += MAX_BATCH_ROWS) {
      const chunk = normalized.slice(offset, offset + MAX_BATCH_ROWS);
      written += await this.writeChunk(chunk);
    }
    if (written > 0) {
      const fork = this.em.fork({ useContext: true });
      await this.rotate(fork);
    }
    return written;
  }
  async writeChunk(chunk) {
    if (!chunk.length) return 0;
    const fork = this.em.fork({ useContext: true });
    const encryption = resolveTenantEncryptionService(fork);
    const createdAt = /* @__PURE__ */ new Date();
    const prepared = await Promise.all(
      chunk.map(async (data) => {
        const fields = Array.isArray(data.fields) && data.fields.length ? data.fields : null;
        const context = data.context && Object.keys(data.context).length ? data.context : null;
        const tenantId = data.tenantId ?? null;
        const organizationId = data.organizationId ?? null;
        const encrypted = encryption ? await encryption.encryptEntityPayload(
          E.audit_logs.access_log,
          {
            resourceKind: data.resourceKind,
            resourceId: data.resourceId,
            accessType: data.accessType,
            fieldsJson: fields,
            contextJson: context
          },
          tenantId,
          organizationId
        ) : null;
        return { tenantId, organizationId, data, fields, context, encrypted };
      })
    );
    const placeholders = [];
    const params = [];
    for (const row of prepared) {
      const { tenantId, organizationId, data, fields, context, encrypted } = row;
      const resourceKindOut = encrypted?.resourceKind ?? data.resourceKind;
      const resourceIdOut = encrypted?.resourceId ?? data.resourceId;
      const accessTypeOut = encrypted?.accessType ?? data.accessType;
      const fieldsOut = encrypted?.fieldsJson ?? fields;
      const contextOut = encrypted?.contextJson ?? context;
      placeholders.push("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
      params.push(
        tenantId,
        organizationId,
        data.actorUserId ?? null,
        resourceKindOut,
        resourceIdOut,
        accessTypeOut,
        serializeJsonColumn(fieldsOut),
        serializeJsonColumn(contextOut),
        createdAt,
        null
      );
    }
    if (!placeholders.length) return 0;
    const sql = `insert into "access_logs" ("tenant_id", "organization_id", "actor_user_id", "resource_kind", "resource_id", "access_type", "fields_json", "context_json", "created_at", "deleted_at") values ${placeholders.join(", ")}`;
    await fork.getConnection().execute(sql, params);
    return chunk.length;
  }
  async parseInput(input) {
    const schema = accessLogCreateSchema;
    const canValidate = Boolean(schema && typeof schema.parse === "function");
    const shouldValidate = canValidate && runtimeValidationAvailable !== false;
    if (shouldValidate) {
      try {
        const data = schema.parse(input);
        runtimeValidationAvailable = true;
        return data;
      } catch (err) {
        if (!isZodRuntimeMissing(err) && !validationWarningLogged) {
          validationWarningLogged = true;
          logger.warn("Falling back to permissive access log payload parser", { err });
        }
        if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false;
        return this.normalizeInput(input);
      }
    }
    return this.normalizeInput(input);
  }
  async logInternal(input) {
    const data = await this.parseInput(input);
    if (!data) return null;
    const fork = this.em.fork({ useContext: true });
    const fields = Array.isArray(data.fields) && data.fields.length ? data.fields : null;
    const context = data.context && Object.keys(data.context).length ? data.context : null;
    const createdAt = /* @__PURE__ */ new Date();
    const tenantId = data.tenantId ?? null;
    const organizationId = data.organizationId ?? null;
    const encryption = resolveTenantEncryptionService(fork);
    const encrypted = encryption ? await encryption.encryptEntityPayload(
      E.audit_logs.access_log,
      {
        resourceKind: data.resourceKind,
        resourceId: data.resourceId,
        accessType: data.accessType,
        fieldsJson: fields,
        contextJson: context
      },
      tenantId,
      organizationId
    ) : null;
    const payload = {
      resourceKind: encrypted?.resourceKind ?? data.resourceKind,
      resourceId: encrypted?.resourceId ?? data.resourceId,
      accessType: encrypted?.accessType ?? data.accessType,
      fieldsJson: encrypted?.fieldsJson ?? fields,
      contextJson: encrypted?.contextJson ?? context
    };
    const rows = await fork.getConnection().execute(
      `insert into "access_logs" ("tenant_id", "organization_id", "actor_user_id", "resource_kind", "resource_id", "access_type", "fields_json", "context_json", "created_at", "deleted_at") values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning "id"`,
      [
        tenantId,
        organizationId,
        data.actorUserId ?? null,
        payload.resourceKind,
        payload.resourceId,
        payload.accessType,
        serializeJsonColumn(payload.fieldsJson),
        serializeJsonColumn(payload.contextJson),
        createdAt,
        null
      ]
    );
    await this.rotate(fork);
    const id = Array.isArray(rows) && rows.length > 0 ? rows[0]?.id ?? null : null;
    if (!id) return null;
    const entry = fork.create(AccessLog, {
      id,
      tenantId: data.tenantId ?? null,
      organizationId: data.organizationId ?? null,
      actorUserId: data.actorUserId ?? null,
      resourceKind: data.resourceKind,
      resourceId: data.resourceId,
      accessType: data.accessType,
      fieldsJson: fields,
      contextJson: context,
      createdAt
    });
    return entry;
  }
  normalizeInput(input) {
    if (!input) {
      return {
        tenantId: null,
        organizationId: null,
        actorUserId: null,
        resourceKind: "unknown",
        resourceId: "unknown",
        accessType: "unknown",
        fields: void 0,
        context: void 0
      };
    }
    const UUID_REGEX2 = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
    const toNullableUuid = (value) => {
      if (typeof value !== "string" || value.length === 0) return null;
      const candidate = value.startsWith("api_key:") ? value.slice("api_key:".length) : value;
      return UUID_REGEX2.test(candidate) ? candidate : null;
    };
    const fields = Array.isArray(input.fields) ? input.fields.filter((f) => typeof f === "string" && f.length > 0) : void 0;
    const context = typeof input.context === "object" && input.context !== null ? input.context : void 0;
    return {
      tenantId: toNullableUuid(input.tenantId),
      organizationId: toNullableUuid(input.organizationId),
      actorUserId: toNullableUuid(input.actorUserId),
      resourceKind: String(input.resourceKind || "unknown"),
      resourceId: String(input.resourceId || "unknown"),
      accessType: String(input.accessType || "unknown"),
      fields,
      context
    };
  }
  async list(query) {
    const parsed = accessLogListSchema.parse({
      ...query
    });
    const where = { deletedAt: null };
    if (parsed.tenantId) where.tenantId = parsed.tenantId;
    if (parsed.organizationId) where.organizationId = parsed.organizationId;
    if (parsed.actorUserId) where.actorUserId = parsed.actorUserId;
    if (parsed.resourceKind) where.resourceKind = parsed.resourceKind;
    if (parsed.accessType) where.accessType = parsed.accessType;
    if (parsed.before) where.createdAt = { ...where.createdAt, $lt: parsed.before };
    if (parsed.after) where.createdAt = { ...where.createdAt, $gt: parsed.after };
    const pageSize = parsed.pageSize ?? parsed.limit ?? 50;
    const page = parsed.page ?? 1;
    const offset = (page - 1) * pageSize;
    const [items, total] = await this.em.findAndCount(
      AccessLog,
      where,
      {
        orderBy: { createdAt: "desc" },
        limit: pageSize,
        offset
      }
    );
    for (const item of items) {
      const rawFieldsJson = item.fieldsJson;
      if (typeof rawFieldsJson === "string") {
        const parsed2 = parseDecryptedFieldValue(rawFieldsJson);
        item.fieldsJson = Array.isArray(parsed2) ? parsed2 : null;
      }
      const rawContextJson = item.contextJson;
      if (typeof rawContextJson === "string") {
        const parsed2 = parseDecryptedFieldValue(rawContextJson);
        item.contextJson = parsed2 && typeof parsed2 === "object" && !Array.isArray(parsed2) ? parsed2 : null;
      }
    }
    const totalPages = Math.max(1, Math.ceil((total || 0) / (pageSize || 1)));
    return { items, total, page, pageSize, totalPages };
  }
  async rotate(fork) {
    const now = Date.now();
    if (ROTATE_INTERVAL_MS > 0 && lastRotatedAt !== null && now - lastRotatedAt < ROTATE_INTERVAL_MS) return;
    lastRotatedAt = now;
    const coreCutoff = new Date(now - CORE_RETENTION_MS);
    const nonCoreCutoff = new Date(now - NON_CORE_RETENTION_MS);
    try {
      if (CORE_RESOURCE_KINDS.size > 0) {
        await fork.nativeDelete(AccessLog, {
          resourceKind: { $in: Array.from(CORE_RESOURCE_KINDS) },
          createdAt: { $lt: coreCutoff }
        });
      }
      await fork.nativeDelete(AccessLog, {
        resourceKind: { $nin: Array.from(CORE_RESOURCE_KINDS) },
        createdAt: { $lt: nonCoreCutoff }
      });
    } catch (err) {
      logger.warn("Failed to rotate access logs", { err });
    }
  }
}
export {
  AccessLogService,
  flushAccessLog
};
//# sourceMappingURL=accessLogService.js.map
