<?php

namespace SEOAIC\posts_mass_actions;

use DOMDocument;
use DOMElement;
use DOMText;
use SEOAIC\SEOAICAjaxResponse;

class ContentImprovementAutoImprovement extends AbstractPostsMassAction
{
    protected $backendSingleTagURL = '';
    public const AUTO_IMPROVE_POST_CACHE = 'seoaic_ai_auto_improvements_blocks_';
    private const EDITING_STATUS = 'in_progress';

    public function __construct($seoaic)
    {
        parent::__construct($seoaic);

        $this->backendActionURL = '/api/ai/improve-post-elements';
        $this->backendCheckStatusURL = '/api/ai/improve-post-elements/status';
        $this->backendSingleTagURL = '/api/ai/improve-post-elements/sync';
        $this->backendContentURL = '/api/ai/improve-post-elements/content';
        $this->backendClearURL = '/api/ai/improve-post-elements/clear';
        $this->statusField = 'seoaic_auto_improve_status';

        $this->successfullRunMessage = 'Auto improvement started';
    }

    public function prepareData($incomeData)
    {
        $data = [];
        $postId = !empty($incomeData['postId']) ? intval($incomeData['postId']) : 0;
        $globalSuggestion = !empty($incomeData['globalSuggestedOptions']) ? sanitize_text_field($incomeData['globalSuggestedOptions']) : '';
        $globalSuggestedPrompt = !empty($incomeData['globalSuggestedPrompt']) ? sanitize_text_field($incomeData['globalSuggestedPrompt']) : '';

        if (!empty($globalSuggestion)) {
            $data['mainGoal'] = $globalSuggestion;
        }

        if (!empty($globalSuggestedPrompt)) {
            $data['prompt'] = $globalSuggestedPrompt;
        }

        $blocks = $this->parsePostContentBlocks($postId);

        $data['postId'] = $postId;
        $data['data'] = $blocks;

        $data = array_merge($data, $this->getContentImprovementMainData($postId));

        return $data;
    }

    public function getContentImprovementMainData($postId)
    {
        global $SEOAIC, $SEOAIC_OPTIONS;

        $data = [];
        $postUrl = get_permalink($postId);
        $content_improvement_available = $SEOAIC->content_improvement->isContentImprovementAvailable($postId, $postUrl);

        if ($content_improvement_available) {
            $improvements_ideas = get_post_meta($postId, $SEOAIC->content_improvement::GOOGLE_POST_IMPROVEMENT_IDEAS, true);
            $keywords = get_post_meta($postId, $SEOAIC->content_improvement::QUERY_GOOGLE_FIELD, true);

            $target_audience = $improvements_ideas['audience'] ?? '';
            $main_intents = $improvements_ideas['intents']['main'] ?? '';
            $sub_intents = $improvements_ideas['intents']['sub'] ?? '';
            $content_guidelines = !empty($SEOAIC_OPTIONS['seoaic_content_guidelines']) ? $SEOAIC_OPTIONS['seoaic_content_guidelines'] : '';

            $main_intents_str = is_array($main_intents) ? implode(', ', $main_intents) : (string) $main_intents;
            $sub_intents_str = is_array($sub_intents) ? implode(', ', $sub_intents) : (string) $sub_intents;

            if (!empty($keywords) && is_array($keywords)) {
                $keywords_str = implode(', ', array_column($keywords, 'keyword'));
            } else {
                $keywords_str = '';
            }

            $data['target_audience'] = $target_audience;
            $data['main_intent'] = $main_intents_str;
            $data['sub_intents'] = $sub_intents_str;
            $data['keywords_data'] = $keywords_str;
            $data['content_guidelines'] = $content_guidelines;
        }

        $language = $this->seoaic->multilang->get_post_language($postId);

        if (empty($language)) {
            global $SEOAIC_OPTIONS;
            $language = $SEOAIC_OPTIONS['seoaic_language'];
        }

        $data['language'] = $language;

        return $data;
    }

    public function processActionResults($result)
    {
        if (
            !empty($result['status'])
            && 'success' == $result['status']
        ) {
            $postId = !empty($result['postId']) ? intval($result['postId']) : 0;

            update_post_meta($postId, $this->statusField, self::EDITING_STATUS);

            return true;
        } else {
            throw new \Exception(
                !empty($result['message']) ? $result['message'] : esc_html__('Something went wrong', 'seoaic')
            );
        }
    }

    public function pocessCheckStatusResults($result)
    {
        $returnData = [
            'done' => [],
            'failed' => [],
        ];

        if (!empty($result)) {
            if (
                !empty($result['completed'])
                && is_array($result['completed'])
            ) {
                $returnData['done'] = $this->processCompleted($result['completed']);
            }

            if (!empty($result['failed'])) {
                $this->processFailed($result['failed']);
                $returnData['failed'] = array_merge($returnData['failed'], $result['failed']);
            }

            if (empty($result['pending']) && empty($result['completed']) && empty($result['failed'])) {
                $this->deleteMetaForAllPosts($this->getStatusField(), static::EDITING_STATUS);
            }
        }

        return $returnData;
    }

    public function deleteMetaForAllPosts($meta_key, $meta_value)
    {
        global $wpdb;

        $post_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s",
            $meta_key,
            $meta_value
        ));

        if (!empty($post_ids)) {
            foreach ($post_ids as $post_id) {
                delete_post_meta($post_id, $meta_key);
            }
        }

        return count($post_ids);
    }

    public function processCompleted($ids)
    {
        if (
            !empty($ids)
            && is_array($ids)
        ) {
            $contentResponse = $this->sendContentRequest(['ids' => $ids]);

            if (
                !empty($contentResponse)
                && is_array($contentResponse)
            ) {
                foreach ($contentResponse['result'] as $suggestionResult) {
                    $postId = $suggestionResult['postId'];
                    $transient_key = self::AUTO_IMPROVE_POST_CACHE . $postId;
                    $blocks = $suggestionResult['content'] ?? [];

                    delete_post_meta($postId, $this->statusField);

                    $return[] = [
                        'postId'    => $postId,
                        'blocks'   => $blocks,
                    ];

                    if (!empty($blocks)) {
                        set_transient($transient_key, $blocks, 12 * HOUR_IN_SECONDS);
                    }
                }
            }
        }

        return $return;
    }

    public function processFailed($ids) {}

    public function cronPostsCheckStatus() {}

    public function isRunning()
    {
        $posts = $this->getRequestedAutoImprovementPosts();

        return !empty($posts);
    }

    /**
     * Parses the content of a post and returns an array of content blocks.
     *
     * @param int $postId The ID of the post to parse.
     * @return array An array of content blocks, each block is a string of HTML.
     */
    private function parsePostContentBlocks($postId)
    {
        $post = get_post($postId);
        if (!$post) return [];

        if ($this->isElementor($post->ID)) {
            return $this->parseElementorBlocks($post->ID);
        }

        if ($this->isDivi($post)) {
            return $this->parseDiviBlocks($post);
        }

        return $this->parseDefaultContentBlocks($post);
    }

    private function parseDefaultContentBlocks($post)
    {
        $content = $post->post_content;
        $blocks = [];

        $addBlock = function (string $html) use (&$blocks) {
            $html = trim($html);
            if ($html) {
                $blocks[] = [
                    'content' => $html
                ];
            }
        };

        libxml_use_internal_errors(true);
        $dom = new DOMDocument();
        $dom->loadHTML('<?xml encoding="UTF-8">' . $content);
        $body = $dom->getElementsByTagName('body')->item(0);
        if ($body) {
            foreach ($body->childNodes as $node) {
                $html = $dom->saveHTML($node);
                $html = trim($html);
                if ($html && !preg_match('/^<!--\s*\/?wp:/', $html)) {
                    $addBlock($html);
                }
            }
        }
        libxml_clear_errors();

        return array_values(array_filter($blocks, function ($block) {
            return preg_match('/^<(p|h[1-6])\b/i', trim($block['content']));
        }));
    }

    /**
     * Gets all posts that were sent for review
     * @param array $postIDs options array of IDs to search among
     * @return array
     */
    private function getRequestedAutoImprovementPosts()
    {
        $args = [
            'posts_per_page'    => -1,
            'post_type'         => 'any',
            'post_status'       => 'any',
            'lang'              => '', // disable default lang setting
            'skip_hiding'       => true,
            'meta_query'        => [
                'relation' => 'AND',
                [
                    'key' => $this->statusField,
                    'compare' => 'EXISTS',
                ],
            ],
        ];

        $allPosts = get_posts($args);

        return $allPosts;
    }

    public function stop()
    {
        $this->sendClearRequest(['full' => true]);
    }

    public function sendSingleTagRequest($data = [])
    {
        return $this->sendRequest($this->backendSingleTagURL, $data);
    }

    private function isElementor(int $postId): bool
    {
        return get_post_meta($postId, '_elementor_edit_mode', true) === 'builder';
    }

    private function isDivi($post): bool
    {
        return strpos($post->post_content, '{"et_pb_structure"') !== false;
    }

    private function parseElementorBlocks(int $postId): array
    {
        $blocks = [];
        $elementor_data = get_post_meta($postId, '_elementor_data', true);
        $data = is_string($elementor_data) ? json_decode($elementor_data, true) : null;

        if (is_array($data)) {
            $this->extractElementorBlocks($data, $blocks);
        }

        return $blocks;
    }

    private function extractElementorBlocks(array $elements, array &$blocks): void
    {
        foreach ($elements as $element) {
            if (!empty($element['elType']) && $element['elType'] === 'widget') {
                $editor = $element['settings']['editor'] ?? '';
                if ($editor) {
                    $this->addBlock($blocks, $editor);
                }
            }

            if (!empty($element['elements']) && is_array($element['elements'])) {
                $this->extractElementorBlocks($element['elements'], $blocks);
            }
        }
    }

    private function parseDiviBlocks($post): array
    {
        $blocks = [];
        $pattern = '/<!--(.*?)-->/s';

        if (preg_match($pattern, $post->post_content, $matches)) {
            $json = trim($matches[1]);
            $data = json_decode($json, true);

            if (is_array($data)) {
                $sections = $data['et_pb_structure']['content'] ?? [];
                foreach ($sections as $section) {
                    if (is_string($section)) {
                        $this->addBlock($blocks, $section);
                    } elseif (is_array($section)) {
                        foreach ($section as $item) {
                            if (is_string($item)) {
                                $this->addBlock($blocks, $item);
                            }
                        }
                    }
                }
            }
        }

        return $blocks;
    }

    private function addBlock(array &$blocks, string $html): void
    {
        $html = trim($html);
        if ($html) {
            $blocks[] = ['content' => $html];
        }
    }

    public function replaceContentBlock()
    {
        $params = $this->getSanitizedReplaceParams($_POST);

        if (!$this->validateReplaceParams($params)) {
            SEOAICAjaxResponse::alert(__('Missing or invalid parameters', 'seoaic'))->wpSend();
        }

        $key = self::AUTO_IMPROVE_POST_CACHE . $params['postId'];
        $blocks = get_transient($key);

        if (!$blocks) {
            SEOAICAjaxResponse::alert(__('No cached AI blocks found', 'seoaic'))->wpSend();
        }

        $blockUpdated = $this->markBlockWithAction($blocks, $params['original'], $params['suggested'], $params['actionType']);
        if (!$blockUpdated) {
            SEOAICAjaxResponse::alert(__('Block not found for update', 'seoaic'))->wpSend();
        }

        set_transient($key, $blocks, DAY_IN_SECONDS);

        if ($params['actionType'] === 'accepted') {
            $success = $this->replaceContentInPost($params['postId'], $params['original'], $params['suggested']);

            if (!$success) {
                SEOAICAjaxResponse::alert(__('Block marked, but content replacement failed', 'seoaic'))->wpSend();
            }
        }

        return [
            'message' => __('Block updated successfully', 'seoaic'),
            'actionType' => $params['actionType'],
            'blocks' => $blocks,
        ];
    }

    private function getSanitizedReplaceParams(array $post): array
    {
        $_post = wp_unslash($post);

        return [
            'postId'     => !empty($_post['postId']) ? intval($_post['postId']) : 0,
            'original'   => !empty($_post['original']) ? wp_unslash($_post['original']) : '',
            'suggested'  => !empty($_post['suggested']) ? wp_unslash($_post['suggested']) : '',
            'actionType' => !empty($_post['actionType']) ? sanitize_text_field($_post['actionType']) : '',
        ];
    }

    private function validateReplaceParams(array $params): bool
    {
        return $params['postId'] && $params['original'] && $params['suggested'] &&
            in_array($params['actionType'], ['accepted', 'rejected'], true);
    }

    private function markBlockWithAction(array &$blocks, string $original, string $suggested, string $actionType): bool
    {
        foreach ($blocks as &$block) {
            if (trim($block['original']) === trim($original) && trim($block['suggestion']) === trim($suggested)) {
                $block['action'] = $actionType;
                return true;
            }
        }
        return false;
    }

    private function replaceContentInPost(int $postId, string $original, string $suggested): bool
    {
        $post = get_post($postId);
        if (!$post) {
            return false;
        }

        $postContent = $post->post_content;
        $newContent = '';

        if ($this->isElementor($postId)) {
            return $this->replaceElementorContent($postId, $original, $suggested);
        }

        if ($this->isDivi($post)) {
            return $this->replaceDiviContent($postId, $original, $suggested, $postContent);
        }

        if (function_exists('use_block_editor_for_post') && use_block_editor_for_post($postId)) {
            $newContent = $this->domReplaceHtmlBlockWithWpComments($postContent, $original, $suggested);
        } else {
            $newContent = $this->domReplaceHtmlBlockClassic($postContent, $original, $suggested);
        }

        if (trim($newContent) === trim($postContent)) {
            return false;
        }

        $result = wp_update_post([
            'ID' => $postId,
            'post_content' => $newContent,
        ], true);

        return !is_wp_error($result);
    }

    /**
     * Replaces a specific HTML block in the content with a new block.
     *
     * @param string $html The original HTML content.
     * @param string $originalBlockWithComments The original block to replace, including comments.
     * @param string $replacementBlockWithComments The replacement block, including comments.
     * @return string The updated HTML content with the block replaced.
     */
    private function domReplaceHtmlBlockWithWpComments($html, $originalBlockWithComments, $replacementBlockWithComments)
    {
        $cleanHtml = trim($html);
        $cleanOriginal = trim($originalBlockWithComments);
        $cleanReplacement = trim($replacementBlockWithComments);

        if (strpos($cleanHtml, $cleanOriginal) !== false) {
            return str_replace($cleanOriginal, $cleanReplacement, $cleanHtml);
        }

        return $html;
    }

    /**
     * Replaces a specific HTML block in the content with a new block.
     *
     * @param string $html The original HTML content.
     * @param string $original The original block to replace.
     * @param string $replacement The replacement block.
     * @return string The updated HTML content with the block replaced.
     */
    private function domReplaceHtmlBlockClassic($html, $original, $replacement)
    {
        libxml_use_internal_errors(true);

        $doc = new DOMDocument();
        $doc->loadHTML('<?xml encoding="utf-8">' . $html);

        $originalDoc = new DOMDocument();
        $originalDoc->loadHTML('<?xml encoding="utf-8">' . $original);
        $originalNode = $originalDoc->getElementsByTagName('body')->item(0)->firstChild;

        $replacementDoc = new DOMDocument();
        $replacementDoc->loadHTML('<?xml encoding="utf-8">' . $replacement);
        $replacementNode = $replacementDoc->getElementsByTagName('body')->item(0)->firstChild;

        if (!$originalNode || !$replacementNode) {
            libxml_clear_errors();
            return $html;
        }

        $originalHtml = trim($originalDoc->saveHTML($originalNode));
        $body = $doc->getElementsByTagName('body')->item(0);

        foreach ($body->childNodes as $node) {
            if (!($node instanceof DOMElement || $node instanceof DOMText)) {
                continue;
            }

            $nodeHtml = trim($doc->saveHTML($node));
            if ($nodeHtml === $originalHtml) {
                $imported = $doc->importNode($replacementNode, true);
                $body->replaceChild($imported, $node);
                break;
            }
        }

        libxml_clear_errors();

        $newHtml = $doc->saveHTML($body);
        return preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $newHtml);
    }

    private function replaceElementorContent(int $postId, string $original, string $suggested): bool
    {
        $elementorData = get_post_meta($postId, '_elementor_data', true);
        $data = is_string($elementorData) ? json_decode($elementorData, true) : null;

        if (!is_array($data)) {
            return false;
        }

        $updated = $this->replaceElementorRecursive($data, $original, $suggested);

        if (!$updated) {
            return false;
        }

        update_post_meta($postId, '_elementor_data', wp_slash(json_encode($data)));
        return true;
    }

    private function replaceElementorRecursive(array &$elements, string $original, string $suggested): bool
    {
        $replaced = false;

        foreach ($elements as &$element) {
            if (!empty($element['elType']) && $element['elType'] === 'widget') {
                if (!empty($element['settings']['editor']) && trim($element['settings']['editor']) === trim($original)) {
                    $element['settings']['editor'] = $suggested;
                    $replaced = true;
                }
            }

            if (!empty($element['elements']) && is_array($element['elements'])) {
                if ($this->replaceElementorRecursive($element['elements'], $original, $suggested)) {
                    $replaced = true;
                }
            }
        }

        return $replaced;
    }

    private function replaceDiviContent(int $postId, string $original, string $suggested, string $postContent): bool
    {
        $pattern = '/<!--(.*?)-->/s';
        if (!preg_match($pattern, $postContent, $matches)) {
            return false;
        }

        $json = trim($matches[1]);
        $data = json_decode($json, true);
        if (!is_array($data)) {
            return false;
        }

        $contentKey = 'et_pb_structure';
        if (!isset($data[$contentKey]['content'])) {
            return false;
        }

        $replaced = false;
        $content = &$data[$contentKey]['content'];

        foreach ($content as &$section) {
            if (is_string($section) && trim($section) === trim($original)) {
                $section = $suggested;
                $replaced = true;
            } elseif (is_array($section)) {
                foreach ($section as &$item) {
                    if (is_string($item) && trim($item) === trim($original)) {
                        $item = $suggested;
                        $replaced = true;
                    }
                }
            }
        }

        if (!$replaced) {
            return false;
        }

        $newJson = '<!--' . json_encode($data) . '-->';
        $result = wp_update_post([
            'ID' => $postId,
            'post_content' => $newJson,
        ], true);

        return !is_wp_error($result);
    }
}
