import { Inject, Injectable } from '@nestjs/common';
import { CheckPerformTransactionRequestDto } from './dto/requests/check-perform-transaction-request.dto';
import { CheckPerformTransactionResponseDto } from './dto/responses/check-perform-transaction-response.dto';
import { CreateTransactionResponseDto } from './dto/responses/create-transaction-response.dto';
import { PerformTransactionRequestDto } from './dto/requests/perform-transaction-request.dto';
import { PerformTransactionResponseDto } from './dto/responses/perform-transaction-response.dto';
import { CancelTransactionRequestDto } from './dto/requests/cancel-transaction-request.dto';
import { CancelTransactionResponseDto } from './dto/responses/cancel-transaction-response.dto';
import { CheckTransactionResponseDto } from './dto/responses/check-transaction-response.dto';
import { GetStatementRequestDto } from './dto/requests/get-statement-request.dto';
import {
  GetStatementResponseDto,
  ITransactionResponseItem,
} from './dto/responses/get-statement-response.dto';

import { TransactionMethods } from './types/transaction-methods';
import { CreateTransactionRequestDto } from './dto/requests/create-transaction-request.dto';
import { PaymeError, PaymeException } from '../errors';
import { DateTime } from 'luxon';
import {
  OrderStatus,
  TransactionCancellationReason,
  TransactionState,
  TransactionStatus,
} from './types/enums';
import { InjectModel } from '@nestjs/mongoose';
import {
  PaymeTransaction,
  PaymeTransactionDocument,
} from './schemas/payme-transaction.schema';
import { Model } from 'mongoose';
import { IPaymeStrategy } from './IPaymeStrategy';
import { RequestBody } from './dto/requests/request-body.dto';
import { CheckTransactionRequestDto } from './dto/requests/check-transaction-request.dto';

@Injectable()
export class PaymeService {
  constructor(
    @Inject('PAYME_STRATEGY') private paymeStrategy: IPaymeStrategy,
    @InjectModel(PaymeTransaction.name)
    private readonly transactionModel: Model<PaymeTransactionDocument>,
  ) {}
  async handleTransactionMethods(reqBody: RequestBody) {
    const method = reqBody.method;

    switch (method) {
      case TransactionMethods.CheckPerformTransaction:
        return this.checkPerformTransaction(
          reqBody as CheckPerformTransactionRequestDto,
        );
      case TransactionMethods.CreateTransaction:
        return this.createTransaction(reqBody as CreateTransactionRequestDto);
      case TransactionMethods.PerformTransaction:
        return this.performTransaction(reqBody as PerformTransactionRequestDto);
      case TransactionMethods.CancelTransaction:
        return this.cancelTransaction(reqBody as CancelTransactionRequestDto);
      case TransactionMethods.CheckTransaction:
        return this.checkTransaction(reqBody as CheckTransactionRequestDto);
      case TransactionMethods.GetStatement:
        return this.getStatement(reqBody as GetStatementRequestDto);
      default:
        throw new PaymeException(PaymeError.METHOD_NOT_FOUND);
    }
  }

  private async checkPerformTransaction(
    dto: CheckPerformTransactionRequestDto,
  ): Promise<CheckPerformTransactionResponseDto> {
    const order = await this.paymeStrategy.getOrderByAccount(
      dto.params.account,
    );

    if (!order) {
      throw new PaymeException(PaymeError.ORDER_NOT_FOUND, dto.params.account);
    }

    if (order.status !== OrderStatus.ReadyForPayment) {
      throw new PaymeException(PaymeError.ORDER_IN_PROGRESS);
    }

    if (order.amount * 100 !== dto.params.amount) {
      throw new PaymeException(PaymeError.INVALID_AMOUNT);
    }

    return {
      result: {
        allow: true,
      },
      id: dto.id,
    };
  }

  private async createTransaction(
    dto: CreateTransactionRequestDto,
  ): Promise<CreateTransactionResponseDto> {
    const transId = dto.params.id;

    const transaction = await this.transactionModel
      .findOne({
        trans_id: transId,
      })
      .exec();

    if (!transaction) {
      const pending_transaction = await this.transactionModel
        .findOne({
          state: TransactionState.Pending,
          account: dto.params.account,
        })
        .exec();

      if (pending_transaction) {
        throw new PaymeException(PaymeError.TRANSACTION_ALREADY_CREATED);
      }

      await this.checkPerformTransaction({
        id: dto.id,
        method: TransactionMethods.CheckPerformTransaction,
        params: {
          account: dto.params.account,
          amount: dto.params.amount,
        },
      });

      const newTransaction = await this.transactionModel.create({
        state: TransactionState.Pending,
        trans_id: dto.params.id,
        amount: dto.params.amount,
        account: dto.params.account,
        create_time: new Date(),
      });
      return {
        result: {
          transaction: newTransaction._id.toHexString(),
          create_time: newTransaction.create_time.getTime(),
          state: newTransaction.state,
        },
        id: dto.id,
      };
    } else {
      if (transaction.state !== TransactionState.Pending) {
        throw new PaymeException(PaymeError.INACTIVE_TRANSCACTION);
      }

      if (this.checkTransactionExpiration(transaction.create_time)) {
        transaction.state = TransactionState.PendingCanceled;
        transaction.reason = TransactionCancellationReason.TimeoutCancellation;
        transaction.cancel_time = new Date();
        await transaction.save();
        throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION);
      }

      return {
        result: {
          transaction: transaction._id.toHexString(),
          create_time: transaction.create_time?.getTime() ?? 0,
          state: transaction.state,
        },
        id: dto.id,
      };
    }
  }

  private async performTransaction(
    dto: PerformTransactionRequestDto,
  ): Promise<PerformTransactionResponseDto> {
    const transaction = await this.transactionModel
      .findOne({
        trans_id: dto.params.id,
      })
      .exec();

    if (!transaction) {
      throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
    }

    if (transaction.state === TransactionState.Pending) {
      const isTransactionExpired = this.checkTransactionExpiration(
        transaction.create_time,
      );
      if (isTransactionExpired) {
        transaction.state = TransactionState.PendingCanceled;
        transaction.reason = TransactionCancellationReason.TimeoutCancellation;
        transaction.cancel_time = new Date();
        await transaction.save();
        throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION);
      } else {
        const perform_time = new Date();
        const updated_payment = await this.transactionModel
          .findOneAndUpdate(
            {
              trans_id: dto.params.id,
            },
            {
              state: TransactionState.Paid,
              perform_time,
              status: TransactionStatus.PAID,
            },
            {
              new: true,
            },
          )
          .exec();

        if (!updated_payment) {
          throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
        }
        await this.paymeStrategy.successPayment(updated_payment);
        return {
          result: {
            transaction: updated_payment.id,
            perform_time: updated_payment.perform_time?.getTime() ?? 0,
            state: updated_payment.state,
          },
          id: dto.id,
        };
      }
    } else if (transaction.state === TransactionState.Paid) {
      return {
        result: {
          transaction: transaction.id,
          perform_time: transaction.perform_time?.getTime() ?? 0,
          state: transaction.state,
        },
        id: dto.id,
      };
    } else {
      throw new PaymeException(PaymeError.COULD_NOT_PERFORM_TRANSACTION);
    }
  }

  private async cancelTransaction(
    dto: CancelTransactionRequestDto,
  ): Promise<CancelTransactionResponseDto> {
    const transaction = await this.transactionModel
      .findOne({
        trans_id: dto.params.id,
      })
      .exec();

    if (!transaction) {
      throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
    }

    if (transaction.state === TransactionState.Pending) {
      const cancel_transaction = await this.transactionModel
        .findOneAndUpdate(
          {
            _id: transaction._id,
          },
          {
            state: TransactionState.PendingCanceled,
            reason: dto.params.reason,
            cancel_time: new Date(),
            status: TransactionStatus.CANCELED,
          },
          { new: true },
        )
        .exec();

      if (!cancel_transaction) {
        throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
      }
      return {
        result: {
          transaction: cancel_transaction._id.toHexString(),
          state: cancel_transaction.state,
          cancel_time: cancel_transaction?.cancel_time?.getTime() ?? 0,
        },
        id: dto.id,
      };
    } else if (transaction.state !== TransactionState.Paid) {
      return {
        result: {
          transaction: transaction._id.toHexString(),
          state: transaction.state,
          cancel_time: transaction.cancel_time?.getTime() ?? 0,
        },
        id: dto.id,
      };
    } else {
      const updated_transaction = await this.transactionModel
        .findOneAndUpdate(
          {
            trans_id: dto.params.id,
          },
          {
            status: TransactionStatus.CANCELED,
            state: TransactionState.PaidCanceled,
            cancel_time: new Date(),
            reason: dto.params.reason,
          },
          {
            new: true,
          },
        )
        .exec();

      if (!updated_transaction) {
        throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
      }
      return {
        id: dto.id,
        result: {
          transaction: updated_transaction._id.toHexString(),
          state: updated_transaction.state,
          cancel_time: updated_transaction?.cancel_time?.getTime() ?? 0,
        },
      };
    }
  }

  private async checkTransaction(
    dto: CheckTransactionRequestDto,
  ): Promise<CheckTransactionResponseDto> {
    const transaction = await this.transactionModel
      .findOne({
        trans_id: dto.params.id,
      })
      .exec();

    if (!transaction) {
      throw new PaymeException(PaymeError.TRANSACTION_NOT_FOUND);
    }

    return {
      result: {
        create_time: transaction.create_time?.getTime() ?? 0,
        perform_time: transaction.perform_time
          ? new Date(transaction.perform_time).getTime()
          : 0,
        cancel_time: transaction?.cancel_time
          ? new Date(transaction.cancel_time).getTime()
          : 0,
        transaction: transaction._id.toHexString(),
        state: transaction.state,
        reason: transaction.reason ?? null,
      },
      id: dto.id,
    };
  }

  private async getStatement(
    dto: GetStatementRequestDto,
  ): Promise<GetStatementResponseDto> {
    const transactions = await this.transactionModel
      .find({
        create_time: {
          $gte: dto.params.from,
          $lte: dto.params.to,
        },
      })
      .exec();

    const mappedTransactions = transactions.map(
      (transaction): ITransactionResponseItem => {
        return <ITransactionResponseItem>{
          id: transaction._id.toHexString(),
          account: transaction.account,
          amount: transaction.amount,
          time: transaction?.create_time?.getTime() ?? 0,
          transaction: transaction.id,
          state: transaction.state,
          perform_time: transaction.perform_time?.getTime() ?? 0,
          cancel_time: transaction.cancel_time?.getTime() ?? 0,
          create_time: transaction.create_time?.getTime() ?? 0,
          reason: transaction.reason ?? null,
        };
      },
    );

    // some logic
    return {
      result: {
        transactions: mappedTransactions,
      },
      id: dto.id,
    };
  }

  private checkTransactionExpiration(createdAt: Date) {
    const transactionCreatedAt = new Date(createdAt);
    const timeoutDuration = 720;
    const timeoutThreshold = DateTime.now()
      .minus({
        minutes: timeoutDuration,
      })
      .toJSDate();

    return transactionCreatedAt < timeoutThreshold;
  }
}
