import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, query, param } from 'express-validator';
import { ServiceError } from '@/exceptions';
import BaseController from '../BaseController';
import InventoryAdjustmentService from '@/services/Inventory/InventoryAdjustmentService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AbilitySubject, InventoryAdjustmentAction } from '@/interfaces';
import CheckPolicies from '../../middleware/CheckPolicies';

@Service()
export default class InventoryAdjustmentsController extends BaseController {
  @Inject()
  inventoryAdjustmentService: InventoryAdjustmentService;

  @Inject()
  dynamicListService: DynamicListingService;

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

    router.post(
      '/:id/publish',
      CheckPolicies(
        InventoryAdjustmentAction.EDIT,
        AbilitySubject.InventoryAdjustment
      ),
      [param('id').exists().isNumeric().toInt()],
      this.validationResult,
      this.asyncMiddleware(this.publishInventoryAdjustment.bind(this)),
      this.handleServiceErrors
    );
    router.delete(
      '/:id',
      CheckPolicies(
        InventoryAdjustmentAction.DELETE,
        AbilitySubject.InventoryAdjustment
      ),
      [param('id').exists().isNumeric().toInt()],
      this.validationResult,
      this.asyncMiddleware(this.deleteInventoryAdjustment.bind(this)),
      this.handleServiceErrors
    );
    router.post(
      '/quick',
      CheckPolicies(
        InventoryAdjustmentAction.CREATE,
        AbilitySubject.InventoryAdjustment
      ),
      this.validatateQuickAdjustment,
      this.validationResult,
      this.asyncMiddleware(this.createQuickInventoryAdjustment.bind(this)),
      this.handleServiceErrors
    );
    router.get(
      '/:id',
      CheckPolicies(
        InventoryAdjustmentAction.VIEW,
        AbilitySubject.InventoryAdjustment
      ),
      [param('id').exists().isNumeric().toInt()],
      this.validationResult,
      this.asyncMiddleware(this.getInventoryAdjustment.bind(this)),
      this.handleServiceErrors
    );
    router.get(
      '/',
      CheckPolicies(
        InventoryAdjustmentAction.VIEW,
        AbilitySubject.InventoryAdjustment
      ),
      [...this.validateListQuerySchema],
      this.validationResult,
      this.asyncMiddleware(this.getInventoryAdjustments.bind(this)),
      this.dynamicListService.handlerErrorsToResponse,
      this.handleServiceErrors
    );
    return router;
  }

  /**
   * Validate list query schema
   */
  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('stringified_filter_roles').optional().isJSON(),
    ];
  }

  /**
   * Quick inventory adjustment validation schema.
   */
  get validatateQuickAdjustment() {
    return [
      check('date').exists().isISO8601(),
      check('type')
        .exists()
        .isIn(['increment', 'decrement', 'value_adjustment']),
      check('reference_no').exists(),
      check('adjustment_account_id').exists().isInt().toInt(),
      check('reason').exists().isString().exists(),
      check('description').optional().isString(),
      check('item_id').exists().isInt().toInt(),
      check('quantity')
        .if(check('type').exists().isIn(['increment', 'decrement']))
        .exists()
        .isInt()
        .toInt(),
      check('cost')
        .if(check('type').exists().isIn(['increment']))
        .exists()
        .isFloat()
        .toInt(),
      check('publish').default(false).isBoolean().toBoolean(),

      check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
      check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
    ];
  }

  /**
   * Creates a quick inventory adjustment.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  async createQuickInventoryAdjustment(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId, user } = req;
    const quickInventoryAdjustment = this.matchedBodyData(req);

    try {
      const inventoryAdjustment =
        await this.inventoryAdjustmentService.createQuickAdjustment(
          tenantId,
          quickInventoryAdjustment,
          user
        );

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

  /**
   * Deletes the given inventory adjustment transaction.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  async deleteInventoryAdjustment(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId } = req;
    const { id: adjustmentId } = req.params;

    try {
      await this.inventoryAdjustmentService.deleteInventoryAdjustment(
        tenantId,
        adjustmentId
      );
      return res.status(200).send({
        id: adjustmentId,
        message: 'The inventory adjustment has been deleted successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Publish the given inventory adjustment transaction.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  async publishInventoryAdjustment(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId } = req;
    const { id: adjustmentId } = req.params;

    try {
      await this.inventoryAdjustmentService.publishInventoryAdjustment(
        tenantId,
        adjustmentId
      );
      return res.status(200).send({
        id: adjustmentId,
        message: 'The inventory adjustment has been published successfully.',
      });
    } catch (error) {
      next(error);
    }
  }

  /**
   * Retrieve the specific inventory adjustment transaction of  the given id.
   * @param {Request} req -
   * @param {Response} res -
   * @param {NextFunction} next -
   */
  async getInventoryAdjustment(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId } = req;
    const { id: adjustmentId } = req.params;

    try {
      const inventoryAdjustment =
        await this.inventoryAdjustmentService.getInventoryAdjustment(
          tenantId,
          adjustmentId
        );

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

  /**
   * Retrieve the inventory adjustments paginated list.
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  async getInventoryAdjustments(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const { tenantId } = req;
    const filter = {
      page: 1,
      pageSize: 12,
      columnSortBy: 'created_at',
      sortOrder: 'desc',
      filterRoles: [],
      ...this.matchedQueryData(req),
    };

    try {
      const { pagination, inventoryAdjustments } =
        await this.inventoryAdjustmentService.getInventoryAdjustments(
          tenantId,
          filter
        );

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

  /**
   * Handles service errors.
   * @param {Error} error
   * @param {Request} req
   * @param {Response} res
   * @param {NextFunction} next
   */
  private handleServiceErrors(
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    if (error instanceof ServiceError) {
      if (error.errorType === 'INVENTORY_ADJUSTMENT_NOT_FOUND') {
        return res.status(400).send({
          errors: [
            {
              type: 'INVENTORY_ADJUSTMENT.NOT.FOUND',
              code: 100,
              message: 'The inventory adjustment not found.',
            },
          ],
        });
      }
      if (error.errorType === 'NOT_FOUND') {
        return res.status(400).send({
          errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
        });
      }
      if (error.errorType === 'account_not_found') {
        return res.boom.notFound('The given account not found.', {
          errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
        });
      }
      if (error.errorType === 'ITEM_SHOULD_BE_INVENTORY_TYPE') {
        return res.boom.badRequest(
          'You could not make adjustment on item has no inventory type.',
          { errors: [{ type: 'ITEM_SHOULD_BE_INVENTORY_TYPE', code: 300 }] }
        );
      }
      if (error.errorType === 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED') {
        return res.boom.badRequest('', {
          errors: [
            { type: 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED', code: 400 },
          ],
        });
      }
      if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
        return res.boom.badRequest(null, {
          errors: [
            {
              type: 'TRANSACTIONS_DATE_LOCKED',
              code: 4900,
              data: { ...error.payload },
            },
          ],
        });
      }
    }
    next(error);
  }
}
