<?php

declare(strict_types=1);

namespace Inpsyde\MultilingualPress\Module\WooCommerce\TranslationUi\Product\Ajax;

use Inpsyde\MultilingualPress\Framework\Database\Exception\NonexistentTable;
use Inpsyde\MultilingualPress\Framework\Http\Request;
use Inpsyde\MultilingualPress\Framework\SwitchSiteTrait;
use Inpsyde\MultilingualPress\TranslationUi\Post\Ajax\ContextBuilder;
use Inpsyde\MultilingualPress\TranslationUi\Post\RelationshipContext;
use stdClass;
use wpdb;

use function Inpsyde\MultilingualPress\resolve;
use function Inpsyde\MultilingualPress\translationIds;

/**
 * Functionality for searching products in connected sites
 *
 *
 * @psalm-type productId = int
 * @psalm-type title = string
 *
 */
class Search
{
    use SwitchSiteTrait;

    public const ACTION = 'multilingualpress_remote_post_search';
    public const FILTER_PRODUCT_SEARCH_LIMIT = 'multilingualpress.filter_product_search_limit';

    /**
     * @var ContextBuilder
     */
    private $contextBuilder;

    /**
     * @var wpdb
     */
    private $wpdb;

    /**
     * @var string
     */
    protected $alreadyConnectedNotice;

    public function __construct(ContextBuilder $contextBuilder, wpdb $wpdb, string $alreadyConnectedNotice)
    {
        $this->contextBuilder = $contextBuilder;
        $this->wpdb = $wpdb;
        $this->alreadyConnectedNotice = $alreadyConnectedNotice;
    }

    /**
     * Handles the request and calls find products with the search terms
     * @param Request $request
     * @return void
     */
    public function handle(Request $request)
    {
        $searchQuery = (string)$request->bodyValue(
            'search',
            INPUT_POST,
            FILTER_SANITIZE_SPECIAL_CHARS,
            FILTER_FLAG_NO_ENCODE_QUOTES
        );

        if (!$searchQuery) {
            wp_send_json_error(esc_html__('Missing data.', 'multilingualpress'));
            return;
        }

        wp_send_json_success($this->findProducts($searchQuery, $this->contextBuilder->build()));
    }

    /**
     * Finds the product by given search query.
     *
     * @param string $searchQuery The search query.
     * @param RelationshipContext $context
     * @return array<int, string> A map of product ID to product title.
     * @psalm-return array<productId, title>
     * @throws NonexistentTable
     */
    protected function findProducts(string $searchQuery, RelationshipContext $context): array
    {

        $postType = $context->sourcePost()->post_type;
        if ($postType !== 'product') {
            return [];
        }

        $excludePostId = $context->hasRemotePost() ? $context->remotePost()->ID : 0;

        $sourceSiteId = get_current_blog_id();
        $previousSite = $this->maybeSwitchSite($context->remoteSiteId());

        $productIdsToTitlesMap = array_map(
            function (stdClass $product) use ($sourceSiteId): array {
                $productId = (int)$product->ID;
                $title = $product->post_title;

                $title = $this->isConnectedWithProductOfSite($productId, $sourceSiteId)
                    ? "{$title} ($this->alreadyConnectedNotice)"
                    : $title;

                return ['id' => $productId, 'title' => $title];
            },
            $this->findProductsByNameOrSku($searchQuery, $excludePostId)
        );

        $this->maybeRestoreSite($previousSite);

        return $productIdsToTitlesMap ?? [];
    }

    /**
     * @param string $searchQuery
     * @param int $excludePostId
     * @return array|object|null
     * phpcs:disable Inpsyde.CodeQuality.ReturnTypeDeclaration.NoReturnType
     * phpcs:disable Inpsyde.CodeQuality.LineLength.TooLong
     */
    protected function findProductsByNameOrSku(string $searchQuery, int $excludePostId)
    {
        $sql = <<<SQL
SELECT {$this->wpdb->posts}.ID, {$this->wpdb->posts}.post_title FROM {$this->wpdb->posts}
INNER JOIN {$this->wpdb->postmeta} ON ( {$this->wpdb->posts}.ID = {$this->wpdb->postmeta}.post_id )
WHERE {$this->wpdb->posts}.post_type = 'product'
AND ((({$this->wpdb->posts}.post_title LIKE %s) OR ({$this->wpdb->posts}.post_excerpt LIKE %s) OR ({$this->wpdb->posts}.post_content LIKE %s)))
AND ({$this->wpdb->posts}.post_status IN ('publish', 'future', 'pending', 'private'))
AND ({$this->wpdb->posts}.ID NOT IN (%d))
OR (({$this->wpdb->postmeta}.meta_key = '_sku' AND {$this->wpdb->postmeta}.meta_value LIKE %s))
GROUP BY {$this->wpdb->posts}.ID
ORDER BY {$this->wpdb->posts}.post_date
DESC LIMIT 0, %d
SQL;
        //phpcs:enable

        $escapedLikeQuery = '%' . $this->wpdb->esc_like($searchQuery) . '%';

        //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
        return $this->wpdb->get_results(
            $this->wpdb->prepare(
                $sql,
                $escapedLikeQuery,
                $escapedLikeQuery,
                $escapedLikeQuery,
                $excludePostId,
                $escapedLikeQuery,
                apply_filters(self::FILTER_PRODUCT_SEARCH_LIMIT, resolve('product_search_limit'))
            )
        );
        //phpcs:enable
    }

    /**
     * Checks if the product with given product ID is connected to any product from given site ID.
     *
     * @param int $productId The product ID.
     * @param int $siteId The site ID.
     * @return bool true if is connected, otherwise false.
     * @throws NonexistentTable
     */
    protected function isConnectedWithProductOfSite(int $productId, int $siteId): bool
    {
        $translations = translationIds($productId);
        return in_array($productId, $translations, true) && $translations[$siteId];
    }
}
