<?php

declare(strict_types=1);

namespace PhpMyAdmin\Query;

use PhpMyAdmin\Util;

use function count;
use function implode;
use function is_array;
use function sprintf;

/**
 * Handles generating SQL queries
 */
class Generator
{
    /**
     * returns a segment of the SQL WHERE clause regarding table name and type
     *
     * @param array|string $escapedTableOrTables table(s)
     * @param bool         $tblIsGroup           $table is a table group
     * @param string       $tableType            whether table or view
     *
     * @return string a segment of the WHERE clause
     */
    public static function getTableCondition(
        $escapedTableOrTables,
        bool $tblIsGroup,
        ?string $tableType
    ): string {
        // get table information from information_schema
        if ($escapedTableOrTables) {
            if (is_array($escapedTableOrTables)) {
                $sqlWhereTable = 'AND t.`TABLE_NAME` '
                    . Util::getCollateForIS() . ' IN (\''
                    . implode('\', \'', $escapedTableOrTables)
                    . '\')';
            } elseif ($tblIsGroup === true) {
                $sqlWhereTable = 'AND t.`TABLE_NAME` LIKE \''
                    . Util::escapeMysqlWildcards($escapedTableOrTables)
                    . '%\'';
            } else {
                $sqlWhereTable = 'AND t.`TABLE_NAME` '
                    . Util::getCollateForIS() . ' = \''
                    . $escapedTableOrTables . '\'';
            }
        } else {
            $sqlWhereTable = '';
        }

        if ($tableType) {
            if ($tableType === 'view') {
                $sqlWhereTable .= " AND t.`TABLE_TYPE` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')";
            } elseif ($tableType === 'table') {
                $sqlWhereTable .= " AND t.`TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED')";
            }
        }

        return $sqlWhereTable;
    }

    /**
     * returns the beginning of the SQL statement to fetch the list of tables
     *
     * @param string[] $thisDatabases databases to list
     * @param string   $sqlWhereTable additional condition
     *
     * @return string the SQL statement
     */
    public static function getSqlForTablesFull(array $thisDatabases, string $sqlWhereTable): string
    {
        return 'SELECT *,'
            . ' `TABLE_SCHEMA`       AS `Db`,'
            . ' `TABLE_NAME`         AS `Name`,'
            . ' `TABLE_TYPE`         AS `TABLE_TYPE`,'
            . ' `ENGINE`             AS `Engine`,'
            . ' `ENGINE`             AS `Type`,'
            . ' `VERSION`            AS `Version`,'
            . ' `ROW_FORMAT`         AS `Row_format`,'
            . ' `TABLE_ROWS`         AS `Rows`,'
            . ' `AVG_ROW_LENGTH`     AS `Avg_row_length`,'
            . ' `DATA_LENGTH`        AS `Data_length`,'
            . ' `MAX_DATA_LENGTH`    AS `Max_data_length`,'
            . ' `INDEX_LENGTH`       AS `Index_length`,'
            . ' `DATA_FREE`          AS `Data_free`,'
            . ' `AUTO_INCREMENT`     AS `Auto_increment`,'
            . ' `CREATE_TIME`        AS `Create_time`,'
            . ' `UPDATE_TIME`        AS `Update_time`,'
            . ' `CHECK_TIME`         AS `Check_time`,'
            . ' `TABLE_COLLATION`    AS `Collation`,'
            . ' `CHECKSUM`           AS `Checksum`,'
            . ' `CREATE_OPTIONS`     AS `Create_options`,'
            . ' `TABLE_COMMENT`      AS `Comment`'
            . ' FROM `information_schema`.`TABLES` t'
            . ' WHERE `TABLE_SCHEMA` ' . Util::getCollateForIS()
            . ' IN (\'' . implode("', '", $thisDatabases) . '\')'
            . ' ' . $sqlWhereTable;
    }

    /**
     * Returns SQL for fetching information on table indexes (SHOW INDEXES)
     *
     * @param string $database name of database
     * @param string $table    name of the table whose indexes are to be retrieved
     * @param string $where    additional conditions for WHERE
     *
     * @return string SQL for getting indexes
     */
    public static function getTableIndexesSql(
        string $database,
        string $table,
        ?string $where = null
    ): string {
        $sql = 'SHOW INDEXES FROM ' . Util::backquote($database) . '.'
            . Util::backquote($table);
        if ($where) {
            $sql .= ' WHERE (' . $where . ')';
        }

        return $sql;
    }

    /**
     * Returns SQL query for fetching columns for a table
     *
     * @param string      $database      name of database
     * @param string      $table         name of table to retrieve columns from
     * @param string|null $escapedColumn name of column, null to show all columns
     * @param bool        $full          whether to return full info or only column names
     */
    public static function getColumnsSql(
        string $database,
        string $table,
        ?string $escapedColumn = null,
        bool $full = false
    ): string {
        return 'SHOW ' . ($full ? 'FULL' : '') . ' COLUMNS FROM '
            . Util::backquote($database) . '.' . Util::backquote($table)
            . ($escapedColumn !== null ? " LIKE '"
                . $escapedColumn . "'" : '');
    }

    public static function getInformationSchemaRoutinesRequest(
        string $escapedDb,
        ?string $routineType,
        ?string $escapedRoutineName
    ): string {
        $query = 'SELECT'
            . ' `ROUTINE_SCHEMA` AS `Db`,'
            . ' `SPECIFIC_NAME` AS `Name`,'
            . ' `ROUTINE_TYPE` AS `Type`,'
            . ' `DEFINER` AS `Definer`,'
            . ' `LAST_ALTERED` AS `Modified`,'
            . ' `CREATED` AS `Created`,'
            . ' `SECURITY_TYPE` AS `Security_type`,'
            . ' `ROUTINE_COMMENT` AS `Comment`,'
            . ' `CHARACTER_SET_CLIENT` AS `character_set_client`,'
            . ' `COLLATION_CONNECTION` AS `collation_connection`,'
            . ' `DATABASE_COLLATION` AS `Database Collation`,'
            . ' `DTD_IDENTIFIER`'
            . ' FROM `information_schema`.`ROUTINES`'
            . ' WHERE `ROUTINE_SCHEMA` ' . Util::getCollateForIS()
            . " = '" . $escapedDb . "'";
        if ($routineType !== null) {
            $query .= " AND `ROUTINE_TYPE` = '" . $routineType . "'";
        }

        if ($escapedRoutineName !== null) {
            $query .= ' AND `SPECIFIC_NAME`'
                . " = '" . $escapedRoutineName . "'";
        }

        return $query;
    }

    public static function getInformationSchemaEventsRequest(string $escapedDb, ?string $escapedEventName): string
    {
        $query = 'SELECT'
            . ' `EVENT_SCHEMA` AS `Db`,'
            . ' `EVENT_NAME` AS `Name`,'
            . ' `DEFINER` AS `Definer`,'
            . ' `TIME_ZONE` AS `Time zone`,'
            . ' `EVENT_TYPE` AS `Type`,'
            . ' `EXECUTE_AT` AS `Execute at`,'
            . ' `INTERVAL_VALUE` AS `Interval value`,'
            . ' `INTERVAL_FIELD` AS `Interval field`,'
            . ' `STARTS` AS `Starts`,'
            . ' `ENDS` AS `Ends`,'
            . ' `STATUS` AS `Status`,'
            . ' `ORIGINATOR` AS `Originator`,'
            . ' `CHARACTER_SET_CLIENT` AS `character_set_client`,'
            . ' `COLLATION_CONNECTION` AS `collation_connection`, '
            . '`DATABASE_COLLATION` AS `Database Collation`'
            . ' FROM `information_schema`.`EVENTS`'
            . ' WHERE `EVENT_SCHEMA` ' . Util::getCollateForIS()
            . " = '" . $escapedDb . "'";
        if ($escapedEventName !== null) {
            $query .= ' AND `EVENT_NAME`'
                . " = '" . $escapedEventName . "'";
        }

        return $query;
    }

    public static function getInformationSchemaTriggersRequest(string $escapedDb, ?string $escapedTable): string
    {
        $query = 'SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION'
            . ', EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT'
            . ', EVENT_OBJECT_SCHEMA, EVENT_OBJECT_TABLE, DEFINER'
            . ' FROM information_schema.TRIGGERS'
            . ' WHERE EVENT_OBJECT_SCHEMA ' . Util::getCollateForIS() . '='
            . ' \'' . $escapedDb . '\'';

        if ($escapedTable !== null) {
            $query .= ' AND EVENT_OBJECT_TABLE ' . Util::getCollateForIS()
                . " = '" . $escapedTable . "';";
        }

        return $query;
    }

    public static function getInformationSchemaDataForCreateRequest(string $user, string $host): string
    {
        return 'SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` '
            . "WHERE `PRIVILEGE_TYPE` = 'CREATE USER' AND "
            . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
    }

    public static function getInformationSchemaDataForGranteeRequest(string $user, string $host): string
    {
        return 'SELECT 1 FROM ('
            . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM '
            . '`INFORMATION_SCHEMA`.`COLUMN_PRIVILEGES` UNION '
            . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM '
            . '`INFORMATION_SCHEMA`.`TABLE_PRIVILEGES` UNION '
            . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM '
            . '`INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES` UNION '
            . 'SELECT `GRANTEE`, `IS_GRANTABLE` FROM '
            . '`INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t '
            . "WHERE `IS_GRANTABLE` = 'YES' AND "
            . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1";
    }

    public static function getInformationSchemaForeignKeyConstraintsRequest(
        string $escapedDatabase,
        string $tablesListForQueryCsv
    ): string {
        return 'SELECT'
            . ' TABLE_NAME,'
            . ' COLUMN_NAME,'
            . ' REFERENCED_TABLE_NAME,'
            . ' REFERENCED_COLUMN_NAME'
            . ' FROM information_schema.key_column_usage'
            . ' WHERE referenced_table_name IS NOT NULL'
            . " AND TABLE_SCHEMA = '" . $escapedDatabase . "'"
            . ' AND TABLE_NAME IN (' . $tablesListForQueryCsv . ')'
            . ' AND REFERENCED_TABLE_NAME IN (' . $tablesListForQueryCsv . ');';
    }

    public static function getInformationSchemaDatabasesFullRequest(
        bool $forceStats,
        string $sqlWhereSchema,
        string $sortBy,
        string $sortOrder,
        string $limit
    ): string {
        $sql = 'SELECT *, CAST(BIN_NAME AS CHAR CHARACTER SET utf8) AS SCHEMA_NAME FROM (';
        $sql .= 'SELECT BINARY s.SCHEMA_NAME AS BIN_NAME, s.DEFAULT_COLLATION_NAME';
        if ($forceStats) {
            $sql .= ','
                . ' COUNT(t.TABLE_SCHEMA)  AS SCHEMA_TABLES,'
                . ' SUM(t.TABLE_ROWS)      AS SCHEMA_TABLE_ROWS,'
                . ' SUM(t.DATA_LENGTH)     AS SCHEMA_DATA_LENGTH,'
                . ' SUM(t.MAX_DATA_LENGTH) AS SCHEMA_MAX_DATA_LENGTH,'
                . ' SUM(t.INDEX_LENGTH)    AS SCHEMA_INDEX_LENGTH,'
                . ' SUM(t.DATA_LENGTH + t.INDEX_LENGTH) AS SCHEMA_LENGTH,'
                . ' SUM(IF(t.ENGINE <> \'InnoDB\', t.DATA_FREE, 0)) AS SCHEMA_DATA_FREE';
        }

        $sql .= ' FROM `information_schema`.SCHEMATA s ';
        if ($forceStats) {
            $sql .= ' LEFT JOIN `information_schema`.TABLES t ON BINARY t.TABLE_SCHEMA = BINARY s.SCHEMA_NAME';
        }

        $sql .= $sqlWhereSchema
                . ' GROUP BY BINARY s.SCHEMA_NAME, s.DEFAULT_COLLATION_NAME'
                . ' ORDER BY ';
        if ($sortBy === 'SCHEMA_NAME' || $sortBy === 'DEFAULT_COLLATION_NAME') {
            $sql .= 'BINARY ';
        }

        $sql .= Util::backquote($sortBy)
            . ' ' . $sortOrder
            . $limit;
        $sql .= ') a';

        return $sql;
    }

    public static function getInformationSchemaColumnsFullRequest(
        ?string $escapedDatabase,
        ?string $escapedTable,
        ?string $escapedColumn
    ): array {
        $sqlWheres = [];
        $arrayKeys = [];

        // get columns information from information_schema
        if ($escapedDatabase !== null) {
            $sqlWheres[] = '`TABLE_SCHEMA` = \''
                . $escapedDatabase . '\' ';
        } else {
            $arrayKeys[] = 'TABLE_SCHEMA';
        }

        if ($escapedTable !== null) {
            $sqlWheres[] = '`TABLE_NAME` = \''
                . $escapedTable . '\' ';
        } else {
            $arrayKeys[] = 'TABLE_NAME';
        }

        if ($escapedColumn !== null) {
            $sqlWheres[] = '`COLUMN_NAME` = \''
                . $escapedColumn . '\' ';
        } else {
            $arrayKeys[] = 'COLUMN_NAME';
        }

        // for PMA bc:
        // `[SCHEMA_FIELD_NAME]` AS `[SHOW_FULL_COLUMNS_FIELD_NAME]`
        $sql = 'SELECT *,'
                    . ' `COLUMN_NAME`       AS `Field`,'
                    . ' `COLUMN_TYPE`       AS `Type`,'
                    . ' `COLLATION_NAME`    AS `Collation`,'
                    . ' `IS_NULLABLE`       AS `Null`,'
                    . ' `COLUMN_KEY`        AS `Key`,'
                    . ' `COLUMN_DEFAULT`    AS `Default`,'
                    . ' `EXTRA`             AS `Extra`,'
                    . ' `PRIVILEGES`        AS `Privileges`,'
                    . ' `COLUMN_COMMENT`    AS `Comment`'
               . ' FROM `information_schema`.`COLUMNS`';

        if (count($sqlWheres)) {
            $sql .= "\n" . ' WHERE ' . implode(' AND ', $sqlWheres);
        }

        return [$sql, $arrayKeys];
    }

    /**
     * Function to get sql query for renaming the index using SQL RENAME INDEX Syntax
     */
    public static function getSqlQueryForIndexRename(
        string $dbName,
        string $tableName,
        string $oldIndexName,
        string $newIndexName
    ): string {
        return sprintf(
            'ALTER TABLE %s.%s RENAME INDEX %s TO %s;',
            Util::backquote($dbName),
            Util::backquote($tableName),
            Util::backquote($oldIndexName),
            Util::backquote($newIndexName)
        );
    }

    /**
     * Function to get sql query to re-order the table
     */
    public static function getQueryForReorderingTable(
        string $table,
        string $orderField,
        ?string $order
    ): string {
        return 'ALTER TABLE '
            . Util::backquote($table)
            . ' ORDER BY '
            . Util::backquote($orderField)
            . ($order === 'desc' ? ' DESC;' : ' ASC;');
    }

    /**
     * Function to get sql query to partition the table
     *
     * @param string[] $partitionNames
     */
    public static function getQueryForPartitioningTable(
        string $table,
        string $partitionOperation,
        array $partitionNames
    ): string {
        $sql_query = 'ALTER TABLE '
            . Util::backquote($table) . ' '
            . $partitionOperation
            . ' PARTITION ';

        if ($partitionOperation === 'COALESCE') {
            return $sql_query . count($partitionNames);
        }

        return $sql_query . implode(', ', $partitionNames) . ';';
    }
}
