/**
 * Time entry locking query helpers — time-entry-locking (spec 95).
 *
 * Locks prevent edits/deletes on time entries that have been approved or
 * closed for a billing period. Only managers (time:manage) may lock/unlock.
 *
 * Columns used: locked_at, locked_by, locked_reason (all on time_entries).
 * The canonical "is locked" predicate is: locked_at IS NOT NULL.
 * approvalStatus='locked' is contractor-payouts' billing lock — separate axis.
 */
import { and, eq, inArray, isNotNull, gte, lte } from 'drizzle-orm'
import type { Db } from '../client'
import { auditLog } from './_audit-forward'
import { timeEntries } from '../schema/time'

// ── Types ──────────────────────────────────────────────────────────────────────

export interface LockResult {
  lockedCount: number
  skippedIds: string[]
}

export interface LockedEntryRow {
  id: string
  tenantId: string
  userId: string | null
  projectId: string
  startedAt: Date
  stoppedAt: Date | null
  durationSeconds: number | null
  lockedAt: Date
  lockedBy: string | null
  lockedReason: string | null
}

// ── lockTimeEntries ────────────────────────────────────────────────────────────

/**
 * Lock a batch of time entries. Already-locked entries are skipped.
 * Sets locked_at, locked_by, locked_reason='period_closed'.
 */
export async function lockTimeEntries(
  db: Db,
  tenantId: string,
  userId: string,
  entryIds: string[],
): Promise<LockResult> {
  if (entryIds.length === 0) return { lockedCount: 0, skippedIds: [] }

  return db.transaction(async (tx) => {
    // Fetch existing to detect already-locked
    const existing = await tx
      .select({ id: timeEntries.id, lockedAt: timeEntries.lockedAt })
      .from(timeEntries)
      .where(
        and(
          eq(timeEntries.tenantId, tenantId),
          inArray(timeEntries.id, entryIds),
        ),
      )

    const alreadyLocked = existing.filter((r) => r.lockedAt !== null).map((r) => r.id)
    const tolock = existing.filter((r) => r.lockedAt === null).map((r) => r.id)

    if (tolock.length > 0) {
      const now = new Date()
      await tx
        .update(timeEntries)
        .set({
          lockedAt: now,
          lockedBy: userId,
          lockedReason: 'period_closed',
          updatedAt: now,
        })
        .where(
          and(
            eq(timeEntries.tenantId, tenantId),
            inArray(timeEntries.id, tolock),
          ),
        )

      await tx.insert(auditLog).values({
        tenantId,
        actorId: userId,
        actorType: 'user',
        entityType: 'time_entry',
        entityId: tolock[0]!,
        action: 'time_entry.locked',
      })
    }

    return { lockedCount: tolock.length, skippedIds: alreadyLocked }
  })
}

// ── unlockTimeEntries ──────────────────────────────────────────────────────────

/**
 * Unlock a batch of time entries. Not-locked entries are skipped.
 * Clears locked_at, locked_by, locked_reason.
 * Manager-only: enforced at the route layer (time:manage).
 */
export async function unlockTimeEntries(
  db: Db,
  tenantId: string,
  userId: string,
  entryIds: string[],
): Promise<LockResult> {
  if (entryIds.length === 0) return { lockedCount: 0, skippedIds: [] }

  return db.transaction(async (tx) => {
    const existing = await tx
      .select({ id: timeEntries.id, lockedAt: timeEntries.lockedAt })
      .from(timeEntries)
      .where(
        and(
          eq(timeEntries.tenantId, tenantId),
          inArray(timeEntries.id, entryIds),
        ),
      )

    const notLocked = existing.filter((r) => r.lockedAt === null).map((r) => r.id)
    const tounlock = existing.filter((r) => r.lockedAt !== null).map((r) => r.id)

    if (tounlock.length > 0) {
      const now = new Date()
      await tx
        .update(timeEntries)
        .set({
          lockedAt: null,
          lockedBy: null,
          lockedReason: null,
          updatedAt: now,
        })
        .where(
          and(
            eq(timeEntries.tenantId, tenantId),
            inArray(timeEntries.id, tounlock),
          ),
        )

      await tx.insert(auditLog).values({
        tenantId,
        actorId: userId,
        actorType: 'user',
        entityType: 'time_entry',
        entityId: tounlock[0]!,
        action: 'time_entry.unlocked',
      })
    }

    return { lockedCount: tounlock.length, skippedIds: notLocked }
  })
}

// ── getLockedEntries ───────────────────────────────────────────────────────────

/**
 * List locked entries for a date range (by started_at).
 * Returns only entries with locked_at IS NOT NULL.
 */
export async function getLockedEntries(
  db: Db,
  tenantId: string,
  startDate: Date,
  endDate: Date,
): Promise<LockedEntryRow[]> {
  const rows = await db
    .select({
      id: timeEntries.id,
      tenantId: timeEntries.tenantId,
      userId: timeEntries.userId,
      projectId: timeEntries.projectId,
      startedAt: timeEntries.startedAt,
      stoppedAt: timeEntries.stoppedAt,
      durationSeconds: timeEntries.durationSeconds,
      lockedAt: timeEntries.lockedAt,
      lockedBy: timeEntries.lockedBy,
      lockedReason: timeEntries.lockedReason,
    })
    .from(timeEntries)
    .where(
      and(
        eq(timeEntries.tenantId, tenantId),
        isNotNull(timeEntries.lockedAt),
        gte(timeEntries.startedAt, startDate),
        lte(timeEntries.startedAt, endDate),
      ),
    )

  return rows as LockedEntryRow[]
}
