<?php

if (!defined('ABSPATH')) {
    return;
}

if (defined('FLUENT_SNIPPETS_RUNNING_MU')) {
    return;
}

define('FLUENT_SNIPPETS_RUNNING_MU', true);
define('FLUENT_SNIPPETS_RUNNING_MU_VERSION', '10.32');

class FluentSnippetCondition
{
    public function evaluate($conditionSettings)
    {
        if (empty($conditionSettings) || empty($conditionSettings['status']) || $conditionSettings['status'] != 'yes' || empty($conditionSettings['items'])) {
            return true;
        }
        $conditionItems = array_filter($conditionSettings['items']);
        if (!$conditionItems) {
            return true;
        }
        foreach ($conditionItems as $conditions) {
            if ($this->evaluateItems($conditions)) {
                return true;
            }
        }
        return false;
    }

    private function evaluateItems($conditions)
    {
        foreach ($conditions as $condition) {
            if (!$this->evaluateCondition($condition)) {
                return false;
            }
        }
        return true;
    }

    private function evaluateCondition($condition)
    {
        if (empty($condition['source']) || empty($condition['operator']) || empty($condition['value'])) {
            return false;
        }
        $source = $condition['source'][0];

        switch ($source) {
            case 'user':
                return $this->evaluateUserCondition($condition['source'][1], $condition['operator'], $condition['value']);
            case 'page':
                return $this->evaluatePageCondition($condition['source'][1], $condition['operator'], $condition['value']);
            case 'date':
                return $this->evaluateDateCondition($condition['source'][1], $condition['operator'], $condition['value']);
            case 'fluentcrm':
                return $this->evaluateFluentCrmCondition($condition['source'][1], $condition['operator'], $condition['value']);
            default:
                return false;
        }
    }

    private function evaluateUserCondition($key, $operator, $value)
    {
        if ($key == 'authenticated') {
            return is_user_logged_in();
        }

        if ($key == 'role') {
            $userId = get_current_user_id();
            if ($userId == 0) {
                $roles = [];
            } else {
                $user = get_user_by('ID', $userId);
                $roles = $user->roles;
            }
            return $this->checkValues($roles, $value, $operator);
        }
        return false;
    }

    private function evaluateFluentCrmCondition($key, $operator, $value)
    {
        if (!defined('FLUENTCRM')) {
            return false;
        }

        if ($key == 'exists') {
            return !!fluentcrm_get_current_contact();
        }

        if ($key == 'tags_ids') {
            $currentContact = fluentcrm_get_current_contact();
            if (!$currentContact) {
                $tagIds = [];
            } else {
                $tagIds = $currentContact->tags->pluck('id')->toArray();
            }
            return $this->checkValues($tagIds, $value, $operator);
        }

        if ($key == 'list_ids') {
            $currentContact = fluentcrm_get_current_contact();
            if (!$currentContact) {
                $listIds = [];
            } else {
                $listIds = $currentContact->lists->pluck('id')->toArray();
            }
            return $this->checkValues($listIds, $value, $operator);
        }

        return false;
    }

    private function evaluatePageCondition($key, $operator, $value)
    {
        switch ($key) {
            case 'page_type':
                $currentPageType = $this->getCurrentPageType();
                return $this->checkValues($currentPageType, $value, $operator);
            case 'post_type':
                if (!is_singular() && !is_page()) {
                    return false;
                }

                $postType = get_post_type();
                return $this->checkValues($postType, $value, $operator);
            case 'taxonomy_page':
                $queried_object = get_queried_object();
                $tax = isset($queried_object->taxonomy) ? $queried_object->taxonomy : '';
                if (!$tax) {
                    return false;
                }

                return $this->checkValues($tax, $value, $operator);

            case 'taxonomy_term_page':
                $queried_object = get_queried_object();
                $termId = isset($queried_object->term_id) ? $queried_object->term_id : '';
                if (!$termId) {
                    return false;
                }
                return $this->checkValues($termId, $value, $operator);

            case 'url':
                // get current url
                global $wp;
                $url = isset($wp->request) ? trailingslashit(add_query_arg($_GET, home_url($wp->request))) : '';
                if (!$url) {
                    return false;
                }
                return $this->checkValues($url, $value, $operator);
            case 'page_ids':
                if (!is_singular() && !is_page()) {
                    return false;
                }

                $value = (array) $value;

                $value = array_filter($value);

                if (!$value) {
                    return false;
                }

                $pageId = get_the_ID();
                return $this->checkValues($pageId, $value, $operator);
            default:
                return false;
        }
    }

    private function evaluateDateCondition($key, $operator, $value)
    {
        switch ($key) {
            case 'date_range':
                $currentTime = current_time('timestamp');
                return $this->checkValues($currentTime, $value, $operator);
            case 'day_of_week':
                $dayOfWeek = strtolower(date('D', current_time('timestamp')));
                return $this->checkValues($dayOfWeek, $value, $operator);
            case 'time_range':
                $operator = str_replace('date_', 'number_', $operator);
                $currentTime = date('His', current_time('timestamp'));

                $currentDay = date('Y-m-d', current_time('timestamp'));

                $value = [
                    (int)date('His', strtotime($currentDay . ' ' . $value[0])),
                    (int)date('His', strtotime($currentDay . ' ' . $value[1])),
                ];

                return $this->checkValues($currentTime, $value, $operator);
        }

        return false;
    }

    private function getCurrentPageType()
    {
        global $wp_query;

        if (empty($wp_query)) {
            return '';
        }

        if (is_front_page() || is_home()) {
            return 'is_front_page';
        }
        if (is_singular()) {
            return 'is_singular';
        }
        if (is_archive()) {
            return 'is_archive';
        }
        if (is_search()) {
            return 'is_search';
        }
        if (is_404()) {
            return 'is_404';
        }
        if (is_author()) {
            return 'is_author';
        }

        return '';
    }

    /*
     * $sourceValue = dynamic value
     * $dataValue = user input value
     */
    private function checkValues($sourceValue, $dataValue, $operator)
    {
        switch ($operator) {
            case '=':
                if (is_array($sourceValue)) {
                    return in_array($dataValue, $sourceValue);
                }
                return $sourceValue == $dataValue;
                break;
            case '!=':
                if (is_array($sourceValue)) {
                    return !in_array($dataValue, $sourceValue);
                }
                return $sourceValue != $dataValue;
                break;
            case '>':
                return $sourceValue > $dataValue;
                break;
            case '<':
                return $sourceValue < $dataValue;
                break;
            case '>=':
                return $sourceValue >= $dataValue;
                break;
            case '<=':
                return $sourceValue <= $dataValue;
                break;
            case 'startsWith':
                //  return Str::startsWith($sourceValue, $dataValue);
                break;
            case 'endsWith':
                // return Str::endsWith($sourceValue, $dataValue);
                break;
            case 'contains':
                $sourceValue = strtolower($sourceValue);
                if (is_string($dataValue)) {
                    $dataValue = strtolower($dataValue);
                }
                return str_contains($sourceValue, $dataValue);
                break;
            case 'doNotContains':
            case 'not_contains':
                $sourceValue = strtolower($sourceValue);
                if (is_string($dataValue)) {
                    $dataValue = strtolower($dataValue);
                }
                return !str_contains($sourceValue, $dataValue);
                break;
            case 'length_equal':
                if (is_array($sourceValue)) {
                    return count($sourceValue) == $dataValue;
                }
                $sourceValue = strval($sourceValue);
                return strlen($sourceValue) == $dataValue;
                break;
            case 'length_less_than':
                if (is_array($sourceValue)) {
                    return count($sourceValue) < $dataValue;
                }
                $sourceValue = strval($sourceValue);
                return strlen($sourceValue) < $dataValue;
                break;
            case 'length_greater_than':
                if (is_array($sourceValue)) {
                    return count($sourceValue) > $dataValue;
                }
                $sourceValue = strval($sourceValue);
                return strlen($sourceValue) > $dataValue;
                break;
            case 'match_all':
            case 'in_all':
                $sourceValue = (array)$sourceValue;
                $dataValue = (array)$dataValue;
                sort($sourceValue);
                sort($dataValue);
                return $sourceValue == $dataValue;
                break;
            case 'match_none_of':
            case 'not_in_all':
                $sourceValue = (array)$sourceValue;
                $dataValue = (array)$dataValue;
                return !(array_intersect($sourceValue, $dataValue));
                break;
            case 'in':
                $dataValue = (array)$dataValue;
                if (is_array($sourceValue)) {
                    return !!(array_intersect($sourceValue, $dataValue));
                }
                return in_array($sourceValue, $dataValue);
            case 'not_in':
                $dataValue = (array)$dataValue;
                if (is_array($sourceValue)) {
                    return !(array_intersect($sourceValue, $dataValue));
                }
                return !in_array($sourceValue, $dataValue);
            case 'before':
                return strtotime($sourceValue) < strtotime($dataValue);
            case 'after':
                return strtotime($sourceValue) > strtotime($dataValue);
            case 'date_equal':
                return date('YMD', strtotime($sourceValue)) == date('YMD', strtotime($dataValue));
            case 'date_within':
                $range = [strtotime($dataValue[0]), strtotime($dataValue[1])];
                return strtotime($sourceValue) >= $range[0] && strtotime($sourceValue) <= $range[1];
            case 'date_not_within':
                $range = [strtotime($dataValue[0]), strtotime($dataValue[1])];
                return strtotime($sourceValue) <= $range[0] || strtotime($sourceValue) >= $range[1];
            case 'number_within':
                return $sourceValue >= $dataValue[0] && $sourceValue <= $dataValue[1];
            case 'number_not_within':
                return $sourceValue <= $dataValue[0] || $sourceValue >= $dataValue[1];
            case 'days_before':
                return strtotime($sourceValue) < strtotime("-{$dataValue} days", current_time('timestamp'));
            case 'days_within':
                return strtotime($sourceValue) > strtotime("-{$dataValue} days", current_time('timestamp'));
            case 'is_null':
                return !$sourceValue;
            case 'not_null':
                return !!$sourceValue;
        }
        return false;
    }
}

class CodeRunner
{

    private $storageDir = '';

    public function __construct()
    {
        $this->storageDir = WP_CONTENT_DIR . '/fluent-snippet-storage';
    }

    public function runSnippets()
    {
        if (!is_file($this->storageDir . '/index.php')) {
            return;
        }

        $config = include $this->storageDir . '/index.php';

        if (empty($config) || empty($config['published']) || !is_array($config['published'])) {
            return; // No config or published scripts exist exists
        }

        if (isset($config['meta']['force_disabled']) && $config['meta']['force_disabled'] == 'yes') {
            return; // this forcefully disabled via URL
        }

        $errorFiles = $this->get($config, 'error_files', []);

        $snippets = $config['published'];

        if (!$snippets) {
            return;
        }

        $storageDir = $this->storageDir;

        $hasInvalidFiles = false;

        $conditionalClass = new FluentSnippetCondition();

        $filterMaps = [
            'before_content' => [
                'hook'      => 'the_content',
                'insert'    => 'before',
                'is_single' => true
            ],
            'after_content'  => [
                'hook'      => 'the_content',
                'insert'    => 'after',
                'is_single' => true
            ],
        ];

        foreach ($snippets as $fileName => $snippet) {
            if (isset($_REQUEST['fluent_saving_snippet_name'])) {
                if ($_REQUEST['fluent_saving_snippet_name'] === $fileName && current_user_can('manage_options')) {
                    continue;
                }
            }

            if ($errorFiles && isset($errorFiles[$fileName])) {
                // There has an error. Skip this
                continue;
            }

            $file = $storageDir . '/' . sanitize_file_name($fileName);
            if (!is_file($file)) {
                $hasInvalidFiles = true;
                continue;
            }

            $type = $this->get($snippet, 'type');

            switch ($type) {
                case 'PHP':
                    $conditionSettings = $snippet['condition'];
                    $hookName = 'wp';
                    if (empty($conditionSettings) || empty($conditionSettings['status']) || $conditionSettings['status'] != 'yes' || empty($conditionSettings['items'])) {
                        $hookName = 'setup_theme';
                    }

                    add_action($hookName, function () use ($file, $snippet, $conditionalClass) {
                        if (!$conditionalClass->evaluate($snippet['condition'])) {
                            return;
                        }

                        $runAt = $this->get($snippet, 'run_at', 'all');
                        if ($runAt == 'backend') {
                            if (is_admin()) {
                                require_once $file;
                            }
                            return;
                        }

                        require_once $file;
                    }, $this->get($snippet, 'priority', 10));

                    break;
                case 'js':
                    $runAt = $this->get($snippet, 'run_at', 'wp_footer');
                    if (in_array($runAt, ['wp_head', 'wp_footer', 'admin_head', 'admin_footer'])) {

                        $loadUrl = '';
                        $isFooter = false;

                        if ($this->get($snippet, 'load_as_file') == 'yes') {
                            $cachedFile = str_replace('.php', '.js', $fileName);
                            $loadUrl = $this->getCachedFileUrl($cachedFile);
                            if ($loadUrl) {
                                $isFooter = ($runAt == 'wp_footer' || $runAt == 'admin_footer');
                                $runAt = ($runAt == 'admin_head' || $runAt == 'admin_footer') ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts';
                            }
                        }

                        add_action($runAt, function () use ($file, $snippet, $conditionalClass, $loadUrl, $isFooter) {
                            if (!$conditionalClass->evaluate($snippet['condition'])) {
                                return;
                            }

                            if ($loadUrl) {
                                $snippetScriptName = str_replace('.php', '', $snippet['file_name']);
                                wp_enqueue_script('fluent_snippet_' . $snippetScriptName, $loadUrl, [], strtotime($snippet['updated_at']), $isFooter);
                            } else {
                                $code = $this->parseBlock(file_get_contents($file), true);
                                ?>
                                <script><?php echo $this->escCssJs($code); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped ?></script>
                                <?php
                            }
                        }, $this->get($snippet, 'priority', 10));
                    }
                    break;
                case 'css':
                    $runAt = $this->get($snippet, 'run_at', 'wp_head');

                    $isBlockStyle = $this->get($snippet, 'load_in_block_editor', '') === 'yes';

                    if ($isBlockStyle) {
                        add_filter('block_editor_settings_all', function ($settings) use ($snippet, $file) {
                            $code = $this->parseBlock(file_get_contents($file), true);
                            if ($code) {
                                $settings['styles'][] = array(
                                    'css'            => $code,
                                    '__unstableType' => 'plugin',
                                    'source'         => 'easy_code_manager'
                                );
                            }
                            return $settings;
                        }, $this->get($snippet, 'priority', 10));
                    }

                    if (($runAt == 'everywehere' && is_admin()) || $runAt == 'admin_head') {
                        $runAt = 'admin_head';
                    } else {
                        $runAt = 'wp_head';
                    }

                    $isAdminCss = ($runAt == 'admin_head');

                    $loadUrl = '';
                    if ($this->get($snippet, 'load_as_file') == 'yes') {
                        $cachedFile = str_replace('.php', '.css', $fileName);
                        $loadUrl = $this->getCachedFileUrl($cachedFile);
                        if ($loadUrl) {
                            $runAt = ($runAt == 'admin_head') ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts';
                        }
                    }


                    if($isAdminCss) {
                        add_action('enqueue_block_editor_assets', function() use($file, $snippet, $conditionalClass, $loadUrl) {
                            if (!$conditionalClass->evaluate($snippet['condition'])) {
                                return;
                            }

                            $code = $this->parseBlock(file_get_contents($file), true);
                            wp_add_inline_style('wp-edit-blocks', $code);
                        });
                    }

                    add_action($runAt, function () use ($file, $snippet, $conditionalClass, $loadUrl) {
                        if (!$conditionalClass->evaluate($snippet['condition'])) {
                            return;
                        }

                        if ($loadUrl) {
                            $snippetScriptName = str_replace('.php', '', $snippet['file_name']);
                            wp_enqueue_style('fluent_snippet_' . $snippetScriptName, $loadUrl, [], strtotime($snippet['updated_at']));
                        } else {
                            $code = $this->parseBlock(file_get_contents($file), true);
                            ?>
                            <style><?php echo $this->escCssJs($code); // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped ?></style>
                            <?php
                        }

                    }, $this->get($snippet, 'priority', 10));

                    break;
                case 'php_content':
                    $runAt = $snippet['run_at'];
                    if (in_array($runAt, ['wp_footer', 'wp_head', 'wp_body_open'])) {
                        add_action($runAt, function () use ($file, $snippet, $conditionalClass) {
                            if (!$conditionalClass->evaluate($snippet['condition'])) {
                                return;
                            }
                            require_once $file;
                        }, $snippet['priority']);
                    }
                    if (isset($filterMaps[$runAt])) {
                        $filter = $filterMaps[$runAt];
                        add_filter($filter['hook'], function ($content) use ($file, $snippet, $conditionalClass, $filter) {
                            if (!empty($filter['is_single'])) {
                                if (!is_singular() || !in_the_loop() || !is_main_query()) {
                                    return $content;
                                }
                            }

                            if (!$conditionalClass->evaluate($snippet['condition'])) {
                                return $content;
                            }

                            ob_start();
                            require_once $file;
                            $result = ob_get_clean();
                            if ($result) {
                                if ($filter['insert'] == 'before') {
                                    return $result . $content;
                                }

                                return $content . $result;
                            }
                            return $content;
                        }, $this->get($snippet, 'priority', 10));
                    }
                default:
                    break;
            }
        }

        if ($hasInvalidFiles) {
            do_action('fluent_snippets/rebuild_index', false, true);
        }

        do_action('fluent_snippets/after_run_snippets');
    }


    private function get($array, $key, $default = null)
    {
        if (isset($array[$key])) {
            return $array[$key];
        }

        return $default;
    }

    private function parseBlock($fileContent, $codeOnly = false)
    {
        // get content from // <Internal Doc Start> to // <Internal Doc End>
        $fileContent = explode('// <Internal Doc Start>', $fileContent);

        if (count($fileContent) < 2) {
            if ($codeOnly) {
                return '';
            }
            return [null, null];
        }

        $fileContent = explode('// <Internal Doc End> ?>' . PHP_EOL, $fileContent[1]);
        $docBlock = $fileContent[0];
        $code = $fileContent[1];

        if ($codeOnly) {
            return $code;
        }

        $docBlock = explode('*', $docBlock);
        // Explode by : and get the key and value
        $docBlockArray = [
            'name'        => '',
            'status'      => '',
            'tags'        => '',
            'description' => '',
            'type'        => '',
            'run_at'      => '',
            'group'       => ''
        ];

        foreach ($docBlock as $key => $value) {
            $value = trim($value);
            $arr = explode(':', $value);
            if (count($arr) < 2) {
                continue;
            }

            // get the first item from the array and remove it from $arr
            $key = array_shift($arr);
            $key = trim(str_replace('@', '', $key));
            if (!$key) {
                continue;
            }
            $docBlockArray[$key] = trim(implode(':', $arr));
        }

        return [$docBlockArray, $code];
    }


    private function escCssJs($code)
    {
        $code = preg_replace('/<script[^>]*>/', '', $code);
        $code = preg_replace('/<\/script>/', '', $code);
        // remove opening js tag and closing js tag maybe <script type="text/javascript"> too
        $code = preg_replace('/<style[^>]*>/', '', $code);
        return preg_replace('/<\/style>/', '', $code);
    }

    private function getCachedFileUrl($fileName)
    {
        $file = $this->storageDir . '/cached/' . $fileName;

        if (!file_exists($file)) {
            return false;
        }

        return content_url('/fluent-snippet-storage/cached/' . $fileName);
    }
}

add_action('plugins_loaded', function () {
    if (!defined('FLUENT_SNIPPETS_PLUGIN_PATH')) {
        $disabled = (defined('FLUENT_SNIPPETS_SAFE_MODE') && FLUENT_SNIPPETS_SAFE_MODE) || !apply_filters('fluent_snippets/run_snippets', true);

        if (!$disabled) {
            (new CodeRunner)->runSnippets();
        }
    }
}, 9);
