import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions';
import { IItemDTO, ItemAction, AbilitySubject } from '@/interfaces';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
import CheckAbilities from '@/api/middleware/CheckPolicies';
import { ItemsApplication } from '@/services/Items/ItemsApplication';

@Service()
export default class ItemsController extends BaseController {
  @Inject()
  private itemsApplication: ItemsApplication;

  @Inject()
  private dynamicListService: DynamicListingService;

  /**
   * Router constructor.
   */
  router() {
    const router = Router();

    router.post(
      '/',
      CheckAbilities(ItemAction.CREATE, AbilitySubject.Item),
      this.validateItemSchema,
      this.validationResult,
      this.asyncMiddleware(this.newItem.bind(this)),
      this.handlerServiceErrors
    );
    router.post(
      '/:id/activate',
      CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
      this.validateSpecificItemSchema,
      this.validationResult,
      this.asyncMiddleware(this.activateItem.bind(this)),
      this.handlerServiceErrors
    );
    router.post(
      '/:id/inactivate',
      CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
      [...this.validateSpecificItemSchema],
      this.validationResult,
      this.asyncMiddleware(this.inactivateItem.bind(this)),
      this.handlerServiceErrors
    );
    router.post(
      '/:id',
      CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
      [...this.validateItemSchema, ...this.validateSpecificItemSchema],
      this.validationResult,
      this.asyncMiddleware(this.editItem.bind(this)),
      this.handlerServiceErrors
    );
    router.delete(
      '/:id',
      CheckAbilities(ItemAction.DELETE, AbilitySubject.Item),
      [...this.validateSpecificItemSchema],
      this.validationResult,
      this.asyncMiddleware(this.deleteItem.bind(this)),
      this.handlerServiceErrors
    );
    router.get(
      '/:id',
      CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
      [...this.validateSpecificItemSchema],
      this.validationResult,
      this.asyncMiddleware(this.getItem.bind(this)),
      this.handlerServiceErrors
    );
    router.get(
      '/',
      CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
      [...this.validateListQuerySchema],
      this.validationResult,
      this.asyncMiddleware(this.getItemsList.bind(this)),
      this.dynamicListService.handlerErrorsToResponse,
      this.handlerServiceErrors
    );
    return router;
  }

  /**
   * Validate item schema.
   */
  get validateItemSchema(): ValidationChain[] {
    return [
      check('name')
        .exists()
        .isString()
        .isLength({ max: DATATYPES_LENGTH.STRING }),
      check('type')
        .exists()
        .isString()
        .trim()
        .isIn(['service', 'non-inventory', 'inventory']),
      check('code')
        .optional({ nullable: true })
        .isString()
        .trim()
        .isLength({ max: DATATYPES_LENGTH.STRING }),
      // Purchase attributes.
      check('purchasable').optional().isBoolean().toBoolean(),
      check('cost_price')
        .optional({ nullable: true })
        .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
        .toFloat()
        .if(check('purchasable').equals('true'))
        .exists(),
      check('cost_account_id').if(check('purchasable').equals('true')).exists(),
      check('cost_account_id')
        .optional({ nullable: true })
        .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
        .toInt(),
      // Sell attributes.
      check('sellable').optional().isBoolean().toBoolean(),
      check('sell_price')
        .optional({ nullable: true })
        .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
        .toFloat()
        .if(check('sellable').equals('true'))
        .exists(),
      check('sell_account_id').if(check('sellable').equals('true')).exists(),
      check('sell_account_id')
        .optional({ nullable: true })
        .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
        .toInt(),
      check('inventory_account_id')
        .if(check('type').equals('inventory'))
        .exists(),
      check('inventory_account_id')
        .optional({ nullable: true })
        .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
        .toInt(),
      check('sell_description')
        .optional({ nullable: true })
        .isString()
        .trim()
        .isLength({ max: DATATYPES_LENGTH.TEXT }),
      check('purchase_description')
        .optional({ nullable: true })
        .isString()
        .trim()
        .isLength({ max: DATATYPES_LENGTH.TEXT }),
      check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
      check('purchase_tax_rate_id')
        .optional({ nullable: true })
        .isInt()
        .toInt(),
      check('category_id')
        .optional({ nullable: true })
        .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
        .toInt(),
      check('note')
        .optional()
        .isString()
        .trim()
        .isLength({ max: DATATYPES_LENGTH.TEXT }),
      check('active').optional().isBoolean().toBoolean(),

      check('media_ids').optional().isArray(),
      check('media_ids.*').exists().isNumeric().toInt(),
    ];
  }

  /**
   * Validate specific item params schema.
   * @return {ValidationChain[]}
   */
  get validateSpecificItemSchema(): ValidationChain[] {
    return [param('id').exists().isNumeric().toInt()];
  }

  /**
   * Validate list query schema.
   */
  private get validateListQuerySchema() {
    return [
      query('column_sort_by').optional().trim(),
      query('sort_order').optional().isIn(['desc', 'asc']),

      query('page').optional().isNumeric().toInt(),
      query('page_size').optional().isNumeric().toInt(),

      query('view_slug').optional({ nullable: true }).isString().trim(),
      query('stringified_filter_roles').optional().isJSON(),

      query('inactive_mode').optional().isBoolean().toBoolean(),
      query('search_keyword').optional({ nullable: true }).isString().trim(),
    ];
  }

  /**
   * Stores the given item details to the storage.
   * @param {Request} req
   * @param {Response} res
   */
  private async newItem(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const itemDTO: IItemDTO = this.matchedBodyData(req);

    try {
      const storedItem = await this.itemsApplication.createItem(
        tenantId,
        itemDTO
      );

      return res.status(200).send({
        id: storedItem.id,
        message: 'The item has been created successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Updates the given item details on the storage.
   * @param {Request} req
   * @param {Response} res
   */
  private async editItem(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const itemId: number = req.params.id;
    const item: IItemDTO = this.matchedBodyData(req);

    try {
      await this.itemsApplication.editItem(tenantId, itemId, item);

      return res.status(200).send({
        id: itemId,
        message: 'The item has been edited successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Activates the given item.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  private async activateItem(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;
    const itemId: number = req.params.id;

    try {
      await this.itemsApplication.activateItem(tenantId, itemId);

      return res.status(200).send({
        id: itemId,
        message: 'The item has been activated successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Inactivates the given item.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  private async inactivateItem(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId } = req;
    const itemId: number = req.params.id;

    try {
      await this.itemsApplication.inactivateItem(tenantId, itemId);

      return res.status(200).send({
        id: itemId,
        message: 'The item has been inactivated successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Deletes the given item from the storage.
   * @param {Request} req
   * @param {Response} res
   */
  private async deleteItem(req: Request, res: Response, next: NextFunction) {
    const itemId: number = req.params.id;
    const { tenantId } = req;

    try {
      await this.itemsApplication.deleteItem(tenantId, itemId);

      return res.status(200).send({
        id: itemId,
        message: 'The item has been deleted successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Retrieve details the given item id.
   * @param {Request} req
   * @param {Response} res
   * @return {Response}
   */
  private async getItem(req: Request, res: Response, next: NextFunction) {
    const itemId: number = req.params.id;
    const { tenantId } = req;

    try {
      const item = await this.itemsApplication.getItem(tenantId, itemId);

      return res.status(200).send({
        item: this.transfromToResponse(item),
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Retrieve items datatable list.
   * @param {Request} req
   * @param {Response} res
   */
  private async getItemsList(req: Request, res: Response, next: NextFunction) {
    const { tenantId } = req;

    const filter = {
      sortOrder: 'DESC',
      columnSortBy: 'created_at',
      page: 1,
      pageSize: 12,
      inactiveMode: false,
      ...this.matchedQueryData(req),
    };

    try {
      const { items, pagination, filterMeta } =
        await this.itemsApplication.getItems(tenantId, filter);

      return res.status(200).send({
        items: this.transfromToResponse(items),
        pagination: this.transfromToResponse(pagination),
        filter_meta: this.transfromToResponse(filterMeta),
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Handles service errors.
   * @param {Error} error
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  private handlerServiceErrors(
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    if (error instanceof ServiceError) {
      if (error.errorType === 'NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
        });
      }
      if (error.errorType === 'ITEMS_NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'ITEMS_NOT_FOUND', code: 130 }],
        });
      }
      if (error.errorType === 'ITEM_CATEOGRY_NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 140 }],
        });
      }
      if (error.errorType === 'ITEM_NAME_EXISTS') {
        return res.status(400).send({
          errors: [{ type: 'ITEM.NAME.ALREADY.EXISTS', code: 210 }],
        });
      }
      if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
        return res.status(400).send({
          errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
        });
      }
      if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
        return res.status(400).send({
          errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
        });
      }
      if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
        });
      }
      if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
        return res.status(400).send({
          errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
        });
      }
      if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
        return res.status(400).send({
          errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
        });
      }
      if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
        return res.status(400).send({
          errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
        });
      }
      if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
        });
      }
      if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200 }],
        });
      }
      if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
        return res.status(400).send({
          errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
        });
      }
      if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
        return res.status(400).send({
          errors: [{ type: 'INVENTORY.ACCOUNT.NOT.INVENTORY.TYPE', code: 300 }],
        });
      }
      if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') {
        return res.status(400).send({
          errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }],
        });
      }
      if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
        return res.status(400).send({
          errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
        });
      }
      if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') {
        return res.status(400).send({
          errors: [
            { type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 },
          ],
        });
      }
      if (error.errorType === 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE') {
        return res.status(400).send({
          errors: [
            {
              type: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
              message: 'Cannot change inventory item type',
              code: 340,
            },
          ],
        });
      }
      if (error.errorType === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS') {
        return res.status(400).send({
          errors: [
            {
              type: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
              message:
                'Cannot change item type to inventory with item has associated transactions.',
              code: 350,
            },
          ],
        });
      }
      if (error.errorType === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED') {
        return res.status(400).send({
          errors: [
            {
              type: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
              message:
                'Cannot change item inventory account while the item has transactions.',
              code: 360,
            },
          ],
        });
      }
      if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS') {
        return res.status(400).send({
          errors: [
            {
              type: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
              code: 370,
              message:
                'Could not delete item that has associated transactions.',
            },
          ],
        });
      }
      if (error.errorType === 'PURCHASE_TAX_RATE_NOT_FOUND') {
        return res.status(400).send({
          errors: [
            {
              type: 'PURCHASE_TAX_RATE_NOT_FOUND',
              message: 'Purchase tax rate has not found.',
              code: 410,
            },
          ],
        });
      }
      if (error.errorType === 'SELL_TAX_RATE_NOT_FOUND') {
        return res.status(400).send({
          errors: [
            {
              type: 'SELL_TAX_RATE_NOT_FOUND',
              message: 'Sell tax rate is not found.',
              code: 420,
            },
          ],
        });
      }
    }
    next(error);
  }
}
