<?php declare(strict_types = 1);

namespace MailPoet\Segments\RestApi\Endpoints;

if (!defined('ABSPATH')) exit;


use MailPoet\API\REST\ApiException;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Listing\Handler as ListingHandler;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Segments\DynamicSegments\DynamicSegmentsListingRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Validator\Builder;

class DynamicSegmentsBulkActionEndpoint extends SegmentsEndpoint {
  private const ACTION_TRASH = 'trash';
  private const ACTION_RESTORE = 'restore';
  private const ACTION_DELETE = 'delete';

  private const SUPPORTED_ACTIONS = [
    self::ACTION_TRASH,
    self::ACTION_RESTORE,
    self::ACTION_DELETE,
  ];

  /** @var SegmentsRepository */
  private $segmentsRepository;

  /** @var ListingHandler */
  private $listingHandler;

  /** @var DynamicSegmentsListingRepository */
  private $dynamicSegmentsListingRepository;

  /** @var NewsletterSegmentRepository */
  private $newsletterSegmentRepository;

  public function __construct(
    ListingHandler $listingHandler,
    DynamicSegmentsListingRepository $dynamicSegmentsListingRepository,
    SegmentsRepository $segmentsRepository,
    NewsletterSegmentRepository $newsletterSegmentRepository
  ) {
    $this->listingHandler = $listingHandler;
    $this->dynamicSegmentsListingRepository = $dynamicSegmentsListingRepository;
    $this->segmentsRepository = $segmentsRepository;
    $this->newsletterSegmentRepository = $newsletterSegmentRepository;
  }

  public function handle(Request $request): Response {
    $action = $this->validateAction($request);
    $this->validateGroup(is_string($request->getParam('group')) ? (string)$request->getParam('group') : null);
    $orderParam = $request->getParam('order') ?? $request->getParam('sort_order');
    $this->validateOrder(is_string($orderParam) ? (string)$orderParam : null, 'desc');
    $this->validatePage($request->getParam('page'));
    $this->validateOffset($request->getParam('offset'));
    $this->validatePerPage($request->getParam('per_page') ?? $request->getParam('limit'), 25);
    $orderbyParam = $request->getParam('orderby') ?? $request->getParam('sort_by');
    $orderby = is_string($orderbyParam) && $orderbyParam !== ''
      ? (string)$orderbyParam
      : 'updated_at';
    $allowedSortFields = ['name', 'created_at', 'updated_at'];
    if (!in_array($orderby, $allowedSortFields, true)) {
      throw new ApiException(
        sprintf(
          // translators: %s is the list of supported sort fields.
          __('Unsupported sort field. Allowed values are: %s.', 'mailpoet'),
          implode(', ', $allowedSortFields)
        ),
        400,
        'mailpoet_segments_invalid_orderby'
      );
    }
    $ids = $this->getSelectedIds($request, $orderby, is_string($orderParam) ? strtolower($orderParam) : 'desc');
    $this->validateDynamicSelection($ids);
    if ($action === self::ACTION_DELETE) {
      $this->validateDeletionSelection($ids);
    }

    $result = [
      'updated' => 0,
      'deleted' => 0,
      'skipped' => 0,
      'errors' => [],
    ];

    if ($action === self::ACTION_TRASH) {
      $this->trashSegments($ids, $result);
    } elseif ($action === self::ACTION_RESTORE) {
      $result['updated'] = $this->segmentsRepository->bulkRestore($ids, SegmentEntity::TYPE_DYNAMIC);
    } else {
      $result['deleted'] = $this->segmentsRepository->bulkDelete($ids, SegmentEntity::TYPE_DYNAMIC);
    }

    return new Response($result);
  }

  public static function getRequestSchema(): array {
    return [
      'action' => Builder::string()->required(),
      'ids' => Builder::array(Builder::integer()),
      'select_all' => Builder::boolean(),
      'group' => Builder::string(),
      'search' => Builder::string(),
      'page' => Builder::integer(),
      'per_page' => Builder::integer(),
      'limit' => Builder::integer(),
      'offset' => Builder::integer(),
      'orderby' => Builder::string(),
      'order' => Builder::string(),
      'sort_by' => Builder::string(),
      'sort_order' => Builder::string(),
    ];
  }

  private function validateAction(Request $request): string {
    $action = is_string($request->getParam('action')) ? (string)$request->getParam('action') : '';
    if (!in_array($action, self::SUPPORTED_ACTIONS, true)) {
      throw new ApiException(
        sprintf(
          // translators: %s is the list of supported bulk actions.
          __('Unsupported bulk action. Allowed values are: %s.', 'mailpoet'),
          implode(', ', self::SUPPORTED_ACTIONS)
        ),
        400,
        'mailpoet_dynamic_segments_invalid_bulk_action'
      );
    }
    return $action;
  }

  /**
   * @return int[]
   */
  private function getSelectedIds(Request $request, string $sortBy, string $sortOrder): array {
    if ($request->getParam('select_all') !== true) {
      return $this->validateIds($request->getParam('ids'));
    }

    $definition = $this->listingHandler->getListingDefinition([
      'group' => $this->validateGroup(is_string($request->getParam('group')) ? (string)$request->getParam('group') : null),
      'search' => is_string($request->getParam('search')) ? (string)$request->getParam('search') : null,
      'sort_by' => $sortBy,
      'sort_order' => $sortOrder,
      'params' => ['segments'],
    ]);
    $ids = $this->dynamicSegmentsListingRepository->getActionableIds($definition);
    $ids = array_map('intval', $ids);
    if ($ids === []) {
      throw new ApiException(
        __('At least one segment id is required.', 'mailpoet'),
        400,
        'mailpoet_segments_ids_required'
      );
    }
    return $ids;
  }

  /**
   * @param int[] $ids
   * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
   */
  private function trashSegments(array $ids, array &$result): void {
    $newsletterBlockers = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
    $trashIds = [];
    foreach ($ids as $id) {
      if (isset($newsletterBlockers[$id])) {
        $segment = $this->segmentsRepository->findOneById($id);
        $this->skip($result, $id, sprintf(
          // translators: %1$s is the name of the segment, %2$s is a comma-separated list of emails for which the segment is used.
          _x('Segment \'%1$s\' cannot be deleted because it’s used for \'%2$s\' email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet'),
          $segment instanceof SegmentEntity ? $segment->getName() : (string)$id,
          join("', '", $newsletterBlockers[$id])
        ));
        continue;
      }
      $trashIds[] = $id;
    }
    $result['updated'] = $this->segmentsRepository->bulkTrash($trashIds, SegmentEntity::TYPE_DYNAMIC);
  }

  /**
   * @param int[] $ids
   */
  private function validateDynamicSelection(array $ids): void {
    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if (!$segment instanceof SegmentEntity) {
        throw new ApiException(
          __('One or more selected dynamic segments were not found.', 'mailpoet'),
          400,
          'mailpoet_dynamic_segments_not_found'
        );
      }
      if ($segment->getType() !== SegmentEntity::TYPE_DYNAMIC) {
        throw new ApiException(
          __('This endpoint only supports dynamic segments.', 'mailpoet'),
          400,
          'mailpoet_dynamic_segments_invalid_type'
        );
      }
    }
  }

  /**
   * @param int[] $ids
   */
  private function validateDeletionSelection(array $ids): void {
    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if ($segment instanceof SegmentEntity && $segment->getDeletedAt() === null) {
        throw new ApiException(
          __('Only dynamic segments in the trash can be permanently deleted.', 'mailpoet'),
          400,
          'mailpoet_dynamic_segments_delete_requires_trash'
        );
      }
    }
  }

  /**
   * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
   */
  private function skip(array &$result, ?int $id, string $message): void {
    $result['skipped']++;
    $result['errors'][] = [
      'id' => $id,
      'message' => $message,
    ];
  }
}