<?php

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

namespace apexl\Vault;

use apexl\Config\ConfigurationInterface;
use apexl\Config\Exceptions\MissingConfigException;
use apexl\Vault\interfaces\driver;
use apexl\Vault\operators\drivers\mysql;
use app\vendor\apexl\vault\src\Vault\exceptions\VaultDriverException;
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
{
    /**
     * @var driver[]
     */
    protected array $vaults = [];
    protected array $manualConfig = [];
    protected string $defaultVault;
    protected bool $initialised = false;
    protected string $driver;

    /**
     * @throws Exception
     */
    public function __construct(private readonly ConfigurationInterface $config)
    {
        $this->defaultVault = $this->config->get('vault.default_vault');

        $this->initiateVaults();
    }

    /**
     * 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
    {
        foreach ($this->config->get('vault.databases', []) as $dbName => $dbConfig) {
            $this->initiateVault($dbName);
        }

        $this->initialised = true;
    }

    /**
     * @throws VaultException
     */
    public function initiateVault(string $vaultName): void
    {
        try {
            $dbConfig = $this->config->get(sprintf('vault.databases.%s', $vaultName));
        } catch (MissingConfigException $e) {
            throw new VaultException($e->getMessage(), $e->getCode(), $e);
        }

        // Accept both stdClass and array config shapes. Historically Vault
        // was fed stdClass; the apexl/config layer now returns nested
        // entries as plain arrays — without this guard the `->driver`
        // access silently returns null and the `?? 'mysql'` fallback
        // makes every non-default-shape vault entry quietly become a
        // mysql operator (caught 2026-05-24 when adding the first
        // mongodb-driver vault — see TC plans/vault-mongo.md Z1.c).
        $driver = (is_array($dbConfig)
            ? ($dbConfig['driver'] ?? null)
            : ($dbConfig->driver ?? null)
        ) ?? 'mysql';
        $driverClass = sprintf("\\apexl\\Vault\\operators\\drivers\\%s", $driver);

        $this->vaults[$vaultName] = new $driverClass((object) $dbConfig);

        $this->initialised = true;
    }

    /**
     * @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 VaultDriverException
     */
    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 VaultDriverException(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 __destruct()
    {
        $this->resetVaults();
    }

    public function resetVaults(): void
    {
        foreach ($this->vaults as $vaultName => $driver) {
            $this->__unset($vaultName);
        }
    }

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