/**
 * Invoice payments query helpers — partial-payment-recording (wave-11).
 *
 * All helpers are tenant-filtered.
 * Route files MUST NOT import raw Drizzle tables.
 *
 * recordInvoicePayment / reverseInvoicePayment / listInvoicePayments / getInvoiceBalance
 * are the canonical entry points. The older recordPayment (invoices-core) remains for
 * gateway webhook compat — these new helpers supersede it for the payment sub-resource API.
 */
import { and, desc, eq, sum } from 'drizzle-orm'
import { z } from 'zod'
import type { Db, DbTx } from '../client'
import { invoicePayments } from '../schema/invoice-payments'
import { invoices } from '../schema/invoices'
import { users } from '../schema/users'
import { auditLog } from './_audit-forward'
import type { InvoicePaymentObject, InvoicePaymentsResponse } from '@zync/types'

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

/** Full schema — includes `gateway` for internal webhook/reconcile callers only. */
export const recordPaymentInputSchema = z.object({
  amount: z.number().positive(),
  paidAt: z.string().datetime({ offset: true }).or(z.string().date()),
  source: z.enum(['manual', 'gateway', 'bank_transfer', 'auto_billing']).default('manual'),
  reference: z.string().max(500).optional().nullable(),
  note: z.string().max(2000).optional().nullable(),
})

/** User-facing POST /payments — `gateway` is server-set after provider re-fetch, not client-supplied. */
export const recordPaymentUserInputSchema = recordPaymentInputSchema.extend({
  source: z.enum(['manual', 'bank_transfer', 'auto_billing']).default('manual'),
})

export const reversePaymentInputSchema = z.object({
  reason: z.string().max(2000).optional().nullable(),
})

export type RecordPaymentInput = z.infer<typeof recordPaymentInputSchema>
export type ReversePaymentInput = z.infer<typeof reversePaymentInputSchema>

// ── Custom errors ─────────────────────────────────────────────────────────────

export class ReceiptIssuedError extends Error {
  constructor() {
    super('Cannot reverse a payment with an issued receipt. Void the receipt first.')
    this.name = 'ReceiptIssuedError'
  }
}

// ── Serializer ────────────────────────────────────────────────────────────────

function mapPayment(
  row: typeof invoicePayments.$inferSelect & { recordedByName?: string | null },
): InvoicePaymentObject {
  return {
    id: row.id,
    invoiceId: row.invoiceId,
    amount: parseFloat(row.amount),
    currency: row.currency,
    paidAt: row.paidAt instanceof Date ? row.paidAt.toISOString() : String(row.paidAt),
    source: row.source as InvoicePaymentObject['source'],
    reference: row.reference ?? null,
    recordedBy: row.recordedBy ?? null,
    recordedByName: row.recordedByName ?? null,
    note: row.note ?? null,
    receiptId: row.receiptId ?? null,
    createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt),
  }
}

function deriveZeroPaymentStatus(existing: Pick<
  typeof invoices.$inferSelect,
  'taxIssuedAt' | 'approvedAt' | 'sentAt'
>): typeof invoices.$inferSelect.status {
  if (existing.taxIssuedAt) return 'TAX_ISSUED'
  if (existing.approvedAt) return 'APPROVED'
  if (existing.sentAt) return 'SENT'
  return 'DRAFT'
}

function settlementTotal(total: string): number {
  return Math.abs(parseFloat(total))
}

// ── Query helpers ─────────────────────────────────────────────────────────────

/**
 * List all payments for an invoice, joined with recorder name.
 */
export async function listInvoicePayments(
  db: Db,
  tenantId: string,
  invoiceId: string,
): Promise<InvoicePaymentObject[]> {
  const rows = await db
    .select({
      payment: invoicePayments,
      recordedByName: users.name,
    })
    .from(invoicePayments)
    .leftJoin(users, eq(invoicePayments.recordedBy, users.id))
    .where(
      and(
        eq(invoicePayments.tenantId, tenantId),
        eq(invoicePayments.invoiceId, invoiceId),
      ),
    )
    .orderBy(desc(invoicePayments.paidAt), desc(invoicePayments.createdAt))

  return rows.map((r) => mapPayment({ ...r.payment, recordedByName: r.recordedByName }))
}

/**
 * Get current balance for an invoice.
 */
export async function getInvoiceBalance(
  db: Db,
  tenantId: string,
  invoiceId: string,
): Promise<InvoicePaymentsResponse> {
  const [invoice] = await db
    .select({
      total: invoices.total,
      amountPaid: invoices.amountPaid,
      overpaymentAmount: invoices.overpaymentAmount,
    })
    .from(invoices)
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))
    .limit(1)

  if (!invoice) {
    throw new Error(`Invoice not found: ${invoiceId}`)
  }

  const payments = await listInvoicePayments(db, tenantId, invoiceId)
  const total = parseFloat(invoice.total)
  const amountPaid = parseFloat(invoice.amountPaid)
  const overpaymentAmount = parseFloat(invoice.overpaymentAmount ?? '0')
  const balance = Math.max(0, settlementTotal(invoice.total) - amountPaid)

  return {
    payments,
    amountPaid,
    balance,
    total,
    overpaymentAmount,
  }
}

/**
 * Record a new payment against an invoice.
 * Updates invoices.amount_paid and invoices.status transactionally.
 * Returns the updated balance response.
 */
export async function recordInvoicePayment(
  db: Db,
  tenantId: string,
  invoiceId: string,
  actorId: string,
  input: RecordPaymentInput,
): Promise<InvoicePaymentsResponse> {
  return db.transaction(async (tx) => {
    const [existing] = await tx
      .select()
      .from(invoices)
      .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))
      .limit(1)
      .for('update')

    if (!existing) throw new Error('Invoice not found')
    const allowed = ['TAX_ISSUED', 'PARTIALLY_PAID', 'PAID']
    if (!allowed.includes(existing.status)) {
      throw new Error(
        `Cannot record payment for invoice in status ${existing.status}`,
      )
    }

    const prevPaid = parseFloat(existing.amountPaid)
    const total = parseFloat(existing.total)
    const payableTotal = settlementTotal(existing.total)
    const newPaid = Math.round((prevPaid + input.amount) * 100) / 100

    const overpaymentAmount = newPaid > payableTotal
      ? Math.round((newPaid - payableTotal) * 100) / 100
      : 0
    const newStatus = newPaid >= payableTotal ? 'PAID' : 'PARTIALLY_PAID'

    // Insert payment record
    const paidAtDate = new Date(input.paidAt)
    const [payment] = await tx
      .insert(invoicePayments)
      .values({
        tenantId,
        invoiceId,
        amount: String(input.amount),
        currency: existing.currency ?? 'ILS',
        paidAt: paidAtDate,
        source: input.source,
        reference: input.reference ?? null,
        recordedBy: actorId,
        note: input.note ?? null,
      })
      .returning()

    // Update invoice denorm columns
    await tx
      .update(invoices)
      .set({
        amountPaid: String(newPaid),
        overpaymentAmount: String(overpaymentAmount),
        status: newStatus,
        paidAt: newStatus === 'PAID' ? new Date() : null,
        updatedAt: new Date(),
      })
      .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))

    // Audit log
    await tx.insert(auditLog).values({
      tenantId,
      actorId,
      actorType: 'user',
      entityType: 'invoice',
      entityId: invoiceId,
      action: 'invoice.payment_recorded',
      changes: {
        amount: [null, String(input.amount)],
        source: [null, input.source],
        reference: [null, input.reference ?? null],
        note: [null, input.note ?? null],
      },
    })

    const balance = Math.max(0, payableTotal - newPaid)
    const paymentObj = mapPayment({ ...payment!, recordedByName: null })

    return {
      payments: [paymentObj],
      amountPaid: newPaid,
      balance,
      total,
      overpaymentAmount,
    }
  })
}

/**
 * Tx-aware payment recorder — runs the same logic as recordInvoicePayment but
 * accepts an already-open transaction handle. Call this from services that
 * need to record a payment as part of a larger transaction (e.g. matchUnmatchedPayment).
 *
 * Allowed statuses are expanded to include SENT|APPROVED (reconciliation path)
 * in addition to the existing TAX_ISSUED|PARTIALLY_PAID|PAID.
 */
export async function recordInvoicePaymentTx(
  tx: DbTx,
  tenantId: string,
  invoiceId: string,
  actorId: string,
  input: RecordPaymentInput,
  auditAction = 'invoice.payment_recorded',
): Promise<InvoicePaymentObject> {
  const [existing] = await tx
    .select()
    .from(invoices)
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))
    .limit(1)
    .for('update')

  if (!existing) throw new Error('Invoice not found')
  const allowed = ['SENT', 'APPROVED', 'TAX_ISSUED', 'PARTIALLY_PAID', 'PAID']
  if (!allowed.includes(existing.status)) {
    throw new Error(
      `Cannot record payment for invoice in status ${existing.status}`,
    )
  }

  const prevPaid = parseFloat(existing.amountPaid)
  const total = parseFloat(existing.total)
  const payableTotal = settlementTotal(existing.total)
  const newPaid = Math.round((prevPaid + input.amount) * 100) / 100

  const overpaymentAmount = newPaid > payableTotal
    ? Math.round((newPaid - payableTotal) * 100) / 100
    : 0
  const newStatus = newPaid >= payableTotal ? 'PAID' : 'PARTIALLY_PAID'

  const paidAtDate = new Date(input.paidAt)
  const [payment] = await tx
    .insert(invoicePayments)
    .values({
      tenantId,
      invoiceId,
      amount: String(input.amount),
      currency: existing.currency ?? 'ILS',
      paidAt: paidAtDate,
      source: input.source,
      reference: input.reference ?? null,
      recordedBy: actorId,
      note: input.note ?? null,
    })
    .returning()

  await tx
    .update(invoices)
    .set({
      amountPaid: String(newPaid),
      overpaymentAmount: String(overpaymentAmount),
      status: newStatus,
      paidAt: newStatus === 'PAID' ? new Date() : null,
      updatedAt: new Date(),
    })
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))

  await tx.insert(auditLog).values({
    tenantId,
    actorId,
    actorType: 'user',
    entityType: 'invoice',
    entityId: invoiceId,
    action: auditAction,
    changes: {
      amount: [null, String(input.amount)],
      source: [null, input.source],
      reference: [null, input.reference ?? null],
      note: [null, input.note ?? null],
    },
  })

  return mapPayment({ ...payment!, recordedByName: null })
}

/**
 * Tx-aware payment reversal — deletes a payment row and recomputes invoice denorm columns.
 * Does NOT check receiptId; callers that must reject linked receipts enforce that guard
 * before calling (e.g. reverseInvoicePayment route). Used by receipt void to reverse
 * linked payments inside the void transaction.
 */
export async function reverseInvoicePaymentTx(
  tx: DbTx,
  tenantId: string,
  invoiceId: string,
  paymentId: string,
  actorId: string,
  auditAction = 'invoice.payment_reversed',
): Promise<{ newPaid: number; overpaymentAmount: number; invoiceTotal: number }> {
  const [existing] = await tx
    .select()
    .from(invoices)
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))
    .limit(1)
    .for('update')

  if (!existing) throw new Error('Invoice not found')

  const [payment] = await tx
    .select()
    .from(invoicePayments)
    .where(
      and(
        eq(invoicePayments.tenantId, tenantId),
        eq(invoicePayments.id, paymentId),
        eq(invoicePayments.invoiceId, invoiceId),
      ),
    )
    .limit(1)

  if (!payment) throw new Error('Payment not found')

  await tx
    .delete(invoicePayments)
    .where(and(eq(invoicePayments.tenantId, tenantId), eq(invoicePayments.id, paymentId)))

  const [sumResult] = await tx
    .select({ total: sum(invoicePayments.amount) })
    .from(invoicePayments)
    .where(
      and(
        eq(invoicePayments.tenantId, tenantId),
        eq(invoicePayments.invoiceId, invoiceId),
      ),
    )

  const invoiceTotal = parseFloat(existing.total)
  const payableTotal = settlementTotal(existing.total)
  const newPaid = sumResult?.total ? Math.round(parseFloat(sumResult.total) * 100) / 100 : 0
  const overpaymentAmount = newPaid > payableTotal
    ? Math.round((newPaid - payableTotal) * 100) / 100
    : 0

  let newStatus = existing.status
  if (newPaid <= 0) {
    newStatus = deriveZeroPaymentStatus(existing)
  } else if (newPaid < payableTotal) {
    newStatus = 'PARTIALLY_PAID'
  } else {
    newStatus = 'PAID'
  }

  await tx
    .update(invoices)
    .set({
      amountPaid: String(newPaid),
      overpaymentAmount: String(overpaymentAmount),
      status: newStatus,
      paidAt: newStatus === 'PAID' ? existing.paidAt : null,
      updatedAt: new Date(),
    })
    .where(and(eq(invoices.tenantId, tenantId), eq(invoices.id, invoiceId)))

  await tx.insert(auditLog).values({
    tenantId,
    actorId,
    actorType: 'user',
    entityType: 'invoice',
    entityId: invoiceId,
    action: auditAction,
    changes: {
      amount: [payment.amount, null],
    },
  })

  return { newPaid, overpaymentAmount, invoiceTotal }
}

/**
 * Reverse (delete) a payment entry.
 * Recalculates invoice.amount_paid and status.
 * Throws ReceiptIssuedError if the payment has a linked receipt.
 */
export async function reverseInvoicePayment(
  db: Db,
  tenantId: string,
  invoiceId: string,
  paymentId: string,
  actorId: string,
  input: ReversePaymentInput = {},
): Promise<InvoicePaymentsResponse> {
  return db.transaction(async (tx) => {
    const [payment] = await tx
      .select()
      .from(invoicePayments)
      .where(
        and(
          eq(invoicePayments.tenantId, tenantId),
          eq(invoicePayments.id, paymentId),
          eq(invoicePayments.invoiceId, invoiceId),
        ),
      )
      .limit(1)
      .for('update')

    if (!payment) throw new Error('Payment not found')
    if (payment.receiptId) throw new ReceiptIssuedError()

    const { newPaid, overpaymentAmount, invoiceTotal } = await reverseInvoicePaymentTx(
      tx,
      tenantId,
      invoiceId,
      paymentId,
      actorId,
    )

    const balance = Math.max(0, Math.abs(invoiceTotal) - newPaid)
    const remaining = await tx
      .select({
        payment: invoicePayments,
        recordedByName: users.name,
      })
      .from(invoicePayments)
      .leftJoin(users, eq(invoicePayments.recordedBy, users.id))
      .where(
        and(
          eq(invoicePayments.tenantId, tenantId),
          eq(invoicePayments.invoiceId, invoiceId),
        ),
      )
      .orderBy(desc(invoicePayments.paidAt), desc(invoicePayments.createdAt))

    return {
      payments: remaining.map((r) =>
        mapPayment({ ...r.payment, recordedByName: r.recordedByName }),
      ),
      amountPaid: newPaid,
      balance,
      total: invoiceTotal,
      overpaymentAmount,
    }
  })
}
