<?php declare(strict_types = 1);

namespace MailPoet\Segments;

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


use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Entities\SubscriberSegmentEntity;
use MailPoet\InvalidStateException;
use MailPoet\Logging\LoggerFactory;
use MailPoet\NotFoundException;
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\Segments\DynamicSegments\FilterHandler;
use MailPoet\Settings\SettingsController;
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
use MailPoetVendor\Doctrine\DBAL\Result;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
use MailPoetVendor\Doctrine\ORM\QueryBuilder as ORMQueryBuilder;
use Throwable;

class SegmentSubscribersRepository {
  const BACKFILLED_SETTING_KEY = 'subscribers_segments_count_backfilled';

  /** @var EntityManager */
  private $entityManager;

  /** @var FilterHandler */
  private $filterHandler;

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

  /** @var SettingsController */
  private $settings;

  public function __construct(
    EntityManager $entityManager,
    FilterHandler $filterHandler,
    SegmentsRepository $segmentsRepository,
    SettingsController $settings
  ) {
    $this->entityManager = $entityManager;
    $this->filterHandler = $filterHandler;
    $this->segmentsRepository = $segmentsRepository;
    $this->settings = $settings;
  }

  public function findSubscribersIdsInSegment(int $segmentId, ?array $candidateIds = null): array {
    return $this->loadSubscriberIdsInSegment($segmentId, $candidateIds);
  }

  public function getSubscriberIdsInSegment(int $segmentId): array {
    return $this->loadSubscriberIdsInSegment($segmentId);
  }

  public function getSubscribersCount(int $segmentId, ?string $status = null): int {
    $segment = $this->getSegment($segmentId);
    $result = $this->getSubscribersStatisticsCount($segment);
    return (int)$result[$status ?: 'all'];
  }

  public function getSubscribersCountBySegmentIds(array $segmentIds, ?string $status = null, ?int $filterSegmentId = null): int {
    $segments = $this->segmentsRepository->findByIds($segmentIds);
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $queryBuilder = $this->createCountQueryBuilder();

    $subQueries = [];
    foreach ($segments as $segment) {
      $segmentQb = $this->createCountQueryBuilder();
      $segmentQb->select("{$subscribersTable}.id AS inner_id");

      if ($segment->isStatic()) {
        $segmentQb = $this->filterSubscribersInStaticSegment($segmentQb, $segment, $status);
      } else {
        $segmentQb = $this->filterSubscribersInDynamicSegment($segmentQb, $segment, $status);
      }

      // inner parameters and types have to be merged to outer queryBuilder
      $queryBuilder->setParameters(array_merge(
        $segmentQb->getParameters(),
        $queryBuilder->getParameters()
      ), array_merge(
        $segmentQb->getParameterTypes(),
        $queryBuilder->getParameterTypes()
      ));
      $subQueries[] = $segmentQb->getSQL();
    }

    $queryBuilder->innerJoin(
      $subscribersTable,
      sprintf('(%s)', join(' UNION ', $subQueries)),
      'inner_subscribers',
      "inner_subscribers.inner_id = {$subscribersTable}.id"
    );

    try {
      if (is_int($filterSegmentId)) {
        $filterSegment = $this->segmentsRepository->verifyDynamicSegmentExists($filterSegmentId);
        $filterSegmentQb = $this->createCountQueryBuilder();
        $filterSegmentQb->select("{$subscribersTable}.id AS filter_segment_subscriber_id");
        $filterSegmentQb = $this->filterSubscribersInDynamicSegment($filterSegmentQb, $filterSegment, $status);
        $queryBuilder->setParameters(array_merge($filterSegmentQb->getParameters(), $queryBuilder->getParameters()), array_merge($filterSegmentQb->getParameterTypes(), $queryBuilder->getParameterTypes()));
        $queryBuilder->innerJoin(
          $subscribersTable,
          sprintf('(%s)', $filterSegmentQb->getSQL()),
          'filter_segment',
          "filter_segment.filter_segment_subscriber_id = {$subscribersTable}.id"
        );
      }
    } catch (InvalidStateException $exception) {
      return 0;
    }

    try {
      $statement = $this->executeQuery($queryBuilder);
      /** @var string $result */
      $result = $statement->fetchOne();
      return (int)$result;
    } catch (Throwable $e) {
      $this->logQueryException(null, $e);
      return 0;
    }
  }

  /**
   * @param DynamicSegmentFilterData[] $filters
   * @return int
   * @throws InvalidStateException
   */
  public function getDynamicSubscribersCount(array $filters): int {
    try {
      $segment = new SegmentEntity('temporary segment', SegmentEntity::TYPE_DYNAMIC, '');
      foreach ($filters as $filter) {
        $segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter));
      }
      $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
      $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null);
      $statement = $this->executeQuery($queryBuilder);
      $result = $statement->fetch();

      if (!is_array($result)) {
        $result = $this->logErrorAndReturnEmptyResult(null, $queryBuilder, $result);
      }

      return isset($result['all']) && is_numeric($result['all']) ? (int)$result['all'] : 0;
    } catch (Throwable $e) {
      $this->logQueryException(null, $e);
      return 0;
    }
  }

  private function createCountQueryBuilder(): QueryBuilder {
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    return $this->entityManager
      ->getConnection()
      ->createQueryBuilder()
      ->select("count(DISTINCT $subscribersTable.id)")
      ->from($subscribersTable);
  }

  private function createDynamicStatisticsQueryBuilder(): QueryBuilder {
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    return $this->entityManager
      ->getConnection()
      ->createQueryBuilder()
      ->from($subscribersTable)
      ->addSelect("IFNULL(SUM(
            CASE WHEN $subscribersTable.deleted_at IS NULL
              THEN 1 ELSE 0 END
        ), 0) as `all`")
      ->addSelect("IFNULL(SUM(
            CASE WHEN $subscribersTable.deleted_at IS NOT NULL
              THEN 1 ELSE 0 END
        ), 0) as trash")
      ->addSelect("IFNULL(SUM(
            CASE WHEN $subscribersTable.status = :status_subscribed AND $subscribersTable.deleted_at IS NULL
              THEN 1 ELSE 0 END
        ), 0) as :status_subscribed")
      ->addSelect("IFNULL(SUM(
          CASE WHEN $subscribersTable.status = :status_unsubscribed AND $subscribersTable.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_unsubscribed")
      ->addSelect("IFNULL(SUM(
          CASE WHEN $subscribersTable.status = :status_inactive AND $subscribersTable.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_inactive")
      ->addSelect("IFNULL(SUM(
          CASE WHEN $subscribersTable.status = :status_unconfirmed AND $subscribersTable.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_unconfirmed")
      ->addSelect("IFNULL(SUM(
          CASE WHEN $subscribersTable.status = :status_bounced AND $subscribersTable.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_bounced")
      ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
      ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
      ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
      ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
      ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
  }

  /**
   * Per-status counts for a static segment, derived from cheap indexed reads
   * instead of a single COUNT(DISTINCT) join over the whole membership table.
   *
   * On a large list that join scans millions of rows (tens of seconds on a 5M
   * list). Here the dominant "subscribed" mass is never counted directly: we
   * read the total membership (index-only), subtract the trashed members, and
   * subtract the sparse non-subscribed buckets — each a seek driven from the
   * subscriber status index or the (segment_id, status, subscriber_id) index.
   *
   * subscriber_segment.status is only ever subscribed/unsubscribed, so the
   * buckets partition the non-deleted members exactly and
   * subscribed = all - unsubscribed - inactive - unconfirmed - bounced is exact,
   * equivalent to the old query's "s.status = subscribed AND ss.status =
   * subscribed".
   *
   * @return array<string, int>
   */
  private function getStaticSegmentStatisticsCount(SegmentEntity $segment): array {
    $segmentId = (int)$segment->getId();
    $unsubscribed = SubscriberEntity::STATUS_UNSUBSCRIBED;

    $totalMembership = $this->countStaticSegmentMembers($segmentId);
    $trash = $this->countStaticSegmentMembers($segmentId, function (QueryBuilder $qb): void {
      $qb->andWhere('s.deleted_at IS NOT NULL');
    });

    $inactive = $this->countStaticSegmentMembersWithStatus($segmentId, SubscriberEntity::STATUS_INACTIVE);
    $unconfirmed = $this->countStaticSegmentMembersWithStatus($segmentId, SubscriberEntity::STATUS_UNCONFIRMED);
    $bounced = $this->countStaticSegmentMembersWithStatus($segmentId, SubscriberEntity::STATUS_BOUNCED);

    // unsubscribed = members unsubscribed globally OR unsubscribed from this list.
    // The two halves are disjoint (the second excludes s.status = unsubscribed),
    // so their counts add up without double counting the overlap.
    $unsubscribedGlobal = $this->countStaticSegmentMembers($segmentId, function (QueryBuilder $qb) use ($unsubscribed): void {
      $qb->andWhere('s.deleted_at IS NULL')
        ->andWhere('s.status = :unsubGlobal')
        ->setParameter('unsubGlobal', $unsubscribed);
    });
    $unsubscribedFromList = $this->countStaticSegmentMembers($segmentId, function (QueryBuilder $qb) use ($unsubscribed): void {
      $qb->andWhere('s.deleted_at IS NULL')
        ->andWhere('ss.status = :unsubList')
        ->andWhere('s.status != :notUnsub')
        ->setParameter('unsubList', $unsubscribed)
        ->setParameter('notUnsub', $unsubscribed);
    });
    $unsubscribedCount = $unsubscribedGlobal + $unsubscribedFromList;

    $all = max(0, $totalMembership - $trash);
    $subscribed = max(0, $all - $unsubscribedCount - $inactive - $unconfirmed - $bounced);

    return [
      'all' => $all,
      'trash' => $trash,
      SubscriberEntity::STATUS_SUBSCRIBED => $subscribed,
      SubscriberEntity::STATUS_UNSUBSCRIBED => $unsubscribedCount,
      SubscriberEntity::STATUS_INACTIVE => $inactive,
      SubscriberEntity::STATUS_UNCONFIRMED => $unconfirmed,
      SubscriberEntity::STATUS_BOUNCED => $bounced,
    ];
  }

  /**
   * A non-subscribed global status bucket: members of the list whose subscriber
   * status is $status and who are not unsubscribed from the list (a list
   * unsubscribe wins, placing them in the unsubscribed bucket instead). Driven
   * from the subscriber status index, so it seeks over a sparse population.
   *
   * ss.status is only ever subscribed/unsubscribed, so "not unsubscribed from
   * the list" is expressed as an equality (ss.status = subscribed) rather than
   * ss.status != unsubscribed — same rows, but an equality seek the
   * (segment_id, status, subscriber_id) index can use.
   */
  private function countStaticSegmentMembersWithStatus(int $segmentId, string $status): int {
    return $this->countStaticSegmentMembers($segmentId, function (QueryBuilder $qb) use ($status): void {
      $qb->andWhere('s.deleted_at IS NULL')
        ->andWhere('s.status = :memberStatus')
        ->andWhere('ss.status = :memberSubscribed')
        ->setParameter('memberStatus', $status)
        ->setParameter('memberSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
    });
  }

  /**
   * Count members of a static segment. Without $constrain this is an index-only
   * read of the membership table (no join) — the cheap total the per-status
   * derivation leans on. Pass $constrain to filter on subscriber columns; the
   * subscribers table is joined in (alias 's') only then, so the unconstrained
   * total never pays for the join it doesn't need.
   *
   * @param callable(QueryBuilder): void|null $constrain
   */
  private function countStaticSegmentMembers(int $segmentId, ?callable $constrain = null): int {
    $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
    $queryBuilder = $this->entityManager->getConnection()->createQueryBuilder()
      ->select('COUNT(*)')
      ->from($subscriberSegmentTable, 'ss')
      ->where('ss.segment_id = :segmentId')
      ->setParameter('segmentId', $segmentId);
    if ($constrain !== null) {
      $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
      $queryBuilder->innerJoin('ss', $subscribersTable, 's', 's.id = ss.subscriber_id');
      $constrain($queryBuilder);
    }
    $count = $this->executeQuery($queryBuilder)->fetchOne();
    return is_numeric($count) ? (int)$count : 0;
  }

  private function createStaticGlobalStatusStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
    $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    return $this->entityManager
      ->getConnection()
      ->createQueryBuilder()
      ->from($subscriberSegmentTable, 'subscriber_segment')
      ->where('subscriber_segment.segment_id = :segment_id')
      ->setParameter('segment_id', $segment->getId())
      ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
      ->addSelect('IFNULL(SUM(
            CASE WHEN subscribers.deleted_at IS NULL
              THEN 1 ELSE 0 END
        ), 0) as `all`')
      ->addSelect('IFNULL(SUM(
            CASE WHEN subscribers.deleted_at IS NOT NULL
              THEN 1 ELSE 0 END
        ), 0) as trash')
      ->addSelect('IFNULL(SUM(
            CASE WHEN subscribers.status = :status_subscribed AND subscribers.deleted_at IS NULL
              THEN 1 ELSE 0 END
        ), 0) as :status_subscribed')
      ->addSelect('IFNULL(SUM(
          CASE WHEN subscribers.status = :status_unsubscribed AND subscribers.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_unsubscribed')
      ->addSelect('IFNULL(SUM(
          CASE WHEN subscribers.status = :status_inactive AND subscribers.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_inactive')
      ->addSelect('IFNULL(SUM(
          CASE WHEN subscribers.status = :status_unconfirmed AND subscribers.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_unconfirmed')
      ->addSelect('IFNULL(SUM(
          CASE WHEN subscribers.status = :status_bounced AND subscribers.deleted_at IS NULL
            THEN 1 ELSE 0 END
        ), 0) as :status_bounced')
      ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
      ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
      ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
      ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
      ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
  }

  public function getSubscribersWithoutSegmentCount(): int {
    if ($this->isSegmentsCountColumnReady()) {
      $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
      $count = $this->entityManager->getConnection()->executeQuery(
        "SELECT COUNT(*) FROM {$subscribersTable} WHERE segments_count = 0"
      )->fetchOne();
      return is_numeric($count) ? (int)$count : 0;
    }

    $queryBuilder = $this->entityManager->createQueryBuilder();
    $queryBuilder
      ->select('COUNT(DISTINCT s) AS subscribersCount')
      ->from(SubscriberEntity::class, 's');
    $this->addConstraintsForSubscribersWithoutSegment($queryBuilder);
    return (int)$queryBuilder->getQuery()->getSingleScalarResult();
  }

  /**
   * segments_count is only trustworthy once the backfill sweep has finished
   * (see SubscribersSegmentsCountSync). Until then the read paths fall back to
   * the anti-join so they never report 0 for everyone.
   */
  public function isSegmentsCountColumnReady(): bool {
    return (bool)$this->settings->get(self::BACKFILLED_SETTING_KEY, false);
  }

  /**
   * Flip the readiness flag once the backfill sweep has recomputed the whole
   * table. Called by SubscribersSegmentsCountSync; the setting key lives here
   * because this is the read path that decides whether to trust the column.
   */
  public function markSegmentsCountColumnReady(): void {
    $this->settings->set(self::BACKFILLED_SETTING_KEY, true);
  }

  public function getSubscribersWithoutSegmentStatisticsCount(): array {
    try {
      $queryBuilder = $this->createWithoutSegmentStatisticsQueryBuilder();

      $this->addConstraintsForSubscribersWithoutSegmentToDBAL($queryBuilder);

      $statement = $this->executeQuery($queryBuilder);
      $result = $statement->fetch();

      if (is_array($result)) {
        return $result;
      }

      return $this->logErrorAndReturnEmptyResult(null, $queryBuilder, $result);
    } catch (Throwable $e) {
      $this->logQueryException(null, $e);
      return $this->emptyStatisticsResult();
    }
  }

  private function createWithoutSegmentStatisticsQueryBuilder(): QueryBuilder {
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $queryBuilder = $this->entityManager
      ->getConnection()
      ->createQueryBuilder();
    $queryBuilder
      ->addSelect('IFNULL(SUM(
          CASE WHEN s.deleted_at IS NULL
            THEN 1 ELSE 0 END
      ), 0) as `all`')
      ->addSelect('IFNULL(SUM(
          CASE WHEN s.deleted_at IS NOT NULL
            THEN 1 ELSE 0 END
      ), 0) as trash')
      ->addSelect('IFNULL(SUM(
          CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL
            THEN 1 ELSE 0 END
      ), 0) as :status_subscribed')
      ->addSelect('IFNULL(SUM(
        CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL
          THEN 1 ELSE 0 END
      ), 0) as :status_unsubscribed')
      ->addSelect('IFNULL(SUM(
        CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL
          THEN 1 ELSE 0 END
      ), 0) as :status_inactive')
      ->addSelect('IFNULL(SUM(
        CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL
          THEN 1 ELSE 0 END
      ), 0) as :status_unconfirmed')
      ->addSelect('IFNULL(SUM(
        CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL
          THEN 1 ELSE 0 END
      ), 0) as :status_bounced')
      ->from($subscribersTable, 's')
      ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
      ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
      ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
      ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
      ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);

    return $queryBuilder;
  }

  public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void {
    if ($this->isSegmentsCountColumnReady()) {
      $queryBuilder->andWhere('s.segmentsCount = 0');
      return;
    }

    $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
    $deletedSegmentsQueryBuilder->select('sg.id')
      ->from(SegmentEntity::class, 'sg')
      ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));

    $queryBuilder
      ->leftJoin(
        's.subscriberSegments',
        'ssg',
        Join::WITH,
        (string)$queryBuilder->expr()->andX(
          $queryBuilder->expr()->eq('ssg.subscriber', 's.id'),
          $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
          $queryBuilder->expr()->notIn('ssg.segment', $deletedSegmentsQueryBuilder->getDQL())
        )
      )
      ->andWhere('ssg.id IS NULL')
      ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
  }

  public function addConstraintsForSubscribersWithoutSegmentToDBAL(QueryBuilder $queryBuilder): void {
    if ($this->isSegmentsCountColumnReady()) {
      $queryBuilder->andWhere('s.segments_count = 0');
      return;
    }

    $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
    $subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
    $deletedSegmentsQueryBuilder->select('sg.id')
      ->from(SegmentEntity::class, 'sg')
      ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));

    $queryBuilder
      ->leftJoin(
        's',
        $subscribersSegmentTable,
        'ssg',
        (string)$queryBuilder->expr()->and(
          $queryBuilder->expr()->eq('ssg.subscriber_id', 's.id'),
          $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
          $queryBuilder->expr()->notIn('ssg.segment_id', $deletedSegmentsQueryBuilder->getQuery()->getSQL())
        )
      )
      ->andWhere('ssg.id IS NULL')
      ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
  }

  private function loadSubscriberIdsInSegment(int $segmentId, ?array $candidateIds = null): array {
    $segment = $this->getSegment($segmentId);
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $queryBuilder = $this->entityManager
      ->getConnection()
      ->createQueryBuilder()
      ->select("DISTINCT $subscribersTable.id")
      ->from($subscribersTable);

    if ($segment->isStatic()) {
      $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
    } else {
      $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
    }

    if ($candidateIds) {
      $queryBuilder->andWhere("$subscribersTable.id IN (:candidateIds)")
        ->setParameter('candidateIds', $candidateIds, ArrayParameterType::STRING);
    }

    $statement = $this->executeQuery($queryBuilder);
    $result = $statement->fetchAll();
    return array_column($result, 'id');
  }

  private function filterSubscribersInStaticSegment(
    QueryBuilder $queryBuilder,
    SegmentEntity $segment,
    ?string $status = null
  ): QueryBuilder {
    $subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $parameterName = "segment_{$segment->getId()}"; // When we use this method more times the parameter name has to be unique
    $queryBuilder = $queryBuilder->join(
      $subscribersTable,
      $subscribersSegmentsTable,
      'subsegment',
      "subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :$parameterName"
    )->andWhere("$subscribersTable.deleted_at IS NULL")
      ->setParameter($parameterName, $segment->getId());
    if ($status) {
      $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
        ->andWhere("subsegment.status = :status")
        ->setParameter('status', $status);
    }
    return $queryBuilder;
  }

  private function filterSubscribersInDynamicSegment(
    QueryBuilder $queryBuilder,
    SegmentEntity $segment,
    ?string $status = null
  ): QueryBuilder {
    $filters = [];
    $dynamicFilters = $segment->getDynamicFilters();
    foreach ($dynamicFilters as $dynamicFilter) {
      $filters[] = $dynamicFilter->getFilterData();
    }

    // We don't allow dynamic segment without filers since it would return all subscribers
    // For BC compatibility fetching an empty result
    if (count($filters) === 0) {
      return $queryBuilder->andWhere('0 = 1');
    } elseif ($segment instanceof SegmentEntity) {
      try {
        $queryBuilder = $this->filterHandler->apply($queryBuilder, $segment);
      } catch (InvalidFilterException $e) {
        // If a segment has an invalid filter, we should simply consider it empty instead of throwing
        // an unhandled error. Unhandled errors here can break many admin pages.
        $queryBuilder->andWhere('0 = 1');
      }
    }
    $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
    $queryBuilder = $queryBuilder->andWhere("$subscribersTable.deleted_at IS NULL");
    if ($status) {
      $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
        ->setParameter('status', $status);
    }
    return $queryBuilder;
  }

  private function getSegment(int $id): SegmentEntity {
    $segment = $this->entityManager->find(SegmentEntity::class, $id);
    if (!$segment instanceof SegmentEntity) {
      throw new NotFoundException('Segment not found');
    }
    return $segment;
  }

  private function executeQuery(QueryBuilder $queryBuilder): Result {
    try {
      $this->entityManager->getConnection()->executeStatement('SET SESSION SQL_BIG_SELECTS=1');
    } catch (Throwable $e) {
      // Best-effort: some hosts may not allow SET SESSION, continue without it
    }
    $result = $queryBuilder->execute();
    // Execute for select always returns statement but PHP Stan doesn't know that :(
    if (!$result instanceof Result) {
      throw new InvalidStateException('Invalid query.');
    }
    return $result;
  }

  public function getSubscribersGlobalStatusStatisticsCount(SegmentEntity $segment): array {
    try {
      if ($segment->isStatic()) {
        $queryBuilder = $this->createStaticGlobalStatusStatisticsQueryBuilder($segment);
      } else {
        $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
        $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
      }

      $statement = $this->executeQuery($queryBuilder);
      $result = $statement->fetch();
      if (is_array($result)) {
        return $result;
      }

      return $this->logErrorAndReturnEmptyResult($segment, $queryBuilder, $result);
    } catch (Throwable $e) {
      $this->logQueryException($segment, $e);
      return $this->emptyStatisticsResult();
    }
  }

  public function getSubscribersStatisticsCount(SegmentEntity $segment): array {
    try {
      if ($segment->isStatic()) {
        return $this->getStaticSegmentStatisticsCount($segment);
      }

      $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
      $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
      $statement = $this->executeQuery($queryBuilder);
      $result = $statement->fetch();
      if (is_array($result)) {
        return $result;
      }

      return $this->logErrorAndReturnEmptyResult($segment, $queryBuilder, $result);
    } catch (Throwable $e) {
      $this->logQueryException($segment, $e);
      return $this->emptyStatisticsResult();
    }
  }

  /**
   * @param null|SegmentEntity $segment
   * @param QueryBuilder $queryBuilder
   * @param mixed $result
   * @return int[]
   */
  private function logErrorAndReturnEmptyResult(?SegmentEntity $segment, QueryBuilder $queryBuilder, $result): array {
    $logger = LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SEGMENTS);
    $logger->error('Invalid result for segment statistics count', [
      'segment_id' => $segment ? $segment->getId() : null,
      'result' => $result,
      'query' => $queryBuilder->getSQL(),
    ]);

    return $this->emptyStatisticsResult();
  }

  private function logQueryException(?SegmentEntity $segment, Throwable $e): void {
    $logger = LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SEGMENTS);
    $logger->error('Failed to execute segment subscribers query: ' . $e->getMessage(), [
      'segment_id' => $segment ? $segment->getId() : null,
      'error' => $e->getMessage(),
    ]);
  }

  private function emptyStatisticsResult(): array {
    return [
      'all' => 0,
      'trash' => 0,
      'subscribed' => 0,
      'unsubscribed' => 0,
      'inactive' => 0,
      'unconfirmed' => 0,
      'bounced' => 0,
    ];
  }
}
