import { sql } from "kysely";
import { ActionLog } from "@open-mercato/core/modules/audit_logs/data/entities";
import {
  actionLogCreateSchema,
  actionLogListSchema
} from "@open-mercato/core/modules/audit_logs/data/validators";
import { isRecord } from "@open-mercato/core/modules/audit_logs/lib/changeRows";
import {
  ACTION_LOG_FILTER_TYPES,
  deriveActionLogProjection
} from "@open-mercato/core/modules/audit_logs/lib/projections";
import { decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
import {
  parseDecryptedFieldValue
} from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
import { toOptionalString } from "@open-mercato/shared/lib/string/coerce";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("audit_logs").child({ component: "action-log-service" });
let validationWarningLogged = false;
let runtimeValidationAvailable = null;
let decryptionWarningLogged = false;
const isZodRuntimeMissing = (err) => err instanceof TypeError && typeof err.message === "string" && err.message.includes("_zod");
const SORT_FIELDS = {
  createdAt: "action_logs.created_at"
};
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}$/;
function readString(record, ...keys) {
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "string" && value.length > 0) return value;
  }
  return null;
}
function readRecord(record, ...keys) {
  for (const key of keys) {
    const value = record[key];
    if (isRecord(value)) return value;
  }
  return null;
}
function readValue(record, ...keys) {
  for (const key of keys) {
    if (Object.prototype.hasOwnProperty.call(record, key)) return record[key];
  }
  return void 0;
}
function readStringArray(record, ...keys) {
  for (const key of keys) {
    const value = record[key];
    if (Array.isArray(value)) {
      return value.filter((entry) => typeof entry === "string");
    }
  }
  return null;
}
function stringArraysEqual(left, right) {
  if (!Array.isArray(left)) return false;
  if (left.length !== right.length) return false;
  return left.every((value, index) => value === right[index]);
}
class ActionLogService {
  constructor(em, tenantEncryptionService) {
    this.em = em;
    this.tenantEncryptionService = tenantEncryptionService;
  }
  async decryptEntryPayload(entry) {
    if (!this.tenantEncryptionService?.isEnabled()) return entry;
    try {
      const tenantId = readString(entry, "tenantId", "tenant_id");
      const organizationId = readString(entry, "organizationId", "organization_id");
      const dek = await this.tenantEncryptionService.getDek(tenantId);
      const deepDecrypt = (value) => {
        if (!dek) return value;
        if (typeof value === "string" && value.split(":").length === 4 && value.endsWith(":v1")) {
          const decrypted2 = decryptWithAesGcm(value, dek.key);
          if (decrypted2 === null) return value;
          return parseDecryptedFieldValue(decrypted2);
        }
        if (Array.isArray(value)) return value.map((item) => deepDecrypt(item));
        if (value && typeof value === "object") {
          const copy = {};
          for (const [key, item] of Object.entries(value)) {
            copy[key] = deepDecrypt(item);
          }
          return copy;
        }
        return value;
      };
      const decrypted = await this.tenantEncryptionService.decryptEntityPayload(
        "audit_logs:action_log",
        entry,
        tenantId,
        organizationId
      );
      const merged = {
        ...entry,
        ...decrypted
      };
      const restoreJson = (value) => typeof value === "string" ? parseDecryptedFieldValue(value) : value;
      merged.changesJson = deepDecrypt(restoreJson(merged.changesJson ?? merged.changes_json ?? entry.changesJson ?? entry.changes_json));
      merged.changes_json = merged.changesJson;
      merged.snapshotBefore = deepDecrypt(restoreJson(merged.snapshotBefore ?? merged.snapshot_before ?? entry.snapshotBefore ?? entry.snapshot_before));
      merged.snapshot_before = merged.snapshotBefore;
      merged.snapshotAfter = deepDecrypt(restoreJson(merged.snapshotAfter ?? merged.snapshot_after ?? entry.snapshotAfter ?? entry.snapshot_after));
      merged.snapshot_after = merged.snapshotAfter;
      merged.commandPayload = deepDecrypt(restoreJson(merged.commandPayload ?? merged.command_payload ?? entry.commandPayload ?? entry.command_payload));
      merged.command_payload = merged.commandPayload;
      merged.contextJson = deepDecrypt(restoreJson(merged.contextJson ?? merged.context_json ?? entry.contextJson ?? entry.context_json));
      merged.context_json = merged.contextJson;
      return merged;
    } catch (err) {
      if (!decryptionWarningLogged) {
        decryptionWarningLogged = true;
        logger.warn("Failed to decrypt action log entry", { err });
      }
      return entry;
    }
  }
  async decryptEntries(entries) {
    if (!entries) return;
    const list = Array.isArray(entries) ? entries : [entries];
    for (const entry of list) {
      Object.assign(entry, await this.decryptEntryPayload(entry));
    }
  }
  async log(input) {
    const data = this.parseCreateInput(input);
    const fork = this.em.fork();
    const log = this.createLogEntity(fork, data);
    await fork.persist(log).flush();
    await this.decryptEntries(log);
    return log;
  }
  parseCreateInput(input) {
    let data;
    const schema = actionLogCreateSchema;
    const canValidate = Boolean(schema && typeof schema.parse === "function");
    const shouldValidate = canValidate && runtimeValidationAvailable !== false;
    if (shouldValidate) {
      try {
        data = schema.parse(input);
        runtimeValidationAvailable = true;
      } catch (err) {
        if (!isZodRuntimeMissing(err) && !validationWarningLogged) {
          validationWarningLogged = true;
          logger.warn("Falling back to permissive action log payload parser", { err });
        }
        if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false;
        data = this.normalizeInput(input);
      }
    } else {
      data = this.normalizeInput(input);
    }
    return data;
  }
  createLogEntity(fork, data) {
    const projection = deriveActionLogProjection({
      actorUserId: data.actorUserId ?? null,
      actionLabel: data.actionLabel ?? null,
      changes: isRecord(data.changes) ? data.changes : null,
      commandId: data.commandId,
      context: isRecord(data.context) ? data.context : null,
      snapshotBefore: data.snapshotBefore
    });
    return fork.create(ActionLog, {
      tenantId: data.tenantId ?? null,
      organizationId: data.organizationId ?? null,
      actorUserId: data.actorUserId ?? null,
      commandId: data.commandId,
      actionLabel: data.actionLabel ?? null,
      actionType: projection.actionType,
      resourceKind: data.resourceKind ?? null,
      resourceId: data.resourceId ?? null,
      parentResourceKind: data.parentResourceKind ?? null,
      parentResourceId: data.parentResourceId ?? null,
      relatedResourceKind: toOptionalString(data.relatedResourceKind) ?? null,
      relatedResourceId: toOptionalString(data.relatedResourceId) ?? null,
      executionState: data.executionState ?? "done",
      undoToken: data.undoToken ?? null,
      commandPayload: data.commandPayload ?? null,
      snapshotBefore: data.snapshotBefore ?? null,
      snapshotAfter: data.snapshotAfter ?? null,
      changesJson: isRecord(data.changes) ? data.changes : null,
      changedFields: projection.changedFields,
      primaryChangedField: projection.primaryChangedField,
      contextJson: isRecord(data.context) ? data.context : null,
      sourceKey: projection.sourceKey,
      createdAt: /* @__PURE__ */ new Date(),
      updatedAt: /* @__PURE__ */ new Date()
    });
  }
  normalizeInput(input) {
    if (!input) {
      return {
        tenantId: null,
        organizationId: null,
        actorUserId: null,
        commandId: "unknown",
        actionLabel: void 0,
        resourceKind: void 0,
        resourceId: void 0,
        relatedResourceKind: null,
        relatedResourceId: null,
        executionState: "done",
        undoToken: void 0,
        commandPayload: void 0,
        snapshotBefore: void 0,
        snapshotAfter: void 0,
        changes: 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 normalizeRecordLike = (value) => {
      if (value === null) return null;
      if (Array.isArray(value)) return value;
      if (typeof value === "object") return value;
      return void 0;
    };
    const normalizeContext = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
    return {
      tenantId: toNullableUuid(input.tenantId),
      organizationId: toNullableUuid(input.organizationId),
      actorUserId: toNullableUuid(input.actorUserId),
      commandId: typeof input.commandId === "string" && input.commandId.length > 0 ? input.commandId : "unknown",
      actionLabel: toOptionalString(input.actionLabel) ?? void 0,
      resourceKind: toOptionalString(input.resourceKind) ?? void 0,
      resourceId: toOptionalString(input.resourceId) ?? void 0,
      parentResourceKind: toOptionalString(input.parentResourceKind) ?? null,
      parentResourceId: toOptionalString(input.parentResourceId) ?? null,
      relatedResourceKind: toOptionalString(input.relatedResourceKind) ?? null,
      relatedResourceId: toOptionalString(input.relatedResourceId) ?? null,
      executionState: input.executionState === "undone" || input.executionState === "failed" ? input.executionState : "done",
      undoToken: toOptionalString(input.undoToken) ?? void 0,
      commandPayload: input.commandPayload,
      snapshotBefore: input.snapshotBefore,
      snapshotAfter: input.snapshotAfter,
      changes: normalizeRecordLike(input.changes),
      context: normalizeContext(input.context)
    };
  }
  parseListQuery(query) {
    return actionLogListSchema.parse({
      ...query
    });
  }
  resolveActorUserIds(parsed) {
    const values = [...parsed.actorUserIds ?? []];
    if (parsed.actorUserId) values.push(parsed.actorUserId);
    return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
  }
  resolveFieldNames(parsed) {
    const values = [...parsed.fieldNames ?? []];
    if (parsed.fieldName) values.push(parsed.fieldName);
    return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
  }
  resolveActionTypes(parsed) {
    const values = [...parsed.actionTypes ?? []];
    if (parsed.actionType) values.push(parsed.actionType);
    return Array.from(new Set(values)).filter((value) => ACTION_LOG_FILTER_TYPES.includes(value));
  }
  resolvePagination(parsed) {
    const pageSize = typeof parsed.pageSize === "number" && parsed.pageSize > 0 ? parsed.pageSize : typeof parsed.limit === "number" && parsed.limit > 0 ? parsed.limit : 50;
    const page = typeof parsed.page === "number" && parsed.page > 0 ? parsed.page : 1;
    const offset = typeof parsed.offset === "number" && parsed.offset >= 0 ? parsed.offset : (page - 1) * pageSize;
    return { page, pageSize, offset, limit: pageSize };
  }
  async loadEntries(parsed, options) {
    let query = this.buildListQuery(parsed).select("action_logs.id as id");
    if (options?.paginate !== false) {
      const { limit, offset } = this.resolvePagination(parsed);
      query = query.limit(limit).offset(offset);
    }
    const rows = await query.execute();
    const ids = rows.map((row) => row.id).filter(Boolean);
    if (ids.length === 0) return [];
    const results = await this.em.find(ActionLog, {
      id: { $in: ids },
      deletedAt: null
    });
    await this.decryptEntries(results);
    const byId = new Map(results.map((entry) => [entry.id, entry]));
    return ids.map((id) => byId.get(id)).filter((entry) => Boolean(entry));
  }
  buildListQuery(parsed) {
    let query = this.em.getKysely().selectFrom("action_logs").selectAll().where("action_logs.deleted_at", "is", null);
    if (parsed.tenantId) query = query.where("action_logs.tenant_id", "=", parsed.tenantId);
    if (parsed.organizationId) query = query.where("action_logs.organization_id", "=", parsed.organizationId);
    const actorUserIds = this.resolveActorUserIds(parsed);
    if (actorUserIds.length === 1) query = query.where("action_logs.actor_user_id", "=", actorUserIds[0]);
    if (actorUserIds.length > 1) query = query.where("action_logs.actor_user_id", "in", actorUserIds);
    if (parsed.includeRelated && parsed.resourceKind && parsed.resourceId) {
      query = query.where(
        (eb) => eb.or([
          eb.and([
            eb("action_logs.resource_kind", "=", parsed.resourceKind),
            eb("action_logs.resource_id", "=", parsed.resourceId)
          ]),
          eb.and([
            eb("action_logs.parent_resource_kind", "=", parsed.resourceKind),
            eb("action_logs.parent_resource_id", "=", parsed.resourceId)
          ]),
          eb.and([
            eb("action_logs.related_resource_kind", "=", parsed.resourceKind),
            eb("action_logs.related_resource_id", "=", parsed.resourceId)
          ])
        ])
      );
    } else {
      if (parsed.resourceKind) query = query.where("action_logs.resource_kind", "=", parsed.resourceKind);
      if (parsed.resourceId) query = query.where("action_logs.resource_id", "=", parsed.resourceId);
    }
    if (parsed.undoableOnly) query = query.where("action_logs.undo_token", "is not", null);
    if (parsed.before) query = query.where("action_logs.created_at", "<", parsed.before);
    if (parsed.after) query = query.where("action_logs.created_at", ">", parsed.after);
    const fieldNames = this.resolveFieldNames(parsed);
    if (fieldNames.length === 1) query = query.where("action_logs.primary_changed_field", "=", fieldNames[0]);
    if (fieldNames.length > 1) query = query.where("action_logs.primary_changed_field", "in", fieldNames);
    const actionTypes = this.resolveActionTypes(parsed);
    if (actionTypes.length === 1) query = query.where("action_logs.action_type", "=", actionTypes[0]);
    if (actionTypes.length > 1) query = query.where("action_logs.action_type", "in", actionTypes);
    if (parsed.sortField === "user") {
      query = query.leftJoin("users as audit_actor", "audit_actor.id", "action_logs.actor_user_id");
    }
    const sortDir = parsed.sortDir === "asc" ? "asc" : "desc";
    switch (parsed.sortField) {
      case "user":
        query = query.orderBy(sql`coalesce(nullif(audit_actor.name, ''), audit_actor.email, '')`, sortDir);
        break;
      case "action":
        query = query.orderBy(sql`coalesce(action_logs.action_type, '')`, sortDir);
        break;
      case "field":
        query = query.orderBy(sql`coalesce(action_logs.primary_changed_field, '')`, sortDir);
        break;
      case "source":
        query = query.orderBy(sql`coalesce(action_logs.source_key, '')`, sortDir);
        break;
      case "createdAt":
      default:
        query = query.orderBy(SORT_FIELDS.createdAt, sortDir);
        query = query.orderBy("action_logs.id", sortDir);
        return query;
    }
    query = query.orderBy("action_logs.created_at", "desc");
    query = query.orderBy("action_logs.id", "desc");
    return query;
  }
  async count(query) {
    const parsed = this.parseListQuery(query);
    const row = await this.buildListQuery(parsed).clearSelect().clearOrderBy().select(sql`count(*)`.as("count")).executeTakeFirst();
    if (!row) return 0;
    const rawCount = row.count ?? 0;
    return typeof rawCount === "number" ? rawCount : Number.parseInt(rawCount, 10) || 0;
  }
  async list(query) {
    const parsed = this.parseListQuery(query);
    const { page, pageSize } = this.resolvePagination(parsed);
    const [items, total] = await Promise.all([
      this.loadEntries(parsed),
      this.count(parsed)
    ]);
    const totalPages = Math.max(1, Math.ceil((total || 0) / (pageSize || 1)));
    return { items, total, page, pageSize, totalPages };
  }
  async latestUndoableForActor(actorUserId, scope) {
    const where = {
      actorUserId,
      undoToken: { $ne: null },
      executionState: "done",
      deletedAt: null
    };
    if (scope.tenantId) where.tenantId = scope.tenantId;
    if (scope.organizationId) where.organizationId = scope.organizationId;
    const entry = await this.em.findOne(ActionLog, where, { orderBy: { createdAt: "desc" } });
    await this.decryptEntries(entry);
    return entry;
  }
  async claimForUndo(id) {
    const affected = await this.em.nativeUpdate(
      ActionLog,
      { id, executionState: "done", deletedAt: null },
      { executionState: "undoing" }
    );
    return affected === 1;
  }
  async releaseUndoClaim(id) {
    const affected = await this.em.nativeUpdate(
      ActionLog,
      { id, executionState: "undoing", deletedAt: null },
      { executionState: "done" }
    );
    return affected === 1;
  }
  async markUndone(id, traceInput) {
    const fork = this.em.fork();
    const log = await fork.findOne(ActionLog, { id, deletedAt: null });
    if (!log) return null;
    log.executionState = "undone";
    log.undoToken = null;
    const traceLog = traceInput ? this.createLogEntity(fork, this.parseCreateInput(traceInput)) : null;
    if (traceLog) {
      fork.persist(traceLog);
    }
    await fork.flush();
    await this.decryptEntries(log);
    if (traceLog) await this.decryptEntries(traceLog);
    return log;
  }
  async findByUndoToken(undoToken) {
    const entry = await this.em.findOne(ActionLog, { undoToken, deletedAt: null });
    await this.decryptEntries(entry);
    return entry;
  }
  async findById(id) {
    const entry = await this.em.findOne(ActionLog, { id, deletedAt: null });
    await this.decryptEntries(entry);
    return entry;
  }
  async latestUndoableForResource(params) {
    const where = {
      actorUserId: params.actorUserId,
      undoToken: { $ne: null },
      executionState: "done",
      deletedAt: null
    };
    if (params.tenantId) where.tenantId = params.tenantId;
    if (params.organizationId) where.organizationId = params.organizationId;
    if (params.resourceKind) where.resourceKind = params.resourceKind;
    if (params.resourceId) where.resourceId = params.resourceId;
    const entry = await this.em.findOne(ActionLog, where, { orderBy: { createdAt: "desc" } });
    await this.decryptEntries(entry);
    return entry;
  }
  async latestUndoneForActor(actorUserId, scope) {
    const where = {
      actorUserId,
      executionState: "undone",
      deletedAt: null
    };
    if (scope.tenantId) where.tenantId = scope.tenantId;
    if (scope.organizationId) where.organizationId = scope.organizationId;
    const entry = await this.em.findOne(ActionLog, where, { orderBy: { updatedAt: "desc" } });
    await this.decryptEntries(entry);
    return entry;
  }
  async markRedone(id) {
    const log = await this.em.findOne(ActionLog, { id, deletedAt: null });
    if (!log) return null;
    log.executionState = "redone";
    log.undoToken = null;
    await this.em.flush();
    return log;
  }
  async backfillProjections(options = {}) {
    const batchSize = Math.min(Math.max(Math.trunc(options.batchSize ?? 250), 1), 1e3);
    const logger2 = options.logger ?? (() => {
    });
    const result = {
      errors: 0,
      scanned: 0,
      skipped: 0,
      updated: 0
    };
    let cursorCreatedAt = null;
    let cursorId = null;
    while (true) {
      const rowsQuery = this.em.getKysely().selectFrom("action_logs").select([
        "action_logs.id",
        "action_logs.tenant_id",
        "action_logs.organization_id",
        "action_logs.actor_user_id",
        "action_logs.command_id",
        "action_logs.action_label",
        "action_logs.snapshot_before",
        "action_logs.changes_json",
        "action_logs.context_json",
        "action_logs.action_type",
        "action_logs.source_key",
        "action_logs.changed_fields",
        "action_logs.primary_changed_field",
        "action_logs.created_at"
      ]).where("action_logs.deleted_at", "is", null);
      if (options.tenantId) rowsQuery.where("action_logs.tenant_id", "=", options.tenantId);
      if (options.organizationId) rowsQuery.where("action_logs.organization_id", "=", options.organizationId);
      if (!options.force) {
        rowsQuery.where(
          (eb) => eb.or([
            eb("action_logs.action_type", "is", null),
            eb("action_logs.source_key", "is", null),
            eb("action_logs.changed_fields", "is", null)
          ])
        );
      }
      if (cursorCreatedAt && cursorId) {
        rowsQuery.where(
          (eb) => eb.or([
            eb("action_logs.created_at", ">", cursorCreatedAt),
            eb.and([
              eb("action_logs.created_at", "=", cursorCreatedAt),
              eb("action_logs.id", ">", cursorId)
            ])
          ])
        );
      }
      const rows = await rowsQuery.orderBy("created_at", "asc").orderBy("id", "asc").limit(batchSize);
      if (rows.length === 0) break;
      for (const row of rows) {
        result.scanned += 1;
        try {
          const decrypted = await this.decryptEntryPayload(row);
          const projection = deriveActionLogProjection({
            actorUserId: readString(decrypted, "actorUserId", "actor_user_id"),
            actionLabel: readString(decrypted, "actionLabel", "action_label"),
            changes: readRecord(decrypted, "changesJson", "changes_json"),
            commandId: readString(decrypted, "commandId", "command_id") ?? "unknown",
            context: readRecord(decrypted, "contextJson", "context_json"),
            snapshotBefore: readValue(decrypted, "snapshotBefore", "snapshot_before")
          });
          const needsUpdate = options.force === true || row.action_type !== projection.actionType || row.source_key !== projection.sourceKey || row.primary_changed_field !== projection.primaryChangedField || !stringArraysEqual(row.changed_fields, projection.changedFields);
          if (!needsUpdate) {
            result.skipped += 1;
            continue;
          }
          await this.em.getKysely().updateTable("action_logs").set({
            action_type: projection.actionType,
            changed_fields: projection.changedFields,
            primary_changed_field: projection.primaryChangedField,
            source_key: projection.sourceKey
          }).where("id", "=", row.id).execute();
          result.updated += 1;
        } catch (err) {
          result.errors += 1;
          logger2(`[backfill] Failed for action log ${row.id}: ${err instanceof Error ? err.message : String(err)}`);
        }
      }
      const lastRow = rows[rows.length - 1];
      cursorCreatedAt = lastRow.created_at;
      cursorId = lastRow.id;
      logger2(
        `[backfill] Processed ${result.scanned} action logs (updated: ${result.updated}, skipped: ${result.skipped}, errors: ${result.errors})`
      );
      if (rows.length < batchSize) break;
    }
    return result;
  }
}
export {
  ActionLogService
};
//# sourceMappingURL=actionLogService.js.map
