/**
 * Audit log search - PR1 skeleton. PR10 fills in the cursor pagination
 * and per-port + super-admin scoping; v1 already has the GIN index on
 * `audit_logs.search_text`.
 */

import { and, desc, eq, gte, inArray, lte, sql, type SQL } from 'drizzle-orm';

import { db } from '@/lib/db';
import { auditLogs, type AuditLog } from '@/lib/db/schema/system';

export interface AuditSearchOptions {
  /** Limit results to a single port. Omit for super-admin all-ports view. */
  portId?: string;
  /** Free-text query - runs against the GIN-indexed search_text column. */
  q?: string;
  /** Filter by actor (user id). */
  userId?: string;
  /** Filter by action verb: 'create' | 'update' | 'delete' | ... */
  action?: string;
  /** Filter by entity type: 'client' | 'interest' | 'document' | ... */
  entityType?: string;
  /** Filter by exact entity id (e.g. paste a uuid into search). */
  entityId?: string;
  /** Filter by an explicit list of entity ids (e.g. aggregated activity
   *  for a client across all their interests). Overrides `entityId`
   *  when both are supplied. Empty array short-circuits to zero rows. */
  entityIds?: string[];
  /** Filter by severity ('info' | 'warning' | 'error' | 'critical'). */
  severity?: string;
  /** Filter by source ('user' | 'system' | 'auth' | 'webhook' | 'cron' | 'job'). */
  source?: string;
  /** Inclusive date range. */
  from?: Date;
  to?: Date;
  /** Pagination - cursor on (createdAt, id). */
  cursor?: { createdAt: Date; id: string };
  limit?: number;
}

export interface AuditSearchPage {
  rows: AuditLog[];
  nextCursor: { createdAt: Date; id: string } | null;
}

export async function searchAuditLogs(options: AuditSearchOptions = {}): Promise<AuditSearchPage> {
  const conds: SQL[] = [];
  if (options.portId) conds.push(eq(auditLogs.portId, options.portId));
  if (options.userId) conds.push(eq(auditLogs.userId, options.userId));
  if (options.action) conds.push(eq(auditLogs.action, options.action));
  if (options.entityType) conds.push(eq(auditLogs.entityType, options.entityType));
  if (options.entityIds) {
    if (options.entityIds.length === 0) {
      // Short-circuit: caller passed an empty list → no possible match.
      return { rows: [], nextCursor: null };
    }
    conds.push(inArray(auditLogs.entityId, options.entityIds));
  } else if (options.entityId) {
    conds.push(eq(auditLogs.entityId, options.entityId));
  }
  if (options.severity) conds.push(eq(auditLogs.severity, options.severity));
  if (options.source) conds.push(eq(auditLogs.source, options.source));
  if (options.from) conds.push(gte(auditLogs.createdAt, options.from));
  if (options.to) conds.push(lte(auditLogs.createdAt, options.to));
  if (options.q) {
    // tsquery match against the GENERATED tsvector column.
    conds.push(sql`${auditLogs.searchText} @@ plainto_tsquery('simple', ${options.q})`);
  }
  if (options.cursor) {
    // Strict less-than on (createdAt, id) for stable cursor pagination.
    // ISO-stringify the date so postgres-js binds it cleanly inside a tuple
    // comparison; raw Date objects throw under postgres@3.x parameter binding.
    const cursorAt = options.cursor.createdAt.toISOString();
    conds.push(
      sql`(${auditLogs.createdAt}, ${auditLogs.id}) < (${cursorAt}::timestamptz, ${options.cursor.id})`,
    );
  }

  const limit = Math.min(options.limit ?? 50, 200);
  const rows = await db.query.auditLogs.findMany({
    where: conds.length > 0 ? and(...conds) : undefined,
    orderBy: [desc(auditLogs.createdAt), desc(auditLogs.id)],
    limit: limit + 1,
  });

  const hasMore = rows.length > limit;
  const truncated = hasMore ? rows.slice(0, limit) : rows;
  const last = truncated[truncated.length - 1];
  return {
    rows: truncated,
    nextCursor: hasMore && last ? { createdAt: last.createdAt, id: last.id } : null,
  };
}
