/*
|--------------------------------------------------------------------------
| Agência PSI — agendaBilling service (Fase B1)
|--------------------------------------------------------------------------
| Helpers e loaders relacionados a billing da agenda, extraídos de
| useMelissaAgenda.js pra serem reusados em Rail/Clínica.
|
| Esta sessão (Fase B1) cobre só read-only + helpers puros:
|   - computeSeriePrice          (puro)
|   - generateOccurrenceDates    (puro)
|   - loadStatusChangeContext    (read-only DB)
|   - needsStatusConfirmDialog   (puro)
|
| Fase B2 (mutations) extrairá: applyStatusDecisions, createPackageContract,
| materializeAndChargePerSession.
|
| Convenção: funções recebem `supabase` explícito (não usa import direto)
| pra facilitar teste + reuso fora do contexto Vue. Nenhuma função aqui
| dispara toast — caller decide.
|--------------------------------------------------------------------------
*/

import { dateToISO } from '@/features/agenda/utils/timeHelpers';
import { tenantDb } from '@/lib/supabase/tenantClient';

// ── Helpers puros ─────────────────────────────────────────────────────────

/**
 * Calcula o valor total da série a partir dos commitmentItems.
 *
 * @param {object} recorrencia { qtdSessoes, commitmentItems, serieValorMode }
 * @returns {{ n, perSessao, packagePrice }}
 */
export function computeSeriePrice(recorrencia) {
    const items = recorrencia?.commitmentItems || [];
    const n = recorrencia?.qtdSessoes || 1;
    const totalPorSessao = items.reduce((s, i) => s + (i.final_price ?? 0), 0);
    const pacoteFechado = recorrencia?.serieValorMode === 'dividir';
    return {
        n,
        perSessao: pacoteFechado ? totalPorSessao / n : totalPorSessao,
        packagePrice: pacoteFechado ? totalPorSessao : totalPorSessao * n
    };
}

/**
 * Gera lista de datas ISO ('YYYY-MM-DD') a partir de uma rule de recorrência.
 * Pula datas em exceptionDates (Set). Para até `max` datas. Suporta weekly
 * (interval=1 ou 2 pra quinzenal) e custom_weekdays.
 *
 * @param {object} rule { start_date, interval, weekdays, type }
 * @param {number} max
 * @param {Set<string>} exceptionDates
 * @returns {string[]}
 */
export function generateOccurrenceDates(rule, max, exceptionDates = new Set()) {
    const dates = [];
    const start = new Date(`${rule.start_date}T00:00:00`);
    const interval = Math.max(1, rule.interval || 1);
    const weekdays = Array.isArray(rule.weekdays) && rule.weekdays.length
        ? rule.weekdays.map(Number)
        : [start.getDay()];
    const isCustom = rule.type === 'custom_weekdays';

    const cursor = new Date(start);
    let safety = 0;
    while (dates.length < max && safety < 365 * 3) {
        const iso = dateToISO(cursor);
        const dow = cursor.getDay();
        const inWeekdays = weekdays.includes(dow);
        if (inWeekdays && !exceptionDates.has(iso)) {
            dates.push(iso);
        }
        if (isCustom) {
            cursor.setDate(cursor.getDate() + 1);
        } else if (inWeekdays) {
            cursor.setDate(cursor.getDate() + 7 * interval);
        } else {
            cursor.setDate(cursor.getDate() + 1);
        }
        safety++;
    }
    return dates;
}

/**
 * Decide se o dialog de confirmação de status change deve ser exibido.
 *
 * Pure: depende só do ctx montado por loadStatusChangeContext.
 *
 * Regras:
 *   - faltou/cancelado: mostra se há regra de exceção com charge_mode != 'none'
 *                       OU pacote saldo/upfront
 *   - realizado:        mostra se há pending record OU pacote saldo
 *   - agendado:         (reverse) mostra se há artefatos a desfazer
 */
export function needsStatusConfirmDialog(status, ctx) {
    const isFaltouOrCancel = status === 'faltou' || status === 'cancelado';
    const isRealizado = status === 'realizado';
    const isAgendado = status === 'agendado';
    const hasRegraComCobranca = ctx?.regraExcecao && ctx.regraExcecao.charge_mode !== 'none';
    const isPacoteSaldo = ctx?.billingContract?.charging_style === 'saldo';
    const isPacoteUpfront = ctx?.billingContract?.charging_style === 'upfront';
    const hasPending = !!ctx?.pendingRecord;

    if (isFaltouOrCancel) {
        return hasRegraComCobranca || isPacoteSaldo || isPacoteUpfront;
    }
    if (isRealizado) {
        return hasPending || isPacoteSaldo;
    }
    if (isAgendado) {
        const r = ctx?.reverseArtifacts;
        if (!r) return false;
        const hasActiveRecords = (r.activeRecords?.length || 0) > 0;
        return hasActiveRecords || r.saldoConsumed;
    }
    return false;
}

// ── Loaders (read-only DB) ────────────────────────────────────────────────

/**
 * Carrega contexto pra decisão de status change.
 *
 * Read-only. Não dispara toast (caller decide). Tolerante a erros parciais
 * (loga warn e segue com null).
 *
 * @param {object} opts
 * @param {object} opts.supabase   instância do client
 * @param {object} opts.row        row do agenda_eventos (pode ser parcial — usa fallbacks)
 * @param {string} opts.eventoId   uuid (null pra ocorrências virtuais não materializadas)
 * @param {string} opts.status     'realizado' | 'faltou' | 'cancelado' | 'agendado'
 * @param {string} opts.ownerId    auth.uid() (resolvido pelo caller)
 * @param {string} opts.tenantId   activeTenantId
 *
 * @returns {Promise<{
 *   regraExcecao,
 *   billingContract,
 *   pendingRecord,
 *   existingPaidRecord,
 *   reverseArtifacts: { previousStatus, activeRecords, saldoConsumed } | null
 * }>}
 */
export async function loadStatusChangeContext({ supabase, row, eventoId, status, ownerId, tenantId }) {
    const ctx = {
        regraExcecao: null,
        billingContract: null,
        pendingRecord: null,
        existingPaidRecord: null,
        reverseArtifacts: null
    };

    // 1) Regra de exceção (faltou → patient_no_show, cancelado → patient_cancellation)
    const exceptionTypeMap = { faltou: 'patient_no_show', cancelado: 'patient_cancellation' };
    const excType = exceptionTypeMap[status];
    if (excType && tenantId) {
        try {
            const { data } = await tenantDb().from('financial_exceptions')
                .select('*')
                
                .eq('exception_type', excType)
                .or(`owner_id.eq.${ownerId},owner_id.is.null`)
                .order('owner_id', { ascending: false, nullsLast: true })
                .limit(1)
                .maybeSingle();
            ctx.regraExcecao = data ?? null;
        } catch (e) {
            console.warn('[agendaBilling] regra de exceção:', e?.message);
        }
    }

    // 2) Billing contract — 3 caminhos: row.billing_contract_id direto → query
    //    agenda_eventos.billing_contract_id (recém-materializada) → contrato
    //    ativo do paciente (virtuais).
    const patientId = row?.patient_id ?? row?.paciente_id ?? null;
    const contractId = row?.billing_contract_id ?? null;
    if (contractId) {
        try {
            const { data } = await tenantDb().from('billing_contracts')
                .select('*')
                .eq('id', contractId)
                .maybeSingle();
            ctx.billingContract = data ?? null;
        } catch (e) {
            console.warn('[agendaBilling] contract via id direto:', e?.message);
        }
    }
    if (!ctx.billingContract && eventoId) {
        try {
            const { data: ev } = await tenantDb().from('agenda_eventos')
                .select('billing_contract_id')
                .eq('id', eventoId)
                .maybeSingle();
            if (ev?.billing_contract_id) {
                const { data: c } = await tenantDb().from('billing_contracts')
                    .select('*')
                    .eq('id', ev.billing_contract_id)
                    .maybeSingle();
                ctx.billingContract = c ?? null;
            }
        } catch (e) {
            console.warn('[agendaBilling] contract via agenda_evento:', e?.message);
        }
    }
    if (!ctx.billingContract && patientId && tenantId) {
        try {
            const { data: c } = await tenantDb().from('billing_contracts')
                .select('*')
                
                .eq('patient_id', patientId)
                .eq('status', 'active')
                .eq('type', 'package')
                .order('created_at', { ascending: false })
                .limit(1)
                .maybeSingle();
            ctx.billingContract = c ?? null;
        } catch (e) {
            console.warn('[agendaBilling] contract via patient_id:', e?.message);
        }
    }

    // 3) Pending record
    if (eventoId) {
        try {
            const { data } = await tenantDb().from('financial_records')
                .select('*')
                .eq('agenda_evento_id', eventoId)
                .in('status', ['pending', 'overdue'])
                .order('created_at', { ascending: false })
                .limit(1)
                .maybeSingle();
            ctx.pendingRecord = data ?? null;
        } catch (e) {
            console.warn('[agendaBilling] pending record:', e?.message);
        }
    }

    // 3b) Paid record pré-existente (caso C12: antecipar pagamento).
    if (eventoId) {
        try {
            const { data } = await tenantDb().from('financial_records')
                .select('id, status, amount, final_amount, paid_at, payment_method')
                .eq('agenda_evento_id', eventoId)
                .eq('status', 'paid')
                .order('paid_at', { ascending: false })
                .limit(1)
                .maybeSingle();
            ctx.existingPaidRecord = data ?? null;
        } catch (e) {
            console.warn('[agendaBilling] existing paid record:', e?.message);
        }
    }

    // 4) Reverse transition (status novo='agendado'): artefatos a desfazer.
    if (status === 'agendado' && eventoId) {
        ctx.reverseArtifacts = {
            previousStatus: row?.status || null,
            activeRecords: [],
            saldoConsumed: false
        };
        try {
            const { data: evRow } = await tenantDb().from('agenda_eventos')
                .select('status, billing_contract_id')
                .eq('id', eventoId)
                .maybeSingle();
            if (evRow) {
                ctx.reverseArtifacts.previousStatus = evRow.status;
            }
            const { data: recs } = await tenantDb().from('financial_records')
                .select('id, status, amount, final_amount, description, paid_at, payment_method')
                .eq('agenda_evento_id', eventoId)
                .neq('status', 'cancelled')
                .order('created_at', { ascending: false });
            ctx.reverseArtifacts.activeRecords = recs || [];
            // Heurística saldo consumido: billing_contract_id + previousStatus
            // ≠ 'agendado' + style=saldo. Falso positivo é mitigado pela escolha
            // do user no dialog de "devolver saldo".
            const wasInActiveState = evRow?.billing_contract_id && evRow.status && evRow.status !== 'agendado';
            ctx.reverseArtifacts.saldoConsumed = wasInActiveState && ctx.billingContract?.charging_style === 'saldo';
        } catch (e) {
            console.warn('[agendaBilling] reverse artifacts:', e?.message);
        }
    }

    return ctx;
}

// ── Mutations (Fase B2 — side effects DB) ─────────────────────────────────

const _BRL = (v) => Number(v ?? 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });

/**
 * Aplica as decisões tomadas no dialog de status change (reverse / consume
 * saldo / multa / mark paid / cobrança pacote).
 *
 * Recebe deps explícitas (supabase, toast, ownerId, tenantId) em vez de
 * capturar via closure. Toast pode ser null — quando chamado fora de UI
 * (ex: background job), erros viram exceções no caller.
 *
 * Mantém a lógica idêntica à versão inline original em useMelissaAgenda.
 *
 * @param {object} opts
 * @param {object} opts.supabase
 * @param {object} [opts.toast]    — `{ add: fn }`. Opcional.
 * @param {string} opts.eventoId
 * @param {object} opts.row
 * @param {string} opts.novoStatus
 * @param {object} opts.ctx        — saída de loadStatusChangeContext
 * @param {object} opts.decision
 * @param {string} opts.ownerId
 * @param {string} opts.tenantId
 */
export async function applyStatusDecisions({ supabase, toast, eventoId, row, novoStatus, ctx, decision, ownerId, tenantId }) {
    const uid = ownerId;
    const patientId = row?.patient_id ?? row?.paciente_id ?? null;
    const tasks = [];
    const tx = (entry) => { if (toast?.add) toast.add(entry); };

    // ─── REVERSE TRANSITION (novoStatus='agendado' com artefatos a desfazer) ──
    if (novoStatus === 'agendado' && ctx.reverseArtifacts) {
        const r = ctx.reverseArtifacts;
        // 1) Cancelar records pending/overdue
        if (decision.reverseCancelPending && (r.activeRecords?.length || 0) > 0) {
            const pendingIds = r.activeRecords.filter((rec) => rec.status === 'pending' || rec.status === 'overdue').map((rec) => rec.id);
            if (pendingIds.length > 0) {
                try {
                    const today = new Date().toISOString().slice(0, 10);
                    const reason = `Cancelada via reversão de status (${r.previousStatus} → agendado) em ${today}`;
                    for (const id of pendingIds) {
                        const { error: cErr } = await tenantDb().from('financial_records')
                            .update({
                                status: 'cancelled',
                                notes: `[${today}] ${reason}`,
                                updated_at: new Date().toISOString()
                            })
                            .eq('id', id);
                        if (cErr) throw cErr;
                    }
                } catch (e) {
                    console.error('[agendaBilling/reverse] erro cancelando records:', e?.message);
                    tx({ severity: 'warn', summary: 'Cobrança', detail: e?.message || 'Falha ao cancelar cobranças', life: 5000 });
                }
            }
        }

        // 2) Devolver saldo
        if (decision.reverseRestoreSaldo && r.saldoConsumed && ctx.billingContract?.id) {
            try {
                const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
                    .select('sessions_used, total_sessions, status')
                    .eq('id', ctx.billingContract.id)
                    .maybeSingle();
                if (fetchErr) throw fetchErr;
                const currentUsed = freshContract?.sessions_used ?? 0;
                const totalSessions = freshContract?.total_sessions ?? 0;
                const newUsed = Math.max(0, currentUsed - 1);
                const patch = { sessions_used: newUsed };
                if (currentUsed >= totalSessions) {
                    patch.status = 'active';
                }
                const { error: dErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
                if (dErr) throw dErr;
            } catch (e) {
                console.error('[agendaBilling/reverse] erro decrementando saldo:', e?.message);
                tx({ severity: 'warn', summary: 'Saldo do pacote', detail: e?.message || 'Falha ao devolver saldo', life: 5000 });
            }
        }

        // 3) Desamarrar billing_contract_id (só se devolveu saldo)
        if (decision.reverseRestoreSaldo && r.saldoConsumed) {
            try {
                await tenantDb().from('agenda_eventos').update({ billing_contract_id: null, updated_at: new Date().toISOString() }).eq('id', eventoId);
            } catch (e) {
                console.warn('[agendaBilling/reverse] erro desamarrando billing_contract_id:', e?.message);
            }
        }

        tx({ severity: 'success', summary: 'Sessão reativada', detail: 'Status revertido pra agendada.', life: 2500 });
        return;
    }

    // 1) Consumir saldo
    if (decision.consumeSaldo && ctx.billingContract?.id) {
        tasks.push(
            tenantDb().from('billing_contracts')
                .update({ sessions_used: (ctx.billingContract.sessions_used ?? 0) + 1 })
                .eq('id', ctx.billingContract.id)
        );
    }

    // 1b) Amarra evento ao contrato (universal pra forward em pacote)
    const isForwardStatus = novoStatus === 'realizado' || novoStatus === 'faltou' || novoStatus === 'cancelado';
    if (isForwardStatus && ctx.billingContract?.id && eventoId) {
        tasks.push(
            tenantDb().from('agenda_eventos')
                .update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() })
                .eq('id', eventoId)
        );
    }

    // 2) Aplicar multa
    if (decision.applyFine && decision.fineAmount > 0) {
        const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
        const sessaoLabel = row.inicio_em ? new Date(row.inicio_em).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' }) : '';
        const fineDesc = novoStatus === 'faltou' ? `Multa por falta · sessão ${sessaoLabel}` : `Taxa de cancelamento tardio · sessão ${sessaoLabel}`;
        const finePayload = {
            owner_id: uid,
            patient_id: patientId,
            agenda_evento_id: eventoId,
            amount: decision.fineAmount,
            final_amount: decision.fineAmount,
            description: fineDesc.trim(),
            status: 'pending',
            due_date: dueIso,
            type: 'receita'
        };
        tasks.push(
            tenantDb().from('financial_records')
                .insert(finePayload)
                .then(({ error }) => {
                    if (error) {
                        console.warn('[agendaBilling] INSERT multa falhou:', error?.message, 'payload:', finePayload);
                        throw error;
                    }
                })
        );
    }

    // 2b) Cancelar cobrança original (faltou/cancelado + pendingRecord)
    const isFaltouOuCancelado = novoStatus === 'faltou' || novoStatus === 'cancelado';
    if (isFaltouOuCancelado && ctx.pendingRecord?.id) {
        const reasonText = decision.applyFine
            ? novoStatus === 'faltou'
                ? 'Cancelada — substituída por multa de no-show'
                : 'Cancelada — substituída por taxa de cancelamento tardio'
            : novoStatus === 'faltou'
              ? 'Cancelada — sessão não realizada (paciente faltou)'
              : 'Cancelada — sessão cancelada';
        const today = new Date().toISOString().slice(0, 10);
        const noteEntry = `[${today}] ${reasonText}`;
        const noteText = ctx.pendingRecord.notes ? `${ctx.pendingRecord.notes}\n${noteEntry}` : noteEntry;
        tasks.push(
            tenantDb().from('financial_records')
                .update({
                    status: 'cancelled',
                    notes: noteText,
                    updated_at: new Date().toISOString()
                })
                .eq('id', ctx.pendingRecord.id)
        );
    }

    // 3) Realizado avulsa pendente: marcar pendingRecord como pago (ou só status)
    if (decision.markPaid && ctx.pendingRecord?.id) {
        tasks.push(
            tenantDb().from('financial_records')
                .update({
                    status: 'paid',
                    paid_at: new Date().toISOString(),
                    payment_method: decision.paymentMethod || 'pix',
                    updated_at: new Date().toISOString()
                })
                .eq('id', ctx.pendingRecord.id)
        );
    }

    // 4-pre) Realizado em pacote saldo + paid pré-existente (C12)
    const hasAnticipatedPayment = novoStatus === 'realizado' && ctx.billingContract?.charging_style === 'saldo' && ctx.existingPaidRecord?.id;
    if (hasAnticipatedPayment) {
        if (tasks.length > 0) {
            const results = await Promise.allSettled(tasks);
            const failed = results.filter((r) => r.status === 'rejected');
            if (failed.length > 0) {
                console.warn('[agendaBilling/realizada-paid] tasks com falha:', failed.map((f) => f.reason?.message));
            }
        }
        try {
            const { data: freshContract, error: fetchErr } = await tenantDb().from('billing_contracts')
                .select('sessions_used, total_sessions, status')
                .eq('id', ctx.billingContract.id)
                .maybeSingle();
            if (fetchErr) throw fetchErr;
            const currentUsed = freshContract?.sessions_used ?? 0;
            const newUsed = currentUsed + 1;
            const patch = { sessions_used: newUsed };
            if (newUsed >= (freshContract?.total_sessions ?? 0)) {
                patch.status = 'completed';
            }
            const { error: incErr } = await tenantDb().from('billing_contracts').update(patch).eq('id', ctx.billingContract.id);
            if (incErr) throw incErr;
            tx({ severity: 'success', summary: 'Sessão consumida', detail: `Saldo: ${newUsed}/${freshContract?.total_sessions ?? '?'}. Pagamento já estava registrado.`, life: 4000 });
        } catch (e) {
            console.error('[agendaBilling/realizada-paid] erro consumindo saldo:', e?.message);
            tx({ severity: 'warn', summary: 'Saldo do pacote', detail: e?.message || 'Falha ao consumir saldo', life: 5000 });
        }
        return;
    }

    // 4) Realizado em pacote saldo: amarra + cria cobrança + incrementa saldo
    if (decision.generatePackageCharge && ctx.billingContract?.id) {
        const amount = Number(row.price ?? (ctx.billingContract.total_sessions > 0 ? (Number(ctx.billingContract.package_price) || 0) / ctx.billingContract.total_sessions : 0));
        const dueIso = row.inicio_em ? new Date(row.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);

        try {
            const { error: linkErr } = await tenantDb().from('agenda_eventos').update({ billing_contract_id: ctx.billingContract.id, updated_at: new Date().toISOString() }).eq('id', eventoId);
            if (linkErr) throw linkErr;
        } catch (e) {
            console.error('[agendaBilling] erro amarrando billing_contract_id:', e?.message);
            tx({ severity: 'warn', summary: 'Pacote — amarração', detail: e?.message || 'Falha ao amarrar sessão ao pacote', life: 5000 });
        }

        try {
            const { error: rpcErr } = await supabase.rpc('create_financial_record_for_session', {
                p_tenant_id: tenantId,
                p_owner_id: uid,
                p_patient_id: patientId,
                p_agenda_evento_id: eventoId,
                p_amount: amount,
                p_due_date: dueIso
            });
            if (rpcErr) throw rpcErr;
        } catch (e) {
            console.error('[agendaBilling] erro RPC create_financial_record_for_session:', e?.message);
            tx({ severity: 'error', summary: 'Cobrança', detail: e?.message || 'Falha ao gerar cobrança do pacote', life: 7000 });
        }

        try {
            const newUsed = (ctx.billingContract.sessions_used ?? 0) + 1;
            const patchContract = { sessions_used: newUsed };
            if (newUsed >= (ctx.billingContract.total_sessions ?? 0)) {
                patchContract.status = 'completed';
            }
            const { error: incErr } = await tenantDb().from('billing_contracts').update(patchContract).eq('id', ctx.billingContract.id);
            if (incErr) throw incErr;
        } catch (e) {
            console.error('[agendaBilling] erro incrementando sessions_used:', e?.message);
            tx({ severity: 'warn', summary: 'Saldo do pacote', detail: e?.message || 'Falha ao atualizar saldo do pacote', life: 6000 });
        }
    }

    // Roda tudo em paralelo
    const results = await Promise.allSettled(tasks);
    const failed = results.filter((r) => r.status === 'rejected');
    if (failed.length > 0) {
        const firstErr = failed[0].reason?.message || 'sem detalhe';
        tx({ severity: 'error', summary: 'Erro ao aplicar decisões', detail: `${failed.length} ação(ões) falharam: ${firstErr}`, life: 7000 });
        console.error('[agendaBilling] falhas em applyStatusDecisions:', failed.map((f) => f.reason));
    } else if (tasks.length > 0) {
        tx({ severity: 'success', summary: 'Status atualizado', detail: 'Decisões aplicadas.', life: 2500 });
    }

    // Pós-processamento do record gerado pelo pacote saldo
    if (decision.generatePackageCharge && eventoId) {
        try {
            const { data: newRec } = await tenantDb().from('financial_records')
                .select('id')
                .eq('agenda_evento_id', eventoId)
                .order('created_at', { ascending: false })
                .limit(1)
                .single();
            if (newRec?.id) {
                if (decision.markPaid) {
                    await tenantDb().from('financial_records')
                        .update({
                            status: 'paid',
                            paid_at: new Date().toISOString(),
                            payment_method: decision.paymentMethod,
                            updated_at: new Date().toISOString()
                        })
                        .eq('id', newRec.id);
                } else if (decision.paymentMethod === 'link') {
                    await tenantDb().from('financial_records')
                        .update({ payment_method: 'asaas', updated_at: new Date().toISOString() })
                        .eq('id', newRec.id);
                }
            }
        } catch { /* silencioso */ }
    }
}

/**
 * Cria billing_contract de pacote (upfront ou saldo). Materializa 1ª
 * ocorrência + 1 financial_record (estilo upfront), ou só o contrato
 * (estilo saldo).
 *
 * Retorna { toast: { severity, summary, detail, life } } — caller mostra.
 */
export async function createPackageContract({ supabase, rule, normalized, recorrencia, tenantId, packageStyle = 'upfront', paymentMethod = 'link', markPaidNow = false }) {
    const { n, packagePrice } = computeSeriePrice(recorrencia);
    try {
        const { data: createdContract, error: contractErr } = await tenantDb().from('billing_contracts')
            .insert({
                owner_id: normalized.owner_id,
                patient_id: normalized.paciente_id,
                type: 'package',
                total_sessions: n,
                sessions_used: 0,
                package_price: packagePrice,
                status: 'active',
                charging_style: packageStyle === 'saldo' ? 'saldo' : 'upfront'
            })
            .select('id')
            .single();
        if (contractErr) throw contractErr;
        const contractId = createdContract?.id ?? null;

        if (packageStyle === 'saldo') {
            return {
                toast: {
                    severity: 'success',
                    summary: 'Pacote criado (saldo)',
                    detail: `${n} sessões — total ${_BRL(packagePrice)}. Cobranças individuais conforme sessões.`,
                    life: 3500
                }
            };
        }

        const durMin = rule.duration_min || 50;
        const [hh, mm] = String(rule.start_time || '00:00:00').slice(0, 5).split(':').map(Number);
        const firstISO = rule.start_date;
        const startDt = new Date(`${firstISO}T00:00:00`);
        startDt.setHours(hh, mm, 0, 0);
        const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);

        const { data: createdEvent, error: evErr } = await tenantDb().from('agenda_eventos')
            .insert({
                owner_id: rule.owner_id,
                terapeuta_id: rule.therapist_id ?? null,
                recurrence_id: rule.id,
                recurrence_date: firstISO,
                tipo: 'sessao',
                status: 'agendado',
                titulo: normalized.titulo || 'Sessão',
                inicio_em: startDt.toISOString(),
                fim_em: endDt.toISOString(),
                patient_id: normalized.paciente_id,
                determined_commitment_id: normalized.determined_commitment_id ?? null,
                modalidade: rule.modalidade || normalized.modalidade || 'presencial',
                price: packagePrice,
                billing_contract_id: contractId,
                visibility_scope: normalized.visibility_scope || 'public'
            })
            .select('id')
            .single();
        if (evErr) throw evErr;

        const { error: cobErr } = await supabase.rpc('create_financial_record_for_session', {
            p_tenant_id: tenantId,
            p_owner_id: rule.owner_id,
            p_patient_id: normalized.paciente_id ?? null,
            p_agenda_evento_id: createdEvent.id,
            p_amount: packagePrice,
            p_due_date: firstISO
        });
        if (cobErr) throw cobErr;

        const paidNow = markPaidNow === true && paymentMethod !== 'link';
        const { data: recRow } = await tenantDb().from('financial_records')
            .select('id')
            .eq('agenda_evento_id', createdEvent.id)
            .order('created_at', { ascending: false })
            .limit(1)
            .single();
        if (recRow?.id) {
            const patch = {
                updated_at: new Date().toISOString(),
                payment_method: paymentMethod === 'link' ? 'asaas' : paymentMethod
            };
            if (paidNow) {
                patch.status = 'paid';
                patch.paid_at = new Date().toISOString();
            }
            await tenantDb().from('financial_records').update(patch).eq('id', recRow.id);
        }

        const methodLabel = {
            pix: 'PIX',
            dinheiro: 'dinheiro',
            deposito: 'depósito',
            cartao_maquininha: 'cartão (maquininha)'
        }[paymentMethod] || null;
        return {
            toast: {
                severity: 'success',
                summary: paidNow ? 'Pacote pago' : 'Pacote criado',
                detail: paidNow
                    ? `${n} sessões — ${_BRL(packagePrice)} recebido via ${methodLabel}.`
                    : `${n} sessões — total ${_BRL(packagePrice)} com vencimento em ${firstISO.split('-').reverse().join('/')}.`,
                life: 4000
            }
        };
    } catch (e) {
        return {
            toast: {
                severity: 'warn',
                summary: 'Pacote não gerado',
                detail: e?.message || 'Falha ao criar contrato. Você pode gerar manualmente pelo Financeiro.',
                life: 5000
            }
        };
    }
}

/**
 * chargeMode='per_session': materializa todas as N ocorrências + 1 financial_record
 * por ocorrência. Falha parcial é tolerada (retorna toast warn).
 */
export async function materializeAndChargePerSession({ supabase, rule, normalized, recorrencia, tenantId }) {
    const { n, perSessao } = computeSeriePrice(recorrencia);
    try {
        const exceptionDates = new Set((recorrencia.conflitos || []).map((c) => c.date));
        const dates = generateOccurrenceDates(rule, n + exceptionDates.size, exceptionDates).slice(0, n);

        const durMin = rule.duration_min || 50;
        const [hh, mm] = String(rule.start_time || '00:00:00').slice(0, 5).split(':').map(Number);
        const rows = dates.map((iso) => {
            const startDt = new Date(`${iso}T00:00:00`);
            startDt.setHours(hh, mm, 0, 0);
            const endDt = new Date(startDt.getTime() + durMin * 60 * 1000);
            return {
                owner_id: rule.owner_id,
                terapeuta_id: rule.therapist_id ?? null,
                recurrence_id: rule.id,
                recurrence_date: iso,
                tipo: 'sessao',
                status: 'agendado',
                titulo: normalized.titulo || 'Sessão',
                inicio_em: startDt.toISOString(),
                fim_em: endDt.toISOString(),
                patient_id: normalized.paciente_id,
                determined_commitment_id: normalized.determined_commitment_id ?? null,
                modalidade: rule.modalidade || normalized.modalidade || 'presencial',
                price: perSessao,
                visibility_scope: normalized.visibility_scope || 'public'
            };
        });

        const { data: createdEvents, error: evErr } = await tenantDb().from('agenda_eventos').insert(rows).select('id, inicio_em');
        if (evErr) throw evErr;

        let okCount = 0;
        let failCount = 0;
        for (const ev of createdEvents || []) {
            try {
                const dueDate = ev.inicio_em ? new Date(ev.inicio_em).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10);
                const { error: cobErr } = await supabase.rpc('create_financial_record_for_session', {
                    p_tenant_id: tenantId,
                    p_owner_id: rule.owner_id,
                    p_patient_id: normalized.paciente_id ?? null,
                    p_agenda_evento_id: ev.id,
                    p_amount: perSessao,
                    p_due_date: dueDate
                });
                if (cobErr) throw cobErr;
                okCount++;
            } catch {
                failCount++;
            }
        }

        if (failCount === 0) {
            return {
                toast: {
                    severity: 'success',
                    summary: `${okCount} cobranças geradas`,
                    detail: `${_BRL(perSessao)} por sessão. Total: ${_BRL(perSessao * okCount)}.`,
                    life: 4000
                }
            };
        }
        return {
            toast: {
                severity: 'warn',
                summary: 'Cobranças parcialmente geradas',
                detail: `${okCount} ok, ${failCount} falharam. Gere as faltantes manualmente pelo Financeiro.`,
                life: 6000
            }
        };
    } catch (e) {
        return {
            toast: {
                severity: 'warn',
                summary: 'Falha ao materializar série',
                detail: e?.message || 'Sessões podem ter sido criadas em parte. Confira no Financeiro.',
                life: 6000
            }
        };
    }
}
