/**
 * Entity history — operational-audit-trail (spec 50).
 *
 * captureEntityChange: in-transaction helper that writes a diff row to
 *   tenant_audit_log (before_state / after_state columns).  Must be called
 *   inside an existing db.transaction() callback, on the SAME `tx` as the
 *   business mutation.
 *
 * computeDiff: pure helper returning only the keys whose values changed.
 *
 * listEntityHistory: cursor-paginated read of diff-bearing rows for a single
 *   entity, with role-scoped and tier-scoped window filtering.
 *
 * serializeEntityChange: maps a TenantAuditLogRow → EntityChangeRecord for
 *   the API response.
 *
 * TRACKED_ENTITIES / HISTORY_API_ENTITIES: whitelist constants.
 */
import { and, desc, eq, gte, lt, or, sql } from 'drizzle-orm'
import type { Db } from '../client'
import { tenantAuditLog } from '../schema/audit'
import { encodeCursor, decodeCursor } from './audit-list'

// ── Entity whitelist ──────────────────────────────────────────────────────────

/**
 * Maps API entity slug → entity_type value stored in tenant_audit_log.
 * Only entities in this map are instrumented for diff capture.
 */
export const TRACKED_ENTITIES: Record<string, string> = {
  invoices:   'invoice',
  customers:  'customer',
  projects:   'project',
  expenses:   'expense',
  contracts:  'contract',
}

/**
 * Entity slugs allowed in GET /api/:entity/:id/history.
 * Subset of TRACKED_ENTITIES keys; time_entries/invoice_lines are tracked but
 * not surfaced as top-level history endpoints per spec 83.
 */
export const HISTORY_API_ENTITIES = new Set<string>(Object.keys(TRACKED_ENTITIES))

// ── computeDiff ───────────────────────────────────────────────────────────────

/**
 * Returns a shallow diff of only the fields that changed between `before` and
 * `after`.  Values are compared by JSON-serialisation to handle nested objects.
 *
 * Returns null when no fields changed (caller should skip the audit insert).
 */
export function computeDiff(
  before: Record<string, unknown>,
  after: Record<string, unknown>,
): { before: Record<string, unknown>; after: Record<string, unknown> } | null {
  const bDiff: Record<string, unknown> = {}
  const aDiff: Record<string, unknown> = {}

  const allKeys = new Set([...Object.keys(before), ...Object.keys(after)])
  for (const key of allKeys) {
    const bVal = before[key]
    const aVal = after[key]
    if (JSON.stringify(bVal) !== JSON.stringify(aVal)) {
      bDiff[key] = bVal
      aDiff[key] = aVal
    }
  }

  if (Object.keys(bDiff).length === 0) return null
  return { before: bDiff, after: aDiff }
}

// ── captureEntityChange ───────────────────────────────────────────────────────

export interface CaptureEntityChangeParams {
  /** Drizzle transaction instance — MUST be the same tx as the business write. */
  tx: Parameters<Parameters<Db['transaction']>[0]>[0]
  tenantId: string
  userId: string | null
  actorName: string | null
  actorEmail: string | null
  eventType: string
  entityType: string
  entityId: string
  entityLabel: string | null
  /** Diff of changed fields only; null for create events (no "before"). */
  beforeState: Record<string, unknown> | null
  /** Diff of changed fields only; null for delete events (no "after"). */
  afterState: Record<string, unknown> | null
  ipAddress?: string | null
}

/**
 * Insert a diff-bearing row into tenant_audit_log within the caller's transaction.
 *
 * The variable `tenantAuditLog` satisfies the ESLint `require-audit-in-transaction`
 * rule (its name matches /audit/i).
 */
export async function captureEntityChange(params: CaptureEntityChangeParams): Promise<void> {
  const {
    tx,
    tenantId,
    userId,
    actorName,
    actorEmail,
    eventType,
    entityType,
    entityId,
    entityLabel,
    beforeState,
    afterState,
    ipAddress,
  } = params

  await tx.insert(tenantAuditLog).values({
    tenantId,
    userId: userId ?? undefined,
    actorName: actorName ?? null,
    actorEmail: actorEmail ?? null,
    eventType,
    entityType,
    entityId,
    entityLabel: entityLabel ?? null,
    metadata: {},
    ipAddress: ipAddress ?? null,
    beforeState: beforeState ?? null,
    afterState: afterState ?? null,
  })
}

// ── EntityChangeRecord ────────────────────────────────────────────────────────

export interface EntityChangeRecord {
  id: string
  eventType: string
  actorId: string | null
  actorName: string | null
  changedAt: string
  before: Record<string, unknown> | null
  after: Record<string, unknown> | null
}

export function serializeEntityChange(row: typeof tenantAuditLog.$inferSelect): EntityChangeRecord {
  return {
    id: row.id,
    eventType: row.eventType,
    actorId: row.userId ?? null,
    actorName: row.actorName ?? null,
    changedAt: row.createdAt.toISOString(),
    before: (row.beforeState as Record<string, unknown> | null) ?? null,
    after: (row.afterState as Record<string, unknown> | null) ?? null,
  }
}

// ── listEntityHistory ─────────────────────────────────────────────────────────

export interface EntityHistoryOptions {
  /** Filter to own rows only (MEMBER role). */
  userId?: string
  from?: string
  to?: string
  cursor?: string
  limit: number
}

export interface EntityHistoryPage {
  changes: EntityChangeRecord[]
  nextCursor: string | null
  hasMore: boolean
}

export async function listEntityHistory(
  db: Db,
  tenantId: string,
  entityType: string,
  entityId: string,
  opts: EntityHistoryOptions,
): Promise<EntityHistoryPage> {
  const limit = Math.min(opts.limit, 50)

  // Date range
  let fromDate: Date | undefined
  let toDate: Date | undefined
  if (opts.from) {
    const ts = Number(opts.from)
    fromDate = isNaN(ts) ? new Date(opts.from) : new Date(ts * 1000)
  }
  if (opts.to) {
    const ts = Number(opts.to)
    toDate = isNaN(ts) ? new Date(opts.to) : new Date(ts * 1000)
  }

  // Cursor predicate (newest-first, stable tie-break on id DESC)
  let cursorCondition: ReturnType<typeof and> | undefined
  if (opts.cursor) {
    const decoded = decodeCursor(opts.cursor)
    if (decoded) {
      const cursorDate = new Date(decoded.created_at)
      cursorCondition = or(
        lt(tenantAuditLog.createdAt, cursorDate),
        and(
          eq(tenantAuditLog.createdAt, cursorDate),
          lt(tenantAuditLog.id, decoded.id),
        ),
      )
    }
  }

  const rows = await db
    .select()
    .from(tenantAuditLog)
    .where(
      and(
        eq(tenantAuditLog.tenantId, tenantId),
        eq(tenantAuditLog.entityType, entityType),
        eq(tenantAuditLog.entityId, entityId),
        // Only diff-bearing rows or create events
        or(
          sql`${tenantAuditLog.beforeState} IS NOT NULL`,
          sql`${tenantAuditLog.afterState} IS NOT NULL`,
          sql`${tenantAuditLog.eventType} LIKE '%.created'`,
        ),
        opts.userId ? eq(tenantAuditLog.userId, opts.userId) : undefined,
        fromDate ? gte(tenantAuditLog.createdAt, fromDate) : undefined,
        toDate ? lt(tenantAuditLog.createdAt, toDate) : undefined,
        cursorCondition,
      ),
    )
    .orderBy(desc(tenantAuditLog.createdAt), desc(tenantAuditLog.id))
    .limit(limit + 1)

  const hasMore = rows.length > limit
  const items = hasMore ? rows.slice(0, limit) : rows
  const lastItem = items[items.length - 1]
  const nextCursor =
    hasMore && lastItem ? encodeCursor(lastItem.createdAt, lastItem.id) : null

  return {
    changes: items.map(serializeEntityChange),
    nextCursor,
    hasMore,
  }
}
