/**
 * Streaming generator for the Israeli Tax Authority uniform-structure export
 * (מבנה אחיד / BKMVDATA) — wave-13, spec 180.
 *
 * Produces two CP1255-encoded byte arrays:
 *   bkmvdataBytes — the BKMVDATA.txt data file
 *   iniBytes      — the INI.txt index file
 *
 * Both are assembled in memory (suitable for Worker queue handler which
 * receives the job and owns a single execution context). For extremely large
 * date ranges the caller should increase the queue consumer timeout.
 *
 * Steps:
 *  1. Fetch tenant business profile (name + vat_number from settings JSONB)
 *  2. Build A000 + A100 header records
 *  3. Stream C100 + D110 from invoices (status TAX_ISSUED/PAID/PARTIALLY_PAID)
 *  4. Stream C100 + D110 for credit notes (source='credit_note', same table)
 *  5. Stream C100 (doc_type receipt/invoice_receipt) + D120 from receipts
 *  6. B100/B110 — omitted in v1 (counts = 0, documents-only mode)
 *  7. Z900 closing record
 *  8. Build INI.txt
 */
import { eq, and, gte, lte, inArray } from '@zync/db'
import type { Db } from '@zync/db/queries'
import {
  invoices,
  invoiceLines,
  receipts,
  receiptPaymentLines,
  tenants,
} from '@zync/db'
import { encodeCP1255WithReport } from '../../lib/cp1255'
import {
  buildA000,
  buildA100,
  buildC100,
  buildD110,
  buildD120,
  buildZ900,
  agorot,
  dateField,
} from './records'
import { buildINI } from './ini'
import type { A000Header, UniformRecordCode, D120Payment } from './types'
import type { Env } from '@zync/types'
import { hasAccountantLedger, deriveMovements } from '../movements'

const CRLF = '\r\n'

function emitLine(line: string, target: Uint8Array[], substitutionLog: string[]): void {
  const { bytes, substitutions } = encodeCP1255WithReport(line + CRLF)
  target.push(bytes)
  for (const s of substitutions) substitutionLog.push(s)
}

function concatBytes(chunks: Uint8Array[]): Uint8Array {
  const total = chunks.reduce((s, c) => s + c.length, 0)
  const out = new Uint8Array(total)
  let offset = 0
  for (const chunk of chunks) {
    out.set(chunk, offset)
    offset += chunk.length
  }
  return out
}

export interface GenerateUniformExportOptions {
  tenantId: string
  from: string // YYYY-MM-DD
  to: string   // YYYY-MM-DD
  softwareRegNo: string
  /** Optional: Worker env bindings for accountant-ledger derivation (wave-15) */
  env?: Env
}

export interface GenerateUniformExportOutput {
  bkmvdataBytes: Uint8Array
  iniBytes: Uint8Array
  counts: Record<UniformRecordCode, number>
  substitutionLog: string[]
}

export async function generateUniformExport(
  db: Db,
  opts: GenerateUniformExportOptions,
): Promise<GenerateUniformExportOutput> {
  const { tenantId, from, to, softwareRegNo, env } = opts
  const substitutionLog: string[] = []

  // ── 1. Fetch tenant info ───────────────────────────────────────────────────
  const tenant = await db.query.tenants.findFirst({
    where: eq(tenants.id, tenantId),
  })
  if (!tenant) throw new Error(`Tenant not found: ${tenantId}`)

  const settings = (tenant.settings as Record<string, unknown>) ?? {}
  const businessPrimaryId = String(settings['vat_number'] ?? settings['business_primary_id'] ?? '')
  const businessName = tenant.name ?? ''

  const header: A000Header = {
    softwareRegNo,
    businessPrimaryId,
    periodFrom: from.replace(/-/g, ''),
    periodTo: to.replace(/-/g, ''),
    businessName,
  }

  const counts: Record<UniformRecordCode, number> = {
    A000: 1,
    A100: 1,
    C100: 0,
    D110: 0,
    D120: 0,
    B100: 0,
    B110: 0,
    M100: 0,
    Z900: 1,
  }

  const bkmvChunks: Uint8Array[] = []

  // ── 2. Header records ──────────────────────────────────────────────────────
  emitLine(buildA000(header), bkmvChunks, substitutionLog)
  emitLine(buildA100(header.periodFrom, header.periodTo), bkmvChunks, substitutionLog)

  let controlSumIls = 0

  // ── 3. Invoices (TAX_ISSUED / PAID / PARTIALLY_PAID) ──────────────────────
  const invoiceRows = await db.query.invoices.findMany({
    where: and(
      eq(invoices.tenantId, tenantId),
      inArray(invoices.status, ['TAX_ISSUED', 'PAID', 'PARTIALLY_PAID']),
      gte(invoices.taxIssueDate, from),
      lte(invoices.taxIssueDate, to),
      eq(invoices.isTemplate, false),
    ),
    with: {
      customer: { columns: { id: true, name: true } },
    },
  })

  for (const inv of invoiceRows) {
    const docNumber = inv.invoiceNumber ?? inv.id.slice(0, 15)
    const issueDate = inv.taxIssueDate ?? inv.issueDate ?? inv.createdAt.toISOString().slice(0, 10)
    const total = parseFloat(inv.total)
    const subtotal = parseFloat(inv.subtotal)
    const vatAmount = parseFloat(inv.vatAmount)
    const vatRate = parseFloat(inv.vatRate ?? '0')
    const isCreditNote = inv.source === 'credit_note'

    emitLine(buildC100({
      docNumber,
      docType: isCreditNote ? 'credit_note' : 'invoice',
      issueDate: dateField(String(issueDate)),
      customerId: inv.customerId ?? undefined,
      customerName: (inv as unknown as { customer?: { name?: string } }).customer?.name,
      vatRate,
      subtotalAgorot: agorot(subtotal),
      vatAgorot: agorot(vatAmount),
      totalAgorot: agorot(total),
      currency: inv.currency,
    }), bkmvChunks, substitutionLog)
    counts['C100']++
    controlSumIls += total

    // Invoice lines
    const lines = await db.query.invoiceLines.findMany({
      where: and(
        eq(invoiceLines.invoiceId, inv.id),
        eq(invoiceLines.tenantId, tenantId),
      ),
      orderBy: (t, { asc }) => [asc(t.position)],
    })

    for (const line of lines) {
      const unitPrice = parseFloat(line.unitPrice)
      const lineTotal = parseFloat(line.lineTotal)
      const quantity = parseFloat(line.quantity)
      emitLine(buildD110({
        docNumber,
        linePosition: line.position,
        description: line.description,
        quantity,
        unitPriceAgorot: agorot(unitPrice),
        lineTotalAgorot: agorot(lineTotal),
        taxable: line.taxable,
      }), bkmvChunks, substitutionLog)
      counts['D110']++
    }
  }

  // ── 5. Receipts ────────────────────────────────────────────────────────────
  const receiptRows = await db.query.receipts.findMany({
    where: and(
      eq(receipts.tenantId, tenantId),
      eq(receipts.status, 'ISSUED'),
      gte(receipts.issuedAt, new Date(from)),
      lte(receipts.issuedAt, new Date(to + 'T23:59:59Z')),
    ),
    with: {
      customer: { columns: { id: true, name: true } },
    },
  })

  for (const rcpt of receiptRows) {
    const docNumber = rcpt.receiptNumber ?? rcpt.id.slice(0, 15)
    const issueDate = rcpt.issuedAt ? rcpt.issuedAt.toISOString().slice(0, 10) : from
    const total = parseFloat(rcpt.amount)
    const ilsTotal = rcpt.currency === 'ILS'
      ? total
      : (rcpt.amountIls ? parseFloat(String(rcpt.amountIls)) : total)

    emitLine(buildC100({
      docNumber,
      docType: rcpt.docType as 'receipt' | 'invoice_receipt',
      issueDate: dateField(issueDate),
      customerId: rcpt.customerId ?? undefined,
      customerName: (rcpt as unknown as { customer?: { name?: string } }).customer?.name,
      vatRate: 0, // receipts carry the VAT on the linked invoice
      subtotalAgorot: agorot(ilsTotal),
      vatAgorot: 0,
      totalAgorot: agorot(ilsTotal),
      currency: rcpt.currency,
    }), bkmvChunks, substitutionLog)
    counts['C100']++
    controlSumIls += ilsTotal

    // Payment lines (D120)
    const paymentLines = await db.query.receiptPaymentLines.findMany({
      where: eq(receiptPaymentLines.receiptId, rcpt.id),
    })

    for (let i = 0; i < paymentLines.length; i++) {
      const pl = paymentLines[i]!
      emitLine(buildD120({
        docNumber,
        linePosition: i + 1,
        method: pl.method as D120Payment['method'],
        amountAgorot: agorot(parseFloat(pl.amount)),
        chequeNumber: pl.chequeNumber,
        chequeBank: pl.chequeBank,
        chequeBranch: pl.chequeBranch,
        chequeAccount: pl.chequeAccount,
        chequeDueDate: pl.chequeDueDate ? dateField(pl.chequeDueDate) : null,
        cardLastFour: pl.cardLastFour,
        cardBrand: pl.cardBrand,
        reference: pl.reference,
      }), bkmvChunks, substitutionLog)
      counts['D120']++
    }
  }

  // ── 6. B100 journal movements (accountant-export ledger, wave-15) ───────────
  // Only emitted when the tenant has configured a chart of accounts (coa_mappings).
  // When env is not provided, behave as before: documents-only mode (counts = 0).
  if (env) {
    const hasCoa = await hasAccountantLedger(env, tenantId)
    if (hasCoa) {
      const movements = await deriveMovements(env, tenantId, from, to)
      let movLineNo = 1
      for (const m of movements) {
        // B100 — journal movement header (debit/credit accounts + amount)
        const b100Line = [
          'B100',
          dateField(m.date),
          m.debitAccount.padEnd(8, ' ').slice(0, 8),
          m.creditAccount.padEnd(8, ' ').slice(0, 8),
          String(agorot(m.amountIls)).padStart(14, '0'),
          m.reference.padEnd(30, ' ').slice(0, 30),
          m.description.padEnd(40, ' ').slice(0, 40),
          String(movLineNo).padStart(9, '0'),
        ].join('')
        emitLine(b100Line, bkmvChunks, substitutionLog)
        counts['B100']++
        movLineNo++
      }
    }
  }

  // ── 7. Z900 closing record ─────────────────────────────────────────────────
  emitLine(buildZ900(counts, controlSumIls), bkmvChunks, substitutionLog)

  // ── 8. INI.txt ────────────────────────────────────────────────────────────
  const iniChunks: Uint8Array[] = []
  const iniLines = buildINI(header, counts)
  for (const line of iniLines) {
    emitLine(line, iniChunks, substitutionLog)
  }

  return {
    bkmvdataBytes: concatBytes(bkmvChunks),
    iniBytes: concatBytes(iniChunks),
    counts,
    substitutionLog,
  }
}

export type { D120Payment }
