<?php

/**
 * @package   Duplicator/Installer
 * @copyright (c) 2022, Snap Creek LLC
 */

namespace Duplicator\Installer\Core\Deploy\Database;

use Duplicator\Installer\Core\Params\PrmMng;
use Duplicator\Installer\Utils\Log\Log;
use VendorDuplicator\Amk\JsonSerialize\AbstractJsonSerializable;
use DUPX_ArchiveConfig;
use DUPX_DB_Functions;
use DUPX_DB_Tables;
use DUPX_DBInstall;
use Duplicator\Installer\Core\InstState;

class QueryFixes extends AbstractJsonSerializable
{
    const TEMP_POSTFIX                 = 't_';
    const USER_DEFINER_REPLACE_PATTERN = "/^(\s*(?:\/\*!\d+\s)?\s*(?:CREATE.+)?DEFINER\s*=)([^\*\s]+)(.*)$/m";
    const USER_DEFINER_REMOVE_PATTERN  = "/^(\s*(?:\/\*!\d+\s)?\s*(?:CREATE.+)?)(DEFINER\s*=\s*\S+)(.*)$/m";
    const USER_DEFINER_REMOVE_REPLACE  = '$1 $3';
    const SQL_SECURITY_INVOKER_PATTERN = "/^(\s*CREATE.+(?:PROCEDURE|FUNCTION)[\s\S]*)(BEGIN)([\s\S]*)$/";
    const SQL_SECURITY_INVOKER_REPLACE = "$1SQL SECURITY INVOKER\n$2$3";

    /** @var array{search: mixed[], replace: mixed[]} */
    protected $globalRules = array(
        'search'  => array(),
        'replace' => array(),
    );
    /** @var mixed[] */
    protected $tablesPrefixRules = array();
    /** @var string */
    protected $generatorLog = '';

    /**
     * Class constructor
     */
    public function __construct()
    {
        $this->rulesProcAndViews();
        $this->rulesMySQLEngine();
        $this->legacyCharsetAndCollation();
        $this->rulesTableNames();
    }

    /**
     * Filter props on json encode
     *
     * @return string[]
     */
    public function __sleep()
    {
        $props = array_keys(get_object_vars($this));
        return array_diff($props, array('generatorLog'));
    }

    /**
     * Write rules in log
     *
     * @return void
     */
    public function logRules()
    {
        if (strlen($this->generatorLog) == 0) {
            Log::info('NO GENERAL QUERY FIXES');
        } else {
            Log::info('QUERY FIXES');
            Log::info($this->generatorLog);
        }

        if (count($this->globalRules['search']) > 0) {
            Log::info('QUERY FIXES GLOBAL RULES');
            Log::incIndent();
            foreach ($this->globalRules['search'] as $index => $search) {
                Log::info('SEARCH  => ' . $search);
                Log::info('REPLACE => ' . $this->globalRules['replace'][$index] . "\n");
            }
            Log::resetIndent();
        }

        if (count($this->tablesPrefixRules) > 0) {
            Log::info('QUERY FIXES TABLES RULES');
            Log::incIndent();
            foreach ($this->tablesPrefixRules as $indexRulesSet => $ruleSet) {
                Log::info('RULESET ' . ($indexRulesSet + 1));
                Log::incIndent();
                foreach ($ruleSet['search'] as $index => $search) {
                    Log::info('SEARCH  => ' . $search);
                    Log::info('REPLACE => ' . $ruleSet['replace'][$index] . "\n");
                }
                Log::decIndent();
            }
            Log::resetIndent();
        }
    }

    /**
     * @param string $query query to fix
     *
     * @return string The query with appropriate substitutions done
     */
    public function applyFixes($query)
    {
        $query = preg_replace($this->globalRules['search'], $this->globalRules['replace'], $query);

        foreach ($this->tablesPrefixRules as $ruleSet) {
            $query = preg_replace($ruleSet['search'], $ruleSet['replace'], $query);
        }
        return $query;
    }

    /**
     * Set search and replace rules
     *
     * @return void
     */
    protected function rulesProcAndViews()
    {
        if (InstState::isRestoreBackup()) {
            return;
        }

        if (PrmMng::getInstance()->getValue(PrmMng::PARAM_DB_REMOVE_DEFINER)) {
            $this->globalRules['search'][]  = self::USER_DEFINER_REMOVE_PATTERN;
            $this->globalRules['replace'][] = self::USER_DEFINER_REMOVE_REPLACE;
        } else {
            $this->globalRules['search'][] = self::USER_DEFINER_REPLACE_PATTERN;

            $dbHost = PrmMng::getInstance()->getValue(PrmMng::PARAM_DB_HOST);
            $dbUser = PrmMng::getInstance()->getValue(PrmMng::PARAM_DB_USER);

            $definerHost                    = (($dbHost == "localhost" || $dbHost == "127.0.0.1") ? $dbHost : '%');
            $this->globalRules['replace'][] = '$1' . addcslashes("`" . $dbUser . "`@`" . $definerHost . "`", '\\$') . '$3';
        }

        $this->globalRules['search'][]  = self::SQL_SECURITY_INVOKER_PATTERN;
        $this->globalRules['replace'][] = self::SQL_SECURITY_INVOKER_REPLACE;

        $this->generatorLog .= "GLOBAL RULES ADDED: PROC AND VIEWS\n";
    }

    /**
     * Check invalid SQL engines
     *
     * @return void
     */
    protected function rulesMySQLEngine()
    {
        $invalidEngines = array_map(function ($engine) {
            return preg_quote($engine, '/');
        }, DUPX_ArchiveConfig::getInstance()->invalidEngines());

        if (empty($invalidEngines)) {
            return;
        }
        $this->globalRules['search'][]  = '/^(\s*(?:\/\*!\d+\s)?\s*CREATE.+ENGINE=)(' . implode('|', $invalidEngines) . ')(.*)$/ms';
        $this->globalRules['replace'][] = '$1' . DUPX_DB_Functions::getInstance()->getDefaultEngine() . '$3';

        $this->generatorLog .= "GLOBAL RULES ADDED: MYSQL ENGINES\n";
    }

    /**
     * Set legacy charset adn collation rules
     *
     * Regex managed examples
     *  - `meta_value` longtext CHARACTER SET utf16 COLLATE utf16_slovak_ci DEFAULT NULL,
     *  - `comment_author` tinytext COLLATE utf8mb4_unicode_ci NOT NULL,
     *  - ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci_test;
     *  - ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
     *
     * accept ['"`]charset['"`]
     *
     * @return void
     */
    public function legacyCharsetAndCollation()
    {
        $invalidCharsets   = DUPX_ArchiveConfig::getInstance()->invalidCharsets();
        $invalidCollations = DUPX_ArchiveConfig::getInstance()->invalidCollations();
        $defCharsetRegex   = addcslashes(DUPX_DB_Functions::getInstance()->getRealCharsetByParam(), '\\$');
        $defCollateRegex   = addcslashes(DUPX_DB_Functions::getInstance()->getRealCollateByParam(), '\\$');

        if (count($invalidCharsets) > 0) {
            $invalidChrRegex = '(?:' . implode('|', array_map(function ($val) {
                        return preg_quote($val, '/');
            }, $invalidCharsets)) . ')';

            $this->globalRules['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)(' .
                $invalidChrRegex . ')([`\'"]?\s.*COLLATE\s*[\s=]\s*[`\'"]?)([^`\'"\s;,]+)([`\'"]?.*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCharsetRegex . '$3' . $defCollateRegex . '$5';
            $this->globalRules['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)([^`\'"\s;,]+)([`\'"]?\s.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)(' .
                $invalidChrRegex . ')([`\'"]?[\s;,].*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCollateRegex . '$3' . $defCharsetRegex . '$5';
            $this->globalRules['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)(' . $invalidChrRegex . ')([`\'"]?[\s;,].*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCharsetRegex . '$3';

            $this->generatorLog .= "GLOBAL RULES ADDED: INVALID CHARSETS\n";
        }

        if (count($invalidCollations) > 0) {
            $invalidColRegex = '(?:' . implode('|', array_map(function ($val) {
                        return preg_quote($val, '/');
            }, $invalidCollations)) . ')';

            $this->globalRules['search'][]  = '/(^.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)([^`\'"\s;,]+)([`\'"]?\s.*COLLATE\s*[\s=]\s*[`\'"]?)(' .
                $invalidColRegex . ')([`\'"]?[\s;,].*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCharsetRegex . '$3' . $defCollateRegex . '$5';
            $this->globalRules['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)(' .
                $invalidColRegex . ')([`\'"]?\s.*(?:CHARSET|CHARACTER SET)\s*[\s=]\s*[`\'"]?)([^`\'"\s;,]+)([`\'"]?.*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCollateRegex . '$3' . $defCharsetRegex . '$5';
            $this->globalRules['search'][]  = '/(^.*COLLATE\s*[\s=]\s*[`\'"]?)(' . $invalidColRegex . ')([`\'"]?[\s;,].*$)/m';
            $this->globalRules['replace'][] = '$1' . $defCollateRegex . '$3';

            $this->generatorLog .= "GLOBAL RULES ADDED: INVALID COLLATIONS\n";
        }
    }

    /**
     * Set search and replace table prefix rules
     *
     * @return void
     */
    protected function rulesTableNames()
    {
        $mapping = DUPX_DB_Tables::getInstance()->getRenameTablesMapping();

        $oldPrefixes = array_keys($mapping);
        $newPrefixes = array();
        foreach ($mapping as $oldPrefix => $newMapping) {
            $newPrefixes = array_merge($newPrefixes, array_keys($newMapping));
        }
        $newPrefixes = array_unique($newPrefixes);

        // Prevent double transformation with temp prefix
        $doublePrefixes = array_intersect($oldPrefixes, $newPrefixes);
        if (count($doublePrefixes) > 0) {
            $this->generatorLog .= 'DOUBLE PREFIXES ' . Log::v2str($doublePrefixes);
        }

        foreach ($mapping as $oldPrefix => $newMapping) {
            $rulesSet = array(
                'search'  => array(),
                'replace' => array(),
            );

            $quoteOldPrefix = preg_quote($oldPrefix, '/');

            foreach ($newMapping as $newPrefix => $commons) {
                $this->generatorLog .= "TABLES RULES ADDED: CHANGE TABLES PREFIX " . $oldPrefix . " TO " . $newPrefix ;
                if (in_array($newPrefix, $doublePrefixes)) {
                    $this->generatorLog .= " [USE TMP PREFIX]\n";
                    $newPrefix           = $newPrefix . self::TEMP_POSTFIX;
                } else {
                    $this->generatorLog .= "\n";
                }
                $this->generatorLog .= "\tFOR TABLES " . implode(',', $commons) . "\n";

                $quoteNewPrefix = addcslashes($newPrefix, '\\$');
                $quoteCommons   = array_map(
                    function ($val) {
                        return preg_quote($val, '/');
                    },
                    $commons
                );

                for ($i = 0; $i < ceil(count($quoteCommons) / DUPX_DBInstall::TABLES_REGEX_CHUNK_SIZE); $i++) {
                    $subArray = array_slice($quoteCommons, $i * DUPX_DBInstall::TABLES_REGEX_CHUNK_SIZE, DUPX_DBInstall::TABLES_REGEX_CHUNK_SIZE);

                    if (count($subArray) == 0) {
                        break;
                    }

                    $rulesSet['search'][]  = '/' . $quoteOldPrefix . '(' . implode('|', $subArray) . ')/m';
                    $rulesSet['replace'][] = $quoteNewPrefix . '$1';
                }

                $rulesSet['search'][]  = '/(CONSTRAINT[\s\t]+[`\'"]?.+)(?-i)' . $quoteOldPrefix . '(?i)(.+[`\'"]?[\s\t]+FOREIGN[\s\t]+KEY)/mi';
                $rulesSet['replace'][] = '$1' . $quoteNewPrefix . '$2';
            }

            if (count($rulesSet['search']) > 0) {
                $this->tablesPrefixRules[] = $rulesSet;
            }
        }

        if (count($doublePrefixes)) {
            // REMOVE TEMP PREFIXES
            $rulesSet = array(
                'search'  => array(),
                'replace' => array(),
            );

            foreach ($doublePrefixes as $prefix) {
                $quoteTempPrefix = preg_quote($prefix . self::TEMP_POSTFIX, '/');
                $quotePrefix     = addcslashes($prefix, '\\$');

                $rulesSet['search'][]  = '/' . $quoteTempPrefix . '/m';
                $rulesSet['replace'][] = $quotePrefix . '$1';
            }

            $this->tablesPrefixRules[] = $rulesSet;
        }
    }
}
