<?php

// SPDX-FileCopyrightText: 2019 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Module\Icingadb\Common;

use Icinga\Application\Config as AppConfig;
use Icinga\Data\ResourceFactory;
use Icinga\Module\Icingadb\Model\Schema;
use ipl\Sql\Adapter\Pgsql;
use ipl\Sql\Config as SqlConfig;
use ipl\Sql\Connection;
use ipl\Sql\Expression;
use ipl\Sql\QueryBuilder;
use ipl\Sql\Select;
use PDO;
use Pdo\Mysql;

/**
 * Singleton providing access to the Icinga DB and Redis
 */
final class Backend
{
    /** @var ?Connection */
    private static $db;

    /** @var ?int */
    private static $schemaVersion;

    /** @var ?IcingaRedis */
    private static $redis;

    /** @var ?bool Whether the current Icinga DB version supports dependencies */
    private static $supportsDependencies;

    /**
     * Set the connection to the Icinga DB
     *
     * Usually not required, as the connection is created on demand. Useful for testing.
     *
     * @param Connection $db
     *
     * @return void
     */
    public static function setDb(Connection $db): void
    {
        self::$db = $db;
    }

    /**
     * Get the connection to the Icinga DB
     *
     * @return Connection
     */
    public static function getDb(): Connection
    {
        if (self::$db === null) {
            $config = new SqlConfig(ResourceFactory::getResourceConfig(
                AppConfig::module('icingadb')->get('icingadb', 'resource')
            ));

            $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ];
            if ($config->db === 'mysql') {
                $config->options[Mysql::ATTR_INIT_COMMAND]
                    = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES"
                    . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'";
            }

            self::$db = new Connection($config);

            $adapter = self::$db->getAdapter();
            if ($adapter instanceof Pgsql) {
                $quoted = $adapter->quoteIdentifier('user');
                self::$db->getQueryBuilder()
                    ->on(QueryBuilder::ON_SELECT_ASSEMBLED, function (&$sql) use ($quoted) {
                        // user is a reserved key word in PostgreSQL, so we need to quote it.
                        // TODO(lippserd): This is pretty hacky,
                        // reconsider how to properly implement identifier quoting.
                        $sql = preg_replace('/user$/', sprintf(' %s', $quoted), $sql);
                        $sql = str_replace(
                            [' user ', ' user.', '(user.'],
                            [
                                sprintf(' %s ', $quoted),
                                sprintf(' %s.', $quoted),
                                sprintf('(%s.', $quoted),
                            ],
                            $sql
                        );
                    })
                    ->on(QueryBuilder::ON_ASSEMBLE_SELECT, function (Select $select) {
                        // For SELECT DISTINCT, all ORDER BY columns must appear in SELECT list.
                        if (! $select->getDistinct() || ! $select->hasOrderBy()) {
                            return;
                        }

                        $candidates = [];
                        foreach ($select->getOrderBy() as list($columnOrAlias, $_)) {
                            if ($columnOrAlias instanceof Expression) {
                                // Expressions can be and include anything,
                                // also columns that aren't already part of the SELECT list,
                                // so we're not trying to guess anything here.
                                // Such expressions must be in the SELECT list if necessary and
                                // referenced manually with an alias in ORDER BY.
                                continue;
                            }

                            $candidates[$columnOrAlias] = true;
                        }

                        foreach ($select->getColumns() as $alias => $column) {
                            if (is_int($alias)) {
                                if ($column instanceof Expression) {
                                    // This is the complement to the above consideration.
                                    // If it is an unaliased expression, ignore it.
                                    continue;
                                }
                            } else {
                                unset($candidates[$alias]);
                            }

                            if (! $column instanceof Expression) {
                                unset($candidates[$column]);
                            }
                        }

                        if (! empty($candidates)) {
                            $select->columns(array_keys($candidates));
                        }
                    });
            }
        }

        return self::$db;
    }

    /**
     * Get the schema version of the Icinga DB
     *
     * @return int
     */
    public static function getDbSchemaVersion(): int
    {
        if (self::$schemaVersion === null) {
            self::$schemaVersion = Schema::on(self::getDb())
                ->columns('version')
                ->first()
                ->version ?? 0;
        }

        return self::$schemaVersion;
    }

    /**
     * Set the connection to the Icinga Redis
     *
     * Usually not required, as the connection is created on demand. Useful for testing.
     *
     * @param IcingaRedis $redis
     *
     * @return void
     */
    public static function setRedis(IcingaRedis $redis): void
    {
        self::$redis = $redis;
    }

    /**
     * Get the connection to the Icinga Redis
     *
     * @return IcingaRedis
     */
    public static function getRedis(): IcingaRedis
    {
        if (self::$redis === null) {
            self::$redis = new IcingaRedis();
        }

        return self::$redis;
    }

    /**
     * Whether the current Icinga DB version supports dependencies
     *
     * @return bool
     */
    public static function supportsDependencies(): bool
    {
        if (self::$supportsDependencies === null) {
            if (self::getDb()->getAdapter() instanceof Pgsql) {
                self::$supportsDependencies = self::getDbSchemaVersion() >= 5;
            } else {
                self::$supportsDependencies = self::getDbSchemaVersion() >= 7;
            }
        }

        return self::$supportsDependencies;
    }
}
