import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
  CurrencyCode,
  LanguageCode,
  Percentage,
  TextValue,
  UniqueID,
  UtcDate,
  ValidationErrorCollection,
  ValidationErrorDetail,
  extractOrPushError,
  maybeFromNullableVO,
  toNullable,
} from "@repo/rdx-ddd";
import { Collection, Maybe, Result } from "@repo/rdx-utils";
import {
  CustomerInvoice,
  CustomerInvoiceItems,
  CustomerInvoiceNumber,
  CustomerInvoiceProps,
  CustomerInvoiceSerie,
  CustomerInvoiceStatus,
  InvoicePaymentMethod,
} from "../../../domain";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
import { CustomerInvoiceItemDomainMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.mapper";
import { InvoiceRecipientDomainMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.mapper";
import { TaxesDomainMapper as TaxesFullMapper } from "./invoice-taxes.mapper";

export interface ICustomerInvoiceDomainMapper
  extends ISequelizeDomainMapper<
    CustomerInvoiceModel,
    CustomerInvoiceCreationAttributes,
    CustomerInvoice
  > {}

export class CustomerInvoiceDomainMapper
  extends SequelizeDomainMapper<
    CustomerInvoiceModel,
    CustomerInvoiceCreationAttributes,
    CustomerInvoice
  >
  implements ICustomerInvoiceDomainMapper
{
  private _itemsMapper: CustomerInvoiceItemFullMapper;
  private _recipientMapper: InvoiceRecipientFullMapper;
  private _taxesMapper: TaxesFullMapper;

  constructor(params: MapperParamsType) {
    super();

    this._itemsMapper = new CustomerInvoiceItemFullMapper(params); // Instanciar el mapper de items
    this._recipientMapper = new InvoiceRecipientFullMapper();
    this._taxesMapper = new TaxesFullMapper(params);
  }

  private _mapPaymentMethodToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
    const { errors } = params as {
      errors: ValidationErrorDetail[];
    };

    const paymentId = extractOrPushError(
      maybeFromNullableVO(source.payment_method_id, (value) => UniqueID.create(value)),
      "payment_method_id",
      errors
    );

    const paymentDescription = extractOrPushError(
      maybeFromNullableVO(source.payment_method_description, (value) => Result.ok(String(value))),
      "payment_method_description",
      errors
    );

    if (errors.length > 0) {
      return Result.fail(new ValidationErrorCollection("Invoice payment mapping failed", errors));
    }

    if (paymentDescription!.isNone() || paymentId!.isNone()) {
      return Result.ok(Maybe.none<InvoicePaymentMethod>());
    }

    const paymentResult = InvoicePaymentMethod.create(
      {
        paymentDescription: paymentDescription?.getOrUndefined()!,
      },
      paymentId?.getOrUndefined()!
    );

    if (paymentResult.isFailure) {
      return Result.fail(
        new ValidationErrorCollection("Invoice payment method creation failed", [
          { path: "paymentMethod", message: paymentResult.error.message },
        ])
      );
    }

    return Result.ok(Maybe.some<InvoicePaymentMethod>(paymentResult.data));
  }

  private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
    const { errors } = params as {
      errors: ValidationErrorDetail[];
    };

    const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
    const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors);

    const customerId = extractOrPushError(
      UniqueID.create(source.customer_id),
      "customer_id",
      errors
    );

    const isProforma = Boolean(source.is_proforma);

    const status = extractOrPushError(
      CustomerInvoiceStatus.create(source.status),
      "status",
      errors
    );

    const series = extractOrPushError(
      maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)),
      "serie",
      errors
    );

    const invoiceNumber = extractOrPushError(
      maybeFromNullableVO(source.invoice_number, (value) => CustomerInvoiceNumber.create(value)),
      "invoice_number",
      errors
    );

    const invoiceDate = extractOrPushError(
      UtcDate.createFromISO(source.invoice_date),
      "invoice_date",
      errors
    );

    const operationDate = extractOrPushError(
      maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
      "operation_date",
      errors
    );

    const reference = extractOrPushError(
      maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))),
      "reference",
      errors
    );

    const notes = extractOrPushError(
      maybeFromNullableVO(source.notes, (value) => TextValue.create(value)),
      "notes",
      errors
    );

    const languageCode = extractOrPushError(
      LanguageCode.create(source.language_code),
      "language_code",
      errors
    );

    const currencyCode = extractOrPushError(
      CurrencyCode.create(source.currency_code),
      "currency_code",
      errors
    );

    const discountPercentage = extractOrPushError(
      Percentage.create({
        value: source.discount_percentage_value,
        scale: source.discount_percentage_scale,
      }),
      "discount_percentage_value",
      errors
    );

    return {
      invoiceId,
      companyId,
      customerId,
      isProforma,
      status,
      series,
      invoiceNumber,
      invoiceDate,
      operationDate,
      reference,
      notes,
      languageCode,
      currencyCode,
      discountPercentage,
    };
  }

  public mapToDomain(
    source: CustomerInvoiceModel,
    params?: MapperParamsType
  ): Result<CustomerInvoice, Error> {
    try {
      const errors: ValidationErrorDetail[] = [];

      // 1) Valores escalares (atributos generales)
      const attributes = this._mapAttributesToDomain(source, { errors, ...params });

      // 2) Recipient (snapshot en la factura o include)
      const recipientResult = this._recipientMapper.mapToDomain(source, {
        errors,
        attributes,
        ...params,
      });

      if (recipientResult.isFailure) {
        errors.push({
          path: "recipient",

          message: recipientResult.error.message,
        });
      }

      // 3) Items (colección)
      const itemsResults = this._itemsMapper.mapToDomainCollection(
        source.items,
        source.items.length,
        {
          errors,
          attributes,
          ...params,
        }
      );

      if (itemsResults.isFailure) {
        errors.push({
          path: "items",
          message: recipientResult.error.message,
        });
      }

      // 4) Taxes (colección a nivel factura)
      const taxesResults = this._taxesMapper.mapToDomainCollection(
        source.taxes,
        source.taxes.length,
        {
          errors,
          attributes,
          ...params,
        }
      );

      if (taxesResults.isFailure) {
        errors.push({
          path: "taxes",
          message: taxesResults.error.message,
        });
      }

      // Payment method
      const paymentMethodResult = this._mapPaymentMethodToDomain(source, { errors, ...params });

      if (paymentMethodResult.isFailure) {
        errors.push({
          path: "paymentMethod",
          message: paymentMethodResult.error.message,
        });
      }

      // 5) Si hubo errores de mapeo, devolvemos colección de validación
      if (errors.length > 0) {
        return Result.fail(new ValidationErrorCollection("Customer mapping failed", errors));
      }

      // 6) Construcción del agregado (Dominio)

      const recipient = recipientResult.data;
      const paymentMethod = paymentMethodResult.data;

      const items = CustomerInvoiceItems.create({
        languageCode: attributes.languageCode!,
        currencyCode: attributes.currencyCode!,
        items: itemsResults.data.getAll(),
      });

      const invoiceProps: CustomerInvoiceProps = {
        companyId: attributes.companyId!,

        isProforma: attributes.isProforma,
        status: attributes.status!,
        series: attributes.series!,
        invoiceNumber: attributes.invoiceNumber!,
        invoiceDate: attributes.invoiceDate!,
        operationDate: attributes.operationDate!,

        customerId: attributes.customerId!,
        recipient: recipient,

        reference: attributes.reference!,
        notes: attributes.notes!,

        languageCode: attributes.languageCode!,
        currencyCode: attributes.currencyCode!,

        discountPercentage: attributes.discountPercentage!,

        paymentMethod: paymentMethod!,

        items,
      };

      const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId);

      if (createResult.isFailure) {
        return Result.fail(
          new ValidationErrorCollection("Customer invoice entity creation failed", [
            { path: "invoice", message: createResult.error.message },
          ])
        );
      }

      return Result.ok(createResult.data);
    } catch (err: unknown) {
      return Result.fail(err as Error);
    }
  }

  public mapToPersistence(
    source: CustomerInvoice,
    params?: MapperParamsType
  ): Result<CustomerInvoiceCreationAttributes, Error> {
    const errors: ValidationErrorDetail[] = [];

    // 1) Items
    const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, {
      errors,
      parent: source,
      ...params,
    });
    if (itemsResult.isFailure) {
      errors.push({
        path: "items",
        message: itemsResult.error.message,
      });
    }

    const items = itemsResult.data;

    // 1) Taxes

    const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.taxes), {
      errors,
      parent: source,
      ...params,
    });
    if (taxesResult.isFailure) {
      errors.push({
        path: "taxes",
        message: taxesResult.error.message,
      });
    }

    const taxes = taxesResult.data;

    // 3) Calcular totales
    const allAmounts = source.getAllAmounts();

    // 4) Cliente
    const recipient = this._mapRecipientToPersistence(source);

    // 7) Si hubo errores de mapeo, devolvemos colección de validación
    if (errors.length > 0) {
      return Result.fail(
        new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
      );
    }

    const invoiceValues: CustomerInvoiceCreationAttributes = {
      id: source.id.toPrimitive(),
      company_id: source.companyId.toPrimitive(),

      is_proforma: source.isProforma,
      status: source.status.toPrimitive(),
      series: toNullable(source.series, (series) => series.toPrimitive()),
      invoice_number: toNullable(source.invoiceNumber, (invoiceNumber) =>
        invoiceNumber.toPrimitive()
      ),
      invoice_date: source.invoiceDate.toPrimitive(),
      operation_date: toNullable(source.operationDate, (operationDate) =>
        operationDate.toPrimitive()
      ),
      language_code: source.languageCode.toPrimitive(),
      currency_code: source.currencyCode.toPrimitive(),

      reference: toNullable(source.reference, (reference) => reference),
      notes: toNullable(source.notes, (notes) => notes.toPrimitive()),

      subtotal_amount_value: allAmounts.subtotalAmount.value,
      subtotal_amount_scale: allAmounts.subtotalAmount.scale,

      discount_percentage_value: source.discountPercentage.toPrimitive().value,
      discount_percentage_scale: source.discountPercentage.toPrimitive().scale,

      discount_amount_value: allAmounts.discountAmount.value,
      discount_amount_scale: allAmounts.discountAmount.scale,

      taxable_amount_value: allAmounts.taxableAmount.value,
      taxable_amount_scale: allAmounts.taxableAmount.scale,

      taxes_amount_value: allAmounts.taxesAmount.value,
      taxes_amount_scale: allAmounts.taxesAmount.scale,

      total_amount_value: allAmounts.totalAmount.value,
      total_amount_scale: allAmounts.totalAmount.scale,

      payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id),
      payment_method_description: toNullable(
        source.paymentMethod,
        (payment) => payment.toObjectString().payment_description
      ),

      customer_id: source.customerId.toPrimitive(),
      ...recipient,
      taxes,
      items,
    };

    return Result.ok<CustomerInvoiceCreationAttributes>(invoiceValues);
  }

  protected _mapRecipientToPersistence(source: CustomerInvoice, params?: MapperParamsType) {
    const { errors } = params as {
      errors: ValidationErrorDetail[];
    };

    const recipient = source.recipient.getOrUndefined();

    if (!source.isProforma && !recipient) {
      errors.push({
        path: "recipient",
        message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data",
      });
    }

    const recipientValues = {
      customer_tin: !source.isProforma ? recipient!.tin.toPrimitive() : null,
      customer_name: !source.isProforma ? recipient!.name.toPrimitive() : null,
      customer_street: !source.isProforma
        ? toNullable(recipient!.street, (v) => v.toPrimitive())
        : null,
      customer_street2: !source.isProforma
        ? toNullable(recipient!.street2, (v) => v.toPrimitive())
        : null,
      customer_city: !source.isProforma
        ? toNullable(recipient!.city, (v) => v.toPrimitive())
        : null,
      customer_province: !source.isProforma
        ? toNullable(recipient!.province, (v) => v.toPrimitive())
        : null,
      customer_postal_code: !source.isProforma
        ? toNullable(recipient!.postalCode, (v) => v.toPrimitive())
        : null,
      customer_country: !source.isProforma
        ? toNullable(recipient!.country, (v) => v.toPrimitive())
        : null,
    };

    return recipientValues;
  }
}
