import * as plugins from './plugins.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
import type {
  IInvoice,
  IInvoiceLine,
  IBookingRules,
  IBookingInfo,
  TTaxScenario,
  IPaymentInfo
} from './skr.invoice.entity.js';

/**
 * Options for booking an invoice
 */
export interface IBookingOptions {
  autoBook?: boolean;
  confidenceThreshold?: number;
  bookingDate?: Date;
  bookingReference?: string;
  skipValidation?: boolean;
}

/**
 * Result of booking an invoice
 */
export interface IBookingResult {
  success: boolean;
  journalEntry?: JournalEntry;
  bookingInfo?: IBookingInfo;
  confidence: number;
  warnings?: string[];
  errors?: string[];
}

/**
 * Automatic booking engine for invoices
 * Creates journal entries from invoice data based on SKR mapping rules
 */
export class InvoiceBookingEngine {
  private logger: plugins.smartlog.ConsoleLog;
  private skrType: TSKRType;
  private mapper: SKRInvoiceMapper;
  
  constructor(skrType: TSKRType) {
    this.skrType = skrType;
    this.mapper = new SKRInvoiceMapper(skrType);
    this.logger = new plugins.smartlog.ConsoleLog();
  }
  
  /**
   * Book an invoice to the ledger
   */
  public async bookInvoice(
    invoice: IInvoice,
    bookingRules?: Partial<IBookingRules>,
    options?: IBookingOptions
  ): Promise<IBookingResult> {
    try {
      // Get complete booking rules
      const rules = this.mapper.mapInvoiceToSKR(invoice, bookingRules);
      
      // Calculate confidence
      const confidence = this.mapper.calculateConfidence(invoice, rules);
      
      // Check if auto-booking is allowed
      if (options?.autoBook && confidence < (options.confidenceThreshold || 80)) {
        return {
          success: false,
          confidence,
          warnings: [`Confidence score ${confidence}% is below threshold ${options.confidenceThreshold || 80}%`]
        };
      }
      
      // Validate invoice before booking
      if (!options?.skipValidation) {
        const validationErrors = this.validateInvoice(invoice);
        if (validationErrors.length > 0) {
          return {
            success: false,
            confidence,
            errors: validationErrors
          };
        }
      }
      
      // Build journal entry
      const journalEntry = await this.buildJournalEntry(invoice, rules, options);
      
      // Create booking info
      const bookingInfo: IBookingInfo = {
        journalEntryId: journalEntry.id,
        transactionIds: journalEntry.transactionIds || [],
        bookedAt: new Date(),
        bookedBy: 'system',
        bookingRules: {
          vendorAccount: rules.vendorControlAccount,
          customerAccount: rules.customerControlAccount,
          expenseAccounts: this.getUsedExpenseAccounts(invoice, rules),
          revenueAccounts: this.getUsedRevenueAccounts(invoice, rules),
          vatAccounts: this.getUsedVATAccounts(invoice, rules)
        },
        confidence,
        autoBooked: options?.autoBook || false
      };
      
      // Post the journal entry
      // TODO: When MongoDB transactions are available, wrap this in a transaction
      // Example: await db.withTransaction(async (session) => { ... })
      try {
        await journalEntry.validate();
        await journalEntry.post();
        
        // Mark invoice as posted if we have a reference to it
        if (invoice.status !== 'posted') {
          invoice.status = 'posted';
        }
      } catch (postError) {
        this.logger.log('error', `Failed to post journal entry: ${postError}`);
        throw postError; // Re-throw to trigger rollback when transactions are available
      }
      
      return {
        success: true,
        journalEntry,
        bookingInfo,
        confidence,
        warnings: this.generateWarnings(invoice, rules)
      };
    } catch (error) {
      this.logger.log('error', `Failed to book invoice: ${error}`);
      return {
        success: false,
        confidence: 0,
        errors: [`Booking failed: ${error.message}`]
      };
    }
  }
  
  /**
   * Build journal entry from invoice
   */
  private async buildJournalEntry(
    invoice: IInvoice,
    rules: IBookingRules,
    options?: IBookingOptions
  ): Promise<JournalEntry> {
    const lines: IJournalEntryLine[] = [];
    const isInbound = invoice.direction === 'inbound';
    const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
    
    // Determine if we need to reverse the normal booking direction
    const reverseDirection = isCredit;
    
    if (isInbound) {
      // Inbound invoice (AP)
      lines.push(...this.buildAPEntry(invoice, rules, reverseDirection));
    } else {
      // Outbound invoice (AR)
      lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
    }
    
    // Create journal entry
    const journalData: IJournalEntry = {
      date: options?.bookingDate || invoice.issueDate,
      description: this.buildDescription(invoice),
      reference: options?.bookingReference || invoice.invoiceNumber,
      lines,
      skrType: this.skrType
    };
    
    const journalEntry = new JournalEntry(journalData);
    return journalEntry;
  }
  
  /**
   * Build AP (Accounts Payable) journal entry lines
   */
  private buildAPEntry(
    invoice: IInvoice,
    rules: IBookingRules,
    reverseDirection: boolean
  ): IJournalEntryLine[] {
    const lines: IJournalEntryLine[] = [];
    
    // Group lines by account
    const accountGroups = this.groupLinesByAccount(invoice, rules);
    
    // Create expense/asset entries
    for (const [accountNumber, group] of Object.entries(accountGroups)) {
      const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
      
      if (reverseDirection) {
        // Credit note: credit expense account
        lines.push({
          accountNumber,
          credit: Math.abs(amount),
          description: this.getAccountDescription(accountNumber, group)
        });
      } else {
        // Regular invoice: debit expense account
        lines.push({
          accountNumber,
          debit: Math.abs(amount),
          description: this.getAccountDescription(accountNumber, group)
        });
      }
    }
    
    // Create VAT entries
    const vatLines = this.buildVATLines(invoice, rules, 'input', reverseDirection);
    lines.push(...vatLines);
    
    // Create vendor control account entry
    const controlAccount = this.mapper.getControlAccount(invoice, rules);
    const totalAmount = Math.abs(invoice.payableAmount);
    
    if (reverseDirection) {
      // Credit note: debit vendor account
      lines.push({
        accountNumber: controlAccount,
        debit: totalAmount,
        description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`
      });
    } else {
      // Regular invoice: credit vendor account
      lines.push({
        accountNumber: controlAccount,
        credit: totalAmount,
        description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`
      });
    }
    
    return lines;
  }
  
  /**
   * Build AR (Accounts Receivable) journal entry lines
   */
  private buildAREntry(
    invoice: IInvoice,
    rules: IBookingRules,
    reverseDirection: boolean
  ): IJournalEntryLine[] {
    const lines: IJournalEntryLine[] = [];
    
    // Group lines by account
    const accountGroups = this.groupLinesByAccount(invoice, rules);
    
    // Create revenue entries
    for (const [accountNumber, group] of Object.entries(accountGroups)) {
      const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
      
      if (reverseDirection) {
        // Credit note: debit revenue account
        lines.push({
          accountNumber,
          debit: Math.abs(amount),
          description: this.getAccountDescription(accountNumber, group)
        });
      } else {
        // Regular invoice: credit revenue account
        lines.push({
          accountNumber,
          credit: Math.abs(amount),
          description: this.getAccountDescription(accountNumber, group)
        });
      }
    }
    
    // Create VAT entries
    const vatLines = this.buildVATLines(invoice, rules, 'output', reverseDirection);
    lines.push(...vatLines);
    
    // Create customer control account entry
    const controlAccount = this.mapper.getControlAccount(invoice, rules);
    const totalAmount = Math.abs(invoice.payableAmount);
    
    if (reverseDirection) {
      // Credit note: credit customer account
      lines.push({
        accountNumber: controlAccount,
        credit: totalAmount,
        description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`
      });
    } else {
      // Regular invoice: debit customer account
      lines.push({
        accountNumber: controlAccount,
        debit: totalAmount,
        description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`
      });
    }
    
    return lines;
  }
  
  /**
   * Build VAT lines
   */
  private buildVATLines(
    invoice: IInvoice,
    rules: IBookingRules,
    direction: 'input' | 'output',
    reverseDirection: boolean
  ): IJournalEntryLine[] {
    const lines: IJournalEntryLine[] = [];
    const taxScenario = invoice.taxScenario || 'domestic_taxed';
    
    // Handle reverse charge specially
    if (taxScenario === 'reverse_charge') {
      return this.buildReverseChargeVATLines(invoice, rules);
    }
    
    // Standard VAT booking
    for (const vatBreak of invoice.vatBreakdown) {
      if (vatBreak.taxAmount === 0) continue;
      
      const vatAccount = this.mapper.getVATAccount(
        vatBreak.vatCategory,
        direction,
        taxScenario
      );
      
      const amount = Math.abs(vatBreak.taxAmount);
      const description = `VAT ${vatBreak.vatCategory.rate}%`;
      
      if (direction === 'input') {
        // Input VAT (Vorsteuer)
        if (reverseDirection) {
          lines.push({ accountNumber: vatAccount, credit: amount, description });
        } else {
          lines.push({ accountNumber: vatAccount, debit: amount, description });
        }
      } else {
        // Output VAT (Umsatzsteuer)
        if (reverseDirection) {
          lines.push({ accountNumber: vatAccount, debit: amount, description });
        } else {
          lines.push({ accountNumber: vatAccount, credit: amount, description });
        }
      }
    }
    
    return lines;
  }
  
  /**
   * Calculate VAT amount from taxable amount and rate
   */
  private calculateVAT(taxableAmount: number, rate: number): number {
    return Math.round(taxableAmount * rate / 100 * 100) / 100; // Round to 2 decimals
  }
  
  /**
   * Calculate effective VAT rate for the invoice (weighted average)
   */
  private calculateEffectiveVATRate(invoice: IInvoice): number {
    const totalTaxable = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxableAmount, 0);
    if (totalTaxable === 0) {
      return 19; // Default to standard German VAT rate
    }
    
    // Calculate weighted average VAT rate
    const weightedRate = invoice.vatBreakdown.reduce((sum, vb) => {
      return sum + (vb.vatCategory.rate * vb.taxableAmount);
    }, 0);
    
    return Math.round(weightedRate / totalTaxable * 100) / 100;
  }
  
  /**
   * Build reverse charge VAT lines (§13b UStG)
   */
  private buildReverseChargeVATLines(
    invoice: IInvoice,
    rules: IBookingRules
  ): IJournalEntryLine[] {
    const lines: IJournalEntryLine[] = [];
    
    // For reverse charge, we book both input and output VAT
    for (const vatBreak of invoice.vatBreakdown) {
      // For reverse charge, calculate VAT if not provided
      const amount = vatBreak.taxAmount > 0 
        ? Math.abs(vatBreak.taxAmount)
        : this.calculateVAT(Math.abs(vatBreak.taxableAmount), vatBreak.vatCategory.rate);
      
      // Input VAT (deductible)
      const inputVATAccount = this.mapper.getVATAccount(
        vatBreak.vatCategory,
        'input',
        'reverse_charge'
      );
      
      // Output VAT (payable)
      const outputVATAccount = this.mapper.getVATAccount(
        vatBreak.vatCategory,
        'output',
        'reverse_charge'
      );
      
      lines.push(
        {
          accountNumber: inputVATAccount,
          debit: amount,
          description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`
        },
        {
          accountNumber: outputVATAccount,
          credit: amount,
          description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`
        }
      );
    }
    
    return lines;
  }
  
  /**
   * Group invoice lines by account
   */
  private groupLinesByAccount(
    invoice: IInvoice,
    rules: IBookingRules
  ): Record<string, IInvoiceLine[]> {
    const groups: Record<string, IInvoiceLine[]> = {};
    
    for (const line of invoice.lines) {
      const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
      
      if (!groups[account]) {
        groups[account] = [];
      }
      groups[account].push(line);
    }
    
    return groups;
  }
  
  /**
   * Book payment for an invoice
   */
  public async bookPayment(
    invoice: IInvoice,
    payment: IPaymentInfo,
    rules: IBookingRules
  ): Promise<IBookingResult> {
    try {
      const lines: IJournalEntryLine[] = [];
      const isInbound = invoice.direction === 'inbound';
      const controlAccount = this.mapper.getControlAccount(invoice, rules);
      
      // Check for skonto
      const skontoAmount = payment.skontoTaken || 0;
      const paymentAmount = payment.amount;
      const fullAmount = paymentAmount + skontoAmount;
      
      if (isInbound) {
        // Payment for vendor invoice
        lines.push(
          {
            accountNumber: controlAccount,
            debit: fullAmount,
            description: `Payment to ${invoice.supplier.name}`
          },
          {
            accountNumber: '1000', // Bank account (would be configurable)
            credit: paymentAmount,
            description: `Bank payment ${payment.endToEndId || payment.paymentId}`
          }
        );
        
        // Book skonto if taken
        if (skontoAmount > 0) {
          const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
          lines.push({
            accountNumber: skontoAccounts.skontoAccount,
            credit: skontoAmount,
            description: `Skonto received`
          });
          
          // VAT correction for skonto
          if (rules.skontoMethod === 'gross') {
            const effectiveRate = this.calculateEffectiveVATRate(invoice);
            const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
            lines.push(
              {
                accountNumber: skontoAccounts.vatCorrectionAccount,
                credit: vatCorrection,
                description: `Skonto VAT correction`
              }
            );
          }
        }
      } else {
        // Payment from customer
        lines.push(
          {
            accountNumber: '1000', // Bank account
            debit: paymentAmount,
            description: `Payment from ${invoice.customer.name}`
          },
          {
            accountNumber: controlAccount,
            credit: fullAmount,
            description: `Customer payment ${payment.endToEndId || payment.paymentId}`
          }
        );
        
        // Book skonto if granted
        if (skontoAmount > 0) {
          const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
          lines.push({
            accountNumber: skontoAccounts.skontoAccount,
            debit: skontoAmount,
            description: `Skonto granted`
          });
          
          // VAT correction for skonto
          if (rules.skontoMethod === 'gross') {
            const effectiveRate = this.calculateEffectiveVATRate(invoice);
            const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
            lines.push(
              {
                accountNumber: skontoAccounts.vatCorrectionAccount,
                debit: vatCorrection,
                description: `Skonto VAT correction`
              }
            );
          }
        }
      }
      
      // Create journal entry for payment
      const journalData: IJournalEntry = {
        date: payment.paymentDate,
        description: `Payment for invoice ${invoice.invoiceNumber}`,
        reference: payment.endToEndId || payment.remittanceInfo || payment.paymentId,
        lines,
        skrType: this.skrType
      };
      
      const journalEntry = new JournalEntry(journalData);
      await journalEntry.validate();
      await journalEntry.post();
      
      return {
        success: true,
        journalEntry,
        confidence: 100
      };
    } catch (error) {
      this.logger.log('error', `Failed to book payment: ${error}`);
      return {
        success: false,
        confidence: 0,
        errors: [`Payment booking failed: ${error.message}`]
      };
    }
  }
  
  /**
   * Validate invoice before booking
   */
  private validateInvoice(invoice: IInvoice): string[] {
    const errors: string[] = [];
    
    // Check required fields
    if (!invoice.invoiceNumber) {
      errors.push('Invoice number is required');
    }
    
    if (!invoice.issueDate) {
      errors.push('Issue date is required');
    }
    
    if (!invoice.supplier || !invoice.supplier.name) {
      errors.push('Supplier information is required');
    }
    
    if (!invoice.customer || !invoice.customer.name) {
      errors.push('Customer information is required');
    }
    
    if (invoice.lines.length === 0) {
      errors.push('Invoice must have at least one line item');
    }
    
    // Validate amounts
    const calculatedNet = invoice.lines.reduce((sum, line) => sum + line.netAmount, 0);
    const tolerance = 0.01;
    
    if (Math.abs(calculatedNet - invoice.lineNetAmount) > tolerance) {
      errors.push(`Line net amount mismatch: calculated ${calculatedNet}, stated ${invoice.lineNetAmount}`);
    }
    
    // Validate VAT
    const calculatedVAT = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxAmount, 0);
    if (Math.abs(calculatedVAT - invoice.totalVATAmount) > tolerance) {
      errors.push(`VAT amount mismatch: calculated ${calculatedVAT}, stated ${invoice.totalVATAmount}`);
    }
    
    // Validate total
    const calculatedTotal = invoice.taxExclusiveAmount + invoice.totalVATAmount;
    if (Math.abs(calculatedTotal - invoice.taxInclusiveAmount) > tolerance) {
      errors.push(`Total amount mismatch: calculated ${calculatedTotal}, stated ${invoice.taxInclusiveAmount}`);
    }
    
    return errors;
  }
  
  /**
   * Generate warnings for the booking
   */
  private generateWarnings(invoice: IInvoice, rules: IBookingRules): string[] {
    const warnings: string[] = [];
    
    // Warn about default account usage
    const hasDefaultAccounts = invoice.lines.some(line => 
      !line.accountNumber && !line.productCode
    );
    if (hasDefaultAccounts) {
      warnings.push('Some lines are using default expense/revenue accounts');
    }
    
    // Warn about mixed VAT rates
    if (invoice.vatBreakdown.length > 1) {
      warnings.push('Invoice contains mixed VAT rates');
    }
    
    // Warn about reverse charge
    if (invoice.taxScenario === 'reverse_charge') {
      warnings.push('Reverse charge procedure applied - verify VAT treatment');
    }
    
    // Warn about credit notes
    if (invoice.invoiceTypeCode === '381') {
      warnings.push('This is a credit note - amounts will be reversed');
    }
    
    // Warn about foreign currency
    if (invoice.currencyCode !== 'EUR') {
      warnings.push(`Invoice is in foreign currency: ${invoice.currencyCode}`);
    }
    
    return warnings;
  }
  
  /**
   * Build description for journal entry
   */
  private buildDescription(invoice: IInvoice): string {
    const type = invoice.invoiceTypeCode === '381' ? 'Credit Note' : 'Invoice';
    const party = invoice.direction === 'inbound' 
      ? invoice.supplier.name 
      : invoice.customer.name;
    
    return `${type} ${invoice.invoiceNumber} - ${party}`;
  }
  
  /**
   * Get account description for a group of lines
   */
  private getAccountDescription(accountNumber: string, lines: IInvoiceLine[]): string {
    if (lines.length === 1) {
      return lines[0].description;
    }
    
    return `${this.mapper.getAccountDescription(accountNumber)} (${lines.length} items)`;
  }
  
  /**
   * Get used expense accounts
   */
  private getUsedExpenseAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
    if (invoice.direction !== 'inbound') return [];
    
    const accounts = new Set<string>();
    for (const line of invoice.lines) {
      const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
      accounts.add(account);
    }
    return Array.from(accounts);
  }
  
  /**
   * Get used revenue accounts
   */
  private getUsedRevenueAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
    if (invoice.direction !== 'outbound') return [];
    
    const accounts = new Set<string>();
    for (const line of invoice.lines) {
      const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
      accounts.add(account);
    }
    return Array.from(accounts);
  }
  
  /**
   * Get used VAT accounts
   */
  private getUsedVATAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
    const accounts = new Set<string>();
    const direction = invoice.direction === 'inbound' ? 'input' : 'output';
    const taxScenario = invoice.taxScenario || 'domestic_taxed';
    
    for (const vatBreak of invoice.vatBreakdown) {
      const account = this.mapper.getVATAccount(
        vatBreak.vatCategory,
        direction,
        taxScenario
      );
      accounts.add(account);
    }
    
    // Add reverse charge accounts if applicable
    if (taxScenario === 'reverse_charge') {
      for (const vatBreak of invoice.vatBreakdown) {
        const inputAccount = this.mapper.getVATAccount(
          vatBreak.vatCategory,
          'input',
          'reverse_charge'
        );
        const outputAccount = this.mapper.getVATAccount(
          vatBreak.vatCategory,
          'output',
          'reverse_charge'
        );
        accounts.add(inputAccount);
        accounts.add(outputAccount);
      }
    }
    
    return Array.from(accounts);
  }
}