<?php

declare(strict_types=1);

namespace Inpsyde\MultilingualPress\Module\Redirect\RedirectTarget;

use Inpsyde\MultilingualPress\Framework\Api\Translation;
use Inpsyde\MultilingualPress\Framework\Api\Translations;
use Inpsyde\MultilingualPress\Framework\Api\TranslationSearchArgs;
use Inpsyde\MultilingualPress\Framework\Language\Language;
use Inpsyde\MultilingualPress\Framework\WordpressContext;
use Inpsyde\MultilingualPress\Module\Redirect\GeoLocation\GeolocationFinderInterface;
use Inpsyde\MultilingualPress\Module\Redirect\Settings\Repository\Repository;

/**
 * @psalm-type languageCode = string
 */
class LanguageNegotiator
{
    public const FILTER_REDIRECT_URL = 'multilingualpress.redirect_url';
    public const FILTER_POST_STATUS = 'multilingualpress.redirect_post_status';
    public const FILTER_REDIRECT_TARGETS = 'multilingualpress.redirect_targets';

    /**
     * @var Translations
     */
    private $translations;

    /**
     * @var Repository
     */
    private $repository;

    /**
     * A map of language codes to priorities.
     *
     * @var array<string, float>
     * @psalm-var array<languageCode, float>
     */
    protected $userLanguages;

    /**
     * @var GeolocationFinderInterface
     */
    protected $geolocationFinder;

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

    public function __construct(
        Translations $translations,
        Repository $repository,
        array $userLanguages,
        GeolocationFinderInterface $geolocationFinder,
        string $redirectType
    ) {

        $this->translations = $translations;
        $this->repository = $repository;
        $this->userLanguages = $userLanguages;
        $this->geolocationFinder = $geolocationFinder;
        $this->redirectType = $redirectType;
    }

    /**
     * Returns the redirect target data object for the best-matching language version.
     *
     * @param TranslationSearchArgs|null $args
     * @return RedirectTarget
     */
    public function redirectTarget(TranslationSearchArgs $args = null): RedirectTarget
    {
        $targets = $this->redirectTargets($args);

        if (!$targets) {
            return new RedirectTarget($this->userLanguages, $this->geolocationFinder);
        }

        $redirectType = $this->redirectType;

        $targets = array_filter(
            $targets,
            static function (RedirectTarget $target) use ($redirectType): bool {
                return 0 < $target->redirectPriority($redirectType);
            }
        );

        if (!$targets) {
            return new RedirectTarget($this->userLanguages, $this->geolocationFinder);
        }
        uasort(
            $targets,
            static function (RedirectTarget $left, RedirectTarget $right) use ($redirectType): int {
                $leftPriority = $left->priorityIndex() * $left->redirectPriority($redirectType) * $left->redirectFallbackPriority();
                $rightPriority = $right->priorityIndex() * $right->redirectPriority($redirectType) * $right->redirectFallbackPriority();

                return $rightPriority <=> $leftPriority;
            }
        );

        return reset($targets);
    }

    /**
     * Returns the redirect target data objects for all available language versions.
     *
     * @param TranslationSearchArgs|null $args
     * @return RedirectTarget[]
     */
    public function redirectTargets(TranslationSearchArgs $args = null): array
    {
        $currentSiteId = get_current_blog_id();
        $translations = $this->searchTranslations($args ?: new TranslationSearchArgs());
        $targets = [];

        foreach ($translations as $siteId => $translation) {
            $language = $translation->language();
            $remoteUrl = $translation->remoteUrl();

            /**
             * Filters the redirect URL.
             *
             * @param string $remoteUrl
             * @param Language $language
             * @param Translation $translation
             * @param int $currentSiteId
             */
            $url = (string)apply_filters(self::FILTER_REDIRECT_URL, $remoteUrl, $language, $translation, $currentSiteId);

            $targets[] = new RedirectTarget(
                $this->userLanguages,
                $this->geolocationFinder,
                [
                    RedirectTarget::KEY_CONTENT_ID => $translation->remoteContentId(),
                    RedirectTarget::KEY_LANGUAGE => $this->configureLanguageTag($language),
                    RedirectTarget::KEY_PRIORITY_INDEX => 1,
                    RedirectTarget::KEY_SITE_ID => $siteId,
                    RedirectTarget::KEY_URL => $url,
                    RedirectTarget::KEY_REDIRECT_FALLBACK_PRIORITY => $this->languageFallbackPriority($siteId),
                ]
            );
        }

        return $this->configureRedirectTargets($targets, $translations);
    }

    /**
     * Configures the given redirect targets.
     *
     * @param RedirectTarget[] $targets
     * @param Translation[] $translations
     * @return RedirectTarget[]
     */
    private function configureRedirectTargets(array $targets, array $translations): array
    {
        /**
         * Filters the possible redirect target objects.
         *
         * @param RedirectTarget[] $targets
         * @param Translation[] $translations
         */
        $targets = (array)apply_filters(self::FILTER_REDIRECT_TARGETS, $targets, $translations);

        if (!$targets) {
            return [];
        }

        $targets = array_filter(
            $targets,
            function ($target): bool { // phpcs:ignore
                return $target instanceof RedirectTarget;
            }
        );

        if (!$targets) {
            return [];
        }

        uasort(
            $targets,
            static function (RedirectTarget $left, RedirectTarget $right): int {
                return $right->priorityIndex() <=> $left->priorityIndex();
            }
        );

        return $targets;
    }

    /**
     * Returns all translations according to the given arguments.
     *
     * @param TranslationSearchArgs|null $args
     * @return Translation[]
     */
    private function searchTranslations(TranslationSearchArgs $args = null): array
    {
        /**
         * Filters the allowed status for posts to be included as possible redirect targets.
         *
         * @param string[] $postStatuses
         */
        $postStatuses = (array)apply_filters(self::FILTER_POST_STATUS, ['publish']);

        $args or $args = new TranslationSearchArgs();

        $context = new WordpressContext();
        $args->forContentId($context->queriedObjectId())
            ->forSiteId(get_current_blog_id())
            ->forPostType($context->postType())
            ->searchFor(get_search_query())
            ->forType($context->type());

        $args->includeBase()->forPostStatus(...array_filter($postStatuses, 'is_string'));

        $translations = $this->translations->searchTranslations($args);

        return array_filter(
            $translations,
            static function (Translation $translation): bool {
                return (bool)$translation->remoteUrl();
            }
        );
    }

    /**
     * Calculate the redirect language fallback priority
     *
     * @param int $siteId
     * @return float The redirect language fallback priority
     */
    protected function languageFallbackPriority(int $siteId): float
    {
        $redirectFallback = $this->repository->redirectSiteSetting($siteId, $this->repository::OPTION_SITE_ENABLE_REDIRECT_FALLBACK);

        return $redirectFallback ? 1.5 : 1;
    }

    /**
     * Configures the language tag for a given language.
     *
     * Will fix the language tags for language variants
     * and will remove the third part from language ta so de-DE-formal will become de-DE
     *
     * @param Language $language The language Object.
     * @return string The language bcp47 tag.
     */
    protected function configureLanguageTag(Language $language): string
    {
        $languageTag = $language->bcp47tag();
        if ($language->type() !== 'variant') {
            return $languageTag;
        }

        $languageParts = explode('-', $languageTag);

        return $languageParts[0] . '-' . $languageParts[1];
    }
}
