import { NestFactory } from '@nestjs/core';
import { Module, Injectable, Controller, Get, Post, Body, Req, UseInterceptors, UploadedFile, Param, Patch, Delete } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import * as jwt from 'jsonwebtoken';

@Injectable()
class PrismaService extends PrismaClient {}

function auth(req: any) {
  const h = req.headers.authorization || '';
  const token = h.replace('Bearer ', '');
  if (!token) throw new Error('Token ausente');
  return jwt.verify(token, process.env.JWT_SECRET || 'dev') as any;
}

async function getValidFamily(prisma: PrismaService, req: any) {
  const ctx = auth(req) as any;
  if (!ctx?.familyId) return { ctx, family: null };
  const family = await prisma.family.findUnique({ where: { id: ctx.familyId } });
  return { ctx, family };
}
const n = (v:any) => Number(String(v ?? 0).replace(',', '.')) || 0;
const dt = (v:any) => v ? new Date(v) : new Date();
function redactPayload(body:any){
  if (!body || typeof body !== 'object') return body;
  const clone:any = Array.isArray(body) ? [...body] : { ...body };
  for (const k of Object.keys(clone)) if (/password|senha|token|authorization/i.test(k)) clone[k] = '[REDACTED]';
  if (clone.items && Array.isArray(clone.items)) clone.items = clone.items.slice(0, 20);
  return clone;
}

function monthRange(input?: any) {
  const base = input ? new Date(String(input) + '-01T00:00:00') : new Date();
  const start = new Date(base.getFullYear(), base.getMonth(), 1, 0, 0, 0);
  const end = new Date(base.getFullYear(), base.getMonth() + 1, 1, 0, 0, 0);
  const label = start.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
  return { start, end, key: `${start.getFullYear()}-${String(start.getMonth()+1).padStart(2,'0')}`, label };
}
function extractBarcode(rawText = '') {
  const text = String(rawText || '').replace(/\D/g, '');
  const boleto = text.match(/\d{44,48}/);
  return boleto ? boleto[0] : null;
}
function fileToBase64(file:any){ return file?.buffer ? Buffer.from(file.buffer).toString('base64') : null; }
function investmentScenarios(monthlyDeposit:number, currentAmount:number, periodMonths=12){
  const options = [
    { name:'Tesouro Selic / Reserva', risk:'Baixo', annual:0.135, liquidity:'Alta', authority:'BCB/Tesouro', note:'Boa opção para reserva e prazos curtos; acompanha juros básicos com baixa volatilidade.' },
    { name:'CDB 100% CDI liquidez diária', risk:'Baixo', annual:0.135, liquidity:'Alta', authority:'FGC/Instituição financeira', note:'Pode ter garantia do FGC respeitando limites; verificar banco emissor, liquidez e IR.' },
    { name:'CDB/LCI/LCA 105% a 115% CDI', risk:'Baixo/Médio', annual:0.148, liquidity:'Média', authority:'FGC/Instituição financeira', note:'Pode melhorar retorno com prazo fechado; respeitar limite de garantia por CPF/instituição.' },
    { name:'Tesouro IPCA+', risk:'Médio', annual:0.075, realRate:true, liquidity:'Média', authority:'Tesouro Nacional', note:'Protege poder de compra no longo prazo; pode oscilar no resgate antecipado.' },
    { name:'Carteira mista conservadora', risk:'Médio', annual:0.13, liquidity:'Média', authority:'Planejamento financeiro', note:'Combina liquidez, renda fixa pós-fixada e uma parcela pequena de inflação/prefixado.' },
    { name:'ETF/Ações/FIIs controlados', risk:'Alto', annual:0.16, liquidity:'Alta/Média', authority:'Mercado de capitais', note:'Maior volatilidade; indicado apenas para objetivos longos e parcela limitada.' }
  ];
  return options.map(o=>{
    const monthly = Math.pow(1 + o.annual, 1/12) - 1;
    let balance = currentAmount;
    for(let i=0;i<periodMonths;i++) balance = balance * (1+monthly) + monthlyDeposit;
    const contributed = currentAmount + monthlyDeposit * periodMonths;
    return { ...o, periodMonths, estimatedAmount:Number(balance.toFixed(2)), estimatedGain:Number((balance-contributed).toFixed(2)) };
  });
}

function extractBillFields(rawText = ''){
  const text = String(rawText || '').replace(/\s+/g, ' ');
  const amountMatch = text.match(/(?:R\$\s*)?(\d{1,3}(?:\.\d{3})*,\d{2}|\d+\.\d{2})/g);
  const dateMatch = text.match(/(\d{2}[\/\-]\d{2}[\/\-]\d{4}|\d{4}[\/\-]\d{2}[\/\-]\d{2})/);
  const value = amountMatch?.length ? amountMatch.map(x=>n(x.replace('R$','').trim())).sort((a,b)=>b-a)[0] : 0;
  let due = new Date(); due.setDate(due.getDate()+3);
  if (dateMatch) {
    const d = dateMatch[1];
    if (/^\d{4}/.test(d)) due = new Date(d.replace(/\//g,'-'));
    else { const [dd,mm,yyyy]=d.split(/[\/\-]/); due = new Date(`${yyyy}-${mm}-${dd}T09:00:00`); }
  }
  return { amount: value, dueDate: due, title: /energia|luz/i.test(text) ? 'Conta de luz' : /agua|água/i.test(text) ? 'Conta de água' : /internet|fibra|vivo|claro|tim/i.test(text) ? 'Conta de internet' : 'Conta agendada por OCR' };
}

function digitsOnly(v:any){ return String(v || '').replace(/\D/g, ''); }
function isLikelyGtin(v:any){ const d = digitsOnly(v); return [8,12,13,14].includes(d.length); }
function compactName(v:any){ return String(v || '').replace(/\s+/g, ' ').trim().slice(0, 120); }
async function lookupOpenFoodFacts(code:any){
  const gtin = digitsOnly(code);
  if (!isLikelyGtin(gtin)) return null;
  try {
    const url = `https://world.openfoodfacts.org/api/v2/product/${gtin}.json?fields=code,product_name,brands,quantity,categories_tags,image_url,ecoscore_grade,nutriscore_grade`;
    const res:any = await fetch(url, { headers: { 'User-Agent': 'TudoCerto/1.5 local-finance-ocr' } } as any);
    if (!res.ok) return null;
    const data:any = await res.json();
    if (!data || data.status !== 1 || !data.product) return null;
    const p = data.product;
    const normalizedName = compactName(p.product_name || p.generic_name || 'Produto identificado por GTIN');
    if (!normalizedName) return null;
    return { normalizedName, brand: compactName(p.brands || ''), source: 'OPEN_FOOD_FACTS', payload: data, confidence: 0.82 };
  } catch { return null; }
}
async function lookupGs1(code:any){
  const gtin = digitsOnly(code);
  if (!isLikelyGtin(gtin)) return null;
  const apiUrl = process.env.GS1_API_URL;
  const apiKey = process.env.GS1_API_KEY;
  if (!apiUrl || !apiKey) return null;
  try {
    const res:any = await fetch(`${apiUrl.replace(/\/$/, '')}/${gtin}`, { headers: { Authorization: `Bearer ${apiKey}`, 'x-api-key': apiKey } } as any);
    if (!res.ok) return null;
    const data:any = await res.json();
    const normalizedName = compactName(data.description || data.productDescription || data.name || data.tradeItemDescription || 'Produto GS1');
    return { normalizedName, brand: compactName(data.brandName || data.brand || ''), source: 'GS1', payload: data, confidence: 0.92 };
  } catch { return null; }
}
async function learnProduct(prisma: PrismaService, args:any){
  const familyId = args.familyId;
  const merchant = compactName(args.merchant || '') || null;
  const internalCode = args.internalCode ? digitsOnly(args.internalCode) : null;
  const gtin = args.gtin ? digitsOnly(args.gtin) : (isLikelyGtin(internalCode) ? internalCode : null);
  const normalizedName = compactName(args.normalizedName || args.descriptionRaw || 'Produto');
  if (!familyId || (!internalCode && !gtin && !normalizedName)) return null;
  const existing = await prisma.productKnowledge.findFirst({ where: { familyId, OR: [
    ...(merchant && internalCode ? [{ merchant, internalCode }] : []),
    ...(gtin ? [{ gtin }] : []),
    { normalizedName }
  ] } });
  const data:any = { familyId, merchant, internalCode, gtin, descriptionRaw: args.descriptionRaw || null, normalizedName, brand: compactName(args.brand || '') || null, categoryHint: args.categoryHint || null, source: args.source || 'LOCAL_LEARNED', confidence: args.confidence ?? 0.65, payload: args.payload || undefined };
  if (existing) return prisma.productKnowledge.update({ where: { id: existing.id }, data });
  return prisma.productKnowledge.create({ data });
}
async function enrichProduct(prisma: PrismaService, familyId:string, merchant:any, rawItem:any){
  const code = rawItem?.productCode ? digitsOnly(rawItem.productCode) : null;
  const descriptionRaw = compactName(rawItem?.name || rawItem?.product || '');
  const local = await prisma.productKnowledge.findFirst({ where: { familyId, OR: [
    ...(merchant && code ? [{ merchant: compactName(merchant), internalCode: code }] : []),
    ...(code && isLikelyGtin(code) ? [{ gtin: code }] : []),
    ...(descriptionRaw ? [{ normalizedName: { equals: descriptionRaw, mode: 'insensitive' as any } }] : [])
  ] }, orderBy: { confidence: 'desc' } as any });
  if (local) return { productCode: code, normalizedName: local.normalizedName, brand: local.brand, lookupSource: local.source || 'LOCAL_LEARNED', lookupPayload: local.payload || null };
  let external:any = null;
  if (isLikelyGtin(code)) external = await lookupGs1(code) || await lookupOpenFoodFacts(code);
  if (external) {
    await learnProduct(prisma, { familyId, merchant, internalCode: code, gtin: code, descriptionRaw, normalizedName: external.normalizedName, brand: external.brand, source: external.source, confidence: external.confidence, payload: external.payload });
    return { productCode: code, normalizedName: external.normalizedName, brand: external.brand, lookupSource: external.source, lookupPayload: external.payload };
  }
  await learnProduct(prisma, { familyId, merchant, internalCode: code, descriptionRaw, normalizedName: descriptionRaw, source: 'LOCAL_LEARNED', confidence: 0.55 });
  return { productCode: code, normalizedName: descriptionRaw || null, brand: null, lookupSource: code ? 'LOCAL_LEARNED_PENDING' : null, lookupPayload: null };
}


async function recalcExpense(prisma: PrismaService, expenseId: string) {
  const items = await prisma.expenseItem.findMany({ where: { expenseId } });
  const amount = Number(items.reduce((s, i) => s + i.total, 0).toFixed(2));
  return prisma.expense.update({ where: { id: expenseId }, data: { amount } });
}
async function recalcShopping(prisma: PrismaService, sessionId: string) {
  const items = await prisma.shoppingItem.findMany({ where: { sessionId } });
  const total = Number(items.reduce((s, i) => s + i.total, 0).toFixed(2));
  return prisma.shoppingSession.update({ where: { id: sessionId }, data: { total } });
}

function projection(monthlyDeposit: number, currentAmount: number, years = 5) {
  const profiles = [
    { name: 'Baixo risco', risk: 'BAIXO', annual: 0.095, examples: 'Tesouro Selic, CDB liquidez diária, fundos DI' },
    { name: 'Médio risco', risk: 'MEDIO', annual: 0.12, examples: 'CDBs, LCIs/LCAs, fundos multimercado conservadores' },
    { name: 'Alto risco', risk: 'ALTO', annual: 0.18, examples: 'ETFs, ações, fundos mais voláteis, cripto com limite controlado' },
    { name: 'Misto', risk: 'MISTO', annual: 0.135, examples: '60% baixo risco, 25% médio risco, 15% alto risco' }
  ];
  return profiles.map(p => {
    const monthly = Math.pow(1 + p.annual, 1 / 12) - 1;
    let balance = currentAmount;
    for (let i = 0; i < years * 12; i++) balance = balance * (1 + monthly) + monthlyDeposit;
    return { ...p, estimatedAmount: Number(balance.toFixed(2)), estimatedGain: Number((balance - currentAmount - monthlyDeposit * years * 12).toFixed(2)) };
  });
}

function financialAdvice(input: { income:number; budget:number; used:number; vaultMonthly:number; categoryRows:any[] }) {
  const availableAfterVault = Math.max(input.income - input.vaultMonthly, 0);
  const effectiveBudget = Math.min(input.budget, availableAfterVault);
  const remaining = effectiveBudget - input.used;
  const pressure = input.categoryRows
    .filter(r => r.enabled && r.monthlyLimit > 0)
    .map(r => ({ ...r, percent: Math.round((r.used / r.monthlyLimit) * 100) }))
    .sort((a,b) => b.percent - a.percent)
    .slice(0,3);
  const alerts = [] as string[];
  if (input.vaultMonthly > input.income * 0.25) alerts.push('A meta mensal do cofre está acima de 25% da renda. Valide se isso não pressiona gastos essenciais.');
  if (remaining < 0) alerts.push(`O orçamento efetivo já estourou em R$ ${Math.abs(remaining).toFixed(2)} considerando a meta do cofre.`);
  if (pressure[0]?.percent > 85) alerts.push(`A categoria ${pressure[0].name} já consumiu ${pressure[0].percent}% do teto definido.`);
  return { effectiveBudget, availableAfterVault, remainingAfterVault: remaining, recommendedVaultMonthly: Math.max(Math.round(input.income * 0.12), 0), alerts, plan: [
    `Opere o mês com orçamento efetivo de R$ ${effectiveBudget.toFixed(2)} depois de separar R$ ${input.vaultMonthly.toFixed(2)} para o cofre.`,
    pressure.length ? `Priorize controle nas categorias: ${pressure.map(p => p.name).join(', ')}.` : 'Defina tetos por categoria para receber alertas de consumo.',
    'Em cupons fiscais, revise os itens após OCR. O total do gasto será recalculado automaticamente pelos itens corrigidos.'
  ] };
}

@Controller()
class AppController {
  constructor(private prisma: PrismaService) {}

  @Post('auth/register')
  async register(@Body() body: any) {
    const family = await this.prisma.family.create({ data: { name: body.familyName || 'Minha Família', monthlyBudget: n(body.monthlyBudget || 5000), monthlyIncome: n(body.monthlyIncome || 0) } });
    const password = await bcrypt.hash(body.password || '123456', 10);
    const user = await this.prisma.user.create({ data: { name: body.name, email: body.email, password, role: 'ADMIN', familyId: family.id } });
    await this.prisma.vault.create({ data: { familyId: family.id } });
    const cats = await this.prisma.category.findMany();
    for (const c of cats) await this.prisma.categoryBudget.create({ data: { familyId: family.id, categoryId: c.id, monthlyLimit: 0, enabled: true } });
    const token = jwt.sign({ userId: user.id, familyId: family.id, role: user.role }, process.env.JWT_SECRET || 'dev');
    return { token, user: { id: user.id, name: user.name, email: user.email }, family };
  }

  @Post('auth/login')
  async login(@Body() body: any) {
    let user = await this.prisma.user.findUnique({ where: { email: body.email }, include: { family: true } });
    if (!user || !(await bcrypt.compare(body.password, user.password))) return { error: 'Credenciais inválidas' };
    if (!user.family) {
      const family = await this.prisma.family.create({ data: { name: 'Minha Família', monthlyBudget: 5000, monthlyIncome: 0 } });
      await this.prisma.vault.upsert({ where: { familyId: family.id }, update: {}, create: { familyId: family.id } });
      await this.prisma.user.update({ where: { id: user.id }, data: { familyId: family.id } });
      user = await this.prisma.user.findUnique({ where: { email: body.email }, include: { family: true } });
    }
    const token = jwt.sign({ userId: user!.id, familyId: user!.familyId, role: user!.role }, process.env.JWT_SECRET || 'dev');
    return { token, user: { id: user!.id, name: user!.name, email: user!.email }, family: user!.family };
  }

  @Get('dashboard')
  async dashboard(@Req() req: any) {
    const { ctx, family } = await getValidFamily(this.prisma, req);
    const mr = monthRange(req.query?.month);
    if (!family) return { ok:false, code:'FAMILY_NOT_FOUND', message:'Família não encontrada. Limpe o localStorage e faça login novamente.' };
    const expenses = await this.prisma.expense.findMany({ where: { familyId: family.id, date: { gte: mr.start, lt: mr.end } }, include: { category: true, user: true, items: true }, orderBy: { date: 'desc' } });
    const used = expenses.reduce((s, e) => s + e.amount, 0);
    const byCategory: Record<string, number> = {};
    for (const e of expenses) byCategory[e.category.name] = (byCategory[e.category.name] || 0) + e.amount;
    const vault = await this.prisma.vault.findUnique({ where: { familyId: family.id }, include: { transactions: { orderBy: { createdAt: 'desc' }, take: 10 } } });
    const categoryBudgets = await this.prisma.categoryBudget.findMany({ where: { familyId: family.id }, include: { category: true } });
    const categoryRows = categoryBudgets.sort((a:any,b:any)=>a.category.name.localeCompare(b.category.name)).map(b => ({ id:b.id, categoryId:b.categoryId, name:b.category.name, icon:b.category.icon, type:b.category.type, enabled:b.enabled, monthlyLimit:b.monthlyLimit, used: byCategory[b.category.name] || 0, remaining: b.monthlyLimit - (byCategory[b.category.name] || 0) }));
    const budget = { monthly: family?.monthlyBudget || 0, income: family?.monthlyIncome || 0, used, remaining: (family?.monthlyBudget || 0) - used, percent: family?.monthlyBudget ? Math.round((used / family.monthlyBudget) * 100) : 0 };
    return { month: { key: mr.key, label: mr.label, generatedAt: new Date() }, family, budget, categoryBudgets: categoryRows, byCategory, recentExpenses: expenses.slice(0, 12), vault, consultant: financialAdvice({ income: budget.income, budget: budget.monthly, used, vaultMonthly: vault?.monthlyDeposit || 0, categoryRows }), investmentProjection: investmentScenarios(vault?.monthlyDeposit || 500, vault?.currentAmount || 0, 12), monthlyClose: { totalSpent: used, income: budget.income, savingsPotential: Math.max(budget.income - used - (vault?.monthlyDeposit || 0), 0), status: used <= budget.monthly ? 'Dentro do orçamento' : 'Orçamento estourado' }, tips: ['Revise cupons OCR antes de considerar o gasto final fechado.', 'Salve sessões de mercado para comparar intenção de compra versus cupom final.', 'Crie categorias próprias para financiamento, acordo judicial, dívidas ou qualquer gasto recorrente.'] };
  }

  @Get('categories')
  categories() { return this.prisma.category.findMany({ orderBy: { name: 'asc' } }); }

  @Post('categories')
  async createCategory(@Req() req:any, @Body() body:any) {
    const { family } = await getValidFamily(this.prisma, req);
    if (!family) {
      return { ok: false, code: 'FAMILY_NOT_FOUND', message: 'Família não encontrada. Limpe o localStorage, faça login novamente e tente novamente.' };
    }
    const name = String(body.name || '').trim();
    if (!name) return { ok: false, error: 'Nome da categoria é obrigatório' };
    const cat = await this.prisma.category.upsert({
      where: { name },
      update: { icon: body.icon || 'Folder', type: body.type || 'PERSONALIZADA' },
      create: { name, icon: body.icon || 'Folder', type: body.type || 'PERSONALIZADA' }
    });
    await this.prisma.categoryBudget.upsert({
      where: { familyId_categoryId: { familyId: family.id, categoryId: cat.id } },
      update: { enabled: true, monthlyLimit: n(body.monthlyLimit || 0) },
      create: { familyId: family.id, categoryId: cat.id, enabled: true, monthlyLimit: n(body.monthlyLimit || 0) }
    });
    return { ok: true, category: cat };
  }

  @Patch('settings')
  async settings(@Req() req:any, @Body() body:any) {
    const { ctx, family } = await getValidFamily(this.prisma, req);
    if (!family) {
      return { ok: false, code: 'FAMILY_NOT_FOUND', message: 'Família não encontrada. Limpe o localStorage, faça login novamente e recrie a família se necessário.' };
    }
    const updatedFamily = await this.prisma.family.update({
      where: { id: family.id },
      data: { monthlyBudget: n(body.monthlyBudget), monthlyIncome: n(body.monthlyIncome) }
    });
    for (const row of body.categoryBudgets || []) {
      if (!row?.categoryId) continue;
      await this.prisma.categoryBudget.upsert({
        where: { familyId_categoryId: { familyId: family.id, categoryId: row.categoryId } },
        update: { enabled: !!row.enabled, monthlyLimit: n(row.monthlyLimit) },
        create: { familyId: family.id, categoryId: row.categoryId, enabled: !!row.enabled, monthlyLimit: n(row.monthlyLimit) }
      });
    }
    return { ok: true, family: updatedFamily };
  }

  @Post('expenses')
  async expense(@Req() req: any, @Body() body: any) {
    const ctx = auth(req);
    const cat = await this.prisma.category.findFirst({ where: { name: body.category || 'Mercado' } });
    return this.prisma.expense.create({ data: { description: body.description || 'Gasto manual', amount: n(body.amount), date: body.date ? dt(body.date) : new Date(), familyId: ctx.familyId, userId: ctx.userId, categoryId: cat!.id, source: 'MANUAL' }, include: { category:true, user:{select:{id:true,name:true,email:true}}, items:true } });
  }


  @Get('expenses')
  async expenses(@Req() req:any) {
    const ctx = auth(req);
    const mr = monthRange(req.query?.month);
    return this.prisma.expense.findMany({ where: { familyId: ctx.familyId, date: { gte: mr.start, lt: mr.end } }, include: { category:true, user:{select:{id:true,name:true,email:true}}, items:true }, orderBy: { date:'desc' } });
  }

  @Patch('expenses/:id')
  async updateExpense(@Req() req:any, @Param('id') id:string, @Body() body:any) {
    const ctx = auth(req);
    const current = await this.prisma.expense.findFirst({ where: { id, familyId: ctx.familyId } });
    if (!current) return { error:'Gasto não encontrado' };
    const cat = body.categoryId ? await this.prisma.category.findUnique({ where: { id: body.categoryId } }) : null;
    return this.prisma.expense.update({ where:{id}, data:{ description: body.description ?? current.description, amount: n(body.amount ?? current.amount), date: body.date ? dt(body.date) : current.date, categoryId: cat?.id || current.categoryId }, include:{category:true,user:{select:{name:true,email:true}},items:true} });
  }

  @Delete('expenses/:id')
  async deleteExpense(@Req() req:any, @Param('id') id:string) {
    const ctx = auth(req);
    const current = await this.prisma.expense.findFirst({ where: { id, familyId: ctx.familyId } });
    if (!current) return { ok:true };
    await this.prisma.expense.delete({ where:{ id } });
    return { ok:true };
  }

  @Get('monthly-close')
  async monthlyClose(@Req() req:any) {
    const d = await this.dashboard(req);
    const over = (d.categoryBudgets || []).filter((c:any)=>c.monthlyLimit>0 && c.used > c.monthlyLimit);
    const near = (d.categoryBudgets || []).filter((c:any)=>c.monthlyLimit>0 && c.used >= c.monthlyLimit*0.85 && c.used <= c.monthlyLimit);
    return { month: d.month, summary: d.monthlyClose, categoriesOverLimit: over, categoriesNearLimit: near, analysis: [
      over.length ? `Há ${over.length} categoria(s) acima do teto. Reduza gastos ou remaneje orçamento antes do fechamento.` : 'Nenhuma categoria acima do teto no mês.',
      near.length ? `Categorias próximas do limite: ${near.map((x:any)=>x.name).join(', ')}.` : 'Não há categorias críticas próximas do limite.',
      `Saldo potencial após gastos e cofre: ${d.monthlyClose.savingsPotential.toLocaleString('pt-BR',{style:'currency',currency:'BRL'})}.`,
      'Para aumentar os ganhos, priorize quitar dívidas caras, negociar descontos à vista em contas/financiamentos e automatizar aportes no início do mês.'
    ] };
  }


  @Get('monthly-report')
  async monthlyReport(@Req() req:any) {
    const d:any = await this.dashboard(req);
    if (d?.ok === false) return d;
    const categories = (d.categoryBudgets || []).filter((c:any)=>c.enabled && c.monthlyLimit > 0);
    const over = categories.filter((c:any)=>c.used > c.monthlyLimit).sort((a:any,b:any)=>b.used-a.used);
    const atLimit = categories.filter((c:any)=>c.used <= c.monthlyLimit && c.used >= c.monthlyLimit).sort((a:any,b:any)=>b.used-a.used);
    const near = categories.filter((c:any)=>c.used >= c.monthlyLimit*0.85 && c.used < c.monthlyLimit).sort((a:any,b:any)=>(b.used/b.monthlyLimit)-(a.used/a.monthlyLimit));
    const low = categories.filter((c:any)=>c.used < c.monthlyLimit*0.5).sort((a:any,b:any)=>a.used-b.used);
    const income = d.budget?.income || 0;
    const used = d.budget?.used || 0;
    const vaultMonthly = d.vault?.monthlyDeposit || 0;
    const freeCash = income - used - vaultMonthly;
    const topSpending = [...categories].sort((a:any,b:any)=>b.used-a.used).slice(0,5);
    const recommendations = [
      over.length ? `Reduzir imediatamente ou remanejar orçamento nas categorias acima do teto: ${over.map((x:any)=>x.name).join(', ')}.` : 'Manter disciplina: nenhuma categoria está acima do teto definido.',
      near.length ? `Acompanhar diariamente categorias próximas do limite: ${near.map((x:any)=>x.name).join(', ')}.` : 'As categorias configuradas ainda não estão em zona crítica próxima ao teto.',
      freeCash > 0 ? `Há potencial de direcionar ${freeCash.toLocaleString('pt-BR',{style:'currency',currency:'BRL'})} para antecipar meta do cofre, quitar dívidas caras ou negociar pagamento com desconto.` : `O mês está pressionado em ${Math.abs(freeCash).toLocaleString('pt-BR',{style:'currency',currency:'BRL'})}; priorize corte temporário de gastos variáveis e evite novas parcelas.`,
      'Para aumentar ganhos, mapear renda extra recorrente, revisar assinaturas, renegociar serviços fixos e comparar preços de mercado/farmácia usando as sessões salvas.',
      'Usar o OCR de cupom como registro efetivo: revisar itens extraídos, corrigir quantidade/valor e manter o gasto vinculado à categoria correta.'
    ];
    const actionPlan = [
      { priority:'Alta', action:'Revisar gastos acima ou próximos do teto', expectedImpact:'Evitar estouro do orçamento mensal', owner:'Família' },
      { priority:'Alta', action:'Separar aporte do cofre no início do mês', expectedImpact:'Aumentar chance de cumprir a meta', owner:'Responsável financeiro' },
      { priority:'Média', action:'Negociar contas com vencimento próximo e buscar desconto por antecipação', expectedImpact:'Reduzir desembolso total', owner:'Responsável financeiro' },
      { priority:'Média', action:'Comparar sessões de mercado com cupons reais', expectedImpact:'Identificar desvios entre compra planejada e compra efetiva', owner:'Todos os membros' },
      { priority:'Baixa', action:'Revisar assinaturas e pequenos gastos recorrentes', expectedImpact:'Liberar caixa sem afetar essenciais', owner:'Família' }
    ];
    return {
      month: d.month,
      executiveSummary: {
        income,
        used,
        monthlyBudget: d.budget?.monthly || 0,
        remainingBudget: d.budget?.remaining || 0,
        vaultCurrent: d.vault?.currentAmount || 0,
        vaultTarget: d.vault?.targetAmount || 0,
        vaultMonthly,
        freeCash,
        status: d.monthlyClose?.status || 'Sem status'
      },
      topSpending,
      overLimit: over,
      atLimit,
      nearLimit: near,
      underUsed: low.slice(0,5),
      consultant: d.consultant,
      recommendations,
      actionPlan,
      investmentScenarios: d.investmentProjection,
      recentExpenses: d.recentExpenses || [],
      notes: [
        'Relatório gerado com base no mês selecionado no topo da plataforma.',
        'As análises são educacionais e dependem da qualidade dos lançamentos, cupons revisados e categorias parametrizadas.'
      ]
    };
  }

  @Get('receipts')
  async receipts(@Req() req:any) {
    const ctx = auth(req);
    return this.prisma.expense.findMany({ where: { familyId: ctx.familyId, source: 'OCR_RECEIPT' }, include: { category: true, items: true }, orderBy: { date: 'desc' } });
  }

  @Get('receipts/:id')
  async receipt(@Req() req:any, @Param('id') id:string) {
    const ctx = auth(req);
    return this.prisma.expense.findFirst({ where: { id, familyId: ctx.familyId }, include: { category: true, items: true } });
  }

  @Patch('receipts/:id')
  async updateReceipt(@Req() req:any, @Param('id') id:string, @Body() body:any) {
    const ctx = auth(req);
    const cat = body.categoryId ? await this.prisma.category.findUnique({ where: { id: body.categoryId } }) : body.category ? await this.prisma.category.findFirst({ where: { name: body.category } }) : null;
    await this.prisma.expense.update({ where: { id }, data: { description: body.description, categoryId: cat?.id } });
    if (Array.isArray(body.items)) {
      for (const it of body.items) {
        const quantity = n(it.quantity || 1); const unitPrice = n(it.unitPrice ?? it.price); const total = n(it.total || quantity * unitPrice);
        const enriched = await enrichProduct(this.prisma, ctx.familyId, null, { name: it.name, productCode: it.productCode });
        if (it.id && !String(it.id).startsWith('new-')) await this.prisma.expenseItem.update({ where: { id: it.id }, data: { name: it.name, quantity, unitPrice, total, productCode: enriched.productCode || it.productCode || null, normalizedName: enriched.normalizedName || it.name || null, brand: enriched.brand || null, lookupSource: enriched.lookupSource || 'USER_CORRECTED' } });
        else await this.prisma.expenseItem.create({ data: { name: it.name || 'Item corrigido', quantity, unitPrice, total, productCode: enriched.productCode || it.productCode || null, normalizedName: enriched.normalizedName || it.name || null, brand: enriched.brand || null, lookupSource: enriched.lookupSource || 'USER_CORRECTED', expenseId: id } });
        await learnProduct(this.prisma, { familyId: ctx.familyId, internalCode: it.productCode, descriptionRaw: it.name, normalizedName: it.name, source: 'USER_CORRECTED', confidence: 0.9 });
      }
    }
    await recalcExpense(this.prisma, id);
    return this.prisma.expense.findFirst({ where: { id, familyId: ctx.familyId }, include: { category: true, items: true } });
  }

  @Delete('receipt-items/:id')
  async deleteReceiptItem(@Param('id') id:string) {
    const item = await this.prisma.expenseItem.findUnique({ where: { id } });
    if (!item) return { ok: true };
    await this.prisma.expenseItem.delete({ where: { id } });
    await recalcExpense(this.prisma, item.expenseId);
    return { ok: true };
  }

  @Post('ocr/receipt')
  @UseInterceptors(FileInterceptor('file'))
  async ocrReceipt(@Req() req: any, @UploadedFile() file: any, @Body() body:any) {
    const ctx = auth(req);
    const form = new FormData();
    form.append('file', new Blob([file.buffer], { type: file.mimetype }), file.originalname);
    const response = await fetch(`${process.env.OCR_SERVICE_URL}/ocr/receipt`, { method: 'POST', body: form as any });
    const data: any = await response.json();
    const categoryName = body.category || 'Mercado';
    const cat = await this.prisma.category.findFirst({ where: { name: categoryName } }) || await this.prisma.category.findFirst({ where: { name: 'Mercado' } });
    const total = (data.items || []).reduce((s: number, i: any) => s + n(i.total), 0);
    const exp = await this.prisma.expense.create({ data: { description: body.description || data.descriptionSuggestion || data.merchant || `Cupom fiscal OCR - ${categoryName}`, amount: total, merchant: data.merchant || null, accountType: data.accountType || null, rawText: data.rawText || null, familyId: ctx.familyId, userId: ctx.userId, categoryId: cat!.id, source: 'OCR_RECEIPT' } });
    const enrichedItems:any[] = [];
    for (const item of data.items || []) {
      const enriched = await enrichProduct(this.prisma, ctx.familyId, data.merchant || body.merchant || null, item);
      enrichedItems.push({ ...item, ...enriched });
      await this.prisma.expenseItem.create({ data: { name: item.name, quantity: n(item.quantity || 1), unitPrice: n(item.unitPrice), total: n(item.total), productCode: enriched.productCode || item.productCode || null, normalizedName: enriched.normalizedName || null, brand: enriched.brand || null, lookupSource: enriched.lookupSource || null, lookupPayload: enriched.lookupPayload || undefined, expenseId: exp.id } });
    }
    const receipt = await this.prisma.expense.findUnique({ where: { id: exp.id }, include: { category: true, items: true } });
    return { receipt, expense: exp, category: cat?.name, total, items: enrichedItems, rawText: data.rawText, productLookup: { strategy: 'LOCAL_LEARNED + GTIN/EAN + Open Food Facts + GS1(configurável)' }, warnings: data.warnings || [] };
  }

  @Post('shopping-sessions')
  async startShopping(@Req() req: any, @Body() body: any) {
    const ctx = auth(req);
    return this.prisma.shoppingSession.create({ data: { name: body.name || `Compra de Mercado ${new Date().toLocaleDateString('pt-BR')}`, familyId: ctx.familyId } });
  }

  @Get('shopping-sessions')
  async sessions(@Req() req: any) {
    const ctx = auth(req);
    return this.prisma.shoppingSession.findMany({ where: { familyId: ctx.familyId }, include: { items: true }, orderBy: { createdAt: 'desc' } });
  }

  @Get('shopping-sessions/:id')
  async session(@Req() req:any, @Param('id') id:string) {
    const ctx = auth(req);
    return this.prisma.shoppingSession.findFirst({ where: { id, familyId: ctx.familyId }, include: { items: true } });
  }

  @Patch('shopping-sessions/:id')
  async updateSession(@Param('id') id:string, @Body() body:any) {
    return this.prisma.shoppingSession.update({ where: { id }, data: { name: body.name, status: body.status } });
  }

  @Post('shopping-sessions/:id/items')
  async addShoppingItem(@Param('id') id: string, @Body() body: any) {
    const quantity = n(body.quantity || 1); const price = n(body.price); const total = n(body.total || price * quantity);
    const sessionBase = await this.prisma.shoppingSession.findUnique({ where: { id } });
    const enriched = sessionBase ? await enrichProduct(this.prisma, sessionBase.familyId, null, { name: body.product, productCode: body.productCode }) : { productCode: body.productCode || null, normalizedName: body.product || null, brand: null, lookupSource: null, lookupPayload: null };
    const item = await this.prisma.shoppingItem.create({ data: { product: body.product || 'Produto', quantity, price, total, productCode: enriched.productCode || body.productCode || null, normalizedName: enriched.normalizedName || null, brand: enriched.brand || null, lookupSource: enriched.lookupSource || null, lookupPayload: enriched.lookupPayload || undefined, sessionId: id } });
    await recalcShopping(this.prisma, id);
    return item;
  }

  @Patch('shopping-items/:id')
  async updateShoppingItem(@Param('id') id:string, @Body() body:any) {
    const current = await this.prisma.shoppingItem.findUnique({ where: { id } });
    const quantity = n(body.quantity ?? current?.quantity ?? 1); const price = n(body.price ?? current?.price ?? 0); const total = n(body.total || quantity * price);
    const sessionBase = current ? await this.prisma.shoppingSession.findUnique({ where: { id: current.sessionId } }) : null;
    const enriched = sessionBase ? await enrichProduct(this.prisma, sessionBase.familyId, null, { name: body.product, productCode: body.productCode || current?.productCode }) : { productCode: body.productCode || current?.productCode || null, normalizedName: body.product || null, brand: null, lookupSource: null, lookupPayload: null };
    const item = await this.prisma.shoppingItem.update({ where: { id }, data: { product: body.product, quantity, price, total, productCode: enriched.productCode || body.productCode || current?.productCode || null, normalizedName: enriched.normalizedName || body.product || null, brand: enriched.brand || current?.brand || null, lookupSource: enriched.lookupSource || current?.lookupSource || 'USER_CORRECTED' } });
    if (sessionBase) await learnProduct(this.prisma, { familyId: sessionBase.familyId, internalCode: item.productCode, descriptionRaw: item.product, normalizedName: item.product, source: 'USER_CORRECTED', confidence: 0.9 });
    await recalcShopping(this.prisma, item.sessionId);
    return item;
  }

  @Delete('shopping-items/:id')
  async deleteShoppingItem(@Param('id') id:string) {
    const item = await this.prisma.shoppingItem.findUnique({ where: { id } });
    if (!item) return { ok: true };
    await this.prisma.shoppingItem.delete({ where: { id } });
    await recalcShopping(this.prisma, item.sessionId);
    return { ok: true };
  }

  @Post('shopping-sessions/:id/ocr-price')
  @UseInterceptors(FileInterceptor('file'))
  async ocrPrice(@Param('id') id:string, @UploadedFile() file:any) {
    const form = new FormData();
    form.append('file', new Blob([file.buffer], { type: file.mimetype }), file.originalname);
    const response = await fetch(`${process.env.OCR_SERVICE_URL}/ocr/price-label`, { method: 'POST', body: form as any });
    const data:any = await response.json();
    const quantity = n(data.quantity || 1); const price = n(data.price); const total = n(data.total || quantity * price);
    const sessionBase = await this.prisma.shoppingSession.findUnique({ where: { id } });
    const enriched = sessionBase ? await enrichProduct(this.prisma, sessionBase.familyId, null, { name: data.product, productCode: data.productCode }) : { productCode: data.productCode || null, normalizedName: data.product || null, brand: null, lookupSource: null, lookupPayload: null };
    const item = await this.prisma.shoppingItem.create({ data: { product: data.product || 'Produto extraído da etiqueta', quantity, price, total, productCode: enriched.productCode || data.productCode || null, normalizedName: enriched.normalizedName || null, brand: enriched.brand || null, lookupSource: enriched.lookupSource || null, lookupPayload: enriched.lookupPayload || undefined, sessionId: id } });
    await recalcShopping(this.prisma, id);
    const session = await this.prisma.shoppingSession.findUnique({ where: { id }, include: { items: true } });
    return { item, session, rawText: data.rawText, confidence: data.confidence, productLookup: enriched, warnings: data.warnings || [] };
  }


  @Get('product-lookup/:code')
  async productLookup(@Req() req:any, @Param('code') code:string) {
    const ctx = auth(req);
    const enriched = await enrichProduct(this.prisma, ctx.familyId, null, { productCode: code, name: code });
    return { code, ...enriched, strategy: 'local learned -> GS1 if configured -> Open Food Facts -> pending local learning' };
  }

  @Get('members')
  async members(@Req() req:any) {
    const ctx = auth(req);
    return this.prisma.user.findMany({ where: { familyId: ctx.familyId }, select: { id:true, name:true, email:true, phone:true, role:true, notifyEmail:true, notifySms:true, createdAt:true, updatedAt:true }, orderBy: { name: 'asc' } });
  }

  @Post('members')
  async createMember(@Req() req:any, @Body() body:any) {
    const ctx = auth(req);
    const password = await bcrypt.hash(body.password || '123456', 10);
    return this.prisma.user.create({ data: { name: body.name, email: body.email, password, role: body.role || 'ADULTO', phone: body.phone || null, notifyEmail: body.notifyEmail !== false, notifySms: !!body.notifySms, familyId: ctx.familyId }, select: { id:true, name:true, email:true, phone:true, role:true, notifyEmail:true, notifySms:true } });
  }

  @Patch('members/:id')
  async updateMember(@Req() req:any, @Param('id') id:string, @Body() body:any) {
    const ctx = auth(req);
    const current = await this.prisma.user.findFirst({ where: { id, familyId: ctx.familyId } });
    if (!current) return { error: 'Membro não encontrado' };
    return this.prisma.user.update({ where: { id }, data: { name: body.name, email: body.email, phone: body.phone || null, role: body.role || current.role, notifyEmail: !!body.notifyEmail, notifySms: !!body.notifySms }, select: { id:true, name:true, email:true, phone:true, role:true, notifyEmail:true, notifySms:true, updatedAt:true } });
  }

  @Get('bill-reminders')
  async billReminders(@Req() req:any) {
    const ctx = auth(req);
    return this.prisma.billReminder.findMany({ where: { familyId: ctx.familyId }, include: { createdBy: { select: { id:true, name:true, email:true } } }, orderBy: { dueDate: 'asc' } });
  }

  @Post('bill-reminders')
  async createBillReminder(@Req() req:any, @Body() body:any) {
    const ctx = auth(req);
    const dueDate = dt(body.dueDate);
    const remindAt = body.remindAt ? dt(body.remindAt) : dueDate;
    return this.prisma.billReminder.create({ data: { title: body.title || 'Conta agendada', description: body.description || null, amount: n(body.amount), dueDate, remindAt, notification: body.notification || 'EMAIL_SMS', barcode: body.barcode || null, familyId: ctx.familyId, createdById: ctx.userId } });
  }

  @Post('bill-reminders/ocr')
  @UseInterceptors(FileInterceptor('file'))
  async createBillReminderByOcr(@Req() req:any, @UploadedFile() file:any, @Body() body:any) {
    const ctx = auth(req);
    const form = new FormData();
    form.append('file', new Blob([file.buffer], { type: file.mimetype }), file.originalname);
    const response = await fetch(`${process.env.OCR_SERVICE_URL}/ocr/receipt`, { method: 'POST', body: form as any });
    const data:any = await response.json();
    const extracted = extractBillFields(data.rawText || '');
    const dueDate = body.dueDate ? dt(body.dueDate) : extracted.dueDate;
    const remindAt = body.remindAt ? dt(body.remindAt) : dueDate;
    const bill = await this.prisma.billReminder.create({ data: { title: body.title || extracted.title, description: body.description || 'Conta criada a partir de foto/OCR. Revise valor, código de barras e vencimento antes de confiar no alerta.', amount: n(body.amount || extracted.amount), dueDate, remindAt, notification: body.notification || 'EMAIL_SMS', rawText: data.rawText || '', imageName: file?.originalname || null, imageMime: file?.mimetype || null, imageBase64: fileToBase64(file), barcode: body.barcode || extractBarcode(data.rawText || ''), familyId: ctx.familyId, createdById: ctx.userId } });
    return { bill, rawText: data.rawText, extracted, warnings: data.warnings || [] };
  }

  @Patch('bill-reminders/:id')
  async updateBillReminder(@Req() req:any, @Param('id') id:string, @Body() body:any) {
    const ctx = auth(req);
    const current = await this.prisma.billReminder.findFirst({ where: { id, familyId: ctx.familyId } });
    if (!current) return { error: 'Conta agendada não encontrada' };
    return this.prisma.billReminder.update({ where: { id }, data: { title: body.title ?? current.title, description: body.description ?? current.description, amount: n(body.amount ?? current.amount), dueDate: body.dueDate ? dt(body.dueDate) : current.dueDate, remindAt: body.remindAt ? dt(body.remindAt) : current.remindAt, status: body.status || current.status, notification: body.notification || current.notification, barcode: body.barcode ?? current.barcode } });
  }

  @Delete('bill-reminders/:id')
  async deleteBillReminder(@Req() req:any, @Param('id') id:string) {
    const ctx = auth(req);
    const current = await this.prisma.billReminder.findFirst({ where: { id, familyId: ctx.familyId } });
    if (!current) return { ok: true };
    await this.prisma.billReminder.delete({ where: { id } });
    return { ok: true };
  }

  @Get('alerts/due')
  async dueAlerts(@Req() req:any) {
    const ctx = auth(req);
    const now = new Date();
    const bills = await this.prisma.billReminder.findMany({ where: { familyId: ctx.familyId, status: 'SCHEDULED', remindAt: { lte: now } }, orderBy: { remindAt: 'asc' } });
    const members = await this.prisma.user.findMany({ where: { familyId: ctx.familyId }, select: { name:true, email:true, phone:true, notifyEmail:true, notifySms:true } });
    return { generatedAt: now, bills, recipients: members.filter(m => m.notifyEmail || m.notifySms), note: 'V1.3 simula a fila de alertas. Em produção, conectar SMTP/SendGrid e SMS/Twilio/Zenvia.' };
  }

  @Get('audit-logs')
  async auditLogs(@Req() req:any) {
    const ctx = auth(req);
    return this.prisma.auditLog.findMany({ where: { familyId: ctx.familyId }, orderBy: { createdAt: 'desc' }, take: 120 });
  }

  @Patch('vault')
  async vault(@Req() req: any, @Body() body: any) {
    const ctx = auth(req);
    const vault = await this.prisma.vault.update({ where: { familyId: ctx.familyId }, data: { targetAmount: n(body.targetAmount), monthlyDeposit: n(body.monthlyDeposit), riskProfile: body.riskProfile } });
    return { vault, projections: projection(vault.monthlyDeposit, vault.currentAmount) };
  }

  @Get('vault')
  async vaultDetails(@Req() req:any) {
    const ctx = auth(req);
    const vault = await this.prisma.vault.findUnique({ where: { familyId: ctx.familyId }, include: { transactions: { orderBy: { createdAt: 'desc' } } } });
    const dashboard = await this.dashboard(req);
    return { vault, composition: vault?.transactions || [], consultant: dashboard.consultant, investmentProjection: dashboard.investmentProjection };
  }

  @Post('vault/deposit')
  async vaultDeposit(@Req() req:any, @Body() body:any) {
    const ctx = auth(req);
    const vault = await this.prisma.vault.findUnique({ where: { familyId: ctx.familyId } });
    const amount = Math.abs(n(body.amount));
    const updated = await this.prisma.vault.update({ where: { familyId: ctx.familyId }, data: { currentAmount: (vault?.currentAmount || 0) + amount } });
    await this.prisma.vaultTransaction.create({ data: { vaultId: updated.id, amount, note: body.note || 'Aporte no cofre', type: 'DEPOSIT' } });
    return { vault: updated };
  }

  @Post('vault/withdraw')
  async vaultWithdraw(@Req() req:any, @Body() body:any) {
    const ctx = auth(req);
    const vault = await this.prisma.vault.findUnique({ where: { familyId: ctx.familyId } });
    const amount = Math.abs(n(body.amount));
    const updated = await this.prisma.vault.update({ where: { familyId: ctx.familyId }, data: { currentAmount: Math.max((vault?.currentAmount || 0) - amount, 0) } });
    await this.prisma.vaultTransaction.create({ data: { vaultId: updated.id, amount: -amount, note: body.note || 'Retirada do cofre', type: 'WITHDRAW' } });
    return { vault: updated };
  }

  @Get('investments')
  async investments(@Req() req:any) {
    const ctx = auth(req);
    const months = Math.max(1, Math.round(n(req.query?.months || 12)));
    const monthlyDeposit = n(req.query?.monthlyDeposit || 500);
    const currentAmount = n(req.query?.currentAmount || 0);
    return { months, scenarios: investmentScenarios(monthlyDeposit, currentAmount, months), references: [
      'Banco Central: Selic como taxa básica da economia.',
      'Tesouro Direto: títulos públicos disponíveis e histórico de taxas.',
      'FGC: limite de garantia de R$ 250 mil por CPF/CNPJ por instituição ou conglomerado.'
    ], disclaimer: 'Simulação educacional. Rentabilidades são estimativas e não promessa de retorno.' };
  }
}

@Module({ controllers: [AppController], providers: [PrismaService] })
class AppModule {}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  const prisma = app.get(PrismaService);
  app.use((req:any, res:any, next:any) => {
    res.on('finish', async () => {
      try {
        let ctx:any = null;
        let actor:any = null;
        try {
          ctx = auth(req);
          actor = ctx?.userId
            ? await prisma.user.findUnique({ where: { id: ctx.userId }, select: { id:true, name:true, email:true, familyId:true } })
            : null;
        } catch {}

        let validFamilyId: string | null = null;
        const candidateFamilyId = actor?.familyId || ctx?.familyId || null;
        if (candidateFamilyId) {
          const familyExists = await prisma.family.findUnique({ where: { id: candidateFamilyId }, select: { id:true } });
          if (familyExists) validFamilyId = familyExists.id;
        }

        let validUserId: string | null = null;
        const candidateUserId = actor?.id || ctx?.userId || null;
        if (candidateUserId) {
          const userExists = await prisma.user.findUnique({ where: { id: candidateUserId }, select: { id:true } });
          if (userExists) validUserId = userExists.id;
        }

        await prisma.auditLog.create({ data: {
          familyId: validFamilyId,
          userId: validUserId,
          userEmail: actor?.email || null,
          userName: actor?.name || null,
          action: `${req.method} ${req.path}`,
          method: req.method,
          path: req.path,
          statusCode: res.statusCode,
          entity: req.path.split('/')[1] || null,
          entityId: req.params?.id || null,
          ip: req.ip,
          userAgent: req.headers['user-agent'] || null,
          payload: (req.method === 'GET' ? null : redactPayload(req.body || {})) as any
        } });
      } catch (err:any) { console.error('audit-log-error', err?.message || err); }
    });
    next();
  });
  const config = new DocumentBuilder().setTitle('TudoCerto API').setDescription('API financeira familiar').setVersion('1.4.3').addBearerAuth().build();
  SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config));
  await app.listen(4000);
}
bootstrap();
