/**
 * Automated invoice generation — invoices-core wave 3.
 *
 * Consumes queue messages for retainer depletion, task-status triggers, and
 * fixed-price deposit invoices. Uses createInvoice/sendInvoice; idempotent via
 * invoices.dedup_key (partial unique index + ON CONFLICT DO NOTHING).
 */
import { and, eq, inArray, ne } from 'drizzle-orm'
import type { Db } from '../client'
import { invoices } from '../schema/invoices'
import { projects } from '../schema/projects'
import { tasks } from '../schema/tasks'
import { tenants } from '../schema/tenants'
import { createInvoice, getInvoice, sendInvoice } from './invoices'
import { getInvoiceSettings } from './settings-invoices'
import type {
  FixedBillingConfig,
  RetainerBillingConfig,
} from '@zync/types'

export type InvoiceQueueBinding = { send: (msg: unknown) => Promise<unknown> }

export interface RetainerInvoiceJob {
  type: 'retainer.invoice'
  tenant_id: string
  project_id: string
  month: string
  excess_hours: number
}

export interface InvoiceGenerateJob {
  type: 'invoice.generate'
  tenantId: string
  projectId: string
  reason: 'retainer_depleted' | 'task_status' | 'fixed_deposit'
  month?: string
  excessHours?: number
  taskId?: string
}

/** Machine idempotency key for invoices.dedup_key (not stored in notes). */
export function autoGenDedupKey(kind: string, key: string): string {
  return `${kind}:${key}`
}

/** @deprecated Use autoGenDedupKey — notes are no longer used for dedup. */
export function autoGenDedupeNote(kind: string, key: string): string {
  return autoGenDedupKey(kind, key)
}

function round2(n: number): number {
  return Math.round(n * 100) / 100
}

async function projectHasFixedDeposit(
  db: Db,
  tenantId: string,
  projectId: string,
): Promise<boolean> {
  const [row] = await db
    .select({ id: invoices.id })
    .from(invoices)
    .where(
      and(
        eq(invoices.tenantId, tenantId),
        eq(invoices.projectId, projectId),
        eq(invoices.source, 'fixed_deposit'),
        ne(invoices.status, 'VOID'),
      ),
    )
    .limit(1)
  return row !== undefined
}

async function loadTenantCountry(db: Db, tenantId: string): Promise<string> {
  const [row] = await db
    .select({ countryCode: tenants.countryCode })
    .from(tenants)
    .where(eq(tenants.id, tenantId))
    .limit(1)
  return row?.countryCode ?? 'IL'
}

async function loadProjectForInvoice(
  db: Db,
  tenantId: string,
  projectId: string,
): Promise<{
  id: string
  name: string
  customerId: string | null
  currency: string
  billingType: string
  billingConfig: unknown
  createdBy: string
} | null> {
  const [row] = await db
    .select({
      id: projects.id,
      name: projects.name,
      customerId: projects.customerId,
      currency: projects.currency,
      billingType: projects.billingType,
      billingConfig: projects.billingConfig,
      createdBy: projects.createdBy,
    })
    .from(projects)
    .where(and(eq(projects.tenantId, tenantId), eq(projects.id, projectId)))
    .limit(1)
  return row ?? null
}

async function sumProjectInvoiceSubtotals(
  db: Db,
  tenantId: string,
  projectId: string,
): Promise<number> {
  const rows = await db
    .select({ subtotal: invoices.subtotal })
    .from(invoices)
    .where(
      and(
        eq(invoices.tenantId, tenantId),
        eq(invoices.projectId, projectId),
        inArray(invoices.source, ['manual', 'fixed_deposit', 'retainer', 'hourly_auto']),
        ne(invoices.status, 'VOID'),
      ),
    )
  return rows.reduce((sum, r) => sum + parseFloat(String(r.subtotal ?? '0')), 0)
}

function retainerHourlyRate(config: RetainerBillingConfig): number {
  if (config.monthly_hours_included <= 0) return 0
  return round2(config.monthly_amount / config.monthly_hours_included)
}

/** Send when config requires it; resumable on retry if invoice is still DRAFT/REJECTED. */
async function maybeSendDraft(
  db: Db,
  tenantId: string,
  invoiceId: string,
  actorId: string,
  countryCode: string,
  shouldSend: boolean,
): Promise<void> {
  if (!shouldSend) return
  const existing = await getInvoice(db, tenantId, invoiceId)
  if (!existing || (existing.status !== 'DRAFT' && existing.status !== 'REJECTED')) return
  const today = new Date().toISOString().slice(0, 10)
  await sendInvoice(db, tenantId, invoiceId, actorId, countryCode, today)
}

/**
 * Retainer hour-bank depletion → source='retainer' invoice.
 */
export async function processRetainerInvoiceJob(
  db: Db,
  job: RetainerInvoiceJob,
): Promise<{ invoiceId: string | null; skipped: boolean }> {
  const tenantId = job.tenant_id
  const projectId = job.project_id
  const month = job.month
  const excessHours = job.excess_hours

  const project = await loadProjectForInvoice(db, tenantId, projectId)
  if (!project) {
    console.warn('[invoice-automation] retainer: project not found', { tenantId, projectId })
    return { invoiceId: null, skipped: true }
  }
  if (project.billingType !== 'retainer') {
    console.warn('[invoice-automation] retainer: project is not retainer', { projectId })
    return { invoiceId: null, skipped: true }
  }
  if (!project.customerId) {
    console.warn('[invoice-automation] retainer: project has no customer', { projectId })
    return { invoiceId: null, skipped: true }
  }

  const config = (project.billingConfig ?? {}) as RetainerBillingConfig
  const settings = await getInvoiceSettings(db, tenantId)
  const shouldSend =
    config.auto_send_invoice === true || settings.auto_send_retainer_invoice === true

  const lines: Array<{
    description: string
    quantity: number
    unitPrice: number
    discountPct: number
    taxable: boolean
    position: number
  }> = [
    {
      description: `Retainer: ${project.name} — ${month}`,
      quantity: 1,
      unitPrice: config.monthly_amount,
      discountPct: 0,
      taxable: true,
      position: 0,
    },
  ]

  if (config.hour_bank_overflow_action === 'invoice' && excessHours > 0) {
    const hourlyRate = retainerHourlyRate(config)
    lines.push({
      description: `Overtime: ${project.name} — ${month}`,
      quantity: round2(excessHours),
      unitPrice: hourlyRate,
      discountPct: 0,
      taxable: true,
      position: 1,
    })
  }

  const countryCode = await loadTenantCountry(db, tenantId)
  const dedupKey = autoGenDedupKey('retainer', `${projectId}:${month}`)
  const created = await createInvoice(
    db,
    tenantId,
    project.createdBy,
    {
      customerId: project.customerId,
      projectId: project.id,
      currency: project.currency ?? 'ILS',
      notes: `Retainer invoice — ${month}`,
      dedupKey,
      source: 'retainer',
      lines,
    },
    countryCode,
  )

  await maybeSendDraft(
    db,
    tenantId,
    created.id,
    project.createdBy,
    countryCode,
    shouldSend,
  )

  return { invoiceId: created.id, skipped: created.skipped }
}

/**
 * Fixed-price deposit invoice at project creation.
 */
export async function processFixedDepositJob(
  db: Db,
  tenantId: string,
  projectId: string,
): Promise<{ invoiceId: string | null; skipped: boolean }> {
  const project = await loadProjectForInvoice(db, tenantId, projectId)
  if (!project) return { invoiceId: null, skipped: true }
  if (project.billingType !== 'fixed') return { invoiceId: null, skipped: true }
  if (!project.customerId) return { invoiceId: null, skipped: true }

  const config = (project.billingConfig ?? {}) as FixedBillingConfig
  if (!config.deposit_pct || config.deposit_pct <= 0) {
    return { invoiceId: null, skipped: true }
  }

  const depositAmount = round2((config.total_amount * config.deposit_pct) / 100)
  if (depositAmount <= 0) return { invoiceId: null, skipped: true }

  const countryCode = await loadTenantCountry(db, tenantId)
  const dedupKey = autoGenDedupKey('fixed_deposit', projectId)
  const created = await createInvoice(
    db,
    tenantId,
    project.createdBy,
    {
      customerId: project.customerId,
      projectId: project.id,
      currency: project.currency ?? 'ILS',
      notes: `Deposit (${config.deposit_pct}%)`,
      dedupKey,
      source: 'fixed_deposit',
      lines: [
        {
          description: `Deposit: ${project.name} (${config.deposit_pct}%)`,
          quantity: 1,
          unitPrice: depositAmount,
          discountPct: 0,
          taxable: true,
          position: 0,
        },
      ],
    },
    countryCode,
  )

  return { invoiceId: created.id, skipped: created.skipped }
}

/**
 * Task status trigger → at most one auto deposit and one auto final per project.
 */
export async function processTaskStatusInvoiceJob(
  db: Db,
  tenantId: string,
  projectId: string,
  taskId: string,
): Promise<{ invoiceId: string | null; skipped: boolean }> {
  const project = await loadProjectForInvoice(db, tenantId, projectId)
  if (!project) return { invoiceId: null, skipped: true }
  if (project.billingType !== 'fixed') return { invoiceId: null, skipped: true }
  if (!project.customerId) return { invoiceId: null, skipped: true }

  const [taskRow] = await db
    .select({ title: tasks.title })
    .from(tasks)
    .where(and(eq(tasks.tenantId, tenantId), eq(tasks.id, taskId)))
    .limit(1)
  const taskTitle = taskRow?.title ?? 'Task'

  const config = (project.billingConfig ?? {}) as FixedBillingConfig
  const settings = await getInvoiceSettings(db, tenantId)
  const shouldSend = settings.task_status_trigger?.invoice_type === 'sent'

  const hasDeposit = await projectHasFixedDeposit(db, tenantId, projectId)

  let amount: number
  let description: string
  let source: 'fixed_deposit' | 'manual'
  let dedupKey: string
  let notes: string

  if (config.deposit_pct > 0 && !hasDeposit) {
    amount = round2((config.total_amount * config.deposit_pct) / 100)
    description = `Deposit: ${project.name} — ${taskTitle}`
    source = 'fixed_deposit'
    dedupKey = autoGenDedupKey('fixed_deposit', projectId)
    notes = `Task-status deposit — ${taskTitle}`
  } else {
    const invoiced = await sumProjectInvoiceSubtotals(db, tenantId, projectId)
    amount = round2(Math.max(config.total_amount - invoiced, 0))
    description = `Final invoice: ${project.name} — ${taskTitle}`
    source = 'manual'
    dedupKey = autoGenDedupKey('task_final', projectId)
    notes = `Task-status final — ${taskTitle}`
  }

  if (amount <= 0) return { invoiceId: null, skipped: true }

  const countryCode = await loadTenantCountry(db, tenantId)
  const created = await createInvoice(
    db,
    tenantId,
    project.createdBy,
    {
      customerId: project.customerId,
      projectId: project.id,
      currency: project.currency ?? 'ILS',
      notes,
      dedupKey,
      source,
      lines: [
        {
          description,
          quantity: 1,
          unitPrice: amount,
          discountPct: 0,
          taxable: true,
          position: 0,
        },
      ],
    },
    countryCode,
  )

  await maybeSendDraft(
    db,
    tenantId,
    created.id,
    project.createdBy,
    countryCode,
    shouldSend,
  )

  return { invoiceId: created.id, skipped: created.skipped }
}

export async function processInvoiceGenerateJob(
  db: Db,
  job: InvoiceGenerateJob,
): Promise<{ invoiceId: string | null; skipped: boolean }> {
  switch (job.reason) {
    case 'retainer_depleted':
      if (!job.month) return { invoiceId: null, skipped: true }
      return processRetainerInvoiceJob(db, {
        type: 'retainer.invoice',
        tenant_id: job.tenantId,
        project_id: job.projectId,
        month: job.month,
        excess_hours: job.excessHours ?? 0,
      })
    case 'fixed_deposit':
      return processFixedDepositJob(db, job.tenantId, job.projectId)
    case 'task_status':
      if (!job.taskId) return { invoiceId: null, skipped: true }
      return processTaskStatusInvoiceJob(db, job.tenantId, job.projectId, job.taskId)
    default:
      return { invoiceId: null, skipped: true }
  }
}

/** Enqueue fixed-deposit generation after project creation when configured. */
export async function enqueueFixedDepositIfConfigured(
  queue: InvoiceQueueBinding | undefined,
  tenantId: string,
  projectId: string,
  billingType: string,
  billingConfig: unknown,
): Promise<void> {
  if (!queue || billingType !== 'fixed') return
  const config = (billingConfig ?? {}) as FixedBillingConfig
  if (!config.deposit_pct || config.deposit_pct <= 0) return
  if (config.auto_create_deposit !== true) return

  await queue.send({
    type: 'invoice.generate',
    tenantId,
    projectId,
    reason: 'fixed_deposit',
  })
}

/**
 * When a task moves to the tenant-configured trigger status on a fixed-price
 * project, enqueue invoice generation (deposit/final deduped per project at consumer).
 */
export async function maybeEnqueueTaskStatusInvoice(
  db: Db,
  tenantId: string,
  taskId: string,
  newStatusId: string,
  queue?: InvoiceQueueBinding,
): Promise<void> {
  if (!queue) return

  const settings = await getInvoiceSettings(db, tenantId)
  const trigger = settings.task_status_trigger
  if (!trigger?.enabled || trigger.status_id !== newStatusId) return

  const [taskRow] = await db
    .select({ projectId: tasks.projectId })
    .from(tasks)
    .where(and(eq(tasks.tenantId, tenantId), eq(tasks.id, taskId)))
    .limit(1)
  if (!taskRow?.projectId) return

  const [projectRow] = await db
    .select({ billingType: projects.billingType })
    .from(projects)
    .where(and(eq(projects.tenantId, tenantId), eq(projects.id, taskRow.projectId)))
    .limit(1)
  if (projectRow?.billingType !== 'fixed') return

  await queue.send({
    type: 'invoice.generate',
    tenantId,
    projectId: taskRow.projectId,
    reason: 'task_status',
    taskId,
  })
}

/** Bulk status update: enqueue for each task that landed on the trigger status. */
export async function enqueueTaskStatusInvoicesForBulk(
  db: Db,
  tenantId: string,
  taskIds: string[],
  newStatusId: string,
  queue?: InvoiceQueueBinding,
): Promise<void> {
  if (!queue || taskIds.length === 0) return

  const settings = await getInvoiceSettings(db, tenantId)
  const trigger = settings.task_status_trigger
  if (!trigger?.enabled || trigger.status_id !== newStatusId) return

  for (const taskId of taskIds) {
    await maybeEnqueueTaskStatusInvoice(db, tenantId, taskId, newStatusId, queue)
  }
}
