import { IRentalAttr } from '../../interfaces/rental-attr.interface';
import { RentalStatusEnum } from '../../enum/rental-status.enum';
import { RentalRepository } from './rental.repository';
import { ClassError, ObjectBase } from '@tomei/general';
import { ApplicationConfig } from '@tomei/config';
import { AuthContext, LoginUser } from '@tomei/sso';
import { RentalPrice } from '../rental-price/rental-price';
import { Op, QueryTypes } from 'sequelize';
import { ActionEnum, Activity } from '@tomei/activity-history';
import { IRentalFindAllSearchAttr } from '../../interfaces/rental-find-all-search-attr.interface';
import { RentalAccountTypeEnum } from '../../enum/account-type.enum';
import { JointHirer } from '../joint-hirer/joint-hirer';
import { JointHirerModel } from '../../models/joint-hirer.entity';
import { AgreementRepository } from '../agreement/agreement.repository';
import * as rentalDb from '../../database';
import { RentalModel } from '../../models/rental.entity';
import { JointHirerRepository } from '../joint-hirer/joint-hirer.repository';
import { AgreementModel } from '../../models';
import { Agreement } from '../agreement/agreement';
import { AggrementStatusEnum } from '../../enum/aggrement-status.enum';
import { AgreementSignatureRepository } from '../agreement-signature/agreement-signature.repository';
import { AgreementSignatureStatusEnum } from '../../enum/agreement-signature-status.enum';
import { HirerTypeEnum } from '../../enum/hirer-type.enum';
import { AgreementHistoryRepository } from '../agreement-history/agreement-history.repository';
import { DocType, Document, IAccountSystem } from '@tomei/finance';
import { ItemClassMap } from '../../ClassMappings/ItemClassMap';
import { AgreementSignatureVerificationMethodEnum } from '../../enum/agreement-signature-verification-method.enum';

export class Rental extends ObjectBase {
  ObjectId: string;
  ObjectName: string;
  ObjectType = 'Rental';
  TableName: string;
  CustomerId: string;
  CustomerType: string;
  ItemId: string;
  ItemType: string;
  PriceId: string;
  StartDateTime: Date;
  EndDateTime: Date;
  CancelRemarks: string;
  TerminateRemarks: string;
  AgreementNo: string;
  AccountType: RentalAccountTypeEnum;
  JointHirers: JointHirer[] = [];
  Invoices: Document[] = [];
  Item: any;
  protected _Status: RentalStatusEnum | string;
  protected _EscheatmentYN: string = 'N';
  protected _CreatedById: string;
  protected _CreatedAt: Date;
  protected _UpdatedById: string;
  protected _UpdatedAt: Date;
  protected static _Repo = new RentalRepository();
  protected static _AgreementRepo = new AgreementRepository();
  protected static _JointHirerRepo = new JointHirerRepository();
  protected static _AgreementSignatureRepo = new AgreementSignatureRepository();
  protected static _AgreementHistoryRepo = new AgreementHistoryRepository();

  get RentalId(): string {
    return this.ObjectId;
  }

  set RentalId(value: string) {
    this.ObjectId = value;
  }

  get Status(): RentalStatusEnum | string {
    return this._Status;
  }

  get EscheatmentYN(): string {
    return this._EscheatmentYN;
  }

  get CreatedById(): string {
    return this._CreatedById;
  }

  get CreatedAt(): Date {
    return this._CreatedAt;
  }

  get UpdatedById(): string {
    return this._UpdatedById;
  }

  get UpdatedAt(): Date {
    return this._UpdatedAt;
  }

  private setTerminated() {
    this._Status = RentalStatusEnum.TERMINATED;
  }

  protected constructor(rentalAttr?: IRentalAttr) {
    super();
    if (rentalAttr) {
      this.RentalId = rentalAttr.RentalId;
      this.CustomerId = rentalAttr.CustomerId;
      this.CustomerType = rentalAttr.CustomerType;
      this.ItemId = rentalAttr.ItemId;
      this.ItemType = rentalAttr.ItemType;
      this.PriceId = rentalAttr.PriceId;
      this.StartDateTime = rentalAttr.StartDateTime;
      this.EndDateTime = rentalAttr.EndDateTime;
      this.CancelRemarks = rentalAttr.CancelRemarks;
      this.TerminateRemarks = rentalAttr.TerminateRemarks;
      this.AgreementNo = rentalAttr.AgreementNo;
      this.AccountType = rentalAttr.AccountType;
      this._Status = rentalAttr.Status;
      this._EscheatmentYN = rentalAttr.EscheatmentYN;
      this._CreatedById = rentalAttr.CreatedById;
      this._CreatedAt = rentalAttr.CreatedAt;
      this._UpdatedById = rentalAttr.UpdatedById;
      this._UpdatedAt = rentalAttr.UpdatedAt;
    }
  }

  public static async init(dbTransaction?: any, rentalId?: string) {
    try {
      if (rentalId) {
        const rental = await Rental._Repo.findByPk(rentalId, dbTransaction);
        if (rental) {
          return new Rental(rental);
        } else {
          throw new ClassError('Rental', 'RentalErrMsg00', 'Rental Not Found');
        }
      }
      return new Rental();
    } catch (error) {
      throw new ClassError(
        'Rental',
        'RentalErrMsg00',
        'Failed To Initialize Price',
      );
    }
  }

  /**
   * Create a new Rental record.
   * @param {RentalPrice} rentalPrice - The rental pricing information.
   * @param {LoginUser} loginUser - The user initiating the creation.
   * @param {any} [dbTransaction] - Optional database transaction for atomic operations.
   * @throws {ClassError} Throws an error if:
   *   1. loginUser does not have 'Rental - Create' privilege.
   *   2. Rental item is not available at the current date.
   * @returns {Promise<any>} A Promise resolving to the created rental record.
   */
  public async create(
    rentalPrice: RentalPrice,
    loginUser: LoginUser,
    jointHirers?: JointHirer[],
    dbTransaction?: any,
  ): Promise<any> {
    try {
      /*Part 1: Check Privilege*/
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'Rental - Create',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'Rental - Create' privilege.",
        );
      }

      /*Part 2: Make Sure Rental Item Available*/
      const isItemAvailable = await Rental.isItemAvailable(
        this.ItemId,
        this.ItemType,
        this.StartDateTime,
        this.EndDateTime,
        dbTransaction,
      );

      if (!isItemAvailable) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg02',
          'Rental Item is not available at current date.',
        );
      }

      // Part 3: Create Rental Agreement Record
      // Call Rental._AgreementRepo create method by passing:
      // AgreementNo: this.AgreementNo
      // Status: "Not Generated"
      // dbTransaction
      await Rental._AgreementRepo.create(
        {
          AgreementNo: this.AgreementNo,
          Status: 'Not Generated',
        },
        {
          transaction: dbTransaction,
        },
      );

      //Create a record into rental_AgreementSignature table
      //Create the main customer's signature record
      await Rental._AgreementSignatureRepo.create(
        {
          SignatureId: this.createId(),
          AgreementNo: this.AgreementNo,
          Party: HirerTypeEnum.PRIMARY,
          PartyId: this.CustomerId,
          PartyType: 'Customer',
          SignatureStatus: AgreementSignatureStatusEnum.PENDING,
          SignedAt: new Date(),
          VerificationMethod:
            AgreementSignatureVerificationMethodEnum.FINGERPRINT,
          VerificationJustification: '',
          VerifiedById: loginUser.ObjectId,
          CreatedById: loginUser.ObjectId,
          CreatedAt: new Date(),
          UpdatedById: loginUser.ObjectId,
          UpdatedAt: new Date(),
        },
        { transaction: dbTransaction },
      );

      // Create signatures record for joint hirers if any
      if (jointHirers && jointHirers.length > 0) {
        for (const jointHirer of jointHirers) {
          await Rental._AgreementSignatureRepo.create(
            {
              SignatureId: this.createId(),
              AgreementNo: this.AgreementNo,
              Party: HirerTypeEnum.JOINT,
              PartyId: jointHirer.CustomerId,
              PartyType: 'Customer',
              SignatureStatus: AgreementSignatureStatusEnum.PENDING,
              SignedAt: new Date(),
              VerificationMethod:
                AgreementSignatureVerificationMethodEnum.FINGERPRINT,
              VerificationJustification: '',
              VerifiedById: loginUser.ObjectId,
              CreatedById: loginUser.ObjectId,
              CreatedAt: new Date(),
              UpdatedById: loginUser.ObjectId,
              UpdatedAt: new Date(),
            },
            { transaction: dbTransaction },
          );
        }
      }

      /*Part 4: Insert Rental & The Agreed Rental Price*/
      if (!rentalPrice.PriceId) {
        const rentalPriceData = await rentalPrice.create(
          loginUser,
          dbTransaction,
        );
        this.PriceId = rentalPriceData.PriceId;
      } else {
        this.PriceId = rentalPrice.PriceId;
      }
      this.RentalId = this.createId();
      // if (!this.Status) {
      this._Status = RentalStatusEnum.PENDING_SIGNING;
      // }
      this._CreatedById = loginUser.ObjectId;
      this._UpdatedById = loginUser.ObjectId;
      this._CreatedAt = new Date();
      this._UpdatedAt = new Date();

      const data = {
        RentalId: this.RentalId,
        CustomerId: this.CustomerId,
        CustomerType: this.CustomerType,
        ItemId: this.ItemId,
        ItemType: this.ItemType,
        PriceId: this.PriceId,
        StartDateTime: this.StartDateTime,
        EndDateTime: this.EndDateTime,
        CancelRemarks: this.CancelRemarks,
        TerminateRemarks: this.TerminateRemarks,
        AgreementNo: this.AgreementNo,
        AccountType: this.AccountType,
        Status: this.Status,
        EscheatmentYN: this.EscheatmentYN,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      await Rental._Repo.create(data, {
        transaction: dbTransaction,
      });

      //For every data inside Param.jointHirers, update the rentalId and call jointHirer.create
      if (jointHirers) {
        for (let i = 0; i < jointHirers.length; i++) {
          jointHirers[i].RentalId = this.RentalId;
          await jointHirers[i].create(loginUser, dbTransaction);
          this.JointHirers.push(jointHirers[i]);
        }
      }

      /*Part 5: Record Create Rental Activity*/
      const activity = new Activity();
      activity.ActivityId = activity.createId();
      activity.Action = ActionEnum.CREATE;
      activity.Description = 'Add Rental';
      activity.EntityType = 'Rental';
      activity.EntityId = this.RentalId;
      activity.EntityValueBefore = JSON.stringify({});
      activity.EntityValueAfter = JSON.stringify(data);
      await activity.create(loginUser.ObjectId, dbTransaction);

      /*Part 6: Return Result*/
      return this;
    } catch (error) {
      throw error;
    }
  }

  public static async isItemAvailable(
    itemId: string,
    itemType: string,
    startDateTime: Date,
    endDateTime: Date,
    dbTransaction?: any,
  ) {
    try {
      const rental = await Rental._Repo.findOne({
        where: {
          ItemId: itemId,
          ItemType: itemType,
          StartDateTime: {
            [Op.lte]: endDateTime,
          },
          EndDateTime: {
            [Op.gte]: startDateTime,
          },
        },
        transaction: dbTransaction,
      });

      if (!rental) {
        // No rental found → item is available
        return true;
      }

      if (
        rental.Status === RentalStatusEnum.TERMINATED ||
        rental.Status === RentalStatusEnum.CANCELLED
      ) {
        // Rental found but status is Terminated or Cancelled → item is available
        return true;
      }

      // Rental exists and is active → item is not available
      return false;
    } catch (error) {
      throw error;
    }
  }

  public static async findAll(
    authContext: AuthContext,
    dbTransaction: any,
    page?: number,
    row?: number,
    search?: IRentalFindAllSearchAttr,
  ) {
    try {
      const systemCode =
        await ApplicationConfig.getComponentConfigValue('system-code');

      if ('loginUser' in authContext) {
        const isPrivileged = await authContext.loginUser.checkPrivileges(
          systemCode,
          'Rental - View',
        );

        if (!isPrivileged) {
          throw new ClassError(
            'Rental',
            'RentalErrMsg01',
            "You do not have 'Rental - View' privilege.",
          );
        }
      }

      const queryObj: any = {};

      let options: any = {
        transaction: dbTransaction,
        order: [['CreatedAt', 'DESC']],
      };

      if (page && row) {
        options = {
          ...options,
          limit: row,
          offset: row * (page - 1),
        };
      }

      if (search) {
        Object.entries(search).forEach(([key, value]) => {
          if (key === 'StartDateTime') {
            queryObj[key] = {
              [Op.gte]: value,
            };
          } else if (key === 'EndDateTime') {
            queryObj[key] = {
              [Op.lte]: value,
            };
          } else if (key === 'CustomerId') {
            queryObj[key] = {
              [Op.substring]: value,
            };

            options.include = [
              {
                model: JointHirerModel,
                required: false,
                where: {
                  CustomerId: {
                    [Op.substring]: value,
                  },
                },
              },
            ];
          } else {
            queryObj[key] = {
              [Op.substring]: value,
            };
          }
        });
        if (options?.include?.length > 1) {
          options.include.push({
            model: AgreementModel,
            required: false,
          });
        } else {
          options.include = [
            {
              model: AgreementModel,
              required: false,
            },
          ];
        }

        options = {
          ...options,
          where: queryObj,
        };
      }

      return await Rental._Repo.findAndCountAll(options);
    } catch (err) {
      throw err;
    }
  }

  public static async checkActiveByItemId(
    loginUser: LoginUser,
    itemId: string,
    itemType: string,
    dbTransaction?: any,
  ) {
    try {
      const systemCode =
        await ApplicationConfig.getComponentConfigValue('system-code');

      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'Rental - View',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'Rental - View' privilege.",
        );
      }

      const queryObj: any = {
        ItemId: itemId,
        ItemType: itemType,
        Status: RentalStatusEnum.ACTIVE,
      };

      const options: any = {
        transaction: dbTransaction,
        where: queryObj,
      };

      const rental = await Rental._Repo.findOne(options);

      if (rental) {
        return true;
      } else {
        return false;
      }
    } catch (err) {
      throw err;
    }
  }

  /**
   * Sets the StartDateTime property of the Rental class as custom setter.
   * @param {Date} startDateTime - The start date and time of the rental.
   * @throws {ClassError} Throws an error if:
   *   1. startDateTime is a past date.
   *   2. startDateTime is more than the EndDateTime.
   * @returns {void}
   */
  setStartDateTime(startDateTime: Date): void {
    const startDateTimeObject = new Date(startDateTime);
    startDateTimeObject.setHours(0, 0, 0, 0);

    /*Part 1: Make Sure Future Date Time*/
    if (startDateTimeObject < new Date(new Date().setHours(0, 0, 0, 0))) {
      throw new ClassError(
        'Rental',
        'RentalErrMsg02',
        'StartDateTime cannot be a past datetime.',
      );
    }

    /*Part 2: Make Sure Less Than End Date Time*/
    if (this.EndDateTime && startDateTimeObject > this.EndDateTime) {
      throw new ClassError(
        'Rental',
        'RentalErrMsg03',
        'StartDateTime cannot be more than EndDateTime.',
      );
    }

    /*Part 3: Set Status Whether Reserved or Active*/
    if (startDateTimeObject > new Date(new Date().setHours(0, 0, 0, 0))) {
      this._Status = RentalStatusEnum.RESERVED;
    } else {
      this._Status = RentalStatusEnum.ACTIVE;
    }
  }

  public async periodEndProcess(
    loginUser: LoginUser,
    dbTransaction: any,
    stockInventory: any,
  ) {
    try {
      // Privilege Checking
      const systemCode =
        await ApplicationConfig.getComponentConfigValue('system-code');

      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'Rental – PeriodEndProcess',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg04',
          "You do not have 'Rental - PeriodEndProcess' privilege.",
        );
      }

      // Validate Existing Rental
      if (this.AgreementNo === undefined || this.AgreementNo === null) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg05',
          'Rental must be existing rental.',
        );
      }

      // Validate Rental End Period
      if (this.EndDateTime === new Date(new Date().setHours(0, 0, 0, 0))) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg06',
          'Rental period is not ending today.',
        );
      }

      // Mark Rental Item as Available
      await stockInventory.setAvailable(loginUser, dbTransaction);

      // Capture Record Info Before Changes
      const entityValueBefore = {
        RentalId: this.RentalId,
        CustomerId: this.CustomerId,
        CustomerType: this.CustomerType,
        ItemId: this.ItemId,
        ItemType: this.ItemType,
        PriceId: this.PriceId,
        StartDateTime: this.StartDateTime,
        EndDateTime: this.EndDateTime,
        CancelRemarks: this.CancelRemarks,
        TerminateRemarks: this.TerminateRemarks,
        AgreementNo: this.AgreementNo,
        AccountType: this.AccountType,
        Status: this.Status,
        EscheatmentYN: this.EscheatmentYN,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      // Mark Rental as Terminated and Capture Updater Info
      this.setTerminated();
      this._UpdatedById = loginUser.ObjectId;
      this._UpdatedAt = new Date();

      // Capture Record Info after Changes
      const entityValueAfter = {
        RentalId: this.RentalId,
        CustomerId: this.CustomerId,
        CustomerType: this.CustomerType,
        ItemId: this.ItemId,
        ItemType: this.ItemType,
        PriceId: this.PriceId,
        StartDateTime: this.StartDateTime,
        EndDateTime: this.EndDateTime,
        CancelRemarks: this.CancelRemarks,
        TerminateRemarks: this.TerminateRemarks,
        AgreementNo: this.AgreementNo,
        AccountType: this.AccountType,
        Status: this.Status,
        EscheatmentYN: this.EscheatmentYN,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      // Record the Update Activity
      const activity = new Activity();
      activity.ActivityId = activity.createId();
      activity.Action = ActionEnum.UPDATE;
      activity.Description = 'Set Rental as Terminated';
      activity.EntityType = 'Rental';
      activity.EntityId = this.RentalId;
      activity.EntityValueBefore = JSON.stringify(entityValueBefore);
      activity.EntityValueAfter = JSON.stringify(entityValueAfter);
      await activity.create(loginUser.ObjectId, dbTransaction);

      return this;
    } catch (err) {
      throw err;
    }
  }

  async getCustomerActiveRentals(
    loginUser: LoginUser,
    dbTransaction: any,
    CustomerId: string,
  ): Promise<IRentalAttr[]> {
    try {
      // Part 1: Privilege Checking
      // Call loginUser.checkPrivileges() by passing:
      // SystemCode: <get_from_app_config>
      // PrivilegeCode: "RENTAL_VIEW"
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = loginUser.checkPrivileges(systemCode, 'RENTAL_VIEW');

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'Rental - View' privilege.",
        );
      }

      // Part 2: Retrieve Rentals and Returns
      // Call Rental._Repo findAll method by passing:
      // where:
      // Status: "Active"
      // [Op.OR]:
      // CustomerId: Params.CustomerId
      // '$JointHirers.CustomerId$': Params.CustomerId
      // include:
      // model: JointHirerModel
      // attributes: []
      const query = `
      SELECT 
          r.*
      FROM 
          rental_Rental r
      LEFT JOIN 
          rental_JointHirer jh ON r.RentalId = jh.RentalId
      WHERE 
          r.Status = 'Active'
          AND (
              r.CustomerId = '${CustomerId}' 
              OR jh.CustomerId = '${CustomerId}'
          )
      GROUP BY
          r.RentalId
      `;
      const db = rentalDb.getConnection();
      const result = await db.query(query, {
        type: QueryTypes.SELECT,
        transaction: dbTransaction,
        model: RentalModel,
        mapToModel: true,
      });

      // Format the returned records
      const records: IRentalAttr[] = result.map((record: RentalModel) => {
        return {
          RentalId: record.RentalId,
          CustomerId: record.CustomerId,
          CustomerType: record.CustomerType,
          ItemId: record.ItemId,
          ItemType: record.ItemType,
          PriceId: record.PriceId,
          StartDateTime: record.StartDateTime,
          EndDateTime: record.EndDateTime,
          CancelRemarks: record.CancelRemarks,
          TerminateRemarks: record.TerminateRemarks,
          AgreementNo: record.AgreementNo,
          AccountType: record.AccountType,
          Status: record.Status,
          EscheatmentYN: record.EscheatmentYN,
          CreatedById: record.CreatedById,
          CreatedAt: record.CreatedAt,
          UpdatedById: record.UpdatedById,
          UpdatedAt: record.UpdatedAt,
        };
      });
      // Return the returned records.
      return records;
    } catch (error) {
      throw error;
    }
  }

  async getJointHirers(dbTransaction: any) {
    try {
      if (!this.RentalId) {
        throw new ClassError('Rental', 'RentalErrMsg01', 'RentalId is missing');
      }
      if (this.AccountType !== RentalAccountTypeEnum.JOINT) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg07',
          'This rental does not have joint hirers.',
        );
      }

      const jointHirers = await Rental._JointHirerRepo.findAll({
        where: {
          RentalId: this.RentalId,
        },
        transaction: dbTransaction,
      });
      // Map the jointHirers to JointHirer instances
      const jointHirerInstances = Promise.all(
        jointHirers.map(async (hirer: JointHirerModel) => {
          return await JointHirer.init(hirer.HirerId, dbTransaction);
        }),
      );
      return jointHirerInstances;
    } catch (error) {
      console.error('Error in getJointHirers:', error);
      throw error;
    }
  }

  async getCustomerIds(loginUser: LoginUser, dbTransaction: any) {
    try {
      // Part 1: Privilege Checking
      // Call loginUser.checkPrivileges() by passing:
      // SystemCode: <get_from_app_config>
      // PrivilegeCode: "RENTAL_VIEW"
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = loginUser.checkPrivileges(systemCode, 'RENTAL_VIEW');

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'Rental - View' privilege.",
        );
      }

      // Part 2: Validation
      // Make sure this.RentalId exists, if not throw new ClassError by passing
      if (!this.RentalId) {
        throw new ClassError('Rental', 'RentalErrMsg01', 'RentalId is missing');
      }

      //Part 3: Retrieve Listing and Returns
      // 3.1  Call Rental._JointHirerRepo findAll method by passing:
      //      where:
      //          RentalId: this.RentalId
      //      dbTransaction
      const options: any = {
        where: {
          RentalId: this.RentalId,
        },
        transaction: dbTransaction,
      };

      const joinHirers = await Rental._JointHirerRepo.findAll(options);

      // 3.2 Create a new array variable and set its value to joint hirer and Rental.CustomerId
      // array translated to only CustomerId. Then, append this.CustomerId to the array.
      const customerIds: string[] = [this.CustomerId];
      for (let i = 0; i < joinHirers.length; i++) {
        const jointHirer = joinHirers[i];
        customerIds.push(jointHirer.CustomerId);
      }

      // 3.3 Return the array.
      return customerIds;
    } catch (error) {
      throw error;
    }
  }

  async signAgreement(
    loginUser: LoginUser,
    party: string,
    PartyId: string,
    PartyType: string,
    dbTransaction: any,
    statusAfterSign?: RentalStatusEnum | string,
    method?: AgreementSignatureVerificationMethodEnum,
    justification?: string,
  ) {
    try {
      // Part 1: Privilege Checking
      // Call loginUser.checkPrivileges() by passing:
      // SystemCode: <get_from_app_config>
      // PrivilegeCode: "RENTAL_SIGN"
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = loginUser.checkPrivileges(systemCode, 'RENTAL_SIGN');

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'RENTAL_SIGN' privilege.",
        );
      }

      // Part 2: Validation
      // Make sure this.Status is "Pending Signing" and agreementStatus is "Generated", if not throw new ClassError
      const agreement = await Agreement.init(this.AgreementNo, dbTransaction);
      if (
        this.Status !== RentalStatusEnum.PENDING_SIGNING &&
        agreement.Status !== AggrementStatusEnum.GENERATED
      ) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          'Rental is not ready to be signed yet.',
        );
      }

      //Make sure CustomerId inside the listing and SignatureStatus is "Pending"
      const signatureList = await Agreement.getSignatureList(
        loginUser,
        this.AgreementNo,
        dbTransaction,
      );
      const customerSignature = signatureList.find(
        (sig) =>
          sig.PartyId === PartyId &&
          sig.PartyType === PartyType &&
          sig.Party === party &&
          sig.SignatureStatus === AgreementSignatureStatusEnum.PENDING,
      );

      if (!customerSignature) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          'Signature is not pending.',
        );
      }

      //Update rental_AgreementSignature
      const signaturePayload = {
        SignatureStatus: AgreementSignatureStatusEnum.SIGNED,
        SignedAt: new Date(),
        UpdatedById: loginUser.ObjectId,
        UpdatedAt: new Date(),
        VerificationMethod: method,
        VerificationJustification: justification,
        VerifiedById: loginUser.ObjectId,
      };

      await Rental._AgreementSignatureRepo.update(signaturePayload, {
        where: {
          AgreementNo: this.AgreementNo,
          Party: party,
          PartyId: PartyId,
          PartyType: PartyType,
        },
        transaction: dbTransaction,
      });

      const signatureActivity = new Activity();
      signatureActivity.ActivityId = signatureActivity.createId();
      signatureActivity.Action = ActionEnum.UPDATE;
      signatureActivity.Description = 'Update hirer signature';
      signatureActivity.EntityType = 'AgreementSignature';
      signatureActivity.EntityId = this.AgreementNo;
      signatureActivity.EntityValueBefore = JSON.stringify(customerSignature);
      signatureActivity.EntityValueAfter = JSON.stringify(signaturePayload);
      await signatureActivity.create(loginUser.ObjectId, dbTransaction);

      const updatedSignatureList = await Agreement.getSignatureList(
        loginUser,
        this.AgreementNo,
        dbTransaction,
      );

      const pendingSignatures = updatedSignatureList.filter(
        (sig) => sig.SignatureStatus === 'Pending',
      );
      if (pendingSignatures.length > 0) {
        return;
      }

      //Part 3: Update Agreement Status
      // 3.1  Set EntityValueBefore to current agreement instance.
      const entityValueAgreementBefore = {
        AgreementNo: agreement.AgreementNo,
        Status: agreement.Status,
        DateSigned: agreement.DateSigned,
      };

      // 3.2  Set below agreement attributes:
      // DateSigned: current date & time
      // Status: "Signed"
      const payload = {
        DateSigned: new Date(),
        Status: AggrementStatusEnum.SIGNED,
      };

      // 3.3  Call Rental._AgreementRepo update()
      await Rental._AgreementRepo.update(payload, {
        where: {
          AgreementNo: this.AgreementNo,
        },
        transaction: dbTransaction,
      });

      const entityValueAgreementAfter = {
        entityValueAgreementBefore,
        ...payload,
      };

      // Part 4: Record Update Agreement Activity
      const activity = new Activity();
      activity.ActivityId = activity.createId();
      activity.Action = ActionEnum.UPDATE;
      activity.Description = 'Update agreement';
      activity.EntityType = 'RentalAgreement';
      activity.EntityId = this.AgreementNo;
      activity.EntityValueBefore = JSON.stringify(entityValueAgreementBefore);
      activity.EntityValueAfter = JSON.stringify(entityValueAgreementAfter);
      await activity.create(loginUser.ObjectId, dbTransaction);

      // Part 5: Update Rental Status
      // 5.1  Set EntityValueBefore to current rental instance.
      const entityValueRentalBefore = {
        RentalId: this.RentalId,
        CustomerId: this.CustomerId,
        CustomerType: this.CustomerType,
        ItemId: this.ItemId,
        ItemType: this.ItemType,
        PriceId: this.PriceId,
        StartDateTime: this.StartDateTime,
        EndDateTime: this.EndDateTime,
        CancelRemarks: this.CancelRemarks,
        TerminateRemarks: this.TerminateRemarks,
        AgreementNo: this.AgreementNo,
        AccountType: this.AccountType,
        Status: this.Status,
        EscheatmentYN: this.EscheatmentYN,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };
      // 5.2:  Set below rental attributes:
      // Status: "Pending Key Collection"
      // UpdatedById: loginUser.ObjectId
      // UpdatedAt: current date & time

      this._Status = statusAfterSign
        ? statusAfterSign
        : RentalStatusEnum.ACTIVE;

      const payloadRental = {
        Status: this._Status,
        UpdatedById: loginUser.ObjectId,
        UpdatedAt: new Date(),
      };

      // 5.3:  Call Rental._Repo update() method by passing:
      // populate rental instance attributes
      // dbTransaction
      await Rental._Repo.update(payloadRental, {
        where: {
          RentalId: this.RentalId,
        },
        transaction: dbTransaction,
      });

      const entityValueRentalAfter = {
        entityValueRentalBefore,
        ...payloadRental,
      };

      // Part 6: Record Update Agreement Activity
      const rentalActivity = new Activity();
      rentalActivity.ActivityId = activity.createId();
      rentalActivity.Action = ActionEnum.UPDATE;
      rentalActivity.Description = 'Sign rental agreement';
      rentalActivity.EntityType = 'Rental';
      rentalActivity.EntityId = this.RentalId;
      rentalActivity.EntityValueBefore = JSON.stringify(
        entityValueRentalBefore,
      );
      rentalActivity.EntityValueAfter = JSON.stringify(entityValueRentalAfter);
      await rentalActivity.create(loginUser.ObjectId, dbTransaction);
    } catch (error) {
      throw error;
    }
  }

  async generateAgreement(
    loginUser: LoginUser,
    dbTransaction: any,
    MediaId: string,
  ) {
    //This method will generate a new rental agreement.
    try {
      // Part 1: Privilege Checking
      // Call loginUser.checkPrivileges() by passing:
      // SystemCode: "<get_from_app_config>"
      // PrivilegeCode: "RENTAL_AGREEMENT_CREATE"

      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = loginUser.checkPrivileges(
        systemCode,
        'RENTAL_AGREEMENT_CREATE',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'RENTAL_AGREEMENT_CREATE' privilege.",
        );
      }

      // Part 2: Validation
      // Instantiate existing RentalAgreement by passing:
      // dbTransaction
      // AgreementNo: this.AgreementNo
      // Make sure Params.MediaId exists, if not throw new ClassError by passing:
      // Classname: "Rental"
      // MessageCode: "RentalErrMsg0X"
      // Message: "MediaId is required."
      const agreement = await Agreement.init(this.AgreementNo, dbTransaction);
      if (!MediaId) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg0X',
          'MediaId is required.',
        );
      }

      // Part 3: Update Agreement Record
      // Set EntityValueBefore to the agreement instance.
      // Set below agreement attributes:
      // Status: "Generated"
      // MediaId: Params.MediaId

      const EntityValueBefore = agreement;
      agreement.Status = AggrementStatusEnum.GENERATED;
      agreement.MediaId = MediaId;

      // Call Rental._AgreementRepo update method by passing:
      // Status: agreement.Status
      // MediaId: agreement.MediaId
      // dbTransaction

      await Rental._AgreementRepo.update(
        {
          Status: agreement.Status,
          MediaId: agreement.MediaId,
        },
        {
          where: {
            AgreementNo: this.AgreementNo,
          },
          transaction: dbTransaction,
        },
      );

      // Part 4: Record Update Agreement Activity
      // Initialise EntityValueAfter variable and set to agreement instance
      const EntityValueAfter = agreement;
      // Instantiate new activity from Activity class, call createId() method, then set:
      const activity = new Activity();
      // Action: ActionEnum.UPDATE
      // Description: "Generate agreement."
      // EntityType: "RentalAgreement"
      // EntityId: this.AgreementNo
      // EntityValueBefore: EntityValueBefore
      // EntityValueAfter: EntityValueAfter
      activity.ActivityId = activity.createId();
      activity.Action = ActionEnum.UPDATE;
      activity.Description = 'Generate agreement.';
      activity.EntityType = 'RentalAgreement';
      activity.EntityId = this.AgreementNo;
      activity.EntityValueBefore = JSON.stringify(EntityValueBefore);
      activity.EntityValueAfter = JSON.stringify(EntityValueAfter);

      // Call new activity create method by passing:
      // dbTransaction
      // userId: loginUser.ObjectId
      await activity.create(loginUser.ObjectId, dbTransaction);

      // Part 5: Add Record To Agreement History Table
      // Make sure there is a private static readonly _AgreementHistoryRepo variable.
      // Set newRecord to an object with below key-value:
      // AgreementNo: this.AgreementNo
      // MediaId: params.MediaId
      // ActivityCompleted: "Agreement Generated"
      // CreatedAt: Current Timestamp
      // Use _AgreementHistoryRepo.create() method and pass newRecord and dbTransaction.
      const agreementHistory = await Rental._AgreementHistoryRepo.create(
        {
          AgreementNo: this.AgreementNo,
          MediaId,
          ActivityCompleted: 'Agreement Generated',
          CreatedAt: new Date(),
        },
        { transaction: dbTransaction },
      );

      // Part 6: Record Agreement History Activity
      // Initialise EntityValueAfter variable and set to newRecord from previous part.
      // Instantiate new activity from Activity class, call createId() method, then set:
      // Action: ActionEnum.CREATE
      // Description: "Generate agreement."
      // EntityType: "RentalAgreementHistory"
      // EntityId: HistoryId from previous part.
      // EntityValueBefore: empty object.
      // EntityValueAfter: EntityValueAfter
      // Call new activity create method by passing:
      // dbTransaction
      // userId: loginUser.ObjectId
      const entityValueAfter = agreementHistory.get({ plain: true });
      const activityHistory = new Activity();
      activityHistory.ActivityId = activityHistory.createId();
      activityHistory.Action = ActionEnum.CREATE;
      activityHistory.Description = 'Generate agreement.';
      activityHistory.EntityType = 'RentalAgreementHistory';
      activityHistory.EntityId = agreementHistory.HistoryId.toString();
      activityHistory.EntityValueBefore = JSON.stringify({});
      activityHistory.EntityValueAfter = JSON.stringify(entityValueAfter);
      await activityHistory.create(loginUser.ObjectId, dbTransaction);
    } catch (error) {
      throw error;
    }
  }

  public toJSON(): IRentalAttr {
    return {
      RentalId: this.RentalId,
      CustomerId: this.CustomerId,
      CustomerType: this.CustomerType,
      ItemId: this.ItemId,
      ItemType: this.ItemType,
      PriceId: this.PriceId,
      StartDateTime: this.StartDateTime,
      EndDateTime: this.EndDateTime,
      Status: this.Status,
      CancelRemarks: this.CancelRemarks,
      TerminateRemarks: this.TerminateRemarks,
      EscheatmentYN: this.EscheatmentYN,
      AgreementNo: this.AgreementNo,
      AccountType: this.AccountType,
      CreatedById: this.CreatedById,
      CreatedAt: this.CreatedAt,
      UpdatedById: this.UpdatedById,
      UpdatedAt: this.UpdatedAt,
    };
  }

  public async getInvoices(
    loginUser: LoginUser,
    dbTransaction: any,
    accountingSystem?: IAccountSystem,
  ) {
    try {
      // Part 1: Privilege Checking
      // Call loginUser.checkPrivileges() by passing:
      // SystemCode: <get_from_app_config>
      // PrivilegeCode: "RENTAL_VIEW_INVOICE"
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = loginUser.checkPrivileges(
        systemCode,
        'RENTAL_VIEW_INVOICE',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'RENTAL_VIEW_INVOICE' privilege.",
        );
      }

      // Part 2: Validation
      // Make sure this._ObjectId exists, if not throw new ClassError by passing
      if (!this.ObjectId) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          'ObjectId is missing.',
        );
      }

      // Part 3: Check Existing Invoices
      if (this.Invoices.length > 0) {
        return this.Invoices;
      }

      // Part 4: Retrieve Invoices and Returns
      const search = {
        DocType: DocType.INVOICE,
        RelatedObjectId: this.ObjectId,
        RelatedObjectType: this.ObjectType,
      };

      const documents = await Document.findAll(
        loginUser,
        dbTransaction,
        undefined,
        undefined,
        search,
        undefined,
        accountingSystem,
      );

      this.Invoices = documents.rows.map((doc) => {
        const document = new Document(dbTransaction, doc);
        return document;
      });

      return this.Invoices;
    } catch (error) {
      throw error;
    }
  }

  public async getItem(dbTransaction: any): Promise<any> {
    if (this.Item) {
      return this.Item;
    } else {
      const ItemClass = ItemClassMap[this.ItemType];

      if (!ItemClass || typeof ItemClass.init !== 'function') {
        throw new Error(`Invalid or unregistered ItemType: ${this.ItemType}`);
      }

      ItemClass._Repo = ItemClass._Repo || ItemClass.getRepo();

      if (!ItemClass._Repo) {
        throw new Error(
          `ItemType ${this.ItemType} does not have a repository.`,
        );
      }

      const itemInstance = await ItemClass.init(this.ItemId, dbTransaction);

      if (!itemInstance) {
        throw new Error(`${this.ItemType} not found with ID ${this.ItemId}`);
      }
      this.Item = itemInstance;

      // Make sure item has setAvailable method
      if (typeof this.Item.setAvailable !== 'function') {
        throw new Error(`${this.ItemType} does not implement setAvailable()`);
      }
      return this.Item;
    }
  }

  public async cancel(
    loginUser: LoginUser,
    dbTransaction: any,
    remarks: string,
  ): Promise<Rental> {
    try {
      // Part 1: Privilege Checking
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');
      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'RENTAL_CANCEL',
      );

      if (!isPrivileged) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          "You do not have 'RENTAL_CANCEL' privilege.",
        );
      }

      // Part 2: Validation
      if (!this.ObjectId) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          'Rental not found. Rental Id is empty.',
        );
      }

      if (!remarks) {
        throw new ClassError(
          'Rental',
          'RentalErrMsg01',
          'Cancellation remarks is required.',
        );
      }

      // Part 3: Capture Record Info Before Changes
      const entityValueBefore = this.toJSON();

      // Part 4: Update Rental Status and Remarks
      this._Status = RentalStatusEnum.CANCELLED;
      this.CancelRemarks = remarks;
      this._UpdatedById = loginUser.ObjectId;
      this._UpdatedAt = new Date();

      // Part 5: Update Rental Record
      await Rental._Repo.update(
        {
          Status: this.Status,
          CancelRemarks: this.CancelRemarks,
          UpdatedById: this.UpdatedById,
          UpdatedAt: this.UpdatedAt,
        },
        {
          where: {
            RentalId: this.RentalId,
          },
          transaction: dbTransaction,
        },
      );

      // Part 6: Mark Item as Available
      const item = await this.getItem(dbTransaction);

      await item.setAvailable(loginUser, dbTransaction);

      // Part 7: Capture Record Info After Changes
      const entityValueAfter = this.toJSON();
      // Part 8: Record Activity
      const activity = new Activity();
      activity.ActivityId = activity.createId();
      activity.Action = ActionEnum.UPDATE;
      activity.Description = 'Rental Cancellation';
      activity.EntityType = this.ObjectType;
      activity.EntityId = this.ObjectId;
      activity.EntityValueBefore = JSON.stringify(entityValueBefore);
      activity.EntityValueAfter = JSON.stringify(entityValueAfter);
      await activity.create(loginUser.ObjectId, dbTransaction);
      return this;
    } catch (error) {
      throw error;
    }
  }
}
