<?php

declare(strict_types=1);

namespace Inpsyde\MultilingualPress\Module\ExternalSites\Integrations\Redirect;

use Inpsyde\MultilingualPress\Database\Table\ExternalSitesTable;
use Inpsyde\MultilingualPress\Framework\Integration\Integration;
use Inpsyde\MultilingualPress\Framework\WordpressContext;
use Inpsyde\MultilingualPress\Module\ExternalSites\ExternalSite\ExternalSiteInterface;
use Inpsyde\MultilingualPress\Module\ExternalSites\ExternalSitesMetaBox\ExternalSitesMetaBoxView;
use Inpsyde\MultilingualPress\Module\ExternalSites\ExternalSitesRepository\ExternalSitesRepositoryInterface;
use Inpsyde\MultilingualPress\Module\Redirect\GeoLocation\GeolocationFinderInterface;
use Inpsyde\MultilingualPress\Module\Redirect\Redirector\JsRedirector;
use Inpsyde\MultilingualPress\Module\Redirect\Redirector\Redirector;
use Inpsyde\MultilingualPress\Module\Redirect\RedirectTarget\LanguageNegotiator;
use Inpsyde\MultilingualPress\Module\Redirect\RedirectTarget\RedirectTarget;
use Inpsyde\MultilingualPress\Module\Redirect\Settings\Renderers\ViewRenderer;
use Inpsyde\MultilingualPress\Module\Redirect\Settings\Repository\Repository;
use Inpsyde\MultilingualPress\Module\Redirect\Settings\TabView;

use function Inpsyde\MultilingualPress\callExit;

/**
 * @psalm-type languageCode = string
 * @psalm-type url = string
 */
class RedirectIntegration implements Integration
{
    /**
     * @var array<ExternalSiteInterface>
     */
    protected $externalSites;

    /**
     * @var LanguageNegotiator
     */
    protected $languageNegotiator;

    /**
     * @var ExternalSiteRedirectTargetFactoryInterface
     */
    protected $externalSiteRedirectTargetFactory;

    /**
     * @var Repository
     */
    protected $redirectSettingsRepository;

    /**
     * @var ViewRenderer
     */
    protected $externalRedirectFallbackViewRenderer;

    /**
     * @var ExternalSitesRepositoryInterface
     */
    protected $externalSitesRepository;

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

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

    public function __construct(
        array $externalSites,
        LanguageNegotiator $languageNegotiator,
        ExternalSiteRedirectTargetFactoryInterface $externalSiteRedirectTargetFactory,
        Repository $redirectSettingsRepository,
        ViewRenderer $externalRedirectFallbackViewRenderer,
        ExternalSitesRepositoryInterface $externalSitesRepository,
        array $userLanguages,
        GeolocationFinderInterface $geolocationFinder
    ) {

        $this->externalSites = $externalSites;
        $this->languageNegotiator = $languageNegotiator;
        $this->externalSiteRedirectTargetFactory = $externalSiteRedirectTargetFactory;
        $this->redirectSettingsRepository = $redirectSettingsRepository;
        $this->externalRedirectFallbackViewRenderer = $externalRedirectFallbackViewRenderer;
        $this->externalSitesRepository = $externalSitesRepository;
        $this->userLanguages = $userLanguages;
        $this->geolocationFinder = $geolocationFinder;
    }

    /**
     * @inheritDoc
     * phpcs:disable Inpsyde.CodeQuality.NestingLevel.High
     */
    public function integrate(): void
    {
        // phpcs:enable

        if (! $this->isRedirectEnabledForAnyExternalSite()) {
            return;
        }

        /**
         * Filters the redirect targets to add the ones from this module.
         *
         * @param RedirectTarget[] $targets
         * @return RedirectTarget[]
         */
        add_filter(
            LanguageNegotiator::FILTER_REDIRECT_TARGETS,
            function (array $targets): array {
                foreach ($this->externalSites as $externalSite) {
                    if (!$externalSite->isRedirectEnabled()) {
                        continue;
                    }

                    $languageName = str_replace('_', '-', $externalSite->locale());
                    $targets[] = $this->externalSiteRedirectTargetFactory->createExternalSiteRedirectTarget(
                        $this->userLanguages,
                        $this->geolocationFinder,
                        [
                            'locale' => $languageName,
                            'priority' => 1,
                            'siteId' => $externalSite->id(),
                            'url' => $this->externalSiteUrlById($externalSite->id()) ?: $externalSite->siteUrl(),
                        ]
                    );
                }

                return $targets;
            }
        );

        $this->integrateRedirectFallback();
    }

    /**
     * Integrates the redirect fallback functionality for external sites.
     *
     * @return void
     */
    protected function integrateRedirectFallback(): void
    {
        /**
         * Filters the network redirect settings to add the onse from this module.
         *
         * @param ViewRenderer[] $renderers
         * @return ViewRenderer[]
         */
        add_filter(TabView::FILTER_VIEW_MODELS, function (array $renderers) {
            array_splice($renderers, 1, 0, [$this->externalRedirectFallbackViewRenderer]);
            return $renderers;
        });

        /**
         * Integrates the external redirect fallback functionality when the redirect execution is set to PHP.
         */
        add_action(Redirector::ACTION_TARGET_NOT_FOUND, function () {
            $redirectFallbackSiteId = (int)$this->redirectSettingsRepository->settingValue(Repository::MODULE_SETTING_FALLBACK_REDIRECT_SITE_ID);
            $redirectFallbackExternalSiteId = (int)$this->redirectSettingsRepository->settingValue(Repository::MODULE_SETTING_FALLBACK_REDIRECT_EXTERNAL_SITE_ID);

            if ($redirectFallbackSiteId > 0 || $redirectFallbackExternalSiteId < 1) {
                return;
            }

            $externalSite = $this->externalSitesRepository->externalSiteBy(ExternalSitesTable::COLUMN_ID, $redirectFallbackExternalSiteId);

            //phpcs:disable WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit
            //phpcs:disable WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
            wp_redirect($externalSite->siteUrl());
            callExit();
        });

        /**
         * Integrates the external redirect fallback functionality when the redirect execution is set to JavaScript.
         *
         * @param array<string, string> $redirectTarget A map of redirect target language to redirect target URL.
         * @psalm-param array<languageCode, url> $redirectTarget
         * @return array<string, string> A map of redirect target language to redirect target URL.
         * @psalm-return array<languageCode, url>
         */
        add_filter(JsRedirector::FILTER_REDIRECT_TARGET, function (array $redirectTarget): array {
            if (!empty($redirectTarget)) {
                return $redirectTarget;
            }
            $redirectFallbackExternalSiteId = (int)$this->redirectSettingsRepository->settingValue(Repository::MODULE_SETTING_FALLBACK_REDIRECT_EXTERNAL_SITE_ID);

            if (!$redirectFallbackExternalSiteId || $redirectFallbackExternalSiteId < 1) {
                return $redirectTarget;
            }

            $externalSite = $this->externalSitesRepository->externalSiteBy(ExternalSitesTable::COLUMN_ID, $redirectFallbackExternalSiteId);

            return [$externalSite->locale() => $externalSite->siteUrl()];
        });
    }

    /**
     * Gets the external site url from entity meta by given external site ID.
     *
     * @param int $externalSiteId The external site ID.
     * @return string The eternal site url.
     */
    protected function externalSiteUrlById(int $externalSiteId): string
    {
        $context = new WordpressContext();
        $externalSitesMeta = (array)get_post_meta($context->queriedObjectId(), ExternalSitesMetaBoxView::META_NAME, true);

        return $externalSitesMeta[$externalSiteId]['url'] ?? '';
    }

    /**
     * Checks if redirect is enabled for any external site.
     *
     * @return bool true if redirect is enabled for any external site, otherwise false.
     */
    protected function isRedirectEnabledForAnyExternalSite(): bool
    {
        foreach ($this->externalSites as $externalSite) {
            if ($externalSite->isRedirectEnabled()) {
                return true;
            }
        }

        return false;
    }
}
