import { Op } from 'sequelize';
import {
  ProductVariantStatus,
  ProductVariantType,
} from '../../enum/product-variant.enum';
import { YN } from '../../enum/yn.enum';
import { ProductVariantRepository } from './variant-product.repository';
import { ClassError, ObjectBase } from '@tomei/general';
import { IProductVariantAttr } from '../../interfaces/product-variant-attr.interface';
import { ApplicationConfig } from '@tomei/config';
import { LoginUser } from '@tomei/sso';
import { ProductVariantWithInventoryModel } from '../../entities/product-variant-with-inventory.entity';
import { ProductModel } from '../../entities/product.entity';
import cuid from '../../helpers/cuid';
import { ActionEnum, Activity } from '@tomei/activity-history';
import { IProductVariantUpdate } from '../../interfaces/product-variant-update.interface';
import checkifUserHasPrivilege from '../../helpers/privilege-checking';

export class ProductVariant extends ObjectBase {
  ObjectId: string;
  ObjectName: string;
  TableName: string;
  ObjectType = 'ProductVariant';
  private _ProductId: string;
  private _Name: string;
  private _Description: string;
  private _SKU: string;
  private _Size: string;
  private _Colour: string;
  private _Type: ProductVariantType;
  private _Level = 0;
  private _ParentId: string | null = null;
  private _MinWeight: number;
  private _MaxWeight: number;
  private _MinWidth: number;
  private _MaxWidth: number;
  private _MinHeight: number;
  private _MaxHeight: number;
  private _MinLength: number;
  private _MaxLength: number;
  private _Status: ProductVariantStatus = ProductVariantStatus.ACTIVE;
  private _UpdatedSSYN: YN = YN.N;
  private _CreatedAt: Date;
  private _UpdatedAt: Date;
  private _CreatedById: string;
  private _UpdatedById: string;
  private static _Repo = new ProductVariantRepository();

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

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

  get Name(): string {
    return this._Name;
  }

  set Name(Name: string) {
    this._Name = Name;
  }

  get Description(): string {
    return this._Description;
  }

  set Description(Description: string) {
    this._Description = Description;
  }

  get Type(): ProductVariantType {
    return this._Type;
  }

  set Type(Type: ProductVariantType) {
    this._Type = Type;
  }

  get ProductId(): string {
    return this._ProductId;
  }

  set ProductId(ProductId: string) {
    this._ProductId = ProductId;
  }

  get Level(): number {
    return this._Level;
  }

  set Level(Level: number) {
    this._Level = Level;
  }

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

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

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

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

  get UpdatedSSYN(): YN {
    return this._UpdatedSSYN;
  }

  get SKU(): string {
    return this._SKU;
  }

  get Size(): string {
    return this._Size;
  }

  get Colour(): string {
    return this._Colour;
  }

  get MinWeight(): number {
    return this._MinWeight;
  }

  get MaxWeight(): number {
    return this._MaxWeight;
  }

  get MinWidth(): number {
    return this._MinWidth;
  }

  get MaxWidth(): number {
    return this._MaxWidth;
  }

  get MinHeight(): number {
    return this._MinHeight;
  }

  get MaxHeight(): number {
    return this._MaxHeight;
  }

  get MinLength(): number {
    return this._MinLength;
  }

  get MaxLength(): number {
    return this._MaxLength;
  }

  get ParentId(): string | null {
    return this._ParentId;
  }

  get Status(): ProductVariantStatus {
    return this._Status;
  }

  set Status(Status: ProductVariantStatus) {
    this._Status = Status;
  }

  async setSKU(SKU: string) {
    try {
      const where = {
        SKU: SKU,
      };

      if (this.VariantId) {
        where['VariantId'] = {
          [Op.ne]: this.VariantId,
        };
      }
      const variant = await ProductVariant._Repo.findOne({
        where: where,
      });
      if (variant) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg03',
          'SKU must be unique.',
        );
      }
      this._SKU = SKU;
    } catch (error) {
      throw error;
    }
  }

  async setParentId(ParentId: string) {
    try {
      if (this.Level === 0) {
        this._ParentId = null;
      }

      if (this.Level >= 1) {
        const variant = await ProductVariant._Repo.findOne({
          where: {
            VariantId: ParentId,
          },
        });

        if (!variant) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg04',
            'ParentId must be existing Product Variant Id.',
          );
        }

        this._ParentId = ParentId;
      }
    } catch (error) {
      throw error;
    }
  }

  isProductIdMissing(): boolean {
    if (!this.ProductId) {
      return true;
    }
    return false;
  }

  async setSize(Size: string) {
    try {
      if (this.isProductIdMissing()) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg02',
          'ProductId is missing.',
        );
      }

      const where: any = {
        ProductId: this.ProductId,
        Level: this.Level,
        ParentId: this.ParentId,
      };

      if (this.VariantId) {
        where['VariantId'] = {
          [Op.ne]: this.VariantId,
        };
      }
      const variants = await ProductVariant._Repo.findAll({
        where,
      });

      variants.forEach((variant) => {
        if (variant.Size === Size) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg11',
            'Duplicate Size found on other variant of the same product.',
          );
        }
      });

      this._Size = Size;
    } catch (error) {
      throw error;
    }
  }

  async setColour(Colour: string) {
    try {
      if (this.isProductIdMissing()) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg02',
          'ProductId is missing.',
        );
      }
      const where: any = {
        ProductId: this.ProductId,
        Level: this.Level,
        ParentId: this.ParentId,
      };

      if (this.VariantId) {
        where['VariantId'] = {
          [Op.ne]: this.VariantId,
        };
      }
      const variants = await ProductVariant._Repo.findAll({
        where,
      });

      variants.forEach((variant) => {
        if (variant.Colour === Colour) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg12',
            'Duplicate Colour found on other variant of the same product.',
          );
        }
      });

      this._Colour = Colour;
    } catch (error) {
      throw error;
    }
  }

  async setMinMax(
    type:
      | ProductVariantType.HEIGHT
      | ProductVariantType.LENGTH
      | ProductVariantType.WEIGHT
      | ProductVariantType.WIDTH,
    min: number,
    max: number,
  ) {
    try {
      if (this.isProductIdMissing()) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg02',
          'ProductId is missing.',
        );
      }

      const where: any = {
        ProductId: this.ProductId,
        Level: this.Level,
        ParentId: this.ParentId,
        [Op.or]: [
          { ['Min' + type]: { [Op.between]: [min[0], max[1]] } },
          { ['Max' + type]: { [Op.between]: [min[0], max[1]] } },
        ],
      };

      if (this.VariantId) {
        where['VariantId'] = {
          [Op.ne]: this.VariantId,
        };
      }
      const variants = await ProductVariant._Repo.findAll({
        where,
      });

      if (variants) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg13',
          'Min Max fields values cannot overlap.',
        );
      }

      switch (this.Type) {
        case ProductVariantType.WEIGHT:
          this._MinWeight = min;
          this._MaxWeight = max;
          break;
        case ProductVariantType.WIDTH:
          this._MinWidth = min;
          this._MaxWidth = max;
          break;
        case ProductVariantType.HEIGHT:
          this._MinHeight = min;
          this._MaxHeight = max;
          break;
        case ProductVariantType.LENGTH:
          this._MinLength = min;
          this._MaxLength = max;
          break;
        default:
          break;
      }
    } catch (error) {
      throw error;
    }
  }

  validateType() {
    switch (this.Type) {
      case ProductVariantType.SIZE:
        if (this.Size === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg05',
            'Size is compulsory.',
          );
        }
        break;
      case ProductVariantType.COLOUR:
        if (this.Colour === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg06',
            'Colour is compulsory.',
          );
        }
        break;
      case ProductVariantType.WEIGHT:
        if (this.MinWeight === null || this.MaxWeight === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg07',
            'MinWeight and MaxWeight fields must be compulsory.',
          );
        }
        break;
      case ProductVariantType.WIDTH:
        if (this.MinWidth === null || this.MaxWidth === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg08',
            'MinWidth and MaxWidth fields must be compulsory.',
          );
        }
        break;
      case ProductVariantType.HEIGHT:
        if (this.MinHeight === null || this.MaxHeight === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg09',
            'MinHeight and MaxHeight fields must be compulsory.',
          );
        }
        break;
      case ProductVariantType.LENGTH:
        if (this.MinLength === null || this.MaxLength === null) {
          throw new ClassError(
            'ProductVariant',
            'ProductVariantErrMsg10',
            'MinLength and MaxLength fields must be compulsory.',
          );
        }
        break;
      default:
        break;
    }
  }

  protected constructor(variantAttr?: IProductVariantAttr) {
    super();
    if (variantAttr) {
      //populate all the attributes
      this.VariantId = variantAttr.VariantId;
      this.ProductId = variantAttr.ProductId;
      this.Name = variantAttr.Name;
      this.Description = variantAttr.Description;
      this._SKU = variantAttr.SKU;
      this._Size = variantAttr.Size;
      this._Colour = variantAttr.Colour;
      this.Type = variantAttr.Type;
      this.Level = variantAttr.Level;
      this._ParentId = variantAttr.ParentId;
      this._MinWeight = variantAttr.MinWeight;
      this._MaxWeight = variantAttr.MaxWeight;
      this._MinWidth = variantAttr.MinWidth;
      this._MaxWidth = variantAttr.MaxWidth;
      this._MinHeight = variantAttr.MinHeight;
      this._MaxHeight = variantAttr.MaxHeight;
      this._MinLength = variantAttr.MinLength;
      this._MaxLength = variantAttr.MaxLength;
      this._Status = variantAttr.Status;
      this._UpdatedSSYN = variantAttr.UpdatedSSYN;
      this._CreatedAt = variantAttr.CreatedAt;
      this._UpdatedAt = variantAttr.UpdatedAt;
      this._CreatedById = variantAttr.CreatedById;
      this._UpdatedById = variantAttr.UpdatedById;
    }
  }

  public static async init(variantId?: string, dbTransaction?: any) {
    try {
      if (variantId) {
        const variant = await ProductVariant._Repo.findOne({
          where: {
            VariantId: variantId,
          },
          transaction: dbTransaction,
        });
        if (!variant) {
          throw new Error(`Variant not found.`);
        }

        const data: IProductVariantAttr = {
          VariantId: variant.VariantId,
          ProductId: variant.ProductId,
          Name: variant.Name,
          Description: variant.Description,
          SKU: variant.SKU,
          Size: variant.Size,
          Colour: variant.Colour,
          Type: variant.Type,
          Level: variant.Level,
          ParentId: variant.ParentId,
          MinWeight: variant.MinWeight,
          MaxWeight: variant.MaxWeight,
          MinWidth: variant.MinWidth,
          MaxWidth: variant.MaxWidth,
          MinHeight: variant.MinHeight,
          MaxHeight: variant.MaxHeight,
          MinLength: variant.MinLength,
          MaxLength: variant.MaxLength,
          Status: variant.Status,
          CreatedById: variant.CreatedById,
          CreatedAt: variant.CreatedAt,
          UpdatedById: variant.UpdatedById,
          UpdatedAt: variant.UpdatedAt,
          UpdatedSSYN: variant.UpdatedSSYN,
        };
        return new ProductVariant(data);
      } else {
        return new ProductVariant();
      }
    } catch (error) {
      throw error;
    }
  }

  public static async findAll(
    productId: string,
    loginUser: LoginUser,
    page?: number,
    row?: number,
    dbTransaction?: any,
  ) {
    try {
      // Privilege checking
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');

      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'ProductVariant - List',
      );
      if (!isPrivileged) {
        throw new Error('You do not have permission to list product variant.');
      }

      let options: any = {
        where: {
          ProductId: productId,
        },
        include: [
          {
            model: ProductVariantWithInventoryModel,
          },
          {
            model: ProductModel,
          },
        ],
        transaction: dbTransaction,
      };

      if (page && row) {
        options = {
          ...options,
          offset: (page - 1) * row,
          limit: row,
          order: [['CreatedAt', 'DESC']],
        };
      }

      const productBrand = await this._Repo.findAndCountAll(options);
      const data = productBrand.rows.map((variant) => {
        return {
          VariantId: variant.VariantId,
          ProductId: variant.ProductId,
          Name: variant.Name,
          Description: variant.Description,
          SKU: variant.SKU,
          Size: variant.Size,
          Colour: variant.Colour,
          Type: variant.Type,
          Level: variant.Level,
          ParentId: variant.ParentId,
          MinWeight: variant.MinWeight,
          MaxWeight: variant.MaxWeight,
          MinWidth: variant.MinWidth,
          MaxWidth: variant.MaxWidth,
          MinHeight: variant.MinHeight,
          MaxHeight: variant.MaxHeight,
          MinLength: variant.MinLength,
          MaxLength: variant.MaxLength,
          Status: variant.Status,
          CreatedById: variant.CreatedById,
          CreatedAt: variant.CreatedAt,
          UpdatedById: variant.UpdatedById,
          UpdatedAt: variant.UpdatedAt,
          UpdatedSSYN: variant.UpdatedSSYN,
          TotalUnitsAvailable:
            variant.ProductVariantWithInventory.TotalUnitsAvailable,
          TotalUnitsInCurrentOrder:
            variant.ProductVariantWithInventory.TotalUnitsInCurrentOrder,
          TotalUnitsSold: variant.ProductVariantWithInventory.TotalUnitsSold,
          TotalUnitsReserved:
            variant.ProductVariantWithInventory.TotalUnitsReserved,
          TotalUnitsInTransit:
            variant.ProductVariantWithInventory.TotalUnitsInTransit,
          TotalUnitsOnConsignment:
            variant.ProductVariantWithInventory.TotalUnitsOnConsignment,
          TotalUnitsBackOrdered:
            variant.ProductVariantWithInventory.TotalUnitsBackOrdered,
          TotalUnitsPreOrdered:
            variant.ProductVariantWithInventory.TotalUnitsPreOrdered,
          StockLowAlertLevel:
            variant.ProductVariantWithInventory.StockLowAlertLevel,
          StockReorderLevel:
            variant.ProductVariantWithInventory.StockReorderLevel,
          BufferStockLevel:
            variant.ProductVariantWithInventory.BufferStockLevel,
          Products: {
            ...variant.Product,
          },
        };
      });
      return {
        count: productBrand.count,
        rows: data,
      };
    } catch (error) {
      throw error;
    }
  }

  public async create(loginUser: LoginUser, dbTransaction?: any) {
    try {
      // Privilege checking
      const systemCode =
        ApplicationConfig.getComponentConfigValue('system-code');

      const isPrivileged = await loginUser.checkPrivileges(
        systemCode,
        'ProductVariant - Create',
      );
      if (!isPrivileged) {
        throw new Error(
          'You do not have permission to create product variant.',
        );
      }

      if (!this.ProductId) {
        throw new ClassError(
          'ProductVariant',
          'ProductVariantErrMsg02',
          'ProductId is missing.',
        );
      }

      this.validateType();

      this.VariantId = cuid();
      this._CreatedAt = new Date();
      this._UpdatedAt = new Date();
      this._CreatedById = loginUser.ObjectId;
      this._UpdatedById = loginUser.ObjectId;

      const data = {
        VariantId: this.VariantId,
        ProductId: this.ProductId,
        Name: this.Name,
        Description: this.Description,
        SKU: this.SKU,
        Size: this.Size,
        Colour: this.Colour,
        Type: this.Type,
        Level: this.Level,
        ParentId: this.ParentId,
        MinWeight: this.MinWeight,
        MaxWeight: this.MaxWeight,
        MinWidth: this.MinWidth,
        MaxWidth: this.MaxWidth,
        MinHeight: this.MinHeight,
        MaxHeight: this.MaxHeight,
        MinLength: this.MinLength,
        MaxLength: this.MaxLength,
        Status: this.Status,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
        UpdatedSSYN: this.UpdatedSSYN,
      };

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

      //Record Activity History
      const activity = new Activity();
      activity.ActivityId = cuid();
      activity.Action = ActionEnum.CREATE;
      activity.Description = 'Add new product variant';
      activity.EntityType = 'ProductVariant';
      activity.EntityId = data.VariantId;
      activity.EntityValueBefore = JSON.stringify({});
      activity.EntityValueAfter = JSON.stringify(data);

      await activity.create(loginUser.ObjectId, dbTransaction);

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

  public async update(
    payload: IProductVariantUpdate,
    loginUser: LoginUser,
    dbTransaction?: any,
  ) {
    try {
      await checkifUserHasPrivilege(
        loginUser,
        'ProductVariant - Update',
        'You do not have permission to update product variant.',
      );

      const entityValueBefore = {
        VariantId: this.VariantId,
        ProductId: this.ProductId,
        Name: this.Name,
        Description: this.Description,
        SKU: this.SKU,
        Size: this.Size,
        Colour: this.Colour,
        Type: this.Type,
        Level: this.Level,
        ParentId: this.ParentId,
        MinWeight: this.MinWeight,
        MaxWeight: this.MaxWeight,
        MinWidth: this.MinWidth,
        MaxWidth: this.MaxWidth,
        MinHeight: this.MinHeight,
        MaxHeight: this.MaxHeight,
        MinLength: this.MinLength,
        MaxLength: this.MaxLength,
        UpdatedSSYN: this.UpdatedSSYN,
        Status: this.Status,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      this.Name = payload.Name;
      this.Description = payload.Description;
      if (this.SKU !== payload.SKU) {
        await this.setSKU(payload.SKU);
      }
      this.Level = payload.Level;
      if (this.ParentId !== payload.ParentId) {
        await this.setParentId(payload.ParentId);
      }

      const isTypeChanged = this.Type !== payload.Type;
      this.Type = payload.Type;

      if (isTypeChanged) {
        this._Size = null;
        this._Colour = null;
        this._MinWeight = null;
        this._MaxWeight = null;
        this._MinWidth = null;
        this._MaxWidth = null;
        this._MinHeight = null;
        this._MaxHeight = null;
        this._MinLength = null;
        this._MaxLength = null;
      }

      switch (this.Type) {
        case ProductVariantType.COLOUR:
          await this.setColour(payload.Colour);
          break;
        case ProductVariantType.SIZE:
          await this.setSize(payload.Size);
          break;
        case ProductVariantType.HEIGHT:
          await this.setMinMax(this.Type, payload.MinWeight, payload.MaxHeight);
          break;
        case ProductVariantType.LENGTH:
          await this.setMinMax(this.Type, payload.MinHeight, payload.MaxHeight);
          break;
        case ProductVariantType.WIDTH:
          await this.setMinMax(this.Type, payload.MinHeight, payload.MaxHeight);
          break;
        case ProductVariantType.WEIGHT:
          await this.setMinMax(this.Type, payload.MinHeight, payload.MaxHeight);
          break;
        default:
          break;
      }

      this._UpdatedAt = new Date();
      this._CreatedById = loginUser.ObjectId;

      const data: any = {
        Name: this.Name,
        Description: this.Description,
        SKU: this.SKU,
        Size: this.Size,
        Colour: this.Colour,
        Type: this.Type,
        Level: this.Level,
        ParentId: this.ParentId,
        MinWeight: this.MinWeight,
        MaxWeight: this.MaxWeight,
        MinWidth: this.MinWidth,
        MaxWidth: this.MaxWidth,
        MinHeight: this.MinHeight,
        MaxHeight: this.MaxHeight,
        MinLength: this.MinLength,
        MaxLength: this.MaxLength,
        UpdatedSSYN: this.UpdatedSSYN,
        Status: this.Status,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      await ProductVariant._Repo.update(data, {
        where: {
          VariantId: this.VariantId,
        },
        transaction: dbTransaction,
      });

      data.VariantId = this.VariantId;
      data.ProductId = this.ProductId;

      //Record Activity History
      const activity = new Activity();
      activity.ActivityId = cuid();
      activity.Action = ActionEnum.UPDATE;
      activity.Description = 'Update product variant';
      activity.EntityType = 'ProductVariant';
      activity.EntityId = data.VariantId;
      activity.EntityValueBefore = JSON.stringify(entityValueBefore);
      activity.EntityValueAfter = JSON.stringify(data);

      await activity.create(loginUser.ObjectId, dbTransaction);

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

  async delete(loginUser: LoginUser, dbTransaction?: any) {
    try {
      await checkifUserHasPrivilege(
        loginUser,
        'ProductVariant - Delete',
        'You do not have permission to delete product variant.',
      );

      const entityValueBefore = {
        VariantId: this.VariantId,
        ProductId: this.ProductId,
        Name: this.Name,
        Description: this.Description,
        SKU: this.SKU,
        Size: this.Size,
        Colour: this.Colour,
        Type: this.Type,
        Level: this.Level,
        ParentId: this.ParentId,
        MinWeight: this.MinWeight,
        MaxWeight: this.MaxWeight,
        MinWidth: this.MinWidth,
        MaxWidth: this.MaxWidth,
        MinHeight: this.MinHeight,
        MaxHeight: this.MaxHeight,
        MinLength: this.MinLength,
        MaxLength: this.MaxLength,
        UpdatedSSYN: this.UpdatedSSYN,
        Status: this.Status,
        CreatedById: this.CreatedById,
        CreatedAt: this.CreatedAt,
        UpdatedById: this.UpdatedById,
        UpdatedAt: this.UpdatedAt,
      };

      await ProductVariant._Repo.delete(this.VariantId, dbTransaction);

      //Record Activity History
      const activity = new Activity();
      activity.ActivityId = cuid();
      activity.Action = ActionEnum.DELETE;
      activity.Description = 'Delete product variant';
      activity.EntityType = 'ProductVariant';
      activity.EntityId = this.VariantId;
      activity.EntityValueBefore = JSON.stringify(entityValueBefore);
      activity.EntityValueAfter = JSON.stringify({});

      await activity.create(loginUser.ObjectId, dbTransaction);

      return {
        MessageCode: 'ProductVariantDeleted',
        Message: 'Product Variant is deleted.',
      };
    } catch (error) {
      throw error;
    }
  }
}
