import * as moment from 'moment';
import * as R from 'ramda';
import type { Knex } from 'knex';
import { Model, raw } from 'objection';
import { castArray, difference, defaultTo } from 'lodash';
import { BaseModel, PaginationQueryBuilderType } from '@/models/Model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost';
import { DiscountType } from '@/common/types/Discount';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { BillMeta } from './Bill.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { BillDefaultViews } from '../Bills.constants';

@ExportableModel()
@InjectModelMeta(BillMeta)
@InjectModelDefaultViews(BillDefaultViews)
export class Bill extends TenantBaseModel {
  public amount: number;
  public paymentAmount: number;
  public landedCostAmount: number;
  public allocatedCostAmount: number;
  public isInclusiveTax: boolean;
  public taxAmountWithheld: number;
  public exchangeRate: number;
  public vendorId: number;
  public billNumber: string;
  public billDate: Date;
  public dueDate: Date;
  public referenceNo: string;
  public status: string;
  public note: string;
  public currencyCode: string;
  public creditedAmount: number;
  public invLotNumber: string;
  public invoicedAmount: number;
  public openedAt: Date | string;
  public userId: number;

  public discountType: DiscountType;
  public discount: number;
  public adjustment: number;

  public branchId: number;
  public warehouseId: number;
  public projectId: number;

  public createdAt: Date;
  public updatedAt: Date | null;

  public entries?: ItemEntry[];
  public attachments!: Document[];
  public locatedLandedCosts?: BillLandedCost[];
  /**
   * Timestamps columns.
   */
  get timestamps() {
    return ['createdAt', 'updatedAt'];
  }

  /**
   * Virtual attributes.
   */
  static get virtualAttributes() {
    return [
      'balance',
      'dueAmount',
      'isOpen',
      'isPartiallyPaid',
      'isFullyPaid',
      'isPaid',
      'remainingDays',
      'overdueDays',
      'isOverdue',
      'unallocatedCostAmount',
      'localAmount',
      'localAllocatedCostAmount',
      'billableAmount',
      'amountLocal',

      'discountAmount',
      'discountAmountLocal',
      'discountPercentage',

      'adjustmentLocal',

      'subtotal',
      'subtotalLocal',
      'subtotalExludingTax',
      'taxAmountWithheldLocal',
      'total',
      'totalLocal',
    ];
  }

  /**
   * Invoice amount in base currency.
   * @returns {number}
   */
  get amountLocal(): number {
    return this.amount * this.exchangeRate;
  }

  /**
   * Subtotal. (Tax inclusive) if the tax inclusive is enabled.
   * @returns {number}
   */
  get subtotal(): number {
    return this.amount;
  }

  /**
   * Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
   * @returns {number}
   */
  get subtotalLocal(): number {
    return this.amountLocal;
  }

  /**
   * Sale invoice amount excluding tax.
   * @returns {number}
   */
  get subtotalExcludingTax(): number {
    return this.isInclusiveTax
      ? this.subtotal - this.taxAmountWithheld
      : this.subtotal;
  }

  /**
   * Tax amount withheld in base currency.
   * @returns {number}
   */
  get taxAmountWithheldLocal(): number {
    return this.taxAmountWithheld * this.exchangeRate;
  }

  /**
   * Discount amount.
   * @returns {number}
   */
  get discountAmount(): number {
    return this.discountType === DiscountType.Amount
      ? this.discount
      : this.subtotal * (this.discount / 100);
  }

  /**
   * Discount amount in local currency.
   * @returns {number | null}
   */
  get discountAmountLocal(): number | null {
    return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
  }

  /**
  /**
   * Discount percentage.
   * @returns {number | null}
   */
  get discountPercentage(): number | null {
    return this.discountType === DiscountType.Percentage ? this.discount : null;
  }

  /**
   * Adjustment amount in local currency.
   * @returns {number | null}
   */
  get adjustmentLocal(): number | null {
    return this.adjustment ? this.adjustment * this.exchangeRate : null;
  }

  /**
   * Invoice total. (Tax included)
   * @returns {number}
   */
  get total(): number {
    const adjustmentAmount = defaultTo(this.adjustment, 0);

    return R.compose(
      R.add(adjustmentAmount),
      R.subtract(R.__, this.discountAmount),
      R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld)),
    )(this.subtotal);
  }

  /**
   * Invoice total in local currency. (Tax included)
   * @returns {number}
   */
  get totalLocal(): number {
    return this.total * this.exchangeRate;
  }

  /**
   * Invoice amount in organization base currency.
   * @deprecated
   * @returns {number}
   */
  get localAmount(): number {
    return this.amountLocal;
  }

  /**
   * Retrieves the local allocated cost amount.
   * @returns {number}
   */
  get localAllocatedCostAmount(): number {
    return this.allocatedCostAmount * this.exchangeRate;
  }

  /**
   * Retrieves the local landed cost amount.
   * @returns {number}
   */
  get localLandedCostAmount(): number {
    return this.landedCostAmount * this.exchangeRate;
  }

  /**
   * Retrieves the local unallocated cost amount.
   * @returns {number}
   */
  get localUnallocatedCostAmount(): number {
    return this.unallocatedCostAmount * this.exchangeRate;
  }

  /**
   * Retrieve the balance of bill.
   * @return {number}
   */
  get balance(): number {
    return this.paymentAmount + this.creditedAmount;
  }

  /**
   * Due amount of the given.
   * @return {number}
   */
  get dueAmount(): number {
    return Math.max(this.total - this.balance, 0);
  }

  /**
   * Detarmine whether the bill is open.
   * @return {boolean}
   */
  get isOpen(): boolean {
    return !!this.openedAt;
  }

  /**
   * Deetarmine whether the bill paid partially.
   * @return {boolean}
   */
  get isPartiallyPaid(): boolean {
    return this.dueAmount !== this.total && this.dueAmount > 0;
  }

  /**
   * Deetarmine whether the bill paid fully.
   * @return {boolean}
   */
  get isFullyPaid(): boolean {
    return this.dueAmount === 0;
  }

  /**
   * Detarmines whether the bill paid fully or partially.
   * @return {boolean}
   */
  get isPaid(): boolean {
    return this.isPartiallyPaid || this.isFullyPaid;
  }

  /**
   * Retrieve the remaining days in number
   * @return {number|null}
   */
  get remainingDays(): number | null {
    const currentMoment = moment();
    const dueDateMoment = moment(this.dueDate);

    return Math.max(dueDateMoment.diff(currentMoment, 'days'), 0);
  }

  /**
   * Retrieve the overdue days in number.
   * @return {number|null}
   */
  get overdueDays(): number | null {
    const currentMoment = moment();
    const dueDateMoment = moment(this.dueDate);

    return Math.max(currentMoment.diff(dueDateMoment, 'days'), 0);
  }

  /**
   * Detarmines the due date is over.
   * @return {boolean}
   */
  get isOverdue(): boolean {
    return this.overdueDays > 0;
  }

  /**
   * Retrieve the unallocated cost amount.
   * @return {number}
   */
  get unallocatedCostAmount(): number {
    return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0);
  }

  /**
   * Retrieves the calculated amount which have not been invoiced.
   */
  get billableAmount(): number {
    return Math.max(this.total - this.invoicedAmount, 0);
  }

  /**
   * Table name
   */
  static get tableName() {
    return 'bills';
  }

  /**
   * Model modifiers.
   */
  static get modifiers() {
    return {
      /**
       * Filters the bills in draft status.
       */
      draft(query) {
        query.where('opened_at', null);
      },

      /**
       * Filters the opened bills.
       */
      published(query) {
        query.whereNot('openedAt', null);
      },

      /**
       * Filters the opened bills.
       */
      opened(query) {
        query.whereNot('opened_at', null);
      },
      /**
       * Filters the unpaid bills.
       */
      unpaid(query) {
        query.where('payment_amount', 0);
      },
      /**
       * Filters the due bills.
       */
      dueBills(query) {
        query.where(
          raw(`COALESCE(AMOUNT, 0) -
            COALESCE(PAYMENT_AMOUNT, 0) -
            COALESCE(CREDITED_AMOUNT, 0) > 0
          `),
        );
      },
      /**
       * Filters the overdue bills.
       */
      overdue(query) {
        query.where('due_date', '<', moment().format('YYYY-MM-DD'));
      },
      /**
       * Filters the not overdue invoices.
       */
      notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
        query.where('due_date', '>=', asDate);
      },
      /**
       * Filters the partially paid bills.
       */
      partiallyPaid(query) {
        query.whereNot('payment_amount', 0);
        query.whereNot(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
      },
      /**
       * Filters the paid bills.
       */
      paid(query) {
        query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
      },
      /**
       * Filters the bills from the given date.
       */
      fromDate(query, fromDate) {
        query.where('bill_date', '<=', fromDate);
      },

      /**
       * Sort the bills by full-payment bills.
       */
      sortByStatus(query, order) {
        query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
      },

      /**
       * Status filter.
       */
      statusFilter(query, filterType) {
        switch (filterType) {
          case 'draft':
            query.modify('draft');
            break;
          case 'delivered':
            query.modify('delivered');
            break;
          case 'unpaid':
            query.modify('unpaid');
            break;
          case 'overdue':
          default:
            query.modify('overdue');
            break;
          case 'partially-paid':
            query.modify('partiallyPaid');
            break;
          case 'paid':
            query.modify('paid');
            break;
        }
      },

      /**
       * Filters by branches.
       */
      filterByBranches(query, branchesIds) {
        const formattedBranchesIds = castArray(branchesIds);

        query.whereIn('branchId', formattedBranchesIds);
      },

      dueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
        query.modify('dueBills');
        query.modify('notOverdue');
        query.modify('fromDate', asDate);
      },

      overdueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
        query.modify('dueBills');
        query.modify('overdue', asDate);
        query.modify('fromDate', asDate);
      },

      /**
       *
       */
      billable(query) {
        query.where(raw('AMOUNT > INVOICED_AMOUNT'));
      },
    };
  }

  /**
   * Bill model settings.
   */
  // static get meta() {
  //   return BillSettings;
  // }

  /**
   * Relationship mapping.
   */
  static get relationMappings() {
    const { Vendor } = require('../../Vendors/models/Vendor');
    const {
      ItemEntry,
    } = require('../../TransactionItemEntry/models/ItemEntry');
    const {
      BillLandedCost,
    } = require('../../BillLandedCosts/models/BillLandedCost');
    const { Branch } = require('../../Branches/models/Branch.model');
    const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
    const { TaxRateModel } = require('../../TaxRates/models/TaxRate.model');
    const {
      TaxRateTransaction,
    } = require('../../TaxRates/models/TaxRateTransaction.model');
    const { Document } = require('../../ChromiumlyTenancy/models/Document');
    const {
      MatchedBankTransaction,
    } = require('../../BankingMatching/models/MatchedBankTransaction');

    return {
      vendor: {
        relation: Model.BelongsToOneRelation,
        modelClass: Vendor,
        join: {
          from: 'bills.vendorId',
          to: 'contacts.id',
        },
        filter(query) {
          query.where('contact_service', 'vendor');
        },
      },

      entries: {
        relation: Model.HasManyRelation,
        modelClass: ItemEntry,
        join: {
          from: 'bills.id',
          to: 'items_entries.referenceId',
        },
        filter(builder) {
          builder.where('reference_type', 'Bill');
          builder.orderBy('index', 'ASC');
        },
      },

      locatedLandedCosts: {
        relation: Model.HasManyRelation,
        modelClass: BillLandedCost,
        join: {
          from: 'bills.id',
          to: 'bill_located_costs.billId',
        },
      },

      /**
       * Bill may belongs to associated branch.
       */
      branch: {
        relation: Model.BelongsToOneRelation,
        modelClass: Branch,
        join: {
          from: 'bills.branchId',
          to: 'branches.id',
        },
      },

      /**
       * Bill may has associated warehouse.
       */
      warehouse: {
        relation: Model.BelongsToOneRelation,
        modelClass: Warehouse,
        join: {
          from: 'bills.warehouseId',
          to: 'warehouses.id',
        },
      },

      /**
       * Bill may has associated tax rate transactions.
       */
      taxes: {
        relation: Model.HasManyRelation,
        modelClass: TaxRateTransaction,
        join: {
          from: 'bills.id',
          to: 'tax_rate_transactions.referenceId',
        },
        filter(builder) {
          builder.where('reference_type', 'Bill');
        },
      },

      /**
       * Bill may has many attached attachments.
       */
      attachments: {
        relation: Model.ManyToManyRelation,
        modelClass: Document,
        join: {
          from: 'bills.id',
          through: {
            from: 'document_links.modelId',
            to: 'document_links.documentId',
          },
          to: 'documents.id',
        },
        filter(query) {
          query.where('model_ref', 'Bill');
        },
      },

      /**
       * Bill may belongs to matched bank transaction.
       */
      matchedBankTransaction: {
        relation: Model.HasManyRelation,
        modelClass: MatchedBankTransaction,
        join: {
          from: 'bills.id',
          to: 'matched_bank_transactions.referenceId',
        },
        filter(query) {
          query.where('reference_type', 'Bill');
        },
      },
    };
  }

  /**
   * Retrieve the not found bills ids as array that associated to the given vendor.
   * @param {Array} billsIds
   * @param {number} vendorId -
   * @return {Array}
   */
  static async getNotFoundBills(billsIds, vendorId) {
    const storedBills = await this.query().onBuild((builder) => {
      builder.whereIn('id', billsIds);

      if (vendorId) {
        builder.where('vendor_id', vendorId);
      }
    });

    const storedBillsIds = storedBills.map((t) => t.id);

    const notFoundBillsIds = difference(billsIds, storedBillsIds);
    return notFoundBillsIds;
  }

  static changePaymentAmount(
    billId: number,
    amount: number,
    trx: Knex.Transaction,
  ) {
    const changeMethod = amount > 0 ? 'increment' : 'decrement';

    return this.query(trx)
      .where('id', billId)
    [changeMethod]('payment_amount', Math.abs(amount));
  }

  /**
   * Model search attributes.
   */
  static get searchRoles() {
    return [
      { fieldKey: 'bill_number', comparator: 'contains' },
      { condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
      { condition: 'or', fieldKey: 'amount', comparator: 'equals' },
    ];
  }

  /**
   * Prevents mutate base currency since the model is not empty.
   */
  static get preventMutateBaseCurrency() {
    return true;
  }
}
