import { Tax } from "@erp/core/api";
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects";
import { ItemTaxes } from "../item-taxes";
import { CustomerInvoiceItem } from "./customer-invoice-item";

export interface CustomerInvoiceItemsProps {
  items?: CustomerInvoiceItem[];
  languageCode: LanguageCode;
  currencyCode: CurrencyCode;
}

export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
  private _languageCode!: LanguageCode;
  private _currencyCode!: CurrencyCode;

  constructor(props: CustomerInvoiceItemsProps) {
    const { items = [], languageCode, currencyCode } = props;
    super(items);
    this._languageCode = languageCode;
    this._currencyCode = currencyCode;
  }

  public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
    return new CustomerInvoiceItems(props);
  }

  /**
   * @summary Añade un nuevo ítem a la colección.
   * @param item - El ítem de factura a añadir.
   * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado.
   * @remarks
   * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con
   * los de la colección. Si no coinciden, el método devuelve `false` sin modificar
   * la colección.
   */
  add(item: CustomerInvoiceItem): boolean {
    // Antes de añadir un nuevo item, debo comprobar que el item a añadir
    // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
    if (
      !this._languageCode.equals(item.languageCode) ||
      !this._currencyCode.equals(item.currencyCode)
    ) {
      return false;
    }
    return super.add(item);
  }

  /**
   * @summary Calcula el subtotal de todos los ítems sin descuentos ni impuestos.
   * @returns Un `ItemAmount` con el subtotal total de la colección en su moneda.
   */
  public getSubtotalAmount(): ItemAmount {
    return this.getAll().reduce(
      (total, tax) => total.add(tax.getSubtotalAmount()),
      ItemAmount.zero(this._currencyCode.code)
    );
  }

  /**
   * @summary Calcula el importe total de descuentos aplicados a todos los ítems.
   * @returns Un `ItemAmount` con el importe total de descuentos.
   */
  public getDiscountAmount(): ItemAmount {
    return this.getAll().reduce(
      (total, item) => total.add(item.getDiscountAmount()),
      ItemAmount.zero(this._currencyCode.code)
    );
  }

  /**
   * @summary Calcula el importe imponible total (base antes de impuestos).
   * @returns Un `ItemAmount` con el total imponible.
   */
  public getTaxableAmount(): ItemAmount {
    return this.getAll().reduce(
      (total, item) => total.add(item.getTaxableAmount()),
      ItemAmount.zero(this._currencyCode.code)
    );
  }

  /**
   * @summary Calcula el importe total de impuestos de todos los ítems.
   * @returns Un `ItemAmount` con la suma de todos los impuestos aplicados.
   */
  public getTaxesAmount(): ItemAmount {
    return this.getAll().reduce(
      (total, item) => total.add(item.getTaxesAmount()),
      ItemAmount.zero(this._currencyCode.code)
    );
  }

  /**
   * @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos).
   * @returns Un `ItemAmount` con el total global de la colección.
   */
  public getTotalAmount(): ItemAmount {
    return this.getAll().reduce(
      (total, item) => total.add(item.getTotalAmount()),
      ItemAmount.zero(this._currencyCode.code)
    );
  }

  /**
   * @summary Agrupa los importes imponibles e impuestos por tipo de impuesto.
   * @returns Un array con objetos que contienen:
   * - `tax`: El tipo de impuesto.
   * - `taxableAmount`: El total de base imponible asociada a ese impuesto.
   * - `taxesAmount`: El importe total de impuestos calculado.
   * @remarks
   * Los impuestos se agrupan por su `tax.code` (código fiscal).
   * Si existen varios impuestos con el mismo código, sus importes se agregan.
   * En caso de conflicto de datos en impuestos con mismo código, prevalece
   * el primer `Tax` encontrado.
   */
  public getTaxesAmountByTaxes(): Array<{
    tax: Tax;
    taxableAmount: ItemAmount;
    taxesAmount: ItemAmount;
  }> {
    const getTaxCode = (tax: Tax): string => tax.code; // clave estable para Map
    const currencyCode = this._currencyCode.code;

    // Mapeamos por clave (tax code), pero también guardamos el Tax original
    const resultMap = new Map<
      string,
      { tax: Tax; taxableAmount: ItemAmount; taxesAmount: ItemAmount }
    >();

    for (const item of this.getAll()) {
      for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) {
        const key = getTaxCode(tax);
        const current = resultMap.get(key) ?? {
          tax,
          taxableAmount: ItemAmount.zero(currencyCode),
          taxesAmount: ItemAmount.zero(currencyCode),
        };

        resultMap.set(key, {
          tax: current.tax,
          taxableAmount: current.taxableAmount.add(taxableAmount),
          taxesAmount: current.taxesAmount.add(taxesAmount),
        });
      }
    }

    return Array.from(resultMap.values());
  }

  /**
   * @summary Obtiene la lista de impuestos únicos aplicados en todos los ítems.
   * @returns Un objeto `ItemTaxes` que contiene todos los impuestos distintos.
   * @remarks
   * Los impuestos se deduplican por su código (`tax.code`).
   * Si existen varios impuestos con el mismo código, el último encontrado
   * sobrescribirá los anteriores sin generar error.
   */
  public getTaxes(): ItemTaxes {
    const taxes = this.getAll()
      .flatMap((item) => item.taxes.getAll())
      .reduce((map, tax) => map.set(tax.code, tax), new Map<string, Tax>());

    return ItemTaxes.create([...taxes.values()]);
  }
}
