<?php declare(strict_types = 1);

namespace MailPoet\Segments\DynamicSegments\Filters;

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


use MailPoet\Entities\DynamicSegmentFilterData;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
use MailPoet\WooCommerce\Helper;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;

class WooCommerceUsedShippingMethod implements Filter {
  const ACTION = 'usedShippingMethod';

  const VALID_OPERATORS = [
    DynamicSegmentFilterData::OPERATOR_NONE,
    DynamicSegmentFilterData::OPERATOR_ANY,
    DynamicSegmentFilterData::OPERATOR_ALL,
  ];

  /** @var WooFilterHelper */
  private $wooFilterHelper;

  /** @var Helper */
  private $wooHelper;

  /** @var FilterHelper */
  private $filterHelper;

  public function __construct(
    FilterHelper $filterHelper,
    WooFilterHelper $wooFilterHelper,
    Helper $wooHelper
  ) {
    $this->wooFilterHelper = $wooFilterHelper;
    $this->wooHelper = $wooHelper;
    $this->filterHelper = $filterHelper;
  }

  public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
    $filterData = $filter->getFilterData();
    $operator = $filterData->getParam('operator');
    $shippingMethodInstanceIds = $filterData->getParam('shipping_methods');
    $isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;

    $days = $filterData->getParam('days');

    if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
      throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
    }

    if (!is_array($shippingMethodInstanceIds) || empty($shippingMethodInstanceIds)) {
      throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
    }

    $data = $filterData->getData();
    $this->filterHelper->validateDaysPeriodData((array)$data);

    $excludedStatuses = $this->wooFilterHelper->defaultExcludedStatuses();
    $date = is_int($days) ? $this->filterHelper->getDateNDaysAgo($days) : Carbon::now();

    switch ($operator) {
      case DynamicSegmentFilterData::OPERATOR_ANY:
        $this->applyForAnyOperator($queryBuilder, $excludedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
        break;
      case DynamicSegmentFilterData::OPERATOR_ALL:
        $this->applyForAllOperator($queryBuilder, $excludedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
        break;
      case DynamicSegmentFilterData::OPERATOR_NONE:
        $this->applyForNoneOperator($queryBuilder, $excludedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
        break;
    }

    return $queryBuilder;
  }

  public function getLookupData(DynamicSegmentFilterData $filterData): array {
    $lookupData = ['shippingMethods' => []];
    if (!$this->wooHelper->isWooCommerceActive()) {
      return $lookupData;
    }
    $allMethods = $this->wooHelper->getShippingMethodInstancesData();
    $configuredShippingMethodInstanceIds = $filterData->getArrayParam('shipping_methods');

    foreach ($configuredShippingMethodInstanceIds as $instanceId) {
      if (isset($allMethods[$instanceId])) {
        $data = $allMethods[$instanceId];
        $lookupData['shippingMethods'][$instanceId] = $data['name'];
      }
    }

    return $lookupData;
  }

  private function applyForAnyOperator(QueryBuilder $queryBuilder, array $excludedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
    $instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');

    $orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
    $orderItemsTableAlias = 'orderItems';
    $orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
    $orderItemMetaTableAlias = 'orderItemMeta';
    $orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $excludedStatuses);
    $queryBuilder
      ->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
      ->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
      ->andWhere("$orderItemsTableAlias.order_item_type = 'shipping'")
      ->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
      ->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
      ->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING);
    if (!$isAllTime) {
      $dateParam = $this->filterHelper->getUniqueParameterName('date');
      $queryBuilder
        ->andWhere("$orderStatsAlias.date_created >= :$dateParam")
        ->setParameter($dateParam, $date->toDateTimeString());
    }
  }

  private function applyForAllOperator(QueryBuilder $queryBuilder, array $excludedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
    $orderItemTypeParam = $this->filterHelper->getUniqueParameterName('orderItemType');
    $instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');

    $orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
    $orderItemsTableAlias = 'orderItems';
    $orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
    $orderItemMetaTableAlias = 'orderItemMeta';
    $orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $excludedStatuses);

    $queryBuilder
      ->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
      ->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
      ->andWhere("$orderItemsTableAlias.order_item_type = :$orderItemTypeParam")
      ->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
      ->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
      ->setParameter($orderItemTypeParam, 'shipping')
      ->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING)
      ->groupBy('inner_subscriber_id')
      ->having("COUNT(DISTINCT($orderItemMetaTableAlias.meta_value)) = " . count($shippingMethodInstanceIds));

    if (!$isAllTime) {
      $dateParam = $this->filterHelper->getUniqueParameterName('date');
      $queryBuilder
        ->andWhere("$orderStatsAlias.date_created >= :$dateParam")
        ->setParameter($dateParam, $date->toDateTimeString());
    }
  }

  private function applyForNoneOperator(QueryBuilder $queryBuilder, array $excludedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
    $subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
    $this->applyForAnyOperator($subQuery, $excludedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
    $subscribersTable = $this->filterHelper->getSubscribersTable();
    $queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
  }
}
