<?php

namespace PublishPress\FuturePro\Controllers;

use Closure;
use PublishPress\Future\Core\HookableInterface;
use PublishPress\Future\Core\HooksAbstract as CoreHooksAbstract;
use PublishPress\Future\Framework\ModuleInterface;
use PublishPress\Future\Modules\Expirator\ExpirationActionsAbstract;
use PublishPress\Future\Modules\Expirator\HooksAbstract as ExpiratorHooksAbstract;
use PublishPress\Future\Modules\Expirator\Interfaces\SchedulerInterface;
use PublishPress\Future\Modules\Expirator\Models\ExpirablePostModel;
use PublishPress\Future\Modules\Expirator\Models\PostTypeDefaultDataModelFactory;
use PublishPress\Future\Modules\Expirator\PostMetaAbstract;
use PublishPress\FuturePro\Core\HooksAbstract;
use PublishPress\FuturePro\Models\SettingsModel;

defined('ABSPATH') or die('No direct script access allowed.');

class MetadataMappingController implements ModuleInterface
{
    /**
     * @var \PublishPress\Future\Core\HookableInterface
     */
    private $hooks;

    /**
     * @var \PublishPress\FuturePro\Models\SettingsModel
     */
    private $settingsModel;

    /**
     * @var Closure
     */
    private $postModelFactory;

    /**
     * @var \PublishPress\Future\Modules\Expirator\Interfaces\SchedulerInterface
     */
    private $scheduler;

    /**
     * This is true when a WP import is running.
     *
     * @var bool
     */
    private $importIsRunning = false;

    /**
     * Stores the list of post IDs that were imported during the current import process.
     *
     * @var array
     */
    private $importedPostIDs = [];

    /**
     * @var \PublishPress\Future\Modules\Expirator\Models\PostTypeDefaultDataModelFactory
     */
    private $defaultDataModelFactory;

    /**
     * This is true when a WebToffee Import/Export for WooCommerce import is running.
     *
     * @var bool
     */
    private $webToffeeImportIsRunning = false;

    public function __construct(
        HookableInterface $hooks,
        SettingsModel $settingsModel,
        Closure $postModelFactory,
        SchedulerInterface $scheduler,
        PostTypeDefaultDataModelFactory $postTypeDefaultDataModelFactory
    ) {
        $this->hooks = $hooks;
        $this->settingsModel = $settingsModel;
        $this->postModelFactory = $postModelFactory;
        $this->scheduler = $scheduler;
        $this->defaultDataModelFactory = $postTypeDefaultDataModelFactory;
    }

    public function initialize()
    {
        $this->hooks->addAction(
            HooksAbstract::ACTION_IMPORT_START,
            [$this, 'onImportStart']
        );

        $this->hooks->addAction(
            HooksAbstract::ACTION_IMPORT_END,
            [$this, 'onImportEnd']
        );

        $this->hooks->addAction(
            CoreHooksAbstract::ACTION_SAVE_POST,
            [$this, 'onSavePost'],
            10,
            2
        );

        $this->hooks->addAction(
            CoreHooksAbstract::ACTION_UPDATED_POST_META,
            [$this, 'onPostMetaUpdated'],
            10,
            4
        );

        $this->hooks->addAction(
            CoreHooksAbstract::ACTION_ADDED_POST_META,
            [$this, 'onPostMetaAdded'],
            10,
            4
        );

        $this->hooks->addAction(
            HooksAbstract::ACTION_PROCESS_METADATA,
            [$this, 'processMetadataDrivenScheduling'],
            10,
            2
        );

        $this->hooks->addFilter(
            ExpiratorHooksAbstract::FILTER_PREPARE_POST_EXPIRATION_OPTS,
            [$this, 'filterExpirationOptions'],
            10,
            2
        );

        $this->hooks->addFilter(
            ExpiratorHooksAbstract::FILTER_HIDE_METABOX,
            [$this, 'filterHideMetabox'],
            10,
            2
        );

        $this->hooks->addFilter(
            ExpiratorHooksAbstract::FILTER_HIDDEN_METABOX_FIELDS,
            [$this, 'filterHiddenMetaboxFields'],
            10,
            2
        );

        $this->hooks->addFilter(
            ExpiratorHooksAbstract::FILTER_DISPLAY_BULK_ACTION_SYNC,
            [$this, 'filterDisplayBulkActionSync'],
            10,
            2
        );

        $this->hooks->addAction(
            HooksAbstract::ACTION_WOOCOMMERCE_AFTER_PRODUCT_OBJECT_SAVE,
            [$this, 'onWooCommerceProductSave']
        );

        /**
         * Fix import issue with WebToffee Import/Export for WooCommerce.
         *
         * @see https://github.com/publishpress/PublishPress-Future/issues/1181
         */
        $this->hooks->addAction(
            'wt_woocommerce_product_import_before_process_item',
            [$this, 'setWebToffeeImportRunningFlag'],
            -1
        );

        $this->hooks->addFilter(
            'wt_woocommerce_product_import_inserted_product_object',
            [$this, 'processMetadataDrivenSchedulingByWebToffeeImporter'],
            -1
        );
    }

    /**
     * Handles the start of an import process.
     * Sets the import flag and initializes the imported posts array.
     */
    public function onImportStart()
    {
        $this->importIsRunning = true;
        $this->importedPostIDs = [];
    }

    /**
     * Handles the end of an import process.
     * Processes all imported posts for metadata-driven scheduling.
     */
    public function onImportEnd()
    {
        $this->importIsRunning = false;

        if (empty($this->importedPostIDs)) {
            return;
        }

        foreach ($this->importedPostIDs as $postId) {
            $this->hooks->doAction(HooksAbstract::ACTION_PROCESS_METADATA, $postId);
        }
    }

    /**
     * Handles post save events, managing import scenarios and triggering metadata processing.
     *
     * @param int $postId The ID of the saved post
     * @param \WP_Post $post The post object
     */
    public function onSavePost($postId, $post): void
    {
        if ($this->webToffeeImportIsRunning) {
            return;
        }

        if ($this->importIsRunning) {
            $this->importedPostIDs[] = $postId;

            return;
        }

        $this->hooks->doAction(HooksAbstract::ACTION_PROCESS_METADATA, $postId, $post);
    }

    /**
     * Handles post meta updates for mapped custom fields.
     *
     * @param int $metaId The meta ID
     * @param int $postId The post ID
     * @param string $metaKey The meta key that was updated
     * @param mixed $metaValue The new meta value
     */
    public function onPostMetaUpdated($metaId, $postId, $metaKey, $metaValue): void
    {
        $this->handleMetaChange($postId, $metaKey);
    }

    /**
     * Handles post meta additions for mapped custom fields.
     *
     * @param int $metaId The meta ID
     * @param int $postId The post ID
     * @param string $metaKey The meta key that was added
     * @param mixed $metaValue The new meta value
     */
    public function onPostMetaAdded($metaId, $postId, $metaKey, $metaValue): void
    {
        $this->handleMetaChange($postId, $metaKey);
    }

    /**
     * Processes metadata changes for mapped custom fields.
     *
     * @param int $postId The post ID
     * @param string $metaKey The meta key that changed
     */
    private function handleMetaChange($postId, $metaKey): void
    {
        if ($this->webToffeeImportIsRunning || $this->importIsRunning) {
            return;
        }

        $post = get_post($postId);

        if (empty($post)) {
            return;
        }

        $postType = $post->post_type;
        $statuses = $this->settingsModel->getMetadataMappingStatus();

        // Check if metadata mapping is enabled for this post type
        if (!array_key_exists($postType, $statuses) || $statuses[$postType] !== true) {
            return;
        }

        $mapping = $this->settingsModel->getMetadataMapping();
        $postTypeMapping = $mapping[$postType] ?? [];

        // Check if the updated meta key is one of our mapped fields
        $isMappedField = in_array($metaKey, $postTypeMapping);

        if ($isMappedField) {
            $this->hooks->doAction(
                HooksAbstract::ACTION_PROCESS_METADATA,
                $postId,
                $post
            );
        }
    }

    /**
     * Processes metadata-driven scheduling for posts with mapped custom fields.
     *
     * This is the core method that:
     * 1. Checks if metadata mapping is enabled for the post type
     * 2. Ensures all required action data is present
     * 3. Detects metadata changes to avoid unnecessary processing
     * 4. Triggers schedule synchronization when needed
     *
     * @param int $postId The post ID to process
     * @param \WP_Post|null $post Optional post object
     */
    public function processMetadataDrivenScheduling($postId, $post = null): void
    {
        if (empty($post)) {
            $post = get_post($postId);
        }

        if (empty($post) || ! $post instanceof \WP_Post) {
            return;
        }

        // Check if the post type has metadata mapping enabled.
        $postType = $post->post_type;
        $statuses = $this->settingsModel->getMetadataMappingStatus();

        if (! array_key_exists($postType, $statuses) || $statuses[$postType] !== true) {
            return;
        }

        $postModel = ($this->postModelFactory)($postId);
        $mapping = $this->settingsModel->getMetadataMapping();
        $postTypeMapping = $mapping[$postType] ?? [];

        $timestamp = $this->getMetaWithMappingPriority(
            $postModel,
            PostMetaAbstract::EXPIRATION_TIMESTAMP,
            $postTypeMapping,
            null
        );

        if (empty($timestamp)) {
            return;
        }

        $this->garanteeActionDataWithDefaultData($postId);

        $metadataHash = $this->calcMetadataHashWithMappedFields($postId, $postType);

        // Check if the flag is set to avoid infinite loops.
        if ($metadataHash === $postModel->getMetadataHash()) {
            return;
        }

        $postModel->syncScheduleWithPostMeta();

        // Set the flag to avoid infinite loops.
        $postModel->updateMetadataHash($metadataHash);
    }

    /**
     * Ensures all required action data is present,
     * prioritizing mapped custom fields.
     *
     * @param int $postId The post ID to process
     * @return bool True if timestamp is available, false otherwise
     */
    private function garanteeActionDataWithDefaultData($postId): bool
    {
        $postModel = ($this->postModelFactory)($postId);
        $postType = $postModel->getPostType();
        $mapping = $this->settingsModel->getMetadataMapping();
        $postTypeMapping = $mapping[$postType] ?? [];
        $defaultDataModel = $this->defaultDataModelFactory->create($postType);

        if (!$this->ensureTimestamp($postModel, $postTypeMapping)) {
            return false;
        }

        $this->ensureMetaField(
            $postModel,
            PostMetaAbstract::EXPIRATION_TYPE,
            $postTypeMapping,
            $defaultDataModel->getAction()
        );
        $this->ensureMetaField(
            $postModel,
            PostMetaAbstract::EXPIRATION_POST_STATUS,
            $postTypeMapping,
            $defaultDataModel->getNewStatus()
        );
        $this->ensureMetaField(
            $postModel,
            PostMetaAbstract::EXPIRATION_STATUS,
            $postTypeMapping,
            ''
        );

        $status = $postModel->getMeta(PostMetaAbstract::EXPIRATION_STATUS, true);
        if (empty($status)) {
            return false;
        }

        $this->ensureTaxonomyFields($postModel, $postTypeMapping, $defaultDataModel);

        return true;
    }

    /**
     * Ensures the expiration timestamp is properly set, prioritizing mapped fields.
     *
     * @param ExpirablePostModel $postModel The post model instance
     * @param array $postTypeMapping The mapping configuration for this post type
     * @return bool True if a valid timestamp exists, false otherwise
     */
    private function ensureTimestamp($postModel, $postTypeMapping): bool
    {
        $mappedTimestamp = $this->getMappedMetaValue(
            $postModel->getPostId(),
            PostMetaAbstract::EXPIRATION_TIMESTAMP,
            $postTypeMapping
        );

        if (!empty($mappedTimestamp)) {
            $normalizedTimestamp = is_numeric($mappedTimestamp) ? (int)$mappedTimestamp : strtotime($mappedTimestamp);
            $postModel->updateMeta(PostMetaAbstract::EXPIRATION_TIMESTAMP, $normalizedTimestamp);
            return !empty($normalizedTimestamp);
        }

        $timestamp = $postModel->getMeta(PostMetaAbstract::EXPIRATION_TIMESTAMP, true);
        return !empty($timestamp);
    }

    /**
     * Ensures a meta field is properly set using the mapping priority system.
     *
     * @param ExpirablePostModel $postModel The post model instance
     * @param string $metaKey The meta key to ensure
     * @param array $postTypeMapping The mapping configuration
     * @param mixed $defaultValue The default value to use as last resort
     */
    private function ensureMetaField(
        $postModel,
        $metaKey,
        $postTypeMapping,
        $defaultValue
    ): void {
        $mappedValue = $this->getMappedMetaValue(
            $postModel->getPostId(),
            $metaKey,
            $postTypeMapping
        );

        if ($mappedValue !== null && $mappedValue !== '') {
            $postModel->updateMeta($metaKey, $mappedValue);
            return;
        }

        $currentValue = $postModel->getMeta($metaKey, true);
        if (empty($currentValue)) {
            $postModel->updateMeta($metaKey, $defaultValue);
        }
    }

    /**
     * Ensures taxonomy-related fields are properly set for term-based actions.
     *
     * Only processes taxonomy and terms fields when the action type requires them
     * (category add, remove, set, or remove all operations).
     *
     * @param ExpirablePostModel $postModel The post model instance
     * @param array $postTypeMapping The mapping configuration
     * @param PostTypeDefaultDataModel $defaultDataModel Default data provider
     */
    private function ensureTaxonomyFields(
        $postModel,
        $postTypeMapping,
        $defaultDataModel
    ): void {
        $action = $this->getMetaWithMappingPriority(
            $postModel,
            PostMetaAbstract::EXPIRATION_TYPE,
            $postTypeMapping,
            $defaultDataModel->getAction()
        );

        $termRelatedActions = [
            ExpirationActionsAbstract::POST_CATEGORY_REMOVE,
            ExpirationActionsAbstract::POST_CATEGORY_ADD,
            ExpirationActionsAbstract::POST_CATEGORY_REMOVE_ALL,
            ExpirationActionsAbstract::POST_CATEGORY_SET,
        ];

        if (!in_array($action, $termRelatedActions)) {
            return;
        }

        $this->ensureMetaField(
            $postModel,
            PostMetaAbstract::EXPIRATION_TAXONOMY,
            $postTypeMapping,
            $defaultDataModel->getTaxonomy()
        );
        $this->ensureMetaField(
            $postModel,
            PostMetaAbstract::EXPIRATION_TERMS,
            $postTypeMapping,
            $defaultDataModel->getTerms()
        );
    }

    /**
     * Gets a meta value using the mapping priority system without updating the database.
     *
     * This is a read-only version of ensureMetaField that returns the value
     * that would be used based on the priority system.
     *
     * @param ExpirablePostModel $postModel The post model instance
     * @param string $metaKey The meta key to retrieve
     * @param array $postTypeMapping The mapping configuration
     * @param mixed $defaultValue The default value to use as fallback
     * @return mixed The value based on mapping priority
     */
    private function getMetaWithMappingPriority(
        $postModel,
        $metaKey,
        $postTypeMapping,
        $defaultValue
    ): mixed {
        $mappedValue = $this->getMappedMetaValue(
            $postModel->getPostId(),
            $metaKey,
            $postTypeMapping
        );
        if (!empty($mappedValue)) {
            return $mappedValue;
        }

        $currentValue = $postModel->getMeta($metaKey, true);
        if (!empty($currentValue)) {
            return $currentValue;
        }

        return $defaultValue;
    }

    /**
     * Retrieves a mapped meta value for a given original meta key.
     *
     * Looks up the mapped custom field key for the original PublishPress Future
     * meta key and returns its value from the database.
     *
     * @param int $postId The post ID
     * @param string $originalKey The original PublishPress Future meta key
     * @param array $postTypeMapping The mapping configuration for this post type
     * @return mixed|null The mapped meta value, or null if no mapping exists
     */
    private function getMappedMetaValue(
        $postId,
        $originalKey,
        $postTypeMapping
    ): mixed {
        $mappedKey = $postTypeMapping[$originalKey] ?? '';

        if (empty($mappedKey)) {
            return null;
        }

        return get_post_meta($postId, $mappedKey, true);
    }

    /**
     * Calculates a metadata hash that includes mapped custom field values.
     *
     * This extends the standard metadata hash to include values from mapped
     * custom fields, ensuring that changes to those fields trigger rescheduling.
     *
     * @param int $postId The post ID
     * @param string $postType The post type
     * @return string MD5 hash of combined metadata
     */
    private function calcMetadataHashWithMappedFields($postId, $postType): mixed
    {
        $postModel = ($this->postModelFactory)($postId);

        $originalHash = $postModel->calcMetadataHash();

        $mapping = $this->settingsModel->getMetadataMapping();
        if (empty($mapping) || !array_key_exists($postType, $mapping)) {
            return $originalHash;
        }

        $mappedValues = [];
        foreach ($mapping[$postType] as $originalKey => $mappedKey) {
            if (!empty($mappedKey)) {
                $mappedValues[$mappedKey] = get_post_meta($postId, $mappedKey, true);
            }
        }

        return md5($originalHash . wp_json_encode($mappedValues));
    }

    /**
     * Filters expiration options before scheduling, applying mapped field values.
     *
     * This filter intercepts the scheduling process and overrides option values
     * with data from mapped custom fields when available.
     *
     * @param array $opts The expiration options array
     * @param int $postId The post ID being scheduled
     * @return array Modified options with mapped values applied
     */
    public function filterExpirationOptions($opts, $postId): array
    {
        $mapping = $this->settingsModel->getMetadataMapping();
        $postType = get_post_type($postId);
        $statusPerPostType = $this->settingsModel->getMetadataMappingStatus();

        $shouldSkip = empty($mapping)
        || empty($postType)
        || empty($statusPerPostType[$postType])
        || empty($mapping[$postType])
        || !is_array($mapping[$postType]);

        if ($shouldSkip) {
            return $opts;
        }

        foreach ($mapping[$postType] as $originalKey => $mappedKey) {
            if (empty($mappedKey)) {
                continue;
            }

            $mappedValue = get_post_meta($postId, $mappedKey, true);
            if (empty($mappedValue)) {
                continue;
            }

            switch ($originalKey) {
                case PostMetaAbstract::EXPIRATION_TIMESTAMP:
                    $opts['date'] = is_numeric($mappedValue) ? $mappedValue : strtotime($mappedValue);
                    break;
                case PostMetaAbstract::EXPIRATION_TYPE:
                    $opts['expireType'] = $mappedValue;
                    break;
                case PostMetaAbstract::EXPIRATION_POST_STATUS:
                    $opts['newStatus'] = $mappedValue;
                    break;
                case PostMetaAbstract::EXPIRATION_TAXONOMY:
                    $opts['categoryTaxonomy'] = $mappedValue;
                    break;
                case PostMetaAbstract::EXPIRATION_TERMS:
                    $opts['category'] = is_array($mappedValue) ? $mappedValue : explode(',', $mappedValue);
                    break;
                default:
                    break;
            }
        }

        return $opts;
    }

    /**
     * Filters whether the Future Actions metabox should be hidden for a post type.
     *
     * @param bool $hideMetabox Current hide status
     * @param string $postType The post type being checked
     * @return bool Whether to hide the metabox
     */
    public function filterHideMetabox($hideMetabox, $postType): bool
    {
        $statusPerPostType = $this->settingsModel->getMetaboxHideStatus();

        if (array_key_exists($postType, $statusPerPostType)) {
            return (bool)$statusPerPostType[$postType];
        }

        return $hideMetabox;
    }

    /**
     * Filters which metabox fields should be hidden for a post type.
     *
     * @param array $hideMetabox Current hidden fields array
     * @param string $postType The post type being checked
     * @return array Array of field names to hide
     */
    public function filterHiddenMetaboxFields($hideMetabox, $postType): array
    {
        $hiddenFieldsPostType = $this->settingsModel->getHideFields();

        if (array_key_exists($postType, $hiddenFieldsPostType)) {
            return (array)$hiddenFieldsPostType[$postType];
        }

        return $hideMetabox;
    }

    /**
     * Filters whether the bulk action sync button should be displayed.
     *
     * @param bool $displayBulkActionSync Current display status
     * @param string $postType The post type being checked
     * @return bool Whether to display the bulk action sync
     */
    public function filterDisplayBulkActionSync($displayBulkActionSync, $postType): bool
    {
        $statusPerPostType = $this->settingsModel->getMetadataMappingStatus();

        if (array_key_exists($postType, $statusPerPostType)) {
            return (bool)$statusPerPostType[$postType];
        }

        return $displayBulkActionSync;
    }

    /**
     * Handles WooCommerce product save events for metadata processing.
     *
     * @param \WC_Product $product The WooCommerce product object
     */
    public function onWooCommerceProductSave($product)
    {
        if ($this->webToffeeImportIsRunning) {
            return;
        }

        $this->hooks->doAction(HooksAbstract::ACTION_PROCESS_METADATA, $product->get_id());
    }

    /**
     * Sets the WebToffee import running flag to prevent processing during import.
     *
     * @param mixed $data Import data (unused)
     */
    public function setWebToffeeImportRunningFlag($data)
    {
        $this->webToffeeImportIsRunning = true;
    }

    /**
     * Processes metadata-driven scheduling after WebToffee import completion.
     *
     * @param \WC_Product $data The imported product object
     */
    public function processMetadataDrivenSchedulingByWebToffeeImporter($data)
    {
        $this->webToffeeImportIsRunning = false;

        $this->hooks->doAction(HooksAbstract::ACTION_PROCESS_METADATA, $data->get_id());
    }
}
