<?php declare(strict_types = 1);

namespace MailPoet\Logging\RestApi\Endpoints;

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


use DateTimeImmutable;
use MailPoet\API\REST\AbstractListingEndpoint;
use MailPoet\API\REST\ApiException;
use MailPoet\API\REST\Request;
use MailPoet\API\REST\Response;
use MailPoet\Config\AccessControl;
use MailPoet\Entities\LogEntity;
use MailPoet\Listing\Handler as ListingHandler;
use MailPoet\Listing\ListingDefinition;
use MailPoet\Listing\ListingRepository;
use MailPoet\Logging\LogListingRepository;
use MailPoet\Validator\Builder;
use MailPoet\WP\Functions as WPFunctions;

class LogsListingEndpoint extends AbstractListingEndpoint {
  private const ALLOWED_SORT_FIELD = 'created_at';
  private const ALLOWED_SORT_ORDER = 'desc';

  /** @var LogListingRepository */
  private $logListingRepository;

  public function __construct(
    ListingHandler $listingHandler,
    LogListingRepository $logListingRepository
  ) {
    parent::__construct($listingHandler);
    $this->logListingRepository = $logListingRepository;
  }

  public function handle(Request $request): Response {
    $this->validateRequest($request);
    return parent::handle($request);
  }

  public function checkPermissions(): bool {
    return WPFunctions::get()->currentUserCan(AccessControl::PERMISSION_ACCESS_PLUGIN_ADMIN);
  }

  public static function getRequestSchema(): array {
    $schema = parent::getRequestSchema();
    $schema['limit'] = Builder::integer();
    $schema['offset'] = Builder::integer();
    $schema['filter'] = Builder::object();
    return $schema;
  }

  protected function getListingRepository(): ListingRepository {
    return $this->logListingRepository;
  }

  protected function buildItems(array $rows, ListingDefinition $definition): array {
    $items = [];
    foreach ($rows as $row) {
      if (!$row instanceof LogEntity) {
        continue;
      }
      $items[] = $this->buildItem($row);
    }
    return $items;
  }

  private function buildItem(LogEntity $log): array {
    $createdAt = $log->getCreatedAt();
    return [
      'id' => (int)$log->getId(),
      'name' => $log->getName() ?? '',
      'message' => $log->getMessage() ?? '',
      'created_at' => $createdAt ? $createdAt->format('Y-m-d H:i:s') : null,
    ];
  }

  protected function getDefaultSortBy(): string {
    return self::ALLOWED_SORT_FIELD;
  }

  protected function getDefaultSortOrder(): string {
    return self::ALLOWED_SORT_ORDER;
  }

  private function validateRequest(Request $request): void {
    $this->validateSortField($request->getParam('orderby'));
    $this->validateSortField($request->getParam('sort_by'));
    $this->validateSortOrder($request->getParam('order'));
    $this->validateSortOrder($request->getParam('sort_order'));
    $this->validatePositiveInteger($request->getParam('page'), 'page', 1, self::MAX_PAGE);
    $this->validatePositiveInteger($request->getParam('per_page'), 'per_page', 1, self::MAX_PER_PAGE);
    $this->validatePositiveInteger($request->getParam('limit'), 'limit', 1, self::MAX_PER_PAGE);
    $this->validatePositiveInteger($request->getParam('offset'), 'offset', 0, self::MAX_PAGE);
    $this->validateFilters($request->getParam('filter'));
  }

  /** @param mixed $sortField */
  private function validateSortField($sortField): void {
    if ($sortField === null || $sortField === '') {
      return;
    }
    if (!is_string($sortField) || $sortField !== self::ALLOWED_SORT_FIELD) {
      throw new ApiException(
        __('Unsupported sort field. Allowed values are: created_at.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_orderby'
      );
    }
  }

  /** @param mixed $sortOrder */
  private function validateSortOrder($sortOrder): void {
    if ($sortOrder === null || $sortOrder === '') {
      return;
    }
    if (!is_string($sortOrder) || strtolower($sortOrder) !== self::ALLOWED_SORT_ORDER) {
      throw new ApiException(
        __('Unsupported sort order. Allowed values are: desc.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_order'
      );
    }
  }

  /**
   * @param mixed $value
   */
  private function validatePositiveInteger($value, string $name, int $min, int $max): void {
    if ($value === null || $value === '') {
      return;
    }
    $integer = $this->getIntegerValue($value);
    if ($integer === null || $integer < $min || $integer > $max) {
      throw new ApiException(
        sprintf(
          // translators: %1$s is a request parameter name, %2$d is the maximum accepted value.
          __('%1$s must be an integer no greater than %2$d.', 'mailpoet'),
          $name,
          $max
        ),
        400,
        'mailpoet_logs_invalid_' . $name
      );
    }
  }

  /** @param mixed $value */
  private function getIntegerValue($value): ?int {
    if (is_int($value)) {
      return $value;
    }
    if (is_string($value) && ctype_digit($value)) {
      return (int)$value;
    }
    return null;
  }

  /** @param mixed $filters */
  private function validateFilters($filters): void {
    if ($filters === null || $filters === []) {
      return;
    }
    if (!is_array($filters)) {
      throw new ApiException(
        __('Filters must be an object.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_filter'
      );
    }

    $normalizedFilters = [];
    foreach ($filters as $filter => $value) {
      if (!is_string($filter) || !in_array($filter, ['from', 'to'], true)) {
        throw new ApiException(
          __('Unsupported logs filter.', 'mailpoet'),
          400,
          'mailpoet_logs_invalid_filter'
        );
      }
      $normalizedFilters[$filter] = $value;
    }

    $from = $this->validateDateFilter($normalizedFilters, 'from');
    $to = $this->validateDateFilter($normalizedFilters, 'to');
    // Keep in sync with getDateRangeError() in logs/url-state.ts.
    if ($from && $to && $from > $to) {
      throw new ApiException(
        __('The from date must be before or equal to the to date.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_date_range'
      );
    }
  }

  /**
   * @param array<string, mixed> $filters
   */
  private function validateDateFilter(array $filters, string $field): ?DateTimeImmutable {
    if (!array_key_exists($field, $filters) || $filters[$field] === '') {
      return null;
    }
    if (!is_string($filters[$field])) {
      throw new ApiException(
        __('Log date filters must use the YYYY-MM-DD format.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_' . $field
      );
    }

    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $filters[$field]);
    $errors = DateTimeImmutable::getLastErrors();
    if (!$date || (is_array($errors) && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) || $date->format('Y-m-d') !== $filters[$field]) {
      throw new ApiException(
        __('Log date filters must use the YYYY-MM-DD format.', 'mailpoet'),
        400,
        'mailpoet_logs_invalid_' . $field
      );
    }
    return $date;
  }
}
