/**
 * src/integrations/zatca/invoice.ts
 * ZATCA invoice orchestrator — Phase 1.
 *
 * What it does:
 *   1. Validates input (Saudi VAT format, items, currency)
 *   2. Loads tenant seller snapshot (name, VAT #, CR)
 *   3. Computes subtotal / VAT / total at standard 15% (overridable per item)
 *   4. Renders UBL 2.1 invoice XML
 *   5. Builds Phase-1 TLV QR payload + PNG (returned as base64 data URL)
 *   6. Persists in zatca_invoices with status='pending_approval'
 *   7. Creates an approval_request (24h expiry — merchant may be in a meeting)
 *   8. Returns invoice ID + QR + approval ID for the agent to relay
 *
 * What it does NOT do (yet):
 *   - Cryptographic signing / digital stamp (Phase 2)
 *   - Calling ZATCA Fatoora compliance/clearance API (Phase 2)
 *   - Generating a PDF (intentional — WhatsApp prefers image messages, and
 *     the QR PNG is enough; PDF is a follow-up if a customer demands it).
 */
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import QRCode from 'qrcode';
import { createChildLogger } from '../../utils/logger.js';
import { supabase } from '../../utils/supabase.js';
import { requestApproval } from '../../approval/manager.js';
import { deliverMessage } from '../../delivery/delivery.js';
import { buildPhase1QrPayload } from './tlv.js';
import { renderUblInvoice } from './ubl.js';
const log = createChildLogger('zatca:invoice');
// ─── Validation ──────────────────────────────────────────────────────
const SAUDI_VAT_RE = /^3\d{14}$/;
const itemSchema = z.object({
    description: z.string().min(1).max(500),
    quantity: z.number().positive(),
    unitPrice: z.number().nonnegative(),
    vatRate: z.number().min(0).max(100).default(15),
});
const issueInputSchema = z.object({
    customerId: z.string().min(1).max(256),
    buyerName: z.string().min(1).max(200),
    buyerVatNumber: z.string().regex(SAUDI_VAT_RE, 'Buyer VAT must be 15 digits starting with 3').optional(),
    invoiceType: z.enum(['simplified', 'standard']).default('simplified'),
    currency: z.literal('SAR').default('SAR'),
    items: z.array(itemSchema).min(1, 'At least one item is required'),
});
// ─── Helpers ─────────────────────────────────────────────────────────
function genInvoiceNumber(uuid) {
    const now = new Date();
    const date = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
    return `INV-${date}-${uuid.slice(0, 8).toUpperCase()}`;
}
async function loadSellerSnapshot(tenantId) {
    const { data: tenant, error } = await supabase
        .from('tenants')
        .select('business_name, business_name_ar, metadata')
        .eq('id', tenantId)
        .maybeSingle();
    if (error || !tenant) {
        throw new Error(`Cannot issue invoice: tenant ${tenantId} not found. ` +
            `Onboard the business first.`);
    }
    // VAT/CR live in the tenants.metadata JSONB until we add dedicated columns.
    const meta = tenant.metadata || {};
    let vat = typeof meta.vat_number === 'string' ? meta.vat_number : '';
    const cr = typeof meta.cr_number === 'string' ? meta.cr_number : undefined;
    // Device-mode / dev fallback: missing VAT becomes a clearly-labeled test
    // number (NOT real, NOT for ZATCA submission). Signal to the executor
    // (via process.env) so the rendered HTML can show the "test" banner.
    if (!vat) {
        if (process.env.MUASSIS_MODE !== 'control-plane') {
            vat = '300000000000003';
            process.env.MUASSIS_LAST_INVOICE_TEST_VAT = '1';
            log.warn({ tenantId }, 'Tenant has no VAT — using TEST VAT 300000000000003 for device-mode demo. ' +
                'Set tenants.metadata.vat_number for real invoices.');
        }
        else {
            throw new Error('Cannot issue invoice: tenant has no VAT registration number. ' +
                'Set tenants.metadata.vat_number first (15 digits, starts with 3).');
        }
    }
    else {
        process.env.MUASSIS_LAST_INVOICE_TEST_VAT = '0';
    }
    if (!SAUDI_VAT_RE.test(vat)) {
        throw new Error(`Tenant VAT number is malformed: "${vat}". Expected 15 digits starting with 3.`);
    }
    const name = (tenant.business_name_ar || tenant.business_name || '').trim();
    if (!name) {
        throw new Error('Cannot issue invoice: tenant has no business name.');
    }
    // Surface the seller name to the tool executor so the rendered HTML
    // file can show it without a second DB round-trip.
    process.env.MUASSIS_LAST_INVOICE_SELLER = name;
    return { name, vatNumber: vat, crNumber: cr };
}
// ─── Orchestrator ────────────────────────────────────────────────────
export async function issueZatcaInvoice(tenantId, rawInput) {
    // 1. Validate
    const parsed = issueInputSchema.safeParse(rawInput);
    if (!parsed.success) {
        throw new Error(`Invalid invoice input: ${parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
    }
    const input = parsed.data;
    // 2. Seller snapshot
    const seller = await loadSellerSnapshot(tenantId);
    // 3. UUID + invoice number
    const invoiceId = uuidv4();
    const invoiceNumber = genInvoiceNumber(invoiceId);
    const issuedAt = new Date().toISOString();
    // 4. Map items into UBL shape
    const items = input.items.map(it => ({
        description: it.description,
        quantity: it.quantity,
        unitPrice: it.unitPrice,
        vatRate: it.vatRate,
    }));
    // 5. UBL XML + totals
    const { xml, totals } = renderUblInvoice({
        invoiceNumber,
        invoiceUuid: invoiceId,
        issuedAt,
        invoiceType: input.invoiceType,
        seller: {
            name: seller.name,
            vatNumber: seller.vatNumber,
            crNumber: seller.crNumber,
        },
        buyer: {
            name: input.buyerName,
            vatNumber: input.buyerVatNumber,
        },
        items,
        currency: input.currency,
    });
    // 6. Phase-1 TLV QR + PNG data URL
    const qrPayloadBase64 = buildPhase1QrPayload({
        sellerName: seller.name,
        sellerVatNumber: seller.vatNumber,
        invoiceTimestamp: issuedAt,
        totalWithVat: totals.total.toFixed(2),
        vatTotal: totals.vatTotal.toFixed(2),
    });
    const qrDataUrl = await QRCode.toDataURL(qrPayloadBase64, {
        errorCorrectionLevel: 'M',
        margin: 2,
        width: 320,
    });
    // 7. Persist (status starts pending_approval)
    const { error: insertError } = await supabase.from('zatca_invoices').insert({
        id: invoiceId,
        tenant_id: tenantId,
        customer_id: input.customerId,
        invoice_number: invoiceNumber,
        invoice_type: input.invoiceType,
        seller_name: seller.name,
        seller_vat_number: seller.vatNumber,
        seller_cr_number: seller.crNumber || null,
        buyer_name: input.buyerName,
        buyer_vat_number: input.buyerVatNumber || null,
        items: items,
        subtotal: totals.subtotal,
        vat_total: totals.vatTotal,
        total: totals.total,
        currency: input.currency,
        ubl_xml: xml,
        qr_base64: qrPayloadBase64,
        status: 'pending_approval',
        issued_at: issuedAt,
    });
    if (insertError) {
        log.error({ tenantId, invoiceId, error: insertError.message }, 'Failed to persist invoice');
        throw new Error(`Failed to save invoice: ${insertError.message}`);
    }
    // 8. Approval gate — 24h expiry so merchants in meetings don't lose the request
    const approval = await requestApproval(tenantId, null, 'zatca_invoice', {
        invoiceId,
        invoiceNumber,
        buyer: { name: input.buyerName, vatNumber: input.buyerVatNumber },
        total: totals.total,
        vatTotal: totals.vatTotal,
        itemCount: items.length,
    }, `إصدار فاتورة ZATCA #${invoiceNumber} للعميل ${input.buyerName} بإجمالي ${totals.total.toFixed(2)} ${input.currency}`, totals.total >= 50_000 ? 'high' : 'medium', 60 * 24);
    // 9. Link approval request to invoice
    await supabase
        .from('zatca_invoices')
        .update({ approval_request_id: approval.id, updated_at: new Date().toISOString() })
        .eq('id', invoiceId);
    log.info({ tenantId, invoiceId, invoiceNumber, total: totals.total, approvalId: approval.id }, 'ZATCA invoice queued for approval');
    return {
        invoiceId,
        invoiceNumber,
        approvalRequestId: approval.id,
        qrDataUrl,
        qrPayloadBase64,
        totals,
        status: 'pending_approval',
        expiresAt: approval.expires_at,
    };
}
// ─── Post-approval delivery ──────────────────────────────────────────
/**
 * Decode the TLV base64 QR payload back to a PNG, upload it to the tenant's
 * Supabase Storage bucket as a private object, and return a short-lived
 * signed URL Meta can fetch when sending the WhatsApp image.
 */
async function publishQrPng(tenantId, invoiceId, qrPayloadBase64) {
    try {
        const QRCode = (await import('qrcode')).default;
        const buffer = await QRCode.toBuffer(qrPayloadBase64, {
            errorCorrectionLevel: 'M',
            width: 480,
            margin: 2,
        });
        const bucket = `tenant-${tenantId}`;
        const storagePath = `invoices/${invoiceId}-qr.png`;
        const { error: upErr } = await supabase.storage
            .from(bucket)
            .upload(storagePath, buffer, { upsert: true, contentType: 'image/png' });
        if (upErr) {
            // Bucket may not exist; create on demand.
            if (/not found|does not exist|bucket/i.test(upErr.message)) {
                await supabase.storage.createBucket(bucket, { public: false });
                const retry = await supabase.storage
                    .from(bucket)
                    .upload(storagePath, buffer, { upsert: true, contentType: 'image/png' });
                if (retry.error)
                    throw new Error(retry.error.message);
            }
            else {
                throw new Error(upErr.message);
            }
        }
        // 10 minutes is plenty for Meta to fetch + a generous re-try window.
        const { data: signed, error: signErr } = await supabase.storage
            .from(bucket)
            .createSignedUrl(storagePath, 600);
        if (signErr || !signed?.signedUrl) {
            throw new Error(signErr?.message || 'No signed URL returned');
        }
        return { signedUrl: signed.signedUrl, storagePath };
    }
    catch (err) {
        log.warn({ tenantId, invoiceId, error: err instanceof Error ? err.message : String(err) }, 'QR PNG publish failed — will fall back to text-only delivery');
        return null;
    }
}
/**
 * Called from approval/manager.ts when a merchant approves a queued ZATCA
 * invoice. Decodes the stored TLV QR back to PNG, uploads it to the tenant's
 * Supabase Storage with a 10-min signed URL, and sends the customer:
 *   1. The QR PNG with a caption summarizing the invoice
 *   2. A separate text fallback if image delivery fails
 * Marks status='issued' on success.
 */
export async function deliverApprovedZatcaInvoice(tenantId, approvalRequestId) {
    const { data: invoice, error } = await supabase
        .from('zatca_invoices')
        .select('id, invoice_number, customer_id, total, vat_total, currency, buyer_name, qr_base64, status')
        .eq('approval_request_id', approvalRequestId)
        .eq('tenant_id', tenantId)
        .maybeSingle();
    if (error || !invoice) {
        log.warn({ approvalRequestId, tenantId, err: error?.message }, 'No invoice linked to approval — skipping delivery');
        return;
    }
    if (invoice.status === 'issued' || invoice.status === 'cleared') {
        log.info({ invoiceId: invoice.id }, 'Invoice already delivered — skipping');
        return;
    }
    const cur = invoice.currency || 'SAR';
    const caption = `*فاتورة ${invoice.invoice_number}*\n` +
        `${invoice.buyer_name}\n\n` +
        `الإجمالي: *${Number(invoice.total).toFixed(2)} ${cur}*\n` +
        `الضريبة (15٪): ${Number(invoice.vat_total).toFixed(2)} ${cur}\n\n` +
        `🌹 شكراً لتعاملك معنا.`;
    // Resolve customer_id → phone. Phone-shaped already? Use directly. Else lookup.
    let to = invoice.customer_id;
    if (!/^\+?\d{6,}$/.test(to)) {
        const { data: customer } = await supabase
            .from('customers')
            .select('phone')
            .eq('tenant_id', tenantId)
            .eq('id', to)
            .maybeSingle();
        if (customer?.phone)
            to = customer.phone;
    }
    // Try image delivery first (QR PNG via signed URL); fall back to text.
    let imageDelivered = false;
    const qrAsset = await publishQrPng(tenantId, invoice.id, invoice.qr_base64);
    if (qrAsset) {
        try {
            const { whatsappClient } = await import('../../channels/whatsapp/client.js');
            await whatsappClient.sendImage(to, qrAsset.signedUrl, caption);
            imageDelivered = true;
            log.info({ invoiceId: invoice.id, to, storagePath: qrAsset.storagePath }, 'ZATCA invoice QR delivered as image');
        }
        catch (imgErr) {
            log.warn({ invoiceId: invoice.id, error: imgErr instanceof Error ? imgErr.message : String(imgErr) }, 'Image delivery failed — falling back to text');
        }
    }
    if (!imageDelivered) {
        try {
            await deliverMessage({ tenantId, to, message: caption, channel: 'whatsapp' });
            log.info({ invoiceId: invoice.id, to }, 'ZATCA invoice delivered as text (image fallback)');
        }
        catch (err) {
            log.error({ invoiceId: invoice.id, error: err instanceof Error ? err.message : String(err) }, 'Both image and text delivery failed — invoice stays pending_approval');
            throw err;
        }
    }
    await supabase
        .from('zatca_invoices')
        .update({
        status: 'issued',
        approved_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
    })
        .eq('id', invoice.id);
}
//# sourceMappingURL=invoice.js.map