<?php

namespace AcyMailing\Controllers\Configuration;

use AcyMailing\Classes\UrlClass;
use AcyMailing\Classes\UserClass;

trait Security
{
    private array $messagesNoHtml = [];

    public function checkDBAjax(): void
    {
        $this->checkDB();

        if (empty($this->messagesNoHtml)) {
            echo '<i class="acymicon-check-circle acym__color__green"></i>';
        } else {
            $nbMessages = count($this->messagesNoHtml);
            foreach ($this->messagesNoHtml as $i => $oneMsg) {
                echo '<span style="color:'.$oneMsg['color'].'">'.$oneMsg['msg'].'</span>';

                if ($i < $nbMessages) {
                    echo '<br />';
                }
            }
        }

        exit;
    }

    public function checkDB(bool $fromConfiguration = true): array
    {
        $correctTablesStructure = $this->getCorrectTablesStructure();
        $currentTablesStructure = $this->getCurrentTablesStructure($correctTablesStructure);
        $this->fixCurrentStructure($correctTablesStructure, $currentTablesStructure);

        $this->cleanDuplicatedUrls($fromConfiguration);
        $this->addMissingUserKeys();

        return $this->messagesNoHtml;
    }

    private function getCorrectTablesStructure(): array
    {
        $correctTablesStructure = [
            'structure' => [],
            'createTable' => [],
            'indexes' => [],
            'constraints' => [],
        ];

        $queries = file_get_contents(ACYM_BACK.'tables.sql');
        $tables = explode('CREATE TABLE IF NOT EXISTS ', $queries);

        foreach ($tables as $oneTable) {
            if (strpos($oneTable, '`#__') !== 0) {
                continue;
            }

            $tableName = substr($oneTable, 1, strpos($oneTable, '`', 1) - 1);
            $correctTablesStructure['createTable'][$tableName] = 'CREATE TABLE IF NOT EXISTS '.$oneTable;
            $correctTablesStructure['indexes'][$tableName] = [];
            $correctTablesStructure['constraints'][$tableName] = [];

            $fields = explode("\n", $oneTable);
            foreach ($fields as $key => $oneField) {
                if (strpos($oneField, '#__') === 1) {
                    continue;
                }
                $oneField = rtrim(trim($oneField), ',');

                if (substr($oneField, 0, 1) === '`') {
                    $columnName = substr($oneField, 1, strpos($oneField, '`', 1) - 1);
                    $correctTablesStructure['structure'][$tableName][$columnName] = trim($oneField, ',');
                    continue;
                }

                if (strpos($oneField, 'PRIMARY KEY') === 0) {
                    $correctTablesStructure['indexes'][$tableName]['PRIMARY'] = $oneField;
                } elseif (strpos($oneField, 'INDEX') === 0) {
                    $firstBackquotePos = strpos($oneField, '`');
                    $indexName = substr($oneField, $firstBackquotePos + 1, strpos($oneField, '`', $firstBackquotePos + 1) - $firstBackquotePos - 1);

                    $correctTablesStructure['indexes'][$tableName][$indexName] = $oneField;
                } elseif (strpos($oneField, 'FOREIGN KEY') !== false) {
                    preg_match('/(#__fk.*)\`/Uis', $fields[$key - 1], $matchesConstraints);
                    preg_match('/(#__.*)\`\(`(.*)`\)/Uis', $fields[$key + 1], $matchesTable);
                    preg_match('/\`(.*)\`/Uis', $oneField, $matchesColumn);
                    if (!empty($matchesConstraints) && !empty($matchesTable) && !empty($matchesColumn)) {
                        $correctTablesStructure['constraints'][$tableName][$matchesConstraints[1]] = [
                            'table' => $matchesTable[1],
                            'column' => $matchesColumn[1],
                            'table_column' => $matchesTable[2],
                        ];
                    }
                }
            }
        }

        $correctTablesStructure['tableNames'] = array_keys($correctTablesStructure['structure']);

        return $correctTablesStructure;
    }

    private function getCurrentTablesStructure(array $correctTablesStructure): array
    {
        $currentTablesStructure = [];

        foreach ($correctTablesStructure['tableNames'] as $oneTableName) {
            try {
                $columns = acym_loadObjectList('SHOW COLUMNS FROM '.$oneTableName);
            } catch (\Exception $e) {
                $columns = null;
            }

            if (!empty($columns)) {
                foreach ($columns as $oneField) {
                    $currentTablesStructure[$oneTableName][$oneField->Field] = $oneField->Field;
                }
                continue;
            }

            $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_LOAD_COLUMNS_ERROR', $oneTableName, $errorMessage),
            ];

            if (strpos($errorMessage, 'marked as crashed')) {
                try {
                    $isError = acym_query('REPAIR TABLE '.$oneTableName);
                } catch (\Exception $e) {
                    $isError = null;
                }

                if ($isError === null) {
                    $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                    $this->messagesNoHtml[] = [
                        'error' => true,
                        'color' => 'red',
                        'msg' => acym_translationSprintf('ACYM_CHECKDB_REPAIR_TABLE_ERROR', $oneTableName, $errorMessage),
                    ];
                } else {
                    $this->messagesNoHtml[] = [
                        'error' => false,
                        'color' => 'green',
                        'msg' => acym_translationSprintf('ACYM_CHECKDB_REPAIR_TABLE_SUCCESS', $oneTableName),
                    ];
                }
                continue;
            } else {
                try {
                    $isError = acym_query($correctTablesStructure['createTable'][$oneTableName]);
                } catch (\Exception $e) {
                    $isError = null;
                }

                if ($isError === null) {
                    $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                    $this->messagesNoHtml[] = [
                        'error' => true,
                        'color' => 'red',
                        'msg' => acym_translationSprintf('ACYM_CHECKDB_CREATE_TABLE_ERROR', $oneTableName, $errorMessage),
                    ];
                } else {
                    $this->messagesNoHtml[] = [
                        'error' => false,
                        'color' => 'green',
                        'msg' => acym_translationSprintf('ACYM_CHECKDB_CREATE_TABLE_SUCCESS', $oneTableName),
                    ];
                }
            }
        }

        return $currentTablesStructure;
    }

    private function fixCurrentStructure(array $correctTablesStructure, array $currentTablesStructure): void
    {
        foreach ($correctTablesStructure['tableNames'] as $oneTableName) {
            if (empty($currentTablesStructure[$oneTableName])) {
                continue;
            }

            $this->addMissingColumns($correctTablesStructure['structure'][$oneTableName], $currentTablesStructure[$oneTableName], $oneTableName);
            $this->removeExtraColumns($correctTablesStructure['structure'][$oneTableName], $currentTablesStructure[$oneTableName], $oneTableName);
            $this->fixDefaultValues($correctTablesStructure['structure'][$oneTableName], $oneTableName);
            $this->addMissingTableKeys($correctTablesStructure['indexes'][$oneTableName], $oneTableName);
            $this->addMissingTableConstraints($correctTablesStructure['constraints'][$oneTableName], $oneTableName);
        }
    }

    private function addMissingColumns(array $correctTableColumns, array $currentTableColumnNames, string $oneTableName): void
    {
        $idealColumnNames = array_keys($correctTableColumns);
        $missingColumns = array_diff($idealColumnNames, $currentTableColumnNames);

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

        foreach ($missingColumns as $oneColumn) {
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_MISSING_COLUMN', $oneColumn, $oneTableName),
            ];

            try {
                $isError = acym_query('ALTER TABLE '.$oneTableName.' ADD '.$correctTableColumns[$oneColumn]);
            } catch (\Exception $e) {
                $isError = null;
            }

            if ($isError === null) {
                $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                $this->messagesNoHtml[] = [
                    'error' => true,
                    'color' => 'red',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_COLUMN_ERROR', $oneColumn, $oneTableName, $errorMessage),
                ];
            } else {
                $this->messagesNoHtml[] = [
                    'error' => false,
                    'color' => 'green',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_COLUMN_SUCCESS', $oneColumn, $oneTableName),
                ];
            }
        }
    }

    private function removeExtraColumns(array $correctTableColumns, array $currentTableColumnNames, string $oneTableName): void
    {
        $idealColumnNames = array_keys($correctTableColumns);
        $extraColumns = array_diff($currentTableColumnNames, $idealColumnNames);

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

        foreach ($extraColumns as $oneColumn) {
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_EXTRA_COLUMN', $oneColumn, $oneTableName),
            ];

            try {
                $isError = acym_query('ALTER TABLE '.$oneTableName.' DROP COLUMN `'.acym_secureDBColumn($oneColumn).'`');
            } catch (\Exception $e) {
                $isError = null;
            }

            if ($isError === null) {
                $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                $this->messagesNoHtml[] = [
                    'error' => true,
                    'color' => 'red',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_EXTRA_COLUMN_ERROR', $oneColumn, $oneTableName, $errorMessage),
                ];
            } else {
                $this->messagesNoHtml[] = [
                    'error' => false,
                    'color' => 'green',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_EXTRA_COLUMN_SUCCESS', $oneColumn, $oneTableName),
                ];
            }
        }
    }

    private function fixDefaultValues(array $correctTableColumns, string $oneTableName): void
    {
        $oneTableName = str_replace('#__', acym_getPrefix(), $oneTableName);
        try {
            $currentTableColumns = acym_loadObjectList(
                'SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, COLUMN_TYPE 
                FROM information_schema.COLUMNS 
                WHERE TABLE_SCHEMA = DATABASE() 
                    AND TABLE_NAME = '.acym_escapeDB($oneTableName),
                'COLUMN_NAME'
            );

            if (empty($currentTableColumns)) {
                return;
            }
        } catch (\Exception $e) {
            $this->messagesNoHtml[] = [
                'error' => true,
                'color' => 'orange',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_ERROR_GET_COLUMNS', $oneTableName, $e->getMessage()),
            ];

            return;
        }

        foreach ($correctTableColumns as $oneColumn => $oneColumnDefinition) {
            $defaultValue = '';
            if (preg_match('#DEFAULT ([^ ]+)$#Ui', $oneColumnDefinition, $matches)) {
                $defaultValue = $matches[1];
            }

            if (strlen($defaultValue) === 0) {
                continue;
            }
            $currentDefault = $currentTableColumns[$oneColumn]->COLUMN_DEFAULT;
            if (!empty($currentDefault)) {
                $currentDefault = trim($currentDefault, "'\"");
            }
            $expectedDefault = trim($defaultValue, "'\"");

            if (strtoupper($expectedDefault) === 'NULL') {
                $expectedDefault = null;
            }

            $isNullable = strtoupper($currentTableColumns[$oneColumn]->IS_NULLABLE) === 'YES';

            if ($isNullable && $currentDefault === '') {
                $currentDefault = null;
            }

            if (!$isNullable && $currentDefault === null) {
                $currentDefault = '';
            }

            if ($currentDefault === $expectedDefault) {
                continue;
            }

            if (!empty($currentTableColumns[$oneColumn]->COLUMN_DEFAULT) && substr($currentTableColumns[$oneColumn]->COLUMN_DEFAULT, 0, 1) === '"') {
                $currentTableColumns[$oneColumn]->COLUMN_DEFAULT = '\''.substr($currentTableColumns[$oneColumn]->COLUMN_DEFAULT, 1, -1).'\'';
            }

            $isColumnDefaultEmpty = empty($currentTableColumns[$oneColumn]->COLUMN_DEFAULT) && $currentTableColumns[$oneColumn]->COLUMN_DEFAULT !== '0';

            if (!$isColumnDefaultEmpty && $currentTableColumns[$oneColumn]->COLUMN_DEFAULT === $defaultValue) {
                continue;
            }

            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_WRONG_DEFAULT_VALUE', $oneColumn, $oneTableName),
            ];

            try {
                $isError = acym_query('ALTER TABLE '.$oneTableName.' CHANGE `'.acym_secureDBColumn($oneColumn).'` '.$oneColumnDefinition);
            } catch (\Exception $e) {
                $isError = null;
            }

            if ($isError === null) {
                $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                $this->messagesNoHtml[] = [
                    'error' => true,
                    'color' => 'red',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_WRONG_DEFAULT_VALUE_ERROR', $oneColumn, $oneTableName, $errorMessage),
                ];
            } else {
                $this->messagesNoHtml[] = [
                    'error' => false,
                    'color' => 'green',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_WRONG_DEFAULT_VALUE_SUCCESS', $oneColumn, $oneTableName),
                ];
            }
        }
    }

    private function addMissingTableKeys(array $correctTableIndexes, string $oneTableName): void
    {
        $results = acym_loadObjectList('SHOW INDEX FROM '.$oneTableName, 'Key_name');
        if (empty($results)) {
            $results = [];
        }

        foreach ($correctTableIndexes as $name => $query) {
            $name = acym_prepareQuery($name);
            if (in_array($name, array_keys($results))) {
                continue;
            }


            $keyName = $name === 'PRIMARY' ? 'primary key' : 'index '.$name;

            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_MISSING_INDEX', $keyName, $oneTableName),
            ];

            try {
                $isError = acym_query('ALTER TABLE '.$oneTableName.' ADD '.$query);
            } catch (\Exception $e) {
                $isError = null;
            }

            if ($isError === null) {
                $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                $this->messagesNoHtml[] = [
                    'error' => true,
                    'color' => 'red',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_INDEX_ERROR', $keyName, $oneTableName, $errorMessage),
                ];
            } else {
                $this->messagesNoHtml[] = [
                    'error' => false,
                    'color' => 'green',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_INDEX_SUCCESS', $keyName, $oneTableName),
                ];
            }
        }
    }

    private function addMissingTableConstraints(array $correctTableConstraints, string $oneTableName): void
    {
        if (empty($correctTableConstraints)) {
            return;
        }

        $tableNameQuery = str_replace('#__', acym_getPrefix(), $oneTableName);
        $databaseName = acym_loadResult('SELECT DATABASE();');
        $foreignKeys = acym_loadObjectList(
            'SELECT i.CONSTRAINT_NAME, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME, k.COLUMN_NAME
            FROM information_schema.TABLE_CONSTRAINTS AS i 
            JOIN information_schema.KEY_COLUMN_USAGE AS k ON i.CONSTRAINT_NAME = k.CONSTRAINT_NAME 
            WHERE i.TABLE_NAME = '.acym_escapeDB($tableNameQuery).' AND i.CONSTRAINT_TYPE = "FOREIGN KEY" AND i.TABLE_SCHEMA = '.acym_escapeDB($databaseName),
            'CONSTRAINT_NAME'
        );

        acym_query('SET foreign_key_checks = 0');

        foreach ($correctTableConstraints as $constraintName => $constraintInfo) {
            $constraintTableNamePrefix = str_replace('#__', acym_getPrefix(), $constraintInfo['table']);
            $constraintName = str_replace('#__', acym_getPrefix(), $constraintName);

            if (!empty($foreignKeys[$constraintName]) && $foreignKeys[$constraintName]->REFERENCED_TABLE_NAME === $constraintTableNamePrefix && $foreignKeys[$constraintName]->REFERENCED_COLUMN_NAME === $constraintInfo['table_column'] && $foreignKeys[$constraintName]->COLUMN_NAME === $constraintInfo['column']) {
                continue;
            }

            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_WRONG_FOREIGN_KEY', $constraintName, $oneTableName),
            ];

            if (!empty($foreignKeys[$constraintName])) {
                try {
                    $isError = acym_query('ALTER TABLE `'.$oneTableName.'` DROP FOREIGN KEY `'.$constraintName.'`');
                } catch (\Exception $e) {
                    $isError = null;
                }

                if ($isError === null) {
                    $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                    $this->messagesNoHtml[] = [
                        'error' => true,
                        'color' => 'red',
                        'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_FOREIGN_KEY_ERROR', $constraintName, $oneTableName, $errorMessage),
                    ];
                    continue;
                }
            }

            try {
                $isError = acym_query(
                    'ALTER TABLE `'.$oneTableName.'` ADD CONSTRAINT `'.$constraintName.'` FOREIGN KEY (`'.$constraintInfo['column'].'`) REFERENCES `'.$constraintInfo['table'].'` (`'.$constraintInfo['table_column'].'`) ON DELETE NO ACTION ON UPDATE NO ACTION;'
                );
            } catch (\Exception $e) {
                $isError = null;
            }

            if ($isError === null) {
                $errorMessage = (isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200));
                $this->messagesNoHtml[] = [
                    'error' => true,
                    'color' => 'red',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_FOREIGN_KEY_ERROR', $constraintName, $oneTableName, $errorMessage),
                ];
            } else {
                $this->messagesNoHtml[] = [
                    'error' => false,
                    'color' => 'green',
                    'msg' => acym_translationSprintf('ACYM_CHECKDB_ADD_FOREIGN_KEY_SUCCESS', $constraintName, $oneTableName),
                ];
            }
        }

        acym_query('SET foreign_key_checks = 1');
    }

    private function cleanDuplicatedUrls(bool $fromConfiguration): void
    {
        if (!$fromConfiguration) {
            return;
        }

        $urlClass = new UrlClass();
        $duplicatedUrls = $urlClass->getDuplicatedUrls();

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

        $maxExecutionTime = intval(ini_get('max_execution_time'));
        $time = time();
        $interrupted = false;
        $this->messagesNoHtml[] = [
            'error' => false,
            'color' => 'blue',
            'msg' => acym_translation('ACYM_CHECKDB_DUPLICATED_URLS'),
        ];

        if (empty($maxExecutionTime) || $maxExecutionTime - 20 < 20) {
            $maxExecutionTime = 20;
        } else {
            $maxExecutionTime -= 20;
        }

        acym_increasePerf();
        while (!empty($duplicatedUrls)) {
            $urlClass->delete($duplicatedUrls);

            if (time() - $time > $maxExecutionTime) {
                $interrupted = true;
                break;
            }

            $duplicatedUrls = $urlClass->getDuplicatedUrls();
        }

        if (empty($interrupted)) {
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'green',
                'msg' => acym_translation('ACYM_CHECKDB_DUPLICATED_URLS_SUCCESS'),
            ];
        } else {
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'blue',
                'msg' => acym_translation('ACYM_CHECKDB_DUPLICATED_URLS_REMAINING'),
            ];
        }
    }

    private function addMissingUserKeys(): void
    {
        $userClass = new UserClass();
        $nbAddedKeys = $userClass->addMissingKeys();

        if (!empty($nbAddedKeys)) {
            $this->messagesNoHtml[] = [
                'error' => false,
                'color' => 'green',
                'msg' => acym_translationSprintf('ACYM_CHECKDB_ADDED_KEYS', $nbAddedKeys),
            ];
        }
    }

    public function redomigration(): void
    {
        $this->config->saveConfig(['migration' => 0]);
        acym_redirect(acym_completeLink('dashboard', false, true));
    }

    public function scanSiteFiles(): void
    {
        $maliciousFiles = [];
        $siteFiles = acym_getFiles(ACYM_ROOT, '.', true, true);
        foreach ($siteFiles as $oneFilePath) {
            $lastSlashPos = strrpos($oneFilePath, '/');
            if (
                !empty($lastSlashPos)
                && strpos($oneFilePath, ACYM_UPLOAD_FOLDER_THUMBNAIL) !== false
                && preg_match('/.*thumbnail.*php.*$/', substr($oneFilePath, $lastSlashPos + 1))
            ) {
                $maliciousFiles[] = $oneFilePath;
            } elseif (filesize($oneFilePath) < 10000) {
                $fileContent = file_get_contents($oneFilePath);
                if (preg_match('/^<\?php echo "jm"\."te"\."st"; \?>$/U', $fileContent)) {
                    $maliciousFiles[] = $oneFilePath;
                } elseif (preg_match('/^<\?php\n\$[a-z]+\s*=\s*\$_COOKIE\s*;/Ui', $fileContent)) {
                    $maliciousFiles[] = $oneFilePath;
                }
            }
        }

        ob_start();
        if (!empty($maliciousFiles)) {
            $message = acym_translation('ACYM_MALICIOUS_FILES');
            $message .= '<ul><li>'.implode('</li><li>', $maliciousFiles).'</li></ul>';
            acym_display($message, 'error', false);
        } else {
            acym_display(acym_translation('ACYM_NO_MALICIOUS_FILES'), 'success', false);
        }
        $message = ob_get_clean();
        acym_sendAjaxResponse($message);
    }
}
