<?php

/**
 * @TODO write exception class for vault, add better error checking with meaningful errors.
 */

namespace apexl\Vault;

use apexl\Config\Configuration;
use apexl\Vault\interfaces\driver;
use apexl\Vault\operators\drivers\mysql;
use app\vendor\apexl\vault\src\Vault\exceptions\DriverException;
use app\vendor\apexl\vault\src\Vault\exceptions\VaultException;
use Exception;

/**
 * Class databaseConnection
 * PHP Class to manage all database connections and queries. Designed to be secure and efficient
 * @Author Toby New (toby@ubyk.co.uk)
 * @mixin driver
 */
class Vault
{
    private static ?Vault $instance;
    protected Configuration $config;
    /**
     * @var driver[]
     */
    protected array $vaults = [];
    protected array $manualConfig = [];
    protected string $defaultVault = 'default';
    protected bool $initialised = false;
    protected string $driver;
    protected ?int $loggedInUserId;

    /**
     * @throws Exception
     */
    private function __construct()
    {
        //reference the config singleton
        $this->config = Configuration::getInstance();
        //if we have the vault config set, then allow us to initialise it.
        if (isset($this->config->vault)) {
            //initiate vault classes.
            $this->initiateVaults();
        }
    }

    public static function getInstance(): Vault
    {
        if (!isset(self::$instance)) {
            self::$instance = new Vault();
        }

        return self::$instance;
    }

    /**
     * Loop over the provided database configurations and load the appropriate drivers.
     * This is a public method, so it can be invoked manually if config only becomes available after instantiation.
     * @throws Exception
     */
    public function initiateVaults(): void
    {
        if (!isset($this->config->vault)) {
            throw new Exception('"vault" key missing from loaded configuration');
        }
        if (isset($this->config->vault->databases) && !empty($this->config->vault->databases)) {
            foreach ($this->config->vault->databases as $dbName => $dbConfig) {
                $this->driver = isset($dbConfig->driver) && !empty($dbConfig->driver) ? "\\apexl\\Vault\\operators\\drivers\\" . $dbConfig->driver : "\\apexl\\Vault\\operators\\drivers\\mysql";
                $this->vaults[$dbName] = $this->instantiateDriver(new $this->driver($dbConfig));
            }
        }
        $this->initialised = true;
    }

    /**
     * This method allows us to force the provided driver to use the correct interface
     * We also use it to set the logged in userId, if known at this point
     */
    private function instantiateDriver(driver $driver): driver
    {
        if (isset($this->loggedInUserId)) {
            $driver->setLoggedInUserId($this->loggedInUserId);
        }

        return $driver;
    }

    public function setLoggedInUserId(int $userId): void
    {
        $this->loggedInUserId = $userId;
        foreach ($this->vaults as $vaultName => $driver) {
            $this->vaults[$vaultName]->setLoggedInUserId($userId);
        }
    }

    //add setters for manual config.

    public static function reset(): void
    {
        self::$instance = null;
    }

    /**
     * @TODO maybe combine some of the other methods that make this up into one place?
     */
    public function testConnection($dbName = null): bool
    {
        $testDb = $dbName ?? $this->defaultVault;

        return $this->{$testDb}->testConnection();
    }

    /**
     * @throws Exception
     */
    public function initiateVault($vaultName): void
    {
        if (!isset($this->config->vault)) {
            throw new Exception('"vault" key missing from loaded configuration');
        }

        if (isset($this->config->vault->databases->{$vaultName})) {
            $dbConfig = $this->config->vault->databases->{$vaultName};

            $driverClass = mysql::class;

            if (!empty($dbConfig->driver)) {
                $driverClass = sprintf('\apexl\Vault\operators\drivers\%s', $dbConfig->driver);
            }

            $this->driver = $driverClass;

            $this->vaults[$vaultName] = $this->instantiateDriver(
                new $this->driver($this->config->vault->databases->{$vaultName})
            );
        }

        $this->initialised = true;
    }

    /**
     * @throws DriverException
     */
    public function setDriver(?string $driver = ''): Vault
    {
        $driverClass = mysql::class;

        if (!empty($driver)) {
            $driverClass = sprintf('\apexl\Vault\operators\drivers\%s', $driver);
        }

        if (!class_exists($driverClass)) {
            throw new DriverException(sprintf('Driver class \'%s\' does not exist', $driverClass));
        }

        $this->driver = $driver;
        $this->manualConfig['driver'] = $driver;

        return $this;
    }

    public function getDefaultVault(): string
    {
        return $this->defaultVault;
    }

    /**
     * @throws VaultException
     */
    public function setDefaultVault($name): void
    {
        if (!isset($this->vaults[$name])) {
            throw new VaultException(sprintf('%s missing from loaded configuration', $name));
        }

        $this->defaultVault = $name;
    }

    public function isInitialised(): bool
    {
        return $this->initialised;
    }

    public function __get($name)
    {
        return $this->vaults[$name] ?? false;
    }

    public function __call($name, $arguments)
    {
        if (isset($this->vaults[$this->defaultVault])) {
            return $this->vaults[$this->defaultVault]->$name(...$arguments);
        }

        return false;
    }

    public function __isset($name)
    {
        return isset($this->vaults[$name]);
    }

    public function __unset($name)
    {
        unset($this->vaults[$name]);
    }

    public function __destruct()
    {
        $this->vaults = [];
    }
}
