"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateAuditLogRoute = generateAuditLogRoute;
const Generator = __importStar(require("@postxl/generator"));
/**
 * Generates TRPC route for querying audit logs with filters.
 */
function generateAuditLogRoute({ context }) {
    const imports = Generator.ImportGenerator.from(Generator.toFilePath('./routes/audit-log.route.ts'))
        .add({ name: context.trpcRouter._middleware.auth.name, location: context.trpcRouter._middleware._filePath })
        .add({ name: context.trpcRouter._shared.procedure.name, location: context.trpcRouter._shared._filePath })
        .add({ name: context.trpcRouter._shared.router.name, location: context.trpcRouter._shared._filePath });
    return /* ts */ `
import { z } from 'zod'

${imports.generate()}

/**
 * Input schema for querying audit logs
 */
const zAuditLogQuery = z.object({
  /**
   * Filter by model name (e.g., "Post", "User")
   */
  model: z.string(),
  /**
   * Optional filter by specific entity ID (single)
   */
  entityId: z.string().optional(),
  /**
   * Optional filter by multiple entity IDs (for multi-select)
   */
  entityIds: z.array(z.string()).optional(),
  /**
   * Optional filter by specific field name
   */
  field: z.string().optional(),
  /**
   * Number of items to return (default: 20)
   */
  limit: z.number().min(1).max(100).default(20),
  /**
   * Offset for pagination (default: 0)
   */
  offset: z.number().min(0).default(0),
})

export type AuditLogQuery = z.infer<typeof zAuditLogQuery>

/**
 * Represents a single audit log entry with parsed data
 */
export type AuditLogEntry = {
  id: string
  createdAt: Date | null
  actionId: string | null
  actionScope: string | null
  actionType: string | null
  userId: string | null
  userName: string | null
  mutationKind: string | null
  model: string | null
  entityId: string | null
  /**
   * Entity label derived from the result or source entity
   */
  entityLabel: string | null
  /**
   * The source entity before the mutation (parsed JSON)
   */
  sourceEntity: Record<string, unknown> | null
  /**
   * The result entity after the mutation (parsed JSON)
   */
  resultEntity: Record<string, unknown> | null
  /**
   * Fields that changed during this operation
   */
  changedFields: {
    field: string
    oldValue: unknown
    newValue: unknown
  }[]
}

/**
 * Response type for audit log queries
 */
export type AuditLogResponse = {
  entries: AuditLogEntry[]
  total: number
  hasMore: boolean
}

/**
 * Parses JSON string safely, returns null on failure
 */
function safeParseJson(json: string | null): unknown {
  if (!json) return null
  try {
    return JSON.parse(json)
  } catch {
    return null
  }
}

/**
 * Parses JSON string as array, returns empty array on failure
 */
function safeParseJsonArray(json: string | null): unknown[] {
  const parsed = safeParseJson(json)
  return Array.isArray(parsed) ? parsed : []
}

/**
 * Parses JSON string as object, returns null on failure
 */
function safeParseJsonObject(json: string | null): Record<string, unknown> | null {
  const parsed = safeParseJson(json)
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
    return parsed as Record<string, unknown>
  }
  return null
}

/**
 * Checks if an object is empty or null/undefined
 */
function isEmptyOrNull(obj: Record<string, unknown> | null): boolean {
  return !obj || Object.keys(obj).length === 0
}

/**
 * Fields to skip when extracting changes (internal fields)
 */
const SKIP_FIELDS = new Set(['id', 'createdAt', 'updatedAt'])

/**
 * Checks if a field should be included based on filter
 */
function shouldIncludeField(key: string, filterField?: string): boolean {
  if (SKIP_FIELDS.has(key)) return false
  if (filterField && key !== filterField) return false
  return true
}

/**
 * Extracts fields from a create operation (source is empty, result has data)
 */
function extractCreateFields(
  result: Record<string, unknown>,
  filterField?: string,
): { field: string; oldValue: unknown; newValue: unknown }[] {
  const changes: { field: string; oldValue: unknown; newValue: unknown }[] = []
  for (const [key, value] of Object.entries(result)) {
    if (shouldIncludeField(key, filterField)) {
      changes.push({ field: key, oldValue: undefined, newValue: value })
    }
  }
  return changes
}

/**
 * Extracts fields from a delete operation (source has data, result is empty)
 */
function extractDeleteFields(
  source: Record<string, unknown>,
  filterField?: string,
): { field: string; oldValue: unknown; newValue: unknown }[] {
  const changes: { field: string; oldValue: unknown; newValue: unknown }[] = []
  for (const [key, value] of Object.entries(source)) {
    if (shouldIncludeField(key, filterField)) {
      changes.push({ field: key, oldValue: value, newValue: undefined })
    }
  }
  return changes
}

/**
 * Extracts fields from an update operation (compares source and result)
 */
function extractUpdateFields(
  source: Record<string, unknown>,
  result: Record<string, unknown>,
  filterField?: string,
): { field: string; oldValue: unknown; newValue: unknown }[] {
  const changes: { field: string; oldValue: unknown; newValue: unknown }[] = []
  const allKeys = new Set([...Object.keys(source), ...Object.keys(result)])

  for (const key of allKeys) {
    if (!shouldIncludeField(key, filterField)) continue

    const oldValue = source[key]
    const newValue = result[key]

    if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
      changes.push({ field: key, oldValue, newValue })
    }
  }
  return changes
}

/**
 * Extracts changed fields by comparing source and result entities
 */
function extractChangedFields(
  source: Record<string, unknown> | null,
  result: Record<string, unknown> | null,
  filterField?: string,
): { field: string; oldValue: unknown; newValue: unknown }[] {
  if (isEmptyOrNull(source) && isEmptyOrNull(result)) {
    return []
  }

  // Create operation: no source, has result
  if (isEmptyOrNull(source) && result) {
    return extractCreateFields(result, filterField)
  }

  // Delete operation: has source, no result
  if (source && isEmptyOrNull(result)) {
    return extractDeleteFields(source, filterField)
  }

  // Update operation: has both source and result
  if (source && result) {
    return extractUpdateFields(source, result, filterField)
  }

  return []
}

/**
 * Converts a value to string safely, handling objects
 */
function valueToString(value: unknown): string {
  if (value === null || value === undefined) return ''
  if (typeof value === 'string') return value
  if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
    return String(value) // NOSONAR - the above type checks ensure that no objects reach here
  }
  // For objects, arrays, symbols, or any other types - use JSON.stringify
  return JSON.stringify(value)
}

/**
 * Extracts entity label from entity data
 */
function extractEntityLabel(
  entity: Record<string, unknown> | null,
  labelFields: string[] = ['name', 'title', 'label', 'id'],
): string | null {
  if (!entity) return null

  for (const field of labelFields) {
    const value = entity[field]
    if (value !== null && value !== undefined) {
      return valueToString(value)
    }
  }

  return null
}

/**
 * Checks if an operation affects a specific entity ID.
 * For Many operations, entityId is a JSON array of IDs.
 */
function operationAffectsEntity(entityIdField: string | null, targetEntityIds: Set<string>): boolean {
  if (!entityIdField) return false

  // Try to parse as JSON array first (for Many operations)
  try {
    const parsed = JSON.parse(entityIdField)
    if (Array.isArray(parsed)) {
      return parsed.some((id) => targetEntityIds.has(String(id)))
    }
  } catch {
    // Not JSON, treat as single ID
  }

  // Single ID
  return targetEntityIds.has(entityIdField)
}

/**
 * Gets entity IDs from an operation (handles both single and Many operations)
 */
function getEntityIdsFromOperation(entityIdField: string | null): string[] {
  if (!entityIdField) return []

  // Try to parse as JSON array first (for Many operations)
  try {
    const parsed = JSON.parse(entityIdField)
    if (Array.isArray(parsed)) {
      return parsed.map(String)
    }
  } catch {
    // Not JSON, treat as single ID
  }

  return [entityIdField]
}

/**
 * Maps a Many mutation kind to its singular display equivalent
 */
function getManyKindDisplayName(kind: string): string {
  switch (kind) {
    case 'CreateMany':
      return 'Create'
    case 'UpdateMany':
      return 'Update'
    case 'DeleteMany':
      return 'Delete'
    default:
      return kind
  }
}

/**
 * Gets the count of entities in a Many operation
 */
function getManyOperationCount(
  kind: string,
  sourceEntities: unknown[],
  resultEntities: unknown[],
): number {
  switch (kind) {
    case 'CreateMany':
      return resultEntities.length
    case 'UpdateMany':
      return Math.max(sourceEntities.length, resultEntities.length)
    case 'DeleteMany':
      return sourceEntities.length
    default:
      return 0
  }
}

/**
 * Gets the entity ID for a specific index in a Many operation
 */
function getEntityIdAtIndex(
  index: number,
  entityIdsInOp: string[],
  resultEntity: Record<string, unknown> | null,
  sourceEntity: Record<string, unknown> | null,
): string | null {
  if (entityIdsInOp[index]) return entityIdsInOp[index]
  if (resultEntity?.id) return resultEntity.id as string
  if (sourceEntity?.id) return sourceEntity.id as string
  return null
}

/**
 * Creates an audit log entry from operation data
 */
function createAuditLogEntry(params: {
  id: string
  createdAt: Date | null
  actionId: string | null
  actionScope: string | null
  actionType: string | null
  userId: string | null
  userName: string | null
  mutationKind: string
  model: string | null
  entityId: string | null
  sourceEntity: Record<string, unknown> | null
  resultEntity: Record<string, unknown> | null
  changedFields: { field: string; oldValue: unknown; newValue: unknown }[]
}): AuditLogEntry {
  return {
    id: params.id,
    createdAt: params.createdAt,
    actionId: params.actionId,
    actionScope: params.actionScope,
    actionType: params.actionType,
    userId: params.userId,
    userName: params.userName,
    mutationKind: params.mutationKind,
    model: params.model,
    entityId: params.entityId,
    entityLabel: extractEntityLabel(params.resultEntity ?? params.sourceEntity),
    sourceEntity: params.sourceEntity,
    resultEntity: params.resultEntity,
    changedFields: params.changedFields,
  }
}

/**
 * Processes a Many operation and extracts individual entries
 */
function processManyOperation(
  op: { id: string; createdAt: Date | null; actionId: string | null; result: string | null; content: { kind: string; model: string | null; entityId: string | null; sourceEntity: string | null } },
  action: { scope: string | null; type: string | null; userId: string | null } | null,
  user: { name: string | null; email: string | null } | null,
  filterEntityIds: Set<string>,
  field: string | undefined,
): AuditLogEntry[] {
  const entries: AuditLogEntry[] = []
  const kind = op.content.kind

  const sourceEntities = safeParseJsonArray(op.content.sourceEntity)
  const resultEntities = safeParseJsonArray(op.result)
  const entityIdsInOp = getEntityIdsFromOperation(op.content.entityId)

  const count = getManyOperationCount(kind, sourceEntities, resultEntities)
  const displayKind = getManyKindDisplayName(kind)

  for (let i = 0; i < count; i++) {
    const sourceEntity = (sourceEntities[i] as Record<string, unknown>) ?? null
    const resultEntity = (resultEntities[i] as Record<string, unknown>) ?? null
    const entryEntityId = getEntityIdAtIndex(i, entityIdsInOp, resultEntity, sourceEntity)

    // Skip entries that don't match entity filter
    if (filterEntityIds.size > 0 && entryEntityId && !filterEntityIds.has(entryEntityId)) {
      continue
    }

    const changedFields = extractChangedFields(sourceEntity, resultEntity, field)

    // Skip if field filter specified but no changes for that field
    if (field && changedFields.length === 0) continue

    entries.push(createAuditLogEntry({
      id: \`\${op.id}-\${i}\`,
      createdAt: op.createdAt,
      actionId: op.actionId,
      actionScope: action?.scope ?? null,
      actionType: action?.type ?? null,
      userId: action?.userId ?? null,
      userName: user?.name ?? user?.email ?? null,
      mutationKind: displayKind,
      model: op.content.model,
      entityId: entryEntityId,
      sourceEntity,
      resultEntity,
      changedFields,
    }))
  }

  return entries
}

/**
 * Processes a single operation and creates an entry
 */
function processSingleOperation(
  op: { id: string; createdAt: Date | null; actionId: string | null; result: string | null; content: { kind: string; model: string | null; entityId: string | null; sourceEntity: string | null } },
  action: { scope: string | null; type: string | null; userId: string | null } | null,
  user: { name: string | null; email: string | null } | null,
  field: string | undefined,
): AuditLogEntry | null {
  const sourceEntity = safeParseJsonObject(op.content.sourceEntity)
  const resultEntity = safeParseJsonObject(op.result)

  const changedFields = extractChangedFields(sourceEntity, resultEntity, field)

  // Skip if field filter specified but no changes for that field
  if (field && changedFields.length === 0) return null

  return createAuditLogEntry({
    id: op.id,
    createdAt: op.createdAt,
    actionId: op.actionId,
    actionScope: action?.scope ?? null,
    actionType: action?.type ?? null,
    userId: action?.userId ?? null,
    userName: user?.name ?? user?.email ?? null,
    mutationKind: op.content.kind,
    model: op.content.model,
    entityId: op.content.entityId,
    sourceEntity,
    resultEntity,
    changedFields,
  })
}

export const auditLogs = router({
  /**
   * Query audit logs with optional filters
   */
  query: procedure
    .use(authMiddleware)
    .input(zAuditLogQuery)
    .query(async ({ input, ctx }): Promise<AuditLogResponse> => {
      const { model, entityId, entityIds, field, limit, offset } = input

      // Build set of entity IDs to filter by
      const filterEntityIds = new Set<string>()
      if (entityId) filterEntityIds.add(entityId)
      if (entityIds) entityIds.forEach((id) => filterEntityIds.add(id))

      // Filter at database level by model (and content type = Mutation)
      const operations = await ctx.view.actionOperations.data.filter({
        predicate: () => true,
        prisma: {
          content: 'Mutation',
          content_mutation_model: model,
        },
      })

      // Further filter by entityId if specified (done in memory because entityId can be JSON array)
      const filtered = operations.filter((op) => {
        if (op.content.type !== 'Mutation') return false
        if (filterEntityIds.size === 0) return true
        return operationAffectsEntity(op.content.entityId, filterEntityIds)
      })

      // Sort by createdAt descending (most recent first)
      filtered.sort((a, b) => {
        const dateA = a.createdAt?.getTime() ?? 0
        const dateB = b.createdAt?.getTime() ?? 0
        return dateB - dateA
      })

      // Get actions map for scope/type info
      const actionsMap = await ctx.view.actions.getAll(ctx.user)

      // Get users map for user names
      const usersMap = await ctx.view.users.getAll(ctx.user)

      // Process entries
      let entries: AuditLogEntry[] = []

      for (const op of filtered) {
        if (op.content.type !== 'Mutation') continue

        const action = op.actionId ? actionsMap.get(op.actionId) ?? null : null
        const user = action?.userId ? usersMap.get(action.userId) ?? null : null

        // Extract mutation content after type narrowing
        const mutationContent = op.content
        const kind = mutationContent.kind

        // Build operation data object with properly typed content
        const opData = {
          id: op.id,
          createdAt: op.createdAt,
          actionId: op.actionId,
          result: op.result,
          content: {
            kind: mutationContent.kind,
            model: mutationContent.model,
            entityId: mutationContent.entityId,
            sourceEntity: mutationContent.sourceEntity,
          },
        }

        const isManyOperation = kind === 'CreateMany' || kind === 'UpdateMany' || kind === 'DeleteMany'

        if (isManyOperation) {
          const manyEntries = processManyOperation(opData, action, user, filterEntityIds, field)
          entries.push(...manyEntries)
        } else {
          const entry = processSingleOperation(opData, action, user, field)
          if (entry) entries.push(entry)
        }
      }

      const total = entries.length
      const hasMore = offset + limit < total

      // Apply pagination
      entries = entries.slice(offset, offset + limit)

      return {
        entries,
        total,
        hasMore,
      }
    }),
})
`;
}
//# sourceMappingURL=audit-log-route.generator.js.map