/**
 * Bulk operation query helpers — bulk-operations (wave-8 leaf 2).
 *
 * All helpers are tenant-filtered and run in a transaction with audit log entries.
 * Route files import from '@zync/db/queries'.
 */
import { and, eq, inArray, isNull } from 'drizzle-orm'
import { z } from 'zod'
import type { Db } from '../client'
import { invoices } from '../schema/invoices'
import { customers } from '../schema/customers'
import { expenses } from '../schema/expenses'
import { projects } from '../schema/projects'
import { auditLog } from './_audit-forward'
import { sendInvoice, voidInvoice, ConflictError } from './invoices'
import { recordInvoicePayment } from './invoice-payments'

// ── Zod schemas ───────────────────────────────────────────────────────────────

export const bulkInvoiceStatusSchema = z
  .object({
    ids: z.array(z.string().uuid()).min(1).max(100),
    status: z.enum(['SENT', 'PAID', 'VOID']),
    voidReason: z.string().min(1).max(2000).optional(),
  })
  .superRefine((data, ctx) => {
    if (data.status === 'VOID' && !data.voidReason?.trim()) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'voidReason is required when status is VOID',
        path: ['voidReason'],
      })
    }
  })

export const bulkCustomerArchiveSchema = z.object({
  ids: z.array(z.string().uuid()).min(1).max(100),
})

export const bulkExpenseDeleteSchema = z.object({
  ids: z.array(z.string().uuid()).min(1).max(100),
})

export const bulkProjectStatusSchema = z.object({
  ids: z.array(z.string().uuid()).min(1).max(100),
  status: z.enum(['active', 'on_hold', 'completed', 'archived']),
})

export interface BulkInvoiceStatusResult {
  processed: number
  skipped: number
  errors: Array<{ id: string; reason: string }>
}

export interface BulkInvoiceStatusOptions {
  countryCode?: string
  issueDate?: string
  voidReason?: string
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function isSkipError(message: string): boolean {
  return (
    message.includes('Cannot send invoice in status') ||
    message.includes('Cannot record payment for invoice in status') ||
    message.includes('Cannot void a TAX_ISSUED or later invoice')
  )
}

/**
 * Bulk-update invoice status via guarded single-invoice transition helpers.
 * Invalid transitions are skipped and reported per invoice (partial success).
 */
export async function bulkUpdateInvoiceStatus(
  db: Db,
  tenantId: string,
  actorId: string,
  ids: string[],
  status: z.infer<typeof bulkInvoiceStatusSchema>['status'],
  options: BulkInvoiceStatusOptions = {},
): Promise<BulkInvoiceStatusResult> {
  const countryCode = options.countryCode ?? 'IL'
  const issueDate = options.issueDate ?? new Date().toISOString().slice(0, 10)
  const voidReason = options.voidReason?.trim() ?? ''

  let processed = 0
  let skipped = 0
  const errors: Array<{ id: string; reason: string }> = []

  for (const id of ids) {
    try {
      if (status === 'SENT') {
        await sendInvoice(db, tenantId, id, actorId, countryCode, issueDate)
        processed++
        continue
      }

      if (status === 'PAID') {
        const [invoice] = await db
          .select({
            status: invoices.status,
            total: invoices.total,
            amountPaid: invoices.amountPaid,
          })
          .from(invoices)
          .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, id)))
          .limit(1)

        if (!invoice) {
          errors.push({ id, reason: 'Invoice not found' })
          continue
        }

        const payableStatuses = ['TAX_ISSUED', 'PARTIALLY_PAID']
        if (!payableStatuses.includes(invoice.status)) {
          skipped++
          continue
        }

        const total = parseFloat(invoice.total)
        const amountPaid = parseFloat(invoice.amountPaid)
        const balance = Math.round((total - amountPaid) * 100) / 100

        if (balance <= 0) {
          skipped++
          continue
        }

        await recordInvoicePayment(db, tenantId, id, actorId, {
          amount: balance,
          paidAt: new Date().toISOString(),
          source: 'manual',
          note: 'Bulk mark as paid',
        })
        processed++
        continue
      }

      if (status === 'VOID') {
        await voidInvoice(db, tenantId, id, actorId, voidReason)
        processed++
      }
    } catch (err) {
      const reason = err instanceof Error ? err.message : 'Unknown error'

      if (reason.includes('not found')) {
        errors.push({ id, reason: 'Invoice not found' })
        continue
      }

      if (err instanceof ConflictError || isSkipError(reason)) {
        skipped++
        continue
      }

      errors.push({ id, reason })
    }
  }

  return { processed, skipped, errors }
}

/**
 * Bulk-archive customers by setting status = 'archived'.
 * Emits one audit row per customer inside the transaction.
 */
export async function bulkArchiveCustomers(
  db: Db,
  tenantId: string,
  actorId: string,
  ids: string[],
): Promise<{ updated: number }> {
  const now = new Date()

  await db.transaction(async (tx) => {
    await tx
      .update(customers)
      .set({ status: 'archived', updatedAt: now })
      .where(and(eq(customers.tenantId, tenantId), inArray(customers.id, ids)))

    await tx.insert(auditLog).values(
      ids.map((id) => ({
        tenantId,
        actorId,
        actorType: 'user' as const,
        entityType: 'customer',
        entityId: id,
        action: 'bulk_archive',
        changes: null,
      })),
    )
  })

  return { updated: ids.length }
}

/**
 * Bulk soft-delete expenses by setting deleted_at.
 * Skips already-deleted entries (isNull guard in WHERE).
 * Emits one audit row per expense inside the transaction.
 */
export async function bulkDeleteExpenses(
  db: Db,
  tenantId: string,
  actorId: string,
  ids: string[],
): Promise<{ updated: number }> {
  const now = new Date()

  await db.transaction(async (tx) => {
    await tx
      .update(expenses)
      .set({ deletedAt: now, updatedAt: now })
      .where(
        and(
          eq(expenses.tenantId, tenantId),
          inArray(expenses.id, ids),
          isNull(expenses.deletedAt),
        ),
      )

    await tx.insert(auditLog).values(
      ids.map((id) => ({
        tenantId,
        actorId,
        actorType: 'user' as const,
        entityType: 'expense',
        entityId: id,
        action: 'bulk_delete',
        changes: null,
      })),
    )
  })

  return { updated: ids.length }
}

/**
 * Bulk-update project status.
 * Emits one audit row per project inside the transaction.
 */
export async function bulkUpdateProjectStatus(
  db: Db,
  tenantId: string,
  actorId: string,
  ids: string[],
  status: z.infer<typeof bulkProjectStatusSchema>['status'],
): Promise<{ updated: number }> {
  const now = new Date()

  // For completed/archived transitions also set the lifecycle timestamp.
  const extra: Partial<{ completedAt: Date; archivedAt: Date }> = {}
  if (status === 'completed') extra.completedAt = now
  if (status === 'archived') extra.archivedAt = now

  await db.transaction(async (tx) => {
    await tx
      .update(projects)
      .set({ status, updatedAt: now, ...extra })
      .where(and(eq(projects.tenantId, tenantId), inArray(projects.id, ids)))

    await tx.insert(auditLog).values(
      ids.map((id) => ({
        tenantId,
        actorId,
        actorType: 'user' as const,
        entityType: 'project',
        entityId: id,
        action: `bulk_status_${status}`,
        changes: null,
      })),
    )
  })

  return { updated: ids.length }
}
