<?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\Form\FormsRepository;
use MailPoet\Listing\Handler as ListingHandler;
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
use MailPoet\Segments\SegmentListingRepository;
use MailPoet\Segments\SegmentsRepository;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoet\Validator\Builder;

class SegmentsBulkActionEndpoint extends SegmentsEndpoint {
  private const ACTION_TRASH = 'trash';
  private const ACTION_RESTORE = 'restore';
  private const ACTION_DELETE = 'delete';
  private const ACTION_EMPTY_TRASH = 'empty_trash';

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

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

  /** @var SegmentListingRepository */
  private $segmentListingRepository;

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

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

  /** @var FormsRepository */
  private $formsRepository;

  /** @var SubscribersRepository */
  private $subscribersRepository;

  public function __construct(
    ListingHandler $listingHandler,
    SegmentListingRepository $segmentListingRepository,
    SegmentsRepository $segmentsRepository,
    NewsletterSegmentRepository $newsletterSegmentRepository,
    FormsRepository $formsRepository,
    SubscribersRepository $subscribersRepository
  ) {
    $this->listingHandler = $listingHandler;
    $this->segmentListingRepository = $segmentListingRepository;
    $this->segmentsRepository = $segmentsRepository;
    $this->newsletterSegmentRepository = $newsletterSegmentRepository;
    $this->formsRepository = $formsRepository;
    $this->subscribersRepository = $subscribersRepository;
  }

  public function handle(Request $request): Response {
    $action = $this->validateAction($request);
    $this->validateListingParams($request);
    $ids = $this->getSelectedIds($request, $action);
    $this->validateTypeBoundaries($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) {
      $this->restoreSegments($ids, $result);
    } else {
      $this->deleteSegments($ids, $result);
    }

    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(),
      'page' => Builder::integer(),
      'per_page' => 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_segments_invalid_bulk_action'
      );
    }
    return $action;
  }

  private function validateListingParams(Request $request): void {
    $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, 'asc');
    $this->validatePage($request->getParam('page'));
    $this->validatePerPage($request->getParam('per_page'), 20);

    $orderbyParam = $request->getParam('orderby') ?? $request->getParam('sort_by');
    $orderby = is_string($orderbyParam) && $orderbyParam !== ''
      ? (string)$orderbyParam
      : 'name';
    $allowedSortFields = ['name', 'created_at', 'updated_at', 'average_engagement_score'];
    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'
      );
    }
  }

  /**
   * @return int[]
   */
  private function getSelectedIds(Request $request, string $action): array {
    if ($action === self::ACTION_EMPTY_TRASH) {
      return $this->getAllMatchingIds('trash');
    }

    if ($request->getParam('select_all') === true) {
      $group = $this->validateGroup(is_string($request->getParam('group')) ? (string)$request->getParam('group') : null);
      return $this->getAllMatchingIds($group);
    }

    return $this->validateIds($request->getParam('ids'));
  }

  /**
   * @return int[]
   */
  private function getAllMatchingIds(string $group): array {
    $definition = $this->listingHandler->getListingDefinition([
      'group' => $group,
      'sort_by' => 'name',
      'sort_order' => 'asc',
      'params' => ['lists'],
    ]);
    $ids = $this->segmentListingRepository->getActionableIds($definition);
    return array_map('intval', $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);
    $formBlockers = $this->formsRepository->getNamesOfFormsForSegments();

    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if (!$segment instanceof SegmentEntity) {
        $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
        continue;
      }
      if (!in_array($segment->getType(), [SegmentEntity::TYPE_DEFAULT, SegmentEntity::TYPE_WP_USERS], true)) {
        $this->skip($result, $id, __('This list cannot be moved to trash.', 'mailpoet'));
        continue;
      }
      if (isset($newsletterBlockers[$id])) {
        $this->skip($result, $id, str_replace(
          '%1$s',
          "'" . join("', '", $newsletterBlockers[$id]) . "'",
          // translators: %1$s is a comma-separated list of emails for which the segment is used.
          _x('List cannot be deleted because it’s used for %1$s email', 'Alert shown when trying to delete segment, which is assigned to any automatic emails.', 'mailpoet')
        ));
        continue;
      }
      if (isset($formBlockers[$id])) {
        $this->skip($result, $id, str_replace(
          '%1$s',
          "'" . join("', '", $formBlockers[$id]) . "'",
          // translators: %1$s is a comma-separated list of forms for which the segment is used.
          _nx(
            'List cannot be deleted because it’s used for %1$s form',
            'List cannot be deleted because it’s used for %1$s forms',
            count($formBlockers[$id]),
            'Alert shown when trying to delete segment, when it is assigned to a form.',
            'mailpoet'
          )
        ));
        continue;
      }
      if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
        $subscribers = $this->subscribersRepository->findExclusiveSubscribersBySegment((int)$segment->getId());
        $subscriberIds = array_map(static function ($subscriber): int {
          return (int)$subscriber->getId();
        }, $subscribers);
        $this->subscribersRepository->bulkTrash($subscriberIds);
      }
      $result['updated'] += $this->segmentsRepository->doTrash([$id], $segment->getType());
    }
  }

  /**
   * @param int[] $ids
   * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
   */
  private function restoreSegments(array $ids, array &$result): void {
    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if (!$segment instanceof SegmentEntity) {
        $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
        continue;
      }
      if (!in_array($segment->getType(), [SegmentEntity::TYPE_DEFAULT, SegmentEntity::TYPE_WP_USERS], true)) {
        $this->skip($result, $id, __('This list cannot be restored.', 'mailpoet'));
        continue;
      }
      if ($segment->getType() === SegmentEntity::TYPE_WP_USERS) {
        $subscribers = $this->subscribersRepository->findBySegment((int)$segment->getId());
        $subscriberIds = array_map(static function ($subscriber): int {
          return (int)$subscriber->getId();
        }, $subscribers);
        $this->subscribersRepository->bulkRestore($subscriberIds);
      }
      $result['updated'] += $this->segmentsRepository->bulkRestore([$id], $segment->getType());
    }
  }

  /**
   * @param int[] $ids
   * @param array{updated:int,deleted:int,skipped:int,errors:array<int, array{id:int|null,message:string}>} $result
   */
  private function deleteSegments(array $ids, array &$result): void {
    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if (!$segment instanceof SegmentEntity) {
        $this->skip($result, $id, __('This list does not exist.', 'mailpoet'));
        continue;
      }
      if ($segment->getType() !== SegmentEntity::TYPE_DEFAULT) {
        $this->skip($result, $id, __('This list cannot be deleted.', 'mailpoet'));
        continue;
      }
      $result['deleted'] += $this->segmentsRepository->bulkDelete([$id]);
    }
  }

  /**
   * @param int[] $ids
   */
  private function validateTypeBoundaries(array $ids): void {
    foreach ($ids as $id) {
      $segment = $this->segmentsRepository->findOneById($id);
      if ($segment instanceof SegmentEntity && $segment->getType() === SegmentEntity::TYPE_DYNAMIC) {
        throw new ApiException(
          __('This endpoint only supports lists.', 'mailpoet'),
          400,
          'mailpoet_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 lists in the trash can be permanently deleted.', 'mailpoet'),
          400,
          'mailpoet_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,
    ];
  }
}
