# frozen_string_literal: false

# module definition
module Accounting; end

# Provides the routine to build the accounting data and save them in DB
class Accounting::AccountingService
  def initialize
    @currency = ENV.fetch('INTL_CURRENCY') { '' }
    @journal_service = Accounting::AccountingJournalService.new
  end

  # build accounting lines for invoices between the provided dates
  def build(start_date, end_date)
    invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
    build_from_invoices(invoices)
    invoices.map(&:id)
  end

  # build accounting lines for the provided invoices
  def build_from_invoices(invoices)
    lines = []
    processed = []
    invoices.find_each do |i|
      Rails.logger.debug { "[AccountLine] processing invoice #{i.id}..." } unless Rails.env.test?
      if i.main_item.nil?
        Rails.logger.error { "[AccountLine] invoice #{i.id} main_item is nil" } unless Rails.env.test?
      else
        lines.concat(generate_lines(i))
        processed.push(i.id)
      end
    end
    ActiveRecord::Base.transaction do
      ids = invoices.map(&:id)
      AccountingLine.where(invoice_id: ids).delete_all
      AccountingLine.create!(lines)
    end
    processed
  end

  def self.status
    workers = Sidekiq::Workers.new
    workers.each do |_process_id, _thread_id, work|
      return 'building' if work['payload']['class'] == 'AccountingWorker'
    end
    'built'
  end

  private

  def generate_lines(invoice)
    lines = payment_lines(invoice) + items_lines(invoice)

    vat = vat_line(invoice)
    lines << vat unless vat.nil?

    fix_rounding_errors(lines)

    lines
  end

  # Generate the lines associated with the provided invoice, for the sales accounts
  def items_lines(invoice)
    lines = []
    %w[Subscription Reservation WalletTransaction StatisticProfilePrepaidPack OrderItem Error].each do |object_type|
      items = invoice.invoice_items.filter { |ii| ii.object_type == object_type }
      items.each do |item|
        lines << line(
          invoice,
          'item',
          @journal_service.sales_journal(object_type),
          Accounting::AccountingCodeService.sales_account(item),
          item.net_amount,
          account_label: Accounting::AccountingCodeService.sales_account(item, type: :label),
          analytical_code: Accounting::AccountingCodeService.sales_account(item, section: :analytical_section)
        )
      end
    end
    lines
  end

  # Generate the "payment" lines, which contains the debit to the client account, all taxes included
  def payment_lines(invoice)
    lines = []
    invoice.payment_means.each do |details|
      lines << line(
        invoice,
        'payment',
        @journal_service.payment_journal(details[:means]),
        Accounting::AccountingCodeService.payment_account(details[:means]),
        details[:amount],
        account_label: Accounting::AccountingCodeService.payment_account(details[:means], type: :label),
        debit_method: :debit_client,
        credit_method: :credit_client
      )
    end
    lines
  end

  # Generate the "VAT" line, which contains the credit to the VAT account, with total VAT amount only
  def vat_line(invoice)
    vat_rate_groups = VatHistoryService.new.invoice_vat(invoice)
    total_vat = vat_rate_groups.values.pluck(:total_vat).sum
    # we do not render the VAT row if it was disabled for this invoice
    return nil if total_vat.zero?

    line(
      invoice,
      'vat',
      @journal_service.vat_journal,
      Accounting::AccountingCodeService.vat_account,
      total_vat,
      account_label: Accounting::AccountingCodeService.vat_account(type: :label)
    )
  end

  # Generate a row of the export, filling the configured columns with the provided values
  def line(invoice, line_type, journal_code, account_code, amount,
           account_label: '', analytical_code: '', debit_method: :debit, credit_method: :credit)
    {
      line_type: line_type,
      journal_code: journal_code,
      date: invoice.created_at,
      account_code: account_code,
      account_label: account_label,
      analytical_code: analytical_code,
      invoice_id: invoice.id,
      invoicing_profile_id: invoice.invoicing_profile_id,
      debit: method(debit_method).call(invoice, amount),
      credit: method(credit_method).call(invoice, amount),
      currency: @currency,
      summary: summary(invoice)
    }
  end

  # Fill the value of the "debit" column: if the invoice is a refund, returns the given amount, returns 0 otherwise
  def debit(invoice, amount)
    invoice.is_a?(Avoir) ? amount : 0
  end

  # Fill the value of the "credit" column: if the invoice is a refund, returns 0, otherwise, returns the given amount
  def credit(invoice, amount)
    invoice.is_a?(Avoir) ? 0 : amount
  end

  # Fill the value of the "debit" column for the client row: if the invoice is a refund, returns 0, otherwise, returns the given amount
  def debit_client(invoice, amount)
    credit(invoice, amount)
  end

  # Fill the value of the "credit" column, for the client row: if the invoice is a refund, returns the given amount, returns 0 otherwise
  def credit_client(invoice, amount)
    debit(invoice, amount)
  end

  # Create a text from the given invoice, matching the accounting software rules for the labels
  def summary(invoice)
    reference = invoice.reference

    items = invoice.subscription_invoice? ? [I18n.t('accounting_summary.subscription_abbreviation')] : []
    if invoice.main_item.object_type == 'Reservation'
      items.push I18n.t("accounting_summary.#{invoice.main_item.object.reservable_type}_reservation_abbreviation")
    end
    items.push I18n.t('accounting_summary.wallet_abbreviation') if invoice.main_item.object_type == 'WalletTransaction'
    items.push I18n.t('accounting_summary.shop_order_abbreviation') if invoice.main_item.object_type == 'OrderItem'

    "#{reference}, #{items.join(' + ')}"
  end

  # In case of rounding errors, fix the balance by adding or removing a cent to the last item line
  # This case should only happen when a coupon has been used.
  def fix_rounding_errors(lines)
    debit_sum = lines.filter { |l| l[:line_type] == 'payment' }.pluck(:debit).sum
    credit_sum = lines.filter { |l| l[:line_type] != 'payment' }.pluck(:credit).sum

    return if debit_sum == credit_sum

    diff = debit_sum - credit_sum
    fixable_line = lines.filter { |l| l[:line_type] == 'payment' }.last
    fixable_line[:credit] += diff
  end
end
