/**
 * InvoiceService - Invoice Management Service
 *
 * Handles unified invoice management for both commercial and SaaS contexts.
 * Includes CRUD operations, invoice generation, and status workflow.
 *
 * @module Invoices
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import {
  Invoice,
  InvoiceType,
  InvoiceStatus,
  InvoiceContext,
  InvoiceItem,
} from '../entities';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface CreateInvoiceDto {
  invoiceType?: InvoiceType;
  invoiceContext?: InvoiceContext;
  // Commercial fields
  salesOrderId?: string;
  purchaseOrderId?: string;
  partnerId?: string;
  partnerName?: string;
  partnerTaxId?: string;
  // SaaS billing fields
  subscriptionId?: string;
  periodStart?: Date;
  periodEnd?: Date;
  // Billing information
  billingName?: string;
  billingEmail?: string;
  billingAddress?: Record<string, any>;
  taxId?: string;
  // Dates
  invoiceDate?: Date;
  dueDate?: Date;
  // Payment
  currency?: string;
  exchangeRate?: number;
  paymentTermDays?: number;
  paymentMethod?: string;
  // Notes
  notes?: string;
  internalNotes?: string;
  // Items
  items?: CreateInvoiceItemDto[];
}

export interface CreateInvoiceItemDto {
  productId?: string;
  lineNumber?: number;
  productSku?: string;
  productName: string;
  description?: string;
  satProductCode?: string;
  satUnitCode?: string;
  quantity: number;
  uom?: string;
  unitPrice: number;
  discountPercent?: number;
  taxRate?: number;
  withholdingRate?: number;
}

export interface InvoiceFilters {
  invoiceType?: InvoiceType;
  invoiceContext?: InvoiceContext;
  status?: InvoiceStatus;
  partnerId?: string;
  startDate?: Date;
  endDate?: Date;
  overdue?: boolean;
  search?: string;
}

interface InvoiceSummary {
  totalInvoices: number;
  totalAmount: number;
  totalPaid: number;
  totalDue: number;
  byStatus: Partial<Record<InvoiceStatus, { count: number; amount: number }>>;
}

interface DashboardStats {
  totalPending: number;
  totalOverdue: number;
  dueThisWeek: number;
  dueThisMonth: number;
  countPending: number;
  countOverdue: number;
  recentInvoices: Invoice[];
}

export class InvoiceService {
  private repository: Repository<Invoice>;
  private itemRepository: Repository<InvoiceItem>;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(Invoice);
    this.itemRepository = dataSource.getRepository(InvoiceItem);
  }

  // ==================== CRUD OPERATIONS ====================

  /**
   * Find all invoices with pagination and filters
   */
  async findAll(
    ctx: ServiceContext,
    filters: InvoiceFilters = {},
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Invoice>> {
    const queryBuilder = this.repository
      .createQueryBuilder('invoice')
      .where('invoice.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('invoice.deletedAt IS NULL');

    if (filters.invoiceType) {
      queryBuilder.andWhere('invoice.invoiceType = :invoiceType', {
        invoiceType: filters.invoiceType,
      });
    }

    if (filters.invoiceContext) {
      queryBuilder.andWhere('invoice.invoiceContext = :invoiceContext', {
        invoiceContext: filters.invoiceContext,
      });
    }

    if (filters.status) {
      queryBuilder.andWhere('invoice.status = :status', { status: filters.status });
    }

    if (filters.partnerId) {
      queryBuilder.andWhere('invoice.partnerId = :partnerId', {
        partnerId: filters.partnerId,
      });
    }

    if (filters.startDate) {
      queryBuilder.andWhere('invoice.invoiceDate >= :startDate', {
        startDate: filters.startDate,
      });
    }

    if (filters.endDate) {
      queryBuilder.andWhere('invoice.invoiceDate <= :endDate', {
        endDate: filters.endDate,
      });
    }

    if (filters.overdue) {
      queryBuilder.andWhere('invoice.dueDate < :today', { today: new Date() });
      queryBuilder.andWhere('invoice.status IN (:...pendingStatuses)', {
        pendingStatuses: ['draft', 'validated', 'sent', 'partial'],
      });
    }

    if (filters.search) {
      queryBuilder.andWhere(
        '(invoice.invoiceNumber ILIKE :search OR invoice.partnerName ILIKE :search OR invoice.billingName ILIKE :search)',
        { search: `%${filters.search}%` }
      );
    }

    queryBuilder.orderBy('invoice.invoiceDate', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Find invoice by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Invoice | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  /**
   * Find invoice with items
   */
  async findWithItems(ctx: ServiceContext, id: string): Promise<Invoice | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['items'],
    });
  }

  /**
   * Find invoice by number
   */
  async findByNumber(ctx: ServiceContext, invoiceNumber: string): Promise<Invoice | null> {
    return this.repository.findOne({
      where: {
        invoiceNumber,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['items'],
    });
  }

  /**
   * Create new invoice
   */
  async create(ctx: ServiceContext, data: CreateInvoiceDto): Promise<Invoice> {
    const invoiceNumber = await this.generateInvoiceNumber(ctx, data.invoiceType || 'sale');

    const invoice = this.repository.create({
      tenantId: ctx.tenantId,
      invoiceNumber,
      invoiceType: data.invoiceType || 'sale',
      invoiceContext: data.invoiceContext || 'commercial',
      salesOrderId: data.salesOrderId,
      purchaseOrderId: data.purchaseOrderId,
      partnerId: data.partnerId,
      partnerName: data.partnerName,
      partnerTaxId: data.partnerTaxId,
      subscriptionId: data.subscriptionId,
      periodStart: data.periodStart,
      periodEnd: data.periodEnd,
      billingName: data.billingName,
      billingEmail: data.billingEmail,
      billingAddress: data.billingAddress,
      taxId: data.taxId,
      invoiceDate: data.invoiceDate || new Date(),
      dueDate: data.dueDate || this.calculateDueDate(data.invoiceDate, data.paymentTermDays),
      currency: data.currency || 'MXN',
      exchangeRate: data.exchangeRate || 1,
      paymentTermDays: data.paymentTermDays || 30,
      paymentMethod: data.paymentMethod,
      notes: data.notes,
      internalNotes: data.internalNotes,
      status: 'draft',
      createdBy: ctx.userId,
    });

    const savedInvoice = await this.repository.save(invoice);

    // Add items if provided
    if (data.items && data.items.length > 0) {
      await this.addItems(ctx, savedInvoice.id, data.items);
      await this.recalculateTotals(ctx, savedInvoice.id);
    }

    return this.findWithItems(ctx, savedInvoice.id) as Promise<Invoice>;
  }

  /**
   * Update invoice
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreateInvoiceDto>
  ): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (invoice.status !== 'draft') {
      throw new Error('Solo se pueden modificar facturas en borrador');
    }

    const updateData: Partial<Invoice> = {};

    if (data.partnerId !== undefined) updateData.partnerId = data.partnerId;
    if (data.partnerName !== undefined) updateData.partnerName = data.partnerName;
    if (data.partnerTaxId !== undefined) updateData.partnerTaxId = data.partnerTaxId;
    if (data.billingName !== undefined) updateData.billingName = data.billingName;
    if (data.billingEmail !== undefined) updateData.billingEmail = data.billingEmail;
    if (data.billingAddress !== undefined) updateData.billingAddress = data.billingAddress;
    if (data.taxId !== undefined) updateData.taxId = data.taxId;
    if (data.invoiceDate !== undefined) updateData.invoiceDate = data.invoiceDate;
    if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
    if (data.currency !== undefined) updateData.currency = data.currency;
    if (data.exchangeRate !== undefined) updateData.exchangeRate = data.exchangeRate;
    if (data.paymentTermDays !== undefined) updateData.paymentTermDays = data.paymentTermDays;
    if (data.paymentMethod !== undefined) updateData.paymentMethod = data.paymentMethod;
    if (data.notes !== undefined) updateData.notes = data.notes;
    if (data.internalNotes !== undefined) updateData.internalNotes = data.internalNotes;

    updateData.updatedBy = ctx.userId;

    Object.assign(invoice, updateData);
    await this.repository.save(invoice);

    return this.findWithItems(ctx, id);
  }

  /**
   * Soft delete invoice
   */
  async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return false;
    }

    if (invoice.status !== 'draft') {
      throw new Error('Solo se pueden eliminar facturas en borrador');
    }

    invoice.deletedAt = new Date();
    await this.repository.save(invoice);
    return true;
  }

  // ==================== INVOICE ITEMS ====================

  /**
   * Add items to invoice
   */
  async addItems(
    ctx: ServiceContext,
    invoiceId: string,
    items: CreateInvoiceItemDto[]
  ): Promise<InvoiceItem[]> {
    const invoice = await this.findById(ctx, invoiceId);
    if (!invoice) {
      throw new Error('Factura no encontrada');
    }

    if (invoice.status !== 'draft') {
      throw new Error('Solo se pueden agregar items a facturas en borrador');
    }

    const savedItems: InvoiceItem[] = [];
    let lineNumber = 1;

    // Get existing max line number
    const existingItems = await this.itemRepository.find({
      where: { invoiceId },
      order: { lineNumber: 'DESC' },
      take: 1,
    });

    if (existingItems.length > 0) {
      lineNumber = existingItems[0].lineNumber + 1;
    }

    for (const itemData of items) {
      const item = this.itemRepository.create({
        invoiceId,
        productId: itemData.productId,
        lineNumber: itemData.lineNumber || lineNumber++,
        productSku: itemData.productSku,
        productName: itemData.productName,
        description: itemData.description,
        satProductCode: itemData.satProductCode,
        satUnitCode: itemData.satUnitCode,
        quantity: itemData.quantity,
        uom: itemData.uom || 'PZA',
        unitPrice: itemData.unitPrice,
        discountPercent: itemData.discountPercent || 0,
        taxRate: itemData.taxRate ?? 16.0,
        withholdingRate: itemData.withholdingRate || 0,
      });

      // Calculate amounts
      const subtotal = item.quantity * item.unitPrice;
      const discountAmount = subtotal * (item.discountPercent / 100);
      const afterDiscount = subtotal - discountAmount;
      const taxAmount = afterDiscount * (item.taxRate / 100);
      const withholdingAmount = afterDiscount * (item.withholdingRate / 100);
      const total = afterDiscount + taxAmount - withholdingAmount;

      item.subtotal = afterDiscount;
      item.discountAmount = discountAmount;
      item.taxAmount = taxAmount;
      item.withholdingAmount = withholdingAmount;
      item.total = total;

      savedItems.push(await this.itemRepository.save(item));
    }

    await this.recalculateTotals(ctx, invoiceId);
    return savedItems;
  }

  /**
   * Update invoice item
   */
  async updateItem(
    ctx: ServiceContext,
    itemId: string,
    data: Partial<CreateInvoiceItemDto>
  ): Promise<InvoiceItem | null> {
    const item = await this.itemRepository.findOne({
      where: { id: itemId },
      relations: ['invoice'],
    });

    if (!item || item.invoice.tenantId !== ctx.tenantId) {
      return null;
    }

    if (item.invoice.status !== 'draft') {
      throw new Error('Solo se pueden modificar items de facturas en borrador');
    }

    if (data.productName !== undefined) item.productName = data.productName;
    if (data.description !== undefined) item.description = data.description;
    if (data.quantity !== undefined) item.quantity = data.quantity;
    if (data.unitPrice !== undefined) item.unitPrice = data.unitPrice;
    if (data.discountPercent !== undefined) item.discountPercent = data.discountPercent;
    if (data.taxRate !== undefined) item.taxRate = data.taxRate;
    if (data.withholdingRate !== undefined) item.withholdingRate = data.withholdingRate;

    // Recalculate amounts
    const subtotal = item.quantity * item.unitPrice;
    const discountAmount = subtotal * (item.discountPercent / 100);
    const afterDiscount = subtotal - discountAmount;
    const taxAmount = afterDiscount * (item.taxRate / 100);
    const withholdingAmount = afterDiscount * (item.withholdingRate / 100);
    const total = afterDiscount + taxAmount - withholdingAmount;

    item.subtotal = afterDiscount;
    item.discountAmount = discountAmount;
    item.taxAmount = taxAmount;
    item.withholdingAmount = withholdingAmount;
    item.total = total;

    const savedItem = await this.itemRepository.save(item);
    await this.recalculateTotals(ctx, item.invoiceId);

    return savedItem;
  }

  /**
   * Remove invoice item
   */
  async removeItem(ctx: ServiceContext, itemId: string): Promise<boolean> {
    const item = await this.itemRepository.findOne({
      where: { id: itemId },
      relations: ['invoice'],
    });

    if (!item || item.invoice.tenantId !== ctx.tenantId) {
      return false;
    }

    if (item.invoice.status !== 'draft') {
      throw new Error('Solo se pueden eliminar items de facturas en borrador');
    }

    const invoiceId = item.invoiceId;
    await this.itemRepository.remove(item);
    await this.recalculateTotals(ctx, invoiceId);

    return true;
  }

  // ==================== STATUS WORKFLOW ====================

  /**
   * Validate invoice (draft -> validated)
   */
  async validate(ctx: ServiceContext, id: string): Promise<Invoice | null> {
    return this.changeStatus(ctx, id, 'validated', ['draft']);
  }

  /**
   * Send invoice (validated -> sent)
   */
  async send(ctx: ServiceContext, id: string): Promise<Invoice | null> {
    return this.changeStatus(ctx, id, 'sent', ['validated']);
  }

  /**
   * Record partial payment
   */
  async recordPartialPayment(
    ctx: ServiceContext,
    id: string,
    amount: number
  ): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (!['sent', 'partial', 'overdue'].includes(invoice.status)) {
      throw new Error('Estado invalido para registrar pago');
    }

    const newAmountPaid = Number(invoice.amountPaid) + amount;
    const total = Number(invoice.total);

    invoice.amountPaid = newAmountPaid;
    invoice.paidAmount = newAmountPaid;
    invoice.updatedBy = ctx.userId ?? null;

    if (newAmountPaid >= total) {
      invoice.status = 'paid';
      invoice.paidAt = new Date();
      invoice.paymentDate = new Date();
    } else {
      invoice.status = 'partial';
    }

    return this.repository.save(invoice);
  }

  /**
   * Mark as paid (sent/partial -> paid)
   */
  async markAsPaid(
    ctx: ServiceContext,
    id: string,
    paymentReference?: string
  ): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (!['sent', 'partial', 'overdue'].includes(invoice.status)) {
      throw new Error('Estado invalido para marcar como pagada');
    }

    invoice.status = 'paid';
    invoice.amountPaid = invoice.total;
    invoice.paidAmount = invoice.total;
    invoice.paidAt = new Date();
    invoice.paymentDate = new Date();
    invoice.paymentReference = paymentReference ?? invoice.paymentReference;
    invoice.updatedBy = ctx.userId ?? null;

    return this.repository.save(invoice);
  }

  /**
   * Void invoice
   */
  async void(ctx: ServiceContext, id: string, reason?: string): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (invoice.status === 'paid') {
      throw new Error('No se puede anular una factura pagada');
    }

    invoice.status = 'voided';
    invoice.internalNotes = `${invoice.internalNotes || ''}\n[ANULADA]: ${reason || 'Sin motivo especificado'}`;
    invoice.updatedBy = ctx.userId ?? null;

    return this.repository.save(invoice);
  }

  /**
   * Cancel invoice
   */
  async cancel(ctx: ServiceContext, id: string, reason?: string): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (invoice.status === 'paid') {
      throw new Error('No se puede cancelar una factura pagada');
    }

    invoice.status = 'cancelled';
    invoice.internalNotes = `${invoice.internalNotes || ''}\n[CANCELADA]: ${reason || 'Sin motivo especificado'}`;
    invoice.updatedBy = ctx.userId ?? null;

    return this.repository.save(invoice);
  }

  /**
   * Create refund (credit note)
   */
  async createRefund(
    ctx: ServiceContext,
    id: string,
    reason?: string
  ): Promise<Invoice | null> {
    const originalInvoice = await this.findWithItems(ctx, id);
    if (!originalInvoice) {
      return null;
    }

    if (originalInvoice.status !== 'paid') {
      throw new Error('Solo se pueden reembolsar facturas pagadas');
    }

    // Create credit note
    const creditNoteData: CreateInvoiceDto = {
      invoiceType: 'credit_note',
      invoiceContext: originalInvoice.invoiceContext as InvoiceContext,
      partnerId: originalInvoice.partnerId ?? undefined,
      partnerName: originalInvoice.partnerName ?? undefined,
      partnerTaxId: originalInvoice.partnerTaxId ?? undefined,
      billingName: originalInvoice.billingName ?? undefined,
      billingEmail: originalInvoice.billingEmail ?? undefined,
      billingAddress: originalInvoice.billingAddress ?? undefined,
      currency: originalInvoice.currency,
      exchangeRate: originalInvoice.exchangeRate,
      notes: `Nota de credito por factura ${originalInvoice.invoiceNumber}. ${reason || ''}`,
      items: originalInvoice.items?.map((item) => ({
        productId: item.productId,
        productSku: item.productSku,
        productName: item.productName,
        description: item.description,
        quantity: item.quantity,
        uom: item.uom,
        unitPrice: item.unitPrice,
        discountPercent: item.discountPercent,
        taxRate: item.taxRate,
        withholdingRate: item.withholdingRate,
      })),
    };

    const creditNote = await this.create(ctx, creditNoteData);

    // Mark original as refunded
    originalInvoice.status = 'refunded';
    originalInvoice.updatedBy = ctx.userId ?? null;
    await this.repository.save(originalInvoice);

    return creditNote;
  }

  /**
   * Check and update overdue invoices
   */
  async updateOverdueStatus(ctx: ServiceContext): Promise<number> {
    const today = new Date();

    const result = await this.repository
      .createQueryBuilder()
      .update(Invoice)
      .set({ status: 'overdue' })
      .where('tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('status IN (:...statuses)', { statuses: ['sent', 'partial'] })
      .andWhere('dueDate < :today', { today })
      .andWhere('deletedAt IS NULL')
      .execute();

    return result.affected || 0;
  }

  // ==================== GENERATION ====================

  /**
   * Generate invoice from sales order
   */
  async generateFromSalesOrder(
    ctx: ServiceContext,
    salesOrderId: string,
    salesOrderData: {
      partnerId: string;
      partnerName: string;
      partnerTaxId?: string;
      items: CreateInvoiceItemDto[];
      currency?: string;
      paymentTermDays?: number;
    }
  ): Promise<Invoice> {
    const invoiceData: CreateInvoiceDto = {
      invoiceType: 'sale',
      invoiceContext: 'commercial',
      salesOrderId,
      partnerId: salesOrderData.partnerId,
      partnerName: salesOrderData.partnerName,
      partnerTaxId: salesOrderData.partnerTaxId,
      currency: salesOrderData.currency,
      paymentTermDays: salesOrderData.paymentTermDays,
      items: salesOrderData.items,
    };

    return this.create(ctx, invoiceData);
  }

  /**
   * Generate invoice for subscription period
   */
  async generateForSubscription(
    ctx: ServiceContext,
    subscriptionData: {
      subscriptionId: string;
      billingName: string;
      billingEmail: string;
      taxId?: string;
      periodStart: Date;
      periodEnd: Date;
      items: CreateInvoiceItemDto[];
      currency?: string;
    }
  ): Promise<Invoice> {
    const invoiceData: CreateInvoiceDto = {
      invoiceType: 'sale',
      invoiceContext: 'saas',
      subscriptionId: subscriptionData.subscriptionId,
      billingName: subscriptionData.billingName,
      billingEmail: subscriptionData.billingEmail,
      taxId: subscriptionData.taxId,
      periodStart: subscriptionData.periodStart,
      periodEnd: subscriptionData.periodEnd,
      currency: subscriptionData.currency,
      items: subscriptionData.items,
    };

    return this.create(ctx, invoiceData);
  }

  // ==================== REPORTS ====================

  /**
   * Get invoice summary
   */
  async getSummary(
    ctx: ServiceContext,
    filters: InvoiceFilters = {}
  ): Promise<InvoiceSummary> {
    const queryBuilder = this.repository
      .createQueryBuilder('invoice')
      .where('invoice.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('invoice.deletedAt IS NULL');

    if (filters.invoiceContext) {
      queryBuilder.andWhere('invoice.invoiceContext = :context', {
        context: filters.invoiceContext,
      });
    }

    if (filters.startDate) {
      queryBuilder.andWhere('invoice.invoiceDate >= :startDate', {
        startDate: filters.startDate,
      });
    }

    if (filters.endDate) {
      queryBuilder.andWhere('invoice.invoiceDate <= :endDate', {
        endDate: filters.endDate,
      });
    }

    const invoices = await queryBuilder.getMany();

    const summary: InvoiceSummary = {
      totalInvoices: invoices.length,
      totalAmount: 0,
      totalPaid: 0,
      totalDue: 0,
      byStatus: {},
    };

    for (const invoice of invoices) {
      const total = Number(invoice.total) || 0;
      const amountPaid = Number(invoice.amountPaid) || 0;

      summary.totalAmount += total;
      summary.totalPaid += amountPaid;
      summary.totalDue += total - amountPaid;

      if (!summary.byStatus[invoice.status]) {
        summary.byStatus[invoice.status] = { count: 0, amount: 0 };
      }
      summary.byStatus[invoice.status]!.count++;
      summary.byStatus[invoice.status]!.amount += total;
    }

    return summary;
  }

  /**
   * Get dashboard statistics
   */
  async getDashboardStats(ctx: ServiceContext): Promise<DashboardStats> {
    const today = new Date();
    const endOfWeek = new Date(today);
    endOfWeek.setDate(today.getDate() + (7 - today.getDay()));
    const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);

    const baseQuery = this.repository
      .createQueryBuilder('invoice')
      .where('invoice.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('invoice.status IN (:...statuses)', {
        statuses: ['draft', 'validated', 'sent', 'partial'],
      })
      .andWhere('invoice.deletedAt IS NULL');

    // Total pending
    const totalPendingResult = await baseQuery
      .clone()
      .select('SUM(invoice.total - invoice.amountPaid)', 'total')
      .getRawOne();

    // Total overdue
    const totalOverdueResult = await baseQuery
      .clone()
      .andWhere('invoice.dueDate < :today', { today })
      .select('SUM(invoice.total - invoice.amountPaid)', 'total')
      .getRawOne();

    // Due this week
    const dueThisWeekResult = await baseQuery
      .clone()
      .andWhere('invoice.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek })
      .select('SUM(invoice.total - invoice.amountPaid)', 'total')
      .getRawOne();

    // Due this month
    const dueThisMonthResult = await baseQuery
      .clone()
      .andWhere('invoice.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth })
      .select('SUM(invoice.total - invoice.amountPaid)', 'total')
      .getRawOne();

    // Counts
    const countPending = await baseQuery.clone().getCount();
    const countOverdue = await baseQuery
      .clone()
      .andWhere('invoice.dueDate < :today', { today })
      .getCount();

    // Recent invoices
    const recentInvoices = await this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      order: { createdAt: 'DESC' },
      take: 5,
    });

    return {
      totalPending: parseFloat(totalPendingResult?.total) || 0,
      totalOverdue: parseFloat(totalOverdueResult?.total) || 0,
      dueThisWeek: parseFloat(dueThisWeekResult?.total) || 0,
      dueThisMonth: parseFloat(dueThisMonthResult?.total) || 0,
      countPending,
      countOverdue,
      recentInvoices,
    };
  }

  // ==================== PRIVATE HELPERS ====================

  private async changeStatus(
    ctx: ServiceContext,
    id: string,
    newStatus: InvoiceStatus,
    allowedFromStatuses: InvoiceStatus[]
  ): Promise<Invoice | null> {
    const invoice = await this.findById(ctx, id);
    if (!invoice) {
      return null;
    }

    if (!allowedFromStatuses.includes(invoice.status)) {
      throw new Error(
        `Transicion de estado invalida: de ${invoice.status} a ${newStatus}`
      );
    }

    invoice.status = newStatus;
    invoice.updatedBy = ctx.userId ?? null;

    return this.repository.save(invoice);
  }

  private async generateInvoiceNumber(
    ctx: ServiceContext,
    invoiceType: InvoiceType
  ): Promise<string> {
    const year = new Date().getFullYear();
    const prefix = this.getInvoicePrefix(invoiceType);

    const lastInvoice = await this.repository
      .createQueryBuilder('invoice')
      .where('invoice.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('invoice.invoiceNumber LIKE :pattern', { pattern: `${prefix}-${year}%` })
      .orderBy('invoice.invoiceNumber', 'DESC')
      .getOne();

    let sequence = 1;
    if (lastInvoice) {
      const match = lastInvoice.invoiceNumber.match(/(\d+)$/);
      if (match) {
        sequence = parseInt(match[1], 10) + 1;
      }
    }

    return `${prefix}-${year}-${String(sequence).padStart(6, '0')}`;
  }

  private getInvoicePrefix(invoiceType: InvoiceType): string {
    switch (invoiceType) {
      case 'sale':
        return 'FAC';
      case 'purchase':
        return 'FP';
      case 'credit_note':
        return 'NC';
      case 'debit_note':
        return 'ND';
      default:
        return 'INV';
    }
  }

  private calculateDueDate(invoiceDate?: Date, paymentTermDays?: number): Date {
    const baseDate = invoiceDate || new Date();
    const days = paymentTermDays || 30;
    const dueDate = new Date(baseDate);
    dueDate.setDate(dueDate.getDate() + days);
    return dueDate;
  }

  private async recalculateTotals(ctx: ServiceContext, invoiceId: string): Promise<void> {
    const items = await this.itemRepository.find({ where: { invoiceId } });

    let subtotal = 0;
    let discountAmount = 0;
    let taxAmount = 0;
    let withholdingTax = 0;

    for (const item of items) {
      subtotal += Number(item.subtotal) || 0;
      discountAmount += Number(item.discountAmount) || 0;
      taxAmount += Number(item.taxAmount) || 0;
      withholdingTax += Number(item.withholdingAmount) || 0;
    }

    const total = subtotal + taxAmount - withholdingTax;

    await this.repository.update(
      { id: invoiceId, tenantId: ctx.tenantId },
      {
        subtotal,
        discountAmount,
        taxAmount,
        withholdingTax,
        total,
        updatedBy: ctx.userId,
      }
    );
  }
}
