import { query, queryOne, getClient, PoolClient } from '../../config/database.js';
import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
import { AccountMappingType } from './entities/account-mapping.entity.js';
import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js';
import { logger } from '../../shared/utils/logger.js';

// ============================================================================
// TYPES
// ============================================================================

export interface AccountMapping {
  id: string;
  tenant_id: string;
  company_id: string;
  mapping_type: AccountMappingType | string;
  account_id: string;
  account_code?: string;
  account_name?: string;
  description: string | null;
  is_active: boolean;
}

export interface JournalEntryLineInput {
  account_id: string;
  partner_id?: string;
  debit: number;
  credit: number;
  description?: string;
  ref?: string;
}

export interface PostingResult {
  journal_entry_id: string;
  journal_entry_name: string;
  total_debit: number;
  total_credit: number;
  lines_count: number;
}

export interface InvoiceForPosting {
  id: string;
  tenant_id: string;
  company_id: string;
  partner_id: string;
  partner_name?: string;
  invoice_type: 'customer' | 'supplier';
  number: string;
  invoice_date: Date;
  amount_untaxed: number;
  amount_tax: number;
  amount_total: number;
  journal_id?: string;
  lines: InvoiceLineForPosting[];
}

export interface InvoiceLineForPosting {
  id: string;
  product_id?: string;
  description: string;
  quantity: number;
  price_unit: number;
  amount_untaxed: number;
  amount_tax: number;
  amount_total: number;
  account_id?: string;
  tax_ids: string[];
}

// ============================================================================
// SERVICE
// ============================================================================

class GLPostingService {
  /**
   * Get account mapping for a specific type and company
   */
  async getMapping(
    mappingType: AccountMappingType | string,
    tenantId: string,
    companyId: string
  ): Promise<AccountMapping | null> {
    const mapping = await queryOne<AccountMapping>(
      `SELECT am.*, a.code as account_code, a.name as account_name
       FROM financial.account_mappings am
       LEFT JOIN financial.accounts a ON am.account_id = a.id
       WHERE am.tenant_id = $1 AND am.company_id = $2 AND am.mapping_type = $3 AND am.is_active = true`,
      [tenantId, companyId, mappingType]
    );
    return mapping;
  }

  /**
   * Get all active mappings for a company
   */
  async getMappings(tenantId: string, companyId: string): Promise<AccountMapping[]> {
    return query<AccountMapping>(
      `SELECT am.*, a.code as account_code, a.name as account_name
       FROM financial.account_mappings am
       LEFT JOIN financial.accounts a ON am.account_id = a.id
       WHERE am.tenant_id = $1 AND am.company_id = $2 AND am.is_active = true
       ORDER BY am.mapping_type`,
      [tenantId, companyId]
    );
  }

  /**
   * Create or update an account mapping
   */
  async setMapping(
    mappingType: AccountMappingType | string,
    accountId: string,
    tenantId: string,
    companyId: string,
    description?: string,
    userId?: string
  ): Promise<AccountMapping> {
    const result = await queryOne<AccountMapping>(
      `INSERT INTO financial.account_mappings (tenant_id, company_id, mapping_type, account_id, description, created_by)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (tenant_id, company_id, mapping_type)
       DO UPDATE SET account_id = $4, description = $5, updated_by = $6, updated_at = CURRENT_TIMESTAMP
       RETURNING *`,
      [tenantId, companyId, mappingType, accountId, description, userId]
    );
    return result!;
  }

  /**
   * Create a journal entry from a validated invoice
   *
   * For customer invoice (sale):
   * - Debit: Accounts Receivable (partner balance)
   * - Credit: Sales Revenue (per line or default mapping)
   * - Credit: Tax Payable (if taxes apply)
   *
   * For supplier invoice (bill):
   * - Credit: Accounts Payable (partner balance)
   * - Debit: Purchase Expense (per line or default mapping)
   * - Debit: Tax Receivable (if taxes apply)
   */
  async createInvoicePosting(
    invoice: InvoiceForPosting,
    userId: string
  ): Promise<PostingResult> {
    const { tenant_id: tenantId, company_id: companyId } = invoice;

    logger.info('Creating GL posting for invoice', {
      invoiceId: invoice.id,
      invoiceNumber: invoice.number,
      invoiceType: invoice.invoice_type,
    });

    // Validate invoice has lines
    if (!invoice.lines || invoice.lines.length === 0) {
      throw new ValidationError('La factura debe tener al menos una línea para contabilizar');
    }

    // Get required account mappings based on invoice type
    const isCustomerInvoice = invoice.invoice_type === 'customer';

    // Get receivable/payable account
    const partnerAccountType = isCustomerInvoice
      ? AccountMappingType.CUSTOMER_INVOICE
      : AccountMappingType.SUPPLIER_INVOICE;
    const partnerMapping = await this.getMapping(partnerAccountType, tenantId, companyId);

    if (!partnerMapping) {
      throw new ValidationError(
        `No hay cuenta configurada para ${isCustomerInvoice ? 'Cuentas por Cobrar' : 'Cuentas por Pagar'}. Configure account_mappings.`
      );
    }

    // Get default revenue/expense account
    const revenueExpenseType = isCustomerInvoice
      ? AccountMappingType.SALES_REVENUE
      : AccountMappingType.PURCHASE_EXPENSE;
    const revenueExpenseMapping = await this.getMapping(revenueExpenseType, tenantId, companyId);

    // Get tax accounts if there are taxes
    let taxPayableMapping: AccountMapping | null = null;
    let taxReceivableMapping: AccountMapping | null = null;

    if (invoice.amount_tax > 0) {
      if (isCustomerInvoice) {
        taxPayableMapping = await this.getMapping(AccountMappingType.TAX_PAYABLE, tenantId, companyId);
        if (!taxPayableMapping) {
          throw new ValidationError('No hay cuenta configurada para IVA por Pagar');
        }
      } else {
        taxReceivableMapping = await this.getMapping(AccountMappingType.TAX_RECEIVABLE, tenantId, companyId);
        if (!taxReceivableMapping) {
          throw new ValidationError('No hay cuenta configurada para IVA por Recuperar');
        }
      }
    }

    // Build journal entry lines
    const jeLines: JournalEntryLineInput[] = [];

    // Line 1: Partner account (AR/AP)
    if (isCustomerInvoice) {
      // Customer invoice: Debit AR
      jeLines.push({
        account_id: partnerMapping.account_id,
        partner_id: invoice.partner_id,
        debit: invoice.amount_total,
        credit: 0,
        description: `Factura ${invoice.number} - ${invoice.partner_name || 'Cliente'}`,
        ref: invoice.number,
      });
    } else {
      // Supplier invoice: Credit AP
      jeLines.push({
        account_id: partnerMapping.account_id,
        partner_id: invoice.partner_id,
        debit: 0,
        credit: invoice.amount_total,
        description: `Factura ${invoice.number} - ${invoice.partner_name || 'Proveedor'}`,
        ref: invoice.number,
      });
    }

    // Lines for each invoice line (revenue/expense)
    for (const line of invoice.lines) {
      // Use line's account_id if specified, otherwise use default mapping
      const lineAccountId = line.account_id || revenueExpenseMapping?.account_id;

      if (!lineAccountId) {
        throw new ValidationError(
          `No hay cuenta de ${isCustomerInvoice ? 'ingresos' : 'gastos'} configurada para la línea: ${line.description}`
        );
      }

      if (isCustomerInvoice) {
        // Customer invoice: Credit Revenue
        jeLines.push({
          account_id: lineAccountId,
          debit: 0,
          credit: line.amount_untaxed,
          description: line.description,
          ref: invoice.number,
        });
      } else {
        // Supplier invoice: Debit Expense
        jeLines.push({
          account_id: lineAccountId,
          debit: line.amount_untaxed,
          credit: 0,
          description: line.description,
          ref: invoice.number,
        });
      }
    }

    // Tax line if applicable
    if (invoice.amount_tax > 0) {
      if (isCustomerInvoice && taxPayableMapping) {
        // Customer invoice: Credit Tax Payable
        jeLines.push({
          account_id: taxPayableMapping.account_id,
          debit: 0,
          credit: invoice.amount_tax,
          description: `IVA - Factura ${invoice.number}`,
          ref: invoice.number,
        });
      } else if (!isCustomerInvoice && taxReceivableMapping) {
        // Supplier invoice: Debit Tax Receivable
        jeLines.push({
          account_id: taxReceivableMapping.account_id,
          debit: invoice.amount_tax,
          credit: 0,
          description: `IVA - Factura ${invoice.number}`,
          ref: invoice.number,
        });
      }
    }

    // Validate balance
    const totalDebit = jeLines.reduce((sum, l) => sum + l.debit, 0);
    const totalCredit = jeLines.reduce((sum, l) => sum + l.credit, 0);

    if (Math.abs(totalDebit - totalCredit) > 0.01) {
      logger.error('Journal entry not balanced', {
        invoiceId: invoice.id,
        totalDebit,
        totalCredit,
        difference: totalDebit - totalCredit,
      });
      throw new ValidationError(
        `El asiento contable no está balanceado. Débitos: ${totalDebit.toFixed(2)}, Créditos: ${totalCredit.toFixed(2)}`
      );
    }

    // Get journal (use invoice's journal or find default)
    let journalId = invoice.journal_id;
    if (!journalId) {
      const journalType = isCustomerInvoice ? 'sale' : 'purchase';
      const defaultJournal = await queryOne<{ id: string }>(
        `SELECT id FROM financial.journals
         WHERE tenant_id = $1 AND company_id = $2 AND type = $3 AND is_active = true
         LIMIT 1`,
        [tenantId, companyId, journalType]
      );

      if (!defaultJournal) {
        throw new ValidationError(
          `No hay diario de ${isCustomerInvoice ? 'ventas' : 'compras'} configurado`
        );
      }
      journalId = defaultJournal.id;
    }

    // Create journal entry
    const client = await getClient();
    try {
      await client.query('BEGIN');

      // Generate journal entry number
      const jeName = await sequencesService.getNextNumber(SEQUENCE_CODES.JOURNAL_ENTRY, tenantId);

      // Create entry header
      const entryResult = await client.query(
        `INSERT INTO financial.journal_entries (
          tenant_id, company_id, journal_id, name, ref, date, notes, status, created_by
        )
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'posted', $8)
         RETURNING id, name`,
        [
          tenantId,
          companyId,
          journalId,
          jeName,
          invoice.number,
          invoice.invoice_date,
          `Asiento automático - Factura ${invoice.number}`,
          userId,
        ]
      );
      const journalEntry = entryResult.rows[0];

      // Create entry lines
      for (const line of jeLines) {
        await client.query(
          `INSERT INTO financial.journal_entry_lines (
            entry_id, tenant_id, account_id, partner_id, debit, credit, description, ref
          )
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
          [
            journalEntry.id,
            tenantId,
            line.account_id,
            line.partner_id,
            line.debit,
            line.credit,
            line.description,
            line.ref,
          ]
        );
      }

      // Update journal entry posted_at
      await client.query(
        `UPDATE financial.journal_entries SET posted_at = CURRENT_TIMESTAMP, posted_by = $1 WHERE id = $2`,
        [userId, journalEntry.id]
      );

      await client.query('COMMIT');

      logger.info('GL posting created successfully', {
        invoiceId: invoice.id,
        journalEntryId: journalEntry.id,
        journalEntryName: journalEntry.name,
        totalDebit,
        totalCredit,
        linesCount: jeLines.length,
      });

      return {
        journal_entry_id: journalEntry.id,
        journal_entry_name: journalEntry.name,
        total_debit: totalDebit,
        total_credit: totalCredit,
        lines_count: jeLines.length,
      };
    } catch (error) {
      await client.query('ROLLBACK');
      logger.error('Error creating GL posting', {
        invoiceId: invoice.id,
        error: (error as Error).message,
      });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * Create a journal entry from a posted payment
   *
   * For inbound payment (from customer):
   * - Debit: Cash/Bank account
   * - Credit: Accounts Receivable
   *
   * For outbound payment (to supplier):
   * - Credit: Cash/Bank account
   * - Debit: Accounts Payable
   */
  async createPaymentPosting(
    payment: {
      id: string;
      tenant_id: string;
      company_id: string;
      partner_id: string;
      partner_name?: string;
      payment_type: 'inbound' | 'outbound';
      amount: number;
      payment_date: Date;
      ref?: string;
      journal_id: string;
    },
    userId: string,
    client?: PoolClient
  ): Promise<PostingResult> {
    const { tenant_id: tenantId, company_id: companyId } = payment;
    const isInbound = payment.payment_type === 'inbound';

    logger.info('Creating GL posting for payment', {
      paymentId: payment.id,
      paymentType: payment.payment_type,
      amount: payment.amount,
    });

    // Get cash/bank account from journal
    const journal = await queryOne<{ default_debit_account_id: string; default_credit_account_id: string }>(
      `SELECT default_debit_account_id, default_credit_account_id FROM financial.journals WHERE id = $1`,
      [payment.journal_id]
    );

    if (!journal) {
      throw new ValidationError('Diario de pago no encontrado');
    }

    const cashAccountId = isInbound ? journal.default_debit_account_id : journal.default_credit_account_id;
    if (!cashAccountId) {
      throw new ValidationError('El diario no tiene cuenta de efectivo/banco configurada');
    }

    // Get AR/AP account
    const partnerAccountType = isInbound
      ? AccountMappingType.CUSTOMER_PAYMENT
      : AccountMappingType.SUPPLIER_PAYMENT;
    let partnerMapping = await this.getMapping(partnerAccountType, tenantId, companyId);

    // Fall back to invoice mapping if payment-specific not configured
    if (!partnerMapping) {
      const fallbackType = isInbound
        ? AccountMappingType.CUSTOMER_INVOICE
        : AccountMappingType.SUPPLIER_INVOICE;
      partnerMapping = await this.getMapping(fallbackType, tenantId, companyId);
    }

    if (!partnerMapping) {
      throw new ValidationError(
        `No hay cuenta configurada para ${isInbound ? 'Cuentas por Cobrar' : 'Cuentas por Pagar'}`
      );
    }

    // Build journal entry lines
    const jeLines: JournalEntryLineInput[] = [];
    const paymentRef = payment.ref || `PAY-${payment.id.substring(0, 8)}`;

    if (isInbound) {
      // Inbound: Debit Cash, Credit AR
      jeLines.push({
        account_id: cashAccountId,
        debit: payment.amount,
        credit: 0,
        description: `Pago recibido - ${payment.partner_name || 'Cliente'}`,
        ref: paymentRef,
      });
      jeLines.push({
        account_id: partnerMapping.account_id,
        partner_id: payment.partner_id,
        debit: 0,
        credit: payment.amount,
        description: `Pago recibido - ${payment.partner_name || 'Cliente'}`,
        ref: paymentRef,
      });
    } else {
      // Outbound: Credit Cash, Debit AP
      jeLines.push({
        account_id: cashAccountId,
        debit: 0,
        credit: payment.amount,
        description: `Pago emitido - ${payment.partner_name || 'Proveedor'}`,
        ref: paymentRef,
      });
      jeLines.push({
        account_id: partnerMapping.account_id,
        partner_id: payment.partner_id,
        debit: payment.amount,
        credit: 0,
        description: `Pago emitido - ${payment.partner_name || 'Proveedor'}`,
        ref: paymentRef,
      });
    }

    // Create journal entry
    const ownClient = !client;
    const dbClient = client || await getClient();

    try {
      if (ownClient) {
        await dbClient.query('BEGIN');
      }

      // Generate journal entry number
      const jeName = await sequencesService.getNextNumber(SEQUENCE_CODES.JOURNAL_ENTRY, tenantId);

      // Create entry header
      const entryResult = await dbClient.query(
        `INSERT INTO financial.journal_entries (
          tenant_id, company_id, journal_id, name, ref, date, notes, status, created_by, posted_at, posted_by
        )
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'posted', $8, CURRENT_TIMESTAMP, $8)
         RETURNING id, name`,
        [
          tenantId,
          companyId,
          payment.journal_id,
          jeName,
          paymentRef,
          payment.payment_date,
          `Asiento automático - Pago ${paymentRef}`,
          userId,
        ]
      );
      const journalEntry = entryResult.rows[0];

      // Create entry lines
      for (const line of jeLines) {
        await dbClient.query(
          `INSERT INTO financial.journal_entry_lines (
            entry_id, tenant_id, account_id, partner_id, debit, credit, description, ref
          )
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
          [
            journalEntry.id,
            tenantId,
            line.account_id,
            line.partner_id,
            line.debit,
            line.credit,
            line.description,
            line.ref,
          ]
        );
      }

      if (ownClient) {
        await dbClient.query('COMMIT');
      }

      logger.info('Payment GL posting created successfully', {
        paymentId: payment.id,
        journalEntryId: journalEntry.id,
        journalEntryName: journalEntry.name,
      });

      return {
        journal_entry_id: journalEntry.id,
        journal_entry_name: journalEntry.name,
        total_debit: payment.amount,
        total_credit: payment.amount,
        lines_count: 2,
      };
    } catch (error) {
      if (ownClient) {
        await dbClient.query('ROLLBACK');
      }
      throw error;
    } finally {
      if (ownClient) {
        dbClient.release();
      }
    }
  }

  /**
   * Reverse a journal entry (create a contra entry)
   */
  async reversePosting(
    journalEntryId: string,
    tenantId: string,
    reason: string,
    userId: string
  ): Promise<PostingResult> {
    // Get original entry
    const originalEntry = await queryOne<{
      id: string;
      company_id: string;
      journal_id: string;
      name: string;
      ref: string;
      date: Date;
    }>(
      `SELECT id, company_id, journal_id, name, ref, date
       FROM financial.journal_entries WHERE id = $1 AND tenant_id = $2`,
      [journalEntryId, tenantId]
    );

    if (!originalEntry) {
      throw new NotFoundError('Asiento contable no encontrado');
    }

    // Get original lines
    const originalLines = await query<JournalEntryLineInput & { id: string }>(
      `SELECT account_id, partner_id, debit, credit, description, ref
       FROM financial.journal_entry_lines WHERE entry_id = $1`,
      [journalEntryId]
    );

    // Reverse debits and credits
    const reversedLines: JournalEntryLineInput[] = originalLines.map(line => ({
      account_id: line.account_id,
      partner_id: line.partner_id,
      debit: line.credit, // Swap
      credit: line.debit, // Swap
      description: `Reverso: ${line.description || ''}`,
      ref: line.ref,
    }));

    const client = await getClient();
    try {
      await client.query('BEGIN');

      // Generate new entry number
      const jeName = await sequencesService.getNextNumber(SEQUENCE_CODES.JOURNAL_ENTRY, tenantId);

      // Create reversal entry
      const entryResult = await client.query(
        `INSERT INTO financial.journal_entries (
          tenant_id, company_id, journal_id, name, ref, date, notes, status, created_by, posted_at, posted_by
        )
         VALUES ($1, $2, $3, $4, $5, CURRENT_DATE, $6, 'posted', $7, CURRENT_TIMESTAMP, $7)
         RETURNING id, name`,
        [
          tenantId,
          originalEntry.company_id,
          originalEntry.journal_id,
          jeName,
          `REV-${originalEntry.name}`,
          `Reverso de ${originalEntry.name}: ${reason}`,
          userId,
        ]
      );
      const reversalEntry = entryResult.rows[0];

      // Create reversal lines
      for (const line of reversedLines) {
        await client.query(
          `INSERT INTO financial.journal_entry_lines (
            entry_id, tenant_id, account_id, partner_id, debit, credit, description, ref
          )
           VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
          [
            reversalEntry.id,
            tenantId,
            line.account_id,
            line.partner_id,
            line.debit,
            line.credit,
            line.description,
            line.ref,
          ]
        );
      }

      // Mark original as cancelled
      await client.query(
        `UPDATE financial.journal_entries SET status = 'cancelled', cancelled_at = CURRENT_TIMESTAMP, cancelled_by = $1 WHERE id = $2`,
        [userId, journalEntryId]
      );

      await client.query('COMMIT');

      const totalDebit = reversedLines.reduce((sum, l) => sum + l.debit, 0);

      logger.info('GL posting reversed', {
        originalEntryId: journalEntryId,
        reversalEntryId: reversalEntry.id,
        reason,
      });

      return {
        journal_entry_id: reversalEntry.id,
        journal_entry_name: reversalEntry.name,
        total_debit: totalDebit,
        total_credit: totalDebit,
        lines_count: reversedLines.length,
      };
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
}

export const glPostingService = new GLPostingService();
