<?php

declare(strict_types=1);

namespace Inpsyde\MultilingualPress\Module\WooCommerce;

use Inpsyde\Assets\Asset;
use Inpsyde\Assets\AssetManager;
use Inpsyde\Assets\Script;
use Inpsyde\Assets\Style;
use Inpsyde\MultilingualPress\Attachment;
use Inpsyde\MultilingualPress\Core\Entity\ActivePostTypes;
use Inpsyde\MultilingualPress\Core\PostTypeRepository;
use Inpsyde\MultilingualPress\Core\ServiceProvider as CoreServiceProvider;
use Inpsyde\MultilingualPress\Core\TaxonomyRepository;
use Inpsyde\MultilingualPress\Framework\Admin\PersistentAdminNotices;
use Inpsyde\MultilingualPress\Framework\Api\ContentRelations;
use Inpsyde\MultilingualPress\Framework\Api\SiteRelations;
use Inpsyde\MultilingualPress\Framework\Factory\UrlFactory;
use Inpsyde\MultilingualPress\Framework\Http\Request;
use Inpsyde\MultilingualPress\Framework\Http\ServerRequest;
use Inpsyde\MultilingualPress\Framework\Module\Module;
use Inpsyde\MultilingualPress\Framework\Module\ModuleManager;
use Inpsyde\MultilingualPress\Framework\Module\ModuleServiceProvider;
use Inpsyde\MultilingualPress\Framework\PluginProperties;
use Inpsyde\MultilingualPress\Framework\Service\Container;
use Inpsyde\MultilingualPress\Framework\Service\Exception\LateAccessToNotSharedService;
use Inpsyde\MultilingualPress\Framework\Service\Exception\NameNotFound;
use Inpsyde\MultilingualPress\Framework\Service\Exception\NameOverwriteNotAllowed;
use Inpsyde\MultilingualPress\Framework\Service\Exception\WriteAccessOnLockedContainer;
use Inpsyde\MultilingualPress\Module\Comments\CommentsCopy\CommentsCopier;
use Inpsyde\MultilingualPress\Module\Comments\RelationshipContext\CommentsRelationshipContextInterface;
use Inpsyde\MultilingualPress\Module\Comments\SiteSettings\CommentsSettingsRepository;
use Inpsyde\MultilingualPress\Module\Comments\TranslationUi\CommentsListViewTranslationColumn;
use Inpsyde\MultilingualPress\Module\Comments\TranslationUi\MetaboxAction;
use Inpsyde\MultilingualPress\Module\Comments\TranslationUi\CommentMetaboxTab;
use Inpsyde\MultilingualPress\Module\WooCommerce\TranslationUi\Product;
use Inpsyde\MultilingualPress\Module\WooCommerce\TranslationUi\Review\Field\CommentMetaboxReviewRating;
use Inpsyde\MultilingualPress\TranslationUi\MetaboxFieldsHelperFactory;
use Inpsyde\MultilingualPress\TranslationUi\Post;
use Inpsyde\MultilingualPress\Translator\PostTranslator;
use Inpsyde\MultilingualPress\Translator\PostTypeTranslator;
use Inpsyde\MultilingualPress\Translator\TermTranslator;
use Inpsyde\MultilingualPress\Module\Comments\ServiceProvider as CommentsModule;
use WP_Comment;
use wpdb;
use Inpsyde\MultilingualPress\TranslationUi\ServiceProvider as TranslationUiServiceProvider;

class ServiceProvider implements ModuleServiceProvider
{
    public const MODULE_ID = 'woocommerce';

    public const MODULE_SCRIPTS_HANDLER_NAME = 'multilingualpress-woocommerce';
    public const PARAMETER_CONFIG_SHOULD_ENQUEUE_MODULE_ASSETS = 'multilingualpress.woocommerce.shouldEnqueueModuleAssets';
    public const CONFIGURATION_NAME_FOR_URL_TO_MODULE_ASSETS = 'multilingualpress.woocommerce.urlToModuleAssets';

    /**
     * @inheritdoc
     */
    public function registerModule(ModuleManager $moduleManager): bool
    {
        $disabledDescription = '';
        $description = __(
            'Enable WooCommerce Support for MultilingualPress.',
            'multilingualpress'
        );

        if (!$this->isWooCommerceActive()) {
            $disabledDescription = __(
                'The module can be activated only if WooCommerce is active at least in the main site.',
                'multilingualpress'
            );
        }

        return $moduleManager->register(
            new Module(
                self::MODULE_ID,
                [
                    'description' => "{$description} {$disabledDescription}",
                    'name' => __('WooCommerce', 'multilingualpress'),
                    'active' => true,
                    'disabled' => !$this->isWooCommerceActive(),
                ]
            )
        );
    }

    /**
     * @inheritdoc
     *
     * phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong
     * @throws NameOverwriteNotAllowed | WriteAccessOnLockedContainer
     */
    public function register(Container $container)
    {
        // phpcs:enable

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

        $container->addService(
            PermalinkStructure::class,
            static function (): PermalinkStructure {
                return new PermalinkStructure();
            }
        );

        $container->addService(
            AttributesRelationship::class,
            static function () use ($container): AttributesRelationship {
                return new AttributesRelationship(
                    $container[TaxonomyRepository::class],
                    $container[SiteRelations::class],
                    $container[\wpdb::class]
                );
            }
        );

        $container->addService(
            ArchiveProducts::class,
            static function (): ArchiveProducts {
                return new ArchiveProducts();
            }
        );

        $container->addService(
            AvailableTaxonomiesAttributes::class,
            static function (): AvailableTaxonomiesAttributes {
                return new AvailableTaxonomiesAttributes();
            }
        );

        $container->addService(
            AttributeTermTranslateUrl::class,
            static function (Container $container): AttributeTermTranslateUrl {
                return new AttributeTermTranslateUrl(
                    $container[\wpdb::class],
                    $container[UrlFactory::class]
                );
            }
        );

        $container->addService(
            Product\Ajax\Search::class,
            static function (Container $container): Product\Ajax\Search {
                return new Product\Ajax\Search(
                    $container->get(Post\Ajax\ContextBuilder::class),
                    $container->get(wpdb::class),
                    $container->get(TranslationUiServiceProvider::CONFIGURATION_NAME_FOR_ALREADY_CONNECTED_ENTITY_NOTICE)
                );
            }
        );
        $container->addValue('product_search_limit', 1000);

        /**
         * The configuration of the URL to module assets folder.
         */
        $container->share(
            self::CONFIGURATION_NAME_FOR_URL_TO_MODULE_ASSETS,
            static function (Container $container): string {
                $pluginProperties = $container->get(PluginProperties::class);

                return $pluginProperties->dirUrl() . 'src/modules/WooCommerce/public/';
            }
        );

        /**
         * Configuration to check if the current screen is the correct one to enqueue module assets.
         */
        $container->share(
            self::PARAMETER_CONFIG_SHOULD_ENQUEUE_MODULE_ASSETS,
            static function (): bool {
                global $pagenow;

                $postId = (int)filter_input(INPUT_GET, 'post', FILTER_SANITIZE_NUMBER_INT);
                $isAllowedPage = in_array($pagenow, ['post.php', 'post-new.php'], true);
                $requestPostType = (string)filter_input(INPUT_GET, 'post_type', FILTER_SANITIZE_SPECIAL_CHARS);

                $requestPostType = $requestPostType ?: get_post_type($postId);

                return $isAllowedPage && 'product' === $requestPostType;
            }
        );

        $this->disableSettingsForWooCommerceEntities($container);

        $moduleManager = $container[ModuleManager::class];
        if (!$moduleManager->isModuleActive(self::MODULE_ID)) {
            $this->removeWooCommerceSupport($container);
        }
    }

    /**
     * @inheritdoc
     * @throws LateAccessToNotSharedService | NameNotFound
     */
    public function activateModule(Container $container)
    {
        if (!$this->isWooCommerceActive()) {
            return;
        }

        $shouldEnqueueModuleAssets = $container->get(self::PARAMETER_CONFIG_SHOULD_ENQUEUE_MODULE_ASSETS);
        $urlToModuleAssetsFolder = $container->get(self::CONFIGURATION_NAME_FOR_URL_TO_MODULE_ASSETS);

        $this->bootstrapAssets($shouldEnqueueModuleAssets, $urlToModuleAssetsFolder);
        $this->activateBasePermalinkStructures($container);
        $this->activateProductMetaboxes($container);
        $this->removeAttributeTaxonomiesFieldsFromPostMetabox();
        $this->addProductSearchHandler($container);
        $this->postTypeActions($container);
        $this->taxonomyActions($container);

        $moduleManager = $container->get(ModuleManager::class);
        if ($moduleManager->isModuleActive(CommentsModule::MODULE_ID)) {
            $this->handleSupportForReviews($container);
        }
    }

    /**
     * @param Container $container
     */
    private function activateBasePermalinkStructures(Container $container)
    {
        $permalinkStructure = $container[PermalinkStructure::class];
        $postTranslator = $container[PostTranslator::class];
        $termTranslator = $container[TermTranslator::class];

        $postTranslator->registerBaseStructureCallback(
            'product',
            [$permalinkStructure, 'baseforProduct']
        );
        $termTranslator->registerBaseStructureCallback(
            'product_cat',
            [$permalinkStructure, 'forProductCategory']
        );
        $termTranslator->registerBaseStructureCallback(
            'product_tag',
            [$permalinkStructure, 'forProductTag']
        );

        $attributeTaxonomies = wc_get_attribute_taxonomies();
        $attributeTaxonomies and array_walk(
            $attributeTaxonomies,
            static function (\stdClass $attribute) use ($termTranslator, $permalinkStructure) {
                $taxonomySlug = 'pa_' . sanitize_key($attribute->attribute_name);
                $termTranslator->registerBaseStructureCallback(
                    $taxonomySlug,
                    [$permalinkStructure, 'forProductAttribute']
                );
            }
        );
    }

    /**
     * Add Metaboxes for Product
     *
     * @param Container $container
     *
     * phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong
     * phpcs:disable Generic.Metrics.NestingLevel.TooHigh
     */
    private function activateProductMetaboxes(Container $container)
    {
        // phpcs:enable

        $wooCommerceMetaboxFields = new Product\WooCommerceMetaboxFields();
        $panelView = new Product\PanelView(
            new Product\SettingView(
                'general',
                ...$wooCommerceMetaboxFields->generalSettingFields()
            ),
            new Product\SettingView(
                'inventory',
                ...$wooCommerceMetaboxFields->inventorySettingFields()
            ),
            new Product\SettingView(
                'advanced',
                ...$wooCommerceMetaboxFields->advancedSettingFields()
            )
        );
        $metaboxActivator = new ProductMetaboxesBehaviorActivator(
            new Product\MetaboxFields($wooCommerceMetaboxFields),
            $panelView,
            $container[ActivePostTypes::class],
            $container[ContentRelations::class],
            $container[Attachment\Copier::class],
            $container[PersistentAdminNotices::class]
        );

        add_filter(
            Post\Metabox::HOOK_PREFIX . 'tabs',
            [$metaboxActivator, 'setupMetaboxFields'],
            10,
            2
        );

        add_action(
            Product\MetaboxTab::ACTION_BEFORE_METABOX_UI_PANEL,
            [$metaboxActivator, 'renderPanels'],
            20,
            2
        );

        add_action(
            Post\MetaboxAction::ACTION_METABOX_AFTER_RELATE_POSTS,
            [$metaboxActivator, 'saveMetaboxes'],
            10,
            3
        );
    }

    /**
     * Do not add the attributes taxonomies to the list of translatable taxonomies.
     * Them are handled differently within the Product Tab.
     */
    private function removeAttributeTaxonomiesFieldsFromPostMetabox()
    {
        $removeAttributeTaxonomiesNameCallback = static function (array $taxonomiesSlugs): array {
            $attributeTaxonomies = wc_get_attribute_taxonomy_names();
            foreach ($attributeTaxonomies as $taxonomyName) {
                unset($taxonomiesSlugs[$taxonomyName]);
            }

            return $taxonomiesSlugs;
        };

        add_filter(
            Post\Field\TaxonomySlugs::FILTER_FIELD_TAXONOMY_SLUGS,
            $removeAttributeTaxonomiesNameCallback
        );
        add_filter(
            Post\MetaboxAction::FILTER_TAXONOMIES_SLUGS_BEFORE_REMOVE,
            $removeAttributeTaxonomiesNameCallback
        );
        add_filter(
            Post\MetaboxFields::FILTER_TAXONOMIES_AND_TERMS_OF,
            static function ($taxonomiesSlugs) {
                $attributeTaxonomies = wc_get_attribute_taxonomy_names();
                $taxonomiesSlugs = array_filter(
                    $taxonomiesSlugs,
                    static function (string $taxonomySlug) use ($attributeTaxonomies): bool {
                        return !\in_array($taxonomySlug, $attributeTaxonomies, true);
                    }
                );
                return $taxonomiesSlugs;
            }
        );
    }

    /**
     * Sets up assets for WooCommerce module.
     *
     * @param bool $shouldEnqueueModuleAssets Whether should enqueue the assets.
     * @param string $urlToModuleAssetsFolder The url to assets folder.
     */
    private function bootstrapAssets(bool $shouldEnqueueModuleAssets, string $urlToModuleAssetsFolder)
    {
        add_action(
            AssetManager::ACTION_SETUP,
            static function (AssetManager $assetManager) use ($urlToModuleAssetsFolder, $shouldEnqueueModuleAssets) {
                $script = new Script(self::MODULE_SCRIPTS_HANDLER_NAME, "{$urlToModuleAssetsFolder}js/admin.min.js");
                $script
                    ->withDependencies('multilingualpress-admin')
                    ->forLocation(Asset::BACKEND)
                    ->canEnqueue(
                        static function () use ($shouldEnqueueModuleAssets): bool {
                            return $shouldEnqueueModuleAssets;
                        }
                    );

                $style = new Style(self::MODULE_SCRIPTS_HANDLER_NAME, "{$urlToModuleAssetsFolder}css/admin.min.css");
                $style
                    ->withDependencies('multilingualpress-admin', 'jquery-ui-style')
                    ->forLocation(Asset::BACKEND)
                    ->canEnqueue(
                        static function () use ($shouldEnqueueModuleAssets): bool {
                            return $shouldEnqueueModuleAssets;
                        }
                    );

                $assetManager->register($script, $style);
            }
        );
    }

    /**
     * @return bool
     */
    private function isWooCommerceActive(): bool
    {
        return \function_exists('wc');
    }

    /**
     * @param Container $container
     * @return void
     */
    protected function addProductSearchHandler(Container $container)
    {
        $sourcePostId = (int)filter_input(INPUT_POST, 'source_post_id', FILTER_SANITIZE_NUMBER_INT);
        if (!$sourcePostId || get_post_type($sourcePostId) !== 'product') {
            return;
        }

        remove_action(
            'wp_ajax_' . Post\Ajax\Search::ACTION,
            [$container[Post\Ajax\Search::class], 'handle']
        );

        $search = $container[Product\Ajax\Search::class];
        $request = $container[ServerRequest::class];

        add_action(
            'wp_ajax_' . Product\Ajax\Search::ACTION,
            static function () use ($search, $request) {
                $search->handle($request);
            }
        );
    }

    /**
     * @param Container $container
     */
    private function postTypeActions(Container $container)
    {
        add_filter(
            PostTypeTranslator::FILTER_POST_TYPE_PERMALINK,
            [$container[ArchiveProducts::class], 'shopArchiveUrl'],
            10,
            3
        );

        add_filter(
            CoreServiceProvider::FILTER_AVAILABLE_POST_TYPE_FOR_SETTINGS,
            static function (array $allAvailablePostTypes): array {
                unset($allAvailablePostTypes['product']);

                return $allAvailablePostTypes;
            }
        );
    }

    /**
     * @param Container $container
     */
    private function taxonomyActions(Container $container)
    {
        $attributeTermTranslateUrl = $container[AttributeTermTranslateUrl::class];
        $attributesRelationship = $container[AttributesRelationship::class];
        $attributeTaxonomies = wc_get_attribute_taxonomy_names();

        add_filter(
            TermTranslator::FILTER_TRANSLATION,
            [$attributeTermTranslateUrl, 'termLinkByTaxonomyId'],
            10,
            4
        );

        add_action('setup_theme', static function () use ($attributeTermTranslateUrl) {
            global $wp_rewrite;
            $attributeTermTranslateUrl->ensureWpRewrite($wp_rewrite);
        });

        foreach ($attributeTaxonomies as $attributeTaxonomy) {
            add_action(
                "{$attributeTaxonomy}_pre_edit_form",
                [$attributesRelationship, 'createAttributeRelation'],
                10,
                2
            );
        }

        $attributesHookNames = ['woocommerce_attribute_added', 'woocommerce_attribute_updated'];
        array_walk($attributesHookNames, static function (string $hookName) use ($attributesRelationship) {
            add_action($hookName, [$attributesRelationship, 'addSupportForAttribute'], 10, 2);
        });
    }

    /**
     * Handle the WooCommerce reviews support.
     *
     * @param Container $container
     * @throws LateAccessToNotSharedService|NameNotFound
     * phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong
     * phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh
     */
    protected function handleSupportForReviews(Container $container): void
    {
        // phpcs:enable

        $helperFactory = $container->get(MetaboxFieldsHelperFactory::class);

        add_filter(
            CommentMetaboxTab::FILTER_COMMENT_METABOX_TAB . "_tab-base_fields",
            static function (array $fields, CommentsRelationshipContextInterface $context) use ($helperFactory): array {
                $sourceComment = $context->sourceComment();
                if ($sourceComment->comment_type !== 'review') {
                    return $fields;
                }
                $fields[] = new CommentMetaboxReviewRating('rating', __('Rating', 'multilingualpress'), $helperFactory);

                return $fields;
            },
            10,
            2
        );

        add_action(
            MetaboxAction::ACTION_METABOX_AFTER_RELATE_COMMENTS,
            static function (
                CommentsRelationshipContextInterface $context,
                Request $request
            ) use ($helperFactory): void {
                $sourceComment = $context->sourceComment();
                if ($sourceComment->comment_type !== 'review') {
                    return;
                }

                $remoteSiteId = $context->remoteSiteId();

                $helper = $helperFactory->createMetaboxFieldsHelper($remoteSiteId);
                $relationType = $helper->fieldRequestValue($request, 'relationship');

                $changedFields = $helper->fieldRequestValue($request, 'changed_fields') ?? '';
                $changedFieldNames = explode(',', $changedFields);

                if ($relationType !== 'new' &&  !in_array('rating', $changedFieldNames, true)) {
                    return;
                }

                $reviewRatingValue = $helper->fieldRequestValue($request, 'rating') ?? 0;
                if ($reviewRatingValue === 0) {
                    return;
                }

                switch_to_blog($remoteSiteId);
                update_comment_meta($context->remoteCommentId(), 'rating', $reviewRatingValue);
                restore_current_blog();
            },
            10,
            2
        );

        add_filter(
            CommentsSettingsRepository::FILTER_COMMENTS_ENABLED_FOR_POST_TYPE,
            static function (bool $areReviewsEnabled, string $postType): bool {
                if ($postType !== 'product') {
                    return $areReviewsEnabled;
                }

                return wc_review_ratings_enabled();
            },
            10,
            2
        );

        add_action(
            CommentsCopier::ACTION_AFTER_REMOTE_COMMENT_IS_INSERTED,
            static function (CommentsRelationshipContextInterface $context): void {
                $sourceComment = $context->sourceComment();
                if ($sourceComment->comment_type !== 'review') {
                    return;
                }

                $remoteSiteId = $context->remoteSiteId();
                switch_to_blog($context->sourceSiteId());
                $sourceRatingMetaValue = (int)get_comment_meta($context->sourceCommentId(), 'rating', true) ?: 0;
                restore_current_blog();

                if (!$sourceRatingMetaValue) {
                    return;
                }

                switch_to_blog($remoteSiteId);
                update_comment_meta($context->remoteCommentId(), 'rating', $sourceRatingMetaValue);
                restore_current_blog();
            }
        );

        $translationColumn = $container->get(CommentsListViewTranslationColumn::class);

        add_filter(
            'woocommerce_product_reviews_table_columns',
            static function (array $columns) use ($translationColumn): array {
                $columns[$translationColumn->name()] = $translationColumn->title();
                return $columns;
            }
        );

        add_action(
            'woocommerce_product_reviews_table_column_' . $translationColumn->name(),
            static function (WP_Comment $review) use ($translationColumn): void {
                echo wp_kses_post($translationColumn->value((int)$review->comment_ID));
            },
            10,
            2
        );
    }

    /**
     * Perform an actions when WooCommerce support is deactivated
     *
     * If Woo support is deactivated we should disable the translation metabox support for
     * Woo entities(Products, all Woo taxonomies) and also we need to disable the
     * Woo post type and taxonomy settings from MLP global settings
     *
     * @param Container $container
     * phpcs:disable Inpsyde.CodeQuality.NestingLevel.High
     */
    protected function removeWooCommerceSupport(Container $container)
    {
        // phpcs:enable

        $taxonomyRepository = $container[TaxonomyRepository::class];
        $postTypeRepository = $container[PostTypeRepository::class];
        $filters = [
            $taxonomyRepository::FILTER_SUPPORTED_TAXONOMIES,
            $taxonomyRepository::FILTER_ALL_AVAILABLE_TAXONOMIES,
            $postTypeRepository::FILTER_SUPPORTED_POST_TYPES,
            $postTypeRepository::FILTER_ALL_AVAILABLE_POST_TYPES,
        ];
        $entitiesToRemove = ['product_cat', 'product_tag', 'product'];
        foreach ($filters as $filter) {
            add_filter(
                $filter,
                static function (array $supported) use ($entitiesToRemove): array {
                    foreach ($entitiesToRemove as $entity) {
                        if (!key_exists($entity, $supported)) {
                            continue;
                        }
                        unset($supported[$entity]);
                    }

                    return $supported;
                }
            );
        }
    }

    /**
     * Disable MLP settings for certain WooCommerce entities.
     *
     * Regardless of whether the WooCommerce module is active, some WooCommerce entities settings should be removed
     * from admin area, cause some entities like "Attributes" are supported under the hood when the module is active and
     * some are not translatable at all, such as "Orders".
     * This method is for removing the settings of such entities from admin area.
     *
     * @param Container $container
     */
    protected function disableSettingsForWooCommerceEntities(Container $container)
    {
        add_filter(
            PostTypeRepository::FILTER_PUBLIC_POST_TYPES,
            static function ($allAvailablePostTypes) {
                unset($allAvailablePostTypes['shop_order']);
                return $allAvailablePostTypes;
            }
        );

        add_filter(
            TaxonomyRepository::FILTER_ALL_AVAILABLE_TAXONOMIES,
            [$container[AvailableTaxonomiesAttributes::class], 'removeAttributes']
        );
    }
}
