import { ArticleIdentifier } from "@/contexts/catalog/domain";
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IRepositoryManager } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { SequelizeBusinessTransactionType } from "@/contexts/common/infrastructure/sequelize/SequelizeBusinessTransaction";
import {
  Collection,
  CurrencyData,
  Description,
  DomainError,
  ICreateQuote_Request_DTO,
  IDomainError,
  Language,
  Percentage,
  Quantity,
  Result,
  TextValueObject,
  UTCDateValue,
  UniqueID,
  UnitPrice,
  ensureIdIsValid,
} from "@shared/contexts";
import {
  Dealer,
  IQuoteRepository,
  Quote,
  QuoteCustomer,
  QuoteCustomerReference,
  QuoteItem,
  QuoteReference,
  QuoteStatus,
} from "../../domain";
import { ISalesContext } from "../../infrastructure";
import { generateQuoteReferenceForDealer } from "../services";

export type CreateQuoteResponseOrError =
  | Result<never, IUseCaseError> // Misc errors (value objects)
  | Result<Quote, never>; // Success!

export class CreateQuoteUseCase
  implements IUseCase<ICreateQuote_Request_DTO, Promise<CreateQuoteResponseOrError>>
{
  private _adapter: ISequelizeAdapter;
  private _repositoryManager: IRepositoryManager;
  private _dealer: Dealer;
  private _transaction: SequelizeBusinessTransactionType;

  constructor(context: ISalesContext) {
    this._adapter = context.adapter;
    this._repositoryManager = context.repositoryManager;
    this._dealer = context.dealer!;
  }

  async execute(request: ICreateQuote_Request_DTO): Promise<CreateQuoteResponseOrError> {
    const { id } = request;

    // Validaciones de datos
    if (!this._dealer) {
      const message = "Error. Missing Dealer";
      return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message));
    }

    const idOrError = ensureIdIsValid(id);
    if (idOrError.isFailure) {
      const message = idOrError.error.message; //`Quote ID ${quoteDTO.id} is not valid`;
      return Result.fail(
        UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }])
      );
    }

    this._transaction = this._adapter.startTransaction();
    const quoteRepository = this._getQuoteRepository();

    try {
      return await this._transaction.complete(async (t) => {
        const quoteRepo = quoteRepository({ transaction: t });

        // Comprobar que no existe un quote previo con esos datos
        const idExists = await quoteRepo.exists(idOrError.object);
        if (idExists) {
          const message = `Another quote with same ID exists`;
          return Result.fail(
            UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
              path: "id",
            })
          );
        }

        // Generate Reference
        const quoteReference = await generateQuoteReferenceForDealer(this._dealer, quoteRepo);

        // Crear quote
        const quoteOrError = this._tryCreateQuoteInstance(
          request,
          idOrError.object,
          quoteReference
        );

        if (quoteOrError.isFailure) {
          const { error: domainError } = quoteOrError;
          let errorCode = "";
          let message = "";

          switch (domainError.code) {
            case DomainError.INVALID_INPUT_DATA:
              errorCode = UseCaseError.INVALID_INPUT_DATA;
              message = "La cotización tiene algún dato erróneo.";
              break;

            default:
              errorCode = UseCaseError.UNEXCEPTED_ERROR;
              message = domainError.message;
              break;
          }

          return Result.fail(UseCaseError.create(errorCode, message, domainError));
        }

        await quoteRepo.create(quoteOrError.object);

        return Result.ok<Quote>(quoteOrError.object);
      });
    } catch (error: unknown) {
      const _error = error as IInfrastructureError;
      return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
    }
  }

  private _tryCreateQuoteInstance(
    quoteDTO: ICreateQuote_Request_DTO,
    quoteId: UniqueID,
    quoteReference: QuoteReference
  ): Result<Quote, IDomainError> {
    const dealerId: UniqueID = this._dealer.id;

    const statusOrError = QuoteStatus.create(quoteDTO.status);
    if (statusOrError.isFailure) {
      return Result.fail(statusOrError.error);
    }

    const dateOrError = UTCDateValue.create(quoteDTO.date);
    if (dateOrError.isFailure) {
      return Result.fail(dateOrError.error);
    }

    const languageOrError = Language.createFromCode(
      quoteDTO.lang_code ?? this._dealer?.language.code
    );
    if (languageOrError.isFailure) {
      return Result.fail(languageOrError.error);
    }

    const customerReferenceOrError = QuoteCustomerReference.create(quoteDTO.customer_reference);
    if (customerReferenceOrError.isFailure) {
      return Result.fail(customerReferenceOrError.error);
    }

    const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
    if (customerOrError.isFailure) {
      return Result.fail(customerOrError.error);
    }

    const currencyOrError = CurrencyData.createFromCode(
      quoteDTO.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE
    );
    if (currencyOrError.isFailure) {
      return Result.fail(currencyOrError.error);
    }

    const paymentOrError = TextValueObject.create(
      quoteDTO.payment_method ??
        this._dealer?.additionalInfo.get("default_payment_method")?.toString()
    );
    if (paymentOrError.isFailure) {
      return Result.fail(paymentOrError.error);
    }

    const notesOrError = TextValueObject.create(
      quoteDTO.notes ?? this._dealer?.additionalInfo.get("default_notes")?.toString()
    );
    if (notesOrError.isFailure) {
      return Result.fail(notesOrError.error);
    }

    const validityOrError = TextValueObject.create(
      quoteDTO.validity ?? this._dealer?.additionalInfo.get("default_quote_validity")?.toString()
    );
    if (validityOrError.isFailure) {
      return Result.fail(validityOrError.error);
    }

    const discountOrError = Percentage.create(quoteDTO.discount);
    if (discountOrError.isFailure) {
      return Result.fail(discountOrError.error);
    }

    const taxOrError = Percentage.create(
      quoteDTO.tax ?? {
        amount: this._dealer?.additionalInfo.get("default_tax")?.toString(),
        scale: 2,
      }
    );
    if (taxOrError.isFailure) {
      return Result.fail(taxOrError.error);
    }

    const dateSentOrError = UTCDateValue.create(null);
    if (dateSentOrError.isFailure) {
      return Result.fail(dateSentOrError.error);
    }

    // Items

    let items: Collection<QuoteItem>;

    try {
      items = new Collection<QuoteItem>(
        quoteDTO.items?.map((item) => {
          const idArticleOrError = ArticleIdentifier.create(item.id_article);
          if (idArticleOrError.isFailure) {
            throw idArticleOrError.error;
          }

          const descriptionOrError = Description.create(item.description);
          if (descriptionOrError.isFailure) {
            throw descriptionOrError.error;
          }

          const quantityOrError = Quantity.create({
            amount: item.quantity.amount,
            scale: item.quantity.scale,
          });
          if (quantityOrError.isFailure) {
            throw quantityOrError.error;
          }

          const unitPriceOrError = UnitPrice.create({
            amount: item.unit_price?.amount,
            currencyCode: item.unit_price?.currency_code,
            scale: item.unit_price?.scale,
          });
          if (unitPriceOrError.isFailure) {
            throw unitPriceOrError.error;
          }

          const percentageOrError = Percentage.create({
            amount: item.discount?.amount,
            scale: item.discount?.scale,
          });
          if (percentageOrError.isFailure) {
            throw percentageOrError.error;
          }

          const quoteItemOrError = QuoteItem.create({
            idArticle: idArticleOrError.object,
            description: descriptionOrError.object,
            quantity: quantityOrError.object,
            unitPrice: unitPriceOrError.object,
            discount: percentageOrError.object,
          });

          if (quoteItemOrError.isFailure) {
            throw quoteItemOrError.error;
          }

          return quoteItemOrError.object;
        })
      );
    } catch (e: unknown) {
      return Result.fail(e as IDomainError);
    }

    return Quote.create(
      {
        status: statusOrError.object,
        date: dateOrError.object,
        reference: quoteReference,
        language: languageOrError.object,
        customerReference: customerReferenceOrError.object,
        customer: customerOrError.object,
        currency: currencyOrError.object,
        paymentMethod: paymentOrError.object,
        notes: notesOrError.object,
        validity: validityOrError.object,

        discount: discountOrError.object,
        tax: taxOrError.object,

        items,

        dealerId,
        dateSent: dateSentOrError.object,
      },
      quoteId
    );
  }

  private _getQuoteRepository() {
    return this._repositoryManager.getRepository<IQuoteRepository>("Quote");
  }
}
