<?php

namespace Velis\App;

use Exception;
use IteratorAggregate;
use Phalcon\Config\Config as PhalconConfig;
use Traversable;
use Velis\Acl\Module;
use Velis\App;
use Velis\Arrays;
use Velis\Config\ConfigInterface;
use Velis\Config\Ini as IniConfig;
use Velis\Crypt\Crypt;
use Velis\Filter;

/**
 * Application config
 * @author Olek Procki <olo@velis.pl>
 */
class Config implements ConfigInterface, IteratorAggregate
{
    public const string CREDENTIALS_ENCRYPTED_FILENAME = 'shared-credentials';
    public const string CREDENTIALS_DECRYPTED_FILENAME = 'shared-credentials.local.php';

    /**
     * Additional modules enabled
     * @var array<string, string>
     */
    protected static array $modulesEnabled = [];
    private static bool $dbHasChanged = false;

    /**
     * @var PhalconConfig<string,mixed>
     */
    private PhalconConfig $wrapped;

    /**
     * @param array<string,mixed> $data
     */
    public function __construct(array $data)
    {
        $this->wrapped = new PhalconConfig($data);
    }

    /**
     * Loads config data
     * @throws Exception
     */
    public static function load(): Config
    {
        $localConfig = CONFIG_PATH . 'config.' . APP_ENV . '.local.php';
        $applicationConfig = CONFIG_PATH . 'application.ini';
        $serverConfig = CONFIG_PATH . 'server.ini';
        self::$dbHasChanged = false;

        if (
            !file_exists($localConfig)
            || !file_get_contents($localConfig)
            // This internal config file has its extension - it's declared on variable declaration, a few lines above.
            // @phpcs:ignore
            || (!include $localConfig)
            || filemtime($localConfig) < max(filemtime($applicationConfig), filemtime($serverConfig))
        ) {
            $application  = new IniConfig($applicationConfig, APP_ENV);
            $server       = new IniConfig($serverConfig, APP_ENV);

            $mergedConfigs = Arrays::merge(
                $application->toArray(),
                $server->toArray()
            );

            App::$config = (new self($mergedConfigs))->loadSecrets();

            if (file_exists($localConfig)) {
                // This internal config file has its extension - it's declared a few lines above.
                // @phpcs:ignore
                $localConfigs = include $localConfig;
                self::$dbHasChanged = !is_array($localConfigs) || self::hasDbChanged($localConfigs, $mergedConfigs);
            }

            file_put_contents($localConfig, '<?php return ' . var_export($mergedConfigs, true) . ';');
        } else {
            // This internal config file has its extension - it's declared a few lines above.
            // @phpcs:ignore
            App::$config = (new self(include $localConfig))->loadSecrets();
        }

        App::$config->set('crypt', ['appKey' => getenv('APP_KEY')]);

        if (isset(App::$config->settings->modules) && count(App::$config->settings->modules)) {
            self::$modulesEnabled = Arrays::toArray(App::$config->settings->modules);
        }

        return App::$config;
    }

    public function init(array $data = []): void
    {
        $this->wrapped->init($data);
    }

    public function set(string $element, $value): void
    {
        $this->wrapped->set($element, $value);
    }

    public function __set($element, $value): void
    {
        $this->wrapped->{$element} = $value;
    }

    public function offsetSet($offset, $value): void
    {
        $this->wrapped->set($offset, $value);
    }

    public function remove(string $element): void
    {
        $this->wrapped->remove($element);
    }

    public function __isset($name): bool
    {
        return isset($this->wrapped->{$name});
    }

    public function offsetExists($offset): bool
    {
        return isset($this->wrapped->{$offset});
    }

    public function toJson(?int $options = null): string
    {
        return $this->wrapped->toJson($options);
    }

    /**
     * @throws \Phalcon\Config\Exception
     */
    public function merge($toMerge): self
    {
        if ($toMerge instanceof self) {
            $toMerge = $toMerge->wrapped;
        }

        $merged = $this->wrapped->merge($toMerge);

        return new self($merged->toArray());
    }

    /**
     * {@inheritDoc}
     */
    public function get(string $key, $default = null, string $cast = null)
    {
        $data = $this->wrapped->get($key, $default, $cast);

        if ($data instanceof PhalconConfig) {
            $overriddenData = new self($data->toArray());
            $this->set($key, $overriddenData);

            return $overriddenData;
        }

        return $data;
    }

    /**
     * @return mixed|Config<string, mixed>
     */
    public function __get(string $element)
    {
        return $this->get($element);
    }

    /**
     * @return mixed|Config<string, mixed>
     */
    #[\ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return $this->wrapped->toArray();
    }

    /**
     * @param string $name
     */
    public function __unset($name): void
    {
        unset($this->wrapped->{$name});
    }

    /**
     * @param string $offset
     */
    public function offsetUnset($offset): void
    {
        unset($this->wrapped->{$offset});
    }

    public function has(string $element): bool
    {
        return $this->wrapped->has($element);
    }

    public function count(): int
    {
        return $this->wrapped->count();
    }

    /**
     * Returns enabled application modules
     * @return array<string, string>
     */
    public function getEnabledModules(): array
    {
        return self::$modulesEnabled;
    }

    /**
     * Returns true if $module is enabled
     */
    public function isModuleEnabled(string $module): bool
    {
        if (
            $this->anonymize->enabled &&
            in_array($module, Arrays::toArray($this->anonymize->excludedModules))
        ) {
            return false;
        }

        return in_array($module, self::$modulesEnabled);
    }

    /**
     * Adds enabled modules to application
     */
    public function registerEnabledModules(): self
    {
        $modules = Module::listActive();

        foreach ($modules as $module) {
            if (Filter::filterAlpha($module->id())) {
                self::$modulesEnabled[$module->id()] = $module->id();
            }
        }

        return $this;
    }

    /**
     * Appends config settings
     *
     * @param array<string, mixed> $config
     * @return Config<string, mixed>
     * @throws \Phalcon\Config\Exception
     */
    public function append(array $config): Config
    {
        $settings = [];

        foreach ($config as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $key2 => $value2) {
                    $settings[lcfirst($key)][lcfirst($key2)] = $value2;
                }
            } else {
                $settings[lcfirst($key)] = $value;
            }
        }

        $settings = new self($settings);
        App::$config = $settings->merge($this);

        if (App::$config->modules) {
            $modules = [];
            foreach (Arrays::toArray(App::$config->modules) as $key => $value) {
                if ($value) {
                    $modules[$key] = ucfirst($key);
                }
            }
            self::$modulesEnabled = Arrays::merge($modules, self::$modulesEnabled);
        }

        return App::$config;
    }

    /**
     * Checks if application supports multiple time zones
     */
    public function hasTimezoneSupport(): bool
    {
        return ($this->settings->multipleTimezoneSupport || $this->multipleTimezoneSupportApp);
    }

    /**
     * @param array<string, mixed> $currentConfigs
     * @param array<string, mixed> $newConfigs
     */
    private static function hasDbChanged(array $currentConfigs, array $newConfigs): bool
    {
        return ($currentConfigs['db']['host'] != $newConfigs['db']['host'] ||
            $currentConfigs['db']['database'] != $newConfigs['db']['database'] ||
            $currentConfigs['db']['port'] != $newConfigs['db']['port']
        );
    }

    public static function getDbHasChanged(): bool
    {
        return self::$dbHasChanged;
    }

    public function getIterator(): Traversable
    {
        return $this->wrapped->getIterator();
    }

    /**
     * @throws \Phalcon\Config\Exception
     */
    public function loadSecrets(): self
    {
        // phpcs:disable PHPCS_SecurityAudit.BadFunctions.FilesystemFunctions.WarnFilesystem
        $secretsPath = CONFIG_PATH . DIRECTORY_SEPARATOR . self::CREDENTIALS_ENCRYPTED_FILENAME;
        if (!file_exists($secretsPath)) {
            return $this;
        }

        $tempPath = DATA_PATH . 'temp/shared-credentials.php';
        if (
            !file_exists($tempPath)
            || filemtime($tempPath) < filemtime($secretsPath)
        ) {
            $rawContent = file_get_contents($secretsPath);
            $decrypted = (new Crypt(base64_decode(getenv('VELIS_PASS'))))->decrypt($rawContent);
            file_put_contents($tempPath, $decrypted);
        }
        // phpcs:enable

        // This internal config file has .php extension, it's declared a few lines above.
        // phpcs:ignore PHPCS_SecurityAudit.Misc.IncludeMismatch.ErrMiscIncludeMismatchNoExt
        $settings = include($tempPath);

        if (!is_array($settings)) {
            $settings = [];
        }

        $settings = new self($settings);

        return $settings->merge($this);
    }
}
