<?php

declare(strict_types=1);

/**
 * Configuration instance which reads configuration from environment variables,
 * or $options[env_config] in Mage::run/Mage::init.
 *
 * ## General configuration
 *
 * * `MAGENTO_CRYPT_KEY`: Encryption key used by Magento
 *     (`config/global/crypt/key`).
 * * `MAGENTO_CACHE_BACKEND`: Cache type to use for Magento
 *     (`config/global/cache`).
 * * `APP_ENV`: If set to `production` or `staging` it will disable
 *     symlink-support (`default/dev/template/allow_symlink`).
 *
 * ## Database:
 *
 * Database configuration is specified as one or more Database Source Names,
 * all prefixed by `RESOURCE_`. The following characters will be lowercased
 * when added to configuration, eg. `RESOURCE_MY_DATA` will be made available
 * as `my_data` connection.
 *
 * Extra parameters can be added as query-string parameters:
 *
 *  * `initStatements`: The initial statement to run after connecting.
 *  * `profiler`: If the database profiler should be enabled.
 *
 * Example:
 *
 * ```
 * RESOURCE_DEFAULT_SETUP=mysql://admin:foobar@thedatabase:1234/mydatabase?profiler=1
 * RESOURCE_CUSTOM_WRITE=mysql://root@otherdatabase:1234/insecuredatabase?initStatements=SET NAMES latin1
 * ```
 *
 * Database connections can also refer to other connections by name through using
 * the protocol "use":
 *
 * ```
 * RESOURCE_MY_RESOURCE=use://default_setup
 * RESOURCE_MY_WRITE=use://my_resource
 * ```
 *
 * ## Required keys
 *
 * * `MAGENTO_CRYPT_KEY`
 * * `RESOURCE_DEFAULT_SETUP`
 *
 * Usage:
 *
 *   Mage::run("store_code", "store", [
 *       "config_model" => "Awardit_Magento_EnvConfig",
 *       // Use _SERVER if FastCGI environment variables are to be used
 *       "env_config" => $_SERVER,
 *   ]);
 *
 * @psalm-type ElementValue string|Array<string, string|array>
 */
class Awardit_Magento_EnvConfig extends Mage_Core_Model_Config {
    final const ENV = "APP_ENV";
    final const ENV_CACHE = "MAGENTO_CACHE";
    final const ENV_CRYPT_KEY = "MAGENTO_CRYPT_KEY";
    final const ENV_RESOURCE_DEFAULT_SETUP = "RESOURCE_DEFAULT_SETUP";
    final const ENV_SESSION = "MAGENTO_SESSION";
    final const REQUIRED_ENV = [
        self::ENV_CRYPT_KEY,
        self::ENV_RESOURCE_DEFAULT_SETUP,
    ];
    final const REDIS_CACHE_QUERY_DEFAULTS = [
        "lifetime" => "600", // s
    ];
    final const REDIS_CACHE_QUERY_TYPES = [
        "lifetime" => "int",
    ];

    private Varien_Simplexml_Config $localConfig;

    /**
     * @param mixed $data
     */
    public function __construct($data = null) {
        $env = null;

        if(is_array($data) && array_key_exists("env_config", $data) && is_array($data["env_config"])) {
            $env = $data["env_config"];

            // We don't want to propagate this info to the rest of the application
            unset($data["env_config"]);
        }

        $this->localConfig = Varien_Simplexml_Config::fromArray($this->createLocalConfig($env ?: getenv()));

        // parent::__construct($data);
        $this->setCacheId('config_global');
        // Use a config option without any local writable files
        $this->_options = new Awardit_Magento_EnvConfig_Options(is_array($data) ? $data : []);
        $this->_prototype = new Mage_Core_Model_Config_Base();

        // Grandparent
        Mage_Core_Model_Config_Base::__construct();
    }

    /**
     * @param Array<string, string> $env
     * @return Array<string, ElementValue>
     */
    protected function createLocalConfig(array $env): array {
        foreach(self::REQUIRED_ENV as $envName) {
            if( ! array_key_exists($envName, $env)) {
                throw new RuntimeException(sprintf(
                    "%s: Required environment variable '%s' is not set.",
                    __CLASS__,
                    $envName
                ));
            }
        }

        $global = [
            "install" => [
                // TODO: Configurable?
                "date" => gmdate("Y-m-d H:i:s", filemtime(__FILE__)),
            ],
            "crypt" => [
                "key" => $env[self::ENV_CRYPT_KEY],
            ],
            "cache" => $this->createCache($env),
            "disable_local_modules" => "false",
            "resources" => $this->createResources($env),
            // Replace core cache with a slimmer version with more freedom
            "models" => [
                "core" => [
                    "rewrite" => [
                        "cache" => "Awardit_Magento_Cache",
                    ],
                ],
            ],
        ];

        $sessionConfig = $env[self::ENV_SESSION] ?? "files://".dirname(Mage::getRoot()).DIRECTORY_SEPARATOR."var".DIRECTORY_SEPARATOR."sessions";

        // We have to have special logic for files since it does not contain a hostname
        if(stripos($sessionConfig, "files://") === 0) {
            // local files
            $global["session_save"] = "files";
            $global["session_save_path"] = substr($sessionConfig, strlen("files://"));
        }
        else {
            $sessionDsn = parse_url($sessionConfig);

            if( ! $sessionDsn || ! array_key_exists("scheme", $sessionDsn)) {
                throw new RuntimeException(sprintf(
                    "%s: Malformed DSN in config-variable '%s'.",
                    __METHOD__,
                    Awardit_Magento_EnvConfig::ENV_SESSION
                ));
            }

            switch(strtolower($sessionDsn["scheme"])) {
            case "redis":
                $global["session_save"] = "user";
                $global["session_save_path"] = "Awardit_Magento_Session::initRedis";
                $global["session_save_config"] = $sessionConfig;
                break;
            default:
                throw new RuntimeException(sprintf(
                    "%s: Unknown session scheme '%s'",
                    $sessionDsn["scheme"]
                ));
            }
        }

        $data = [
            "global" => $global,
            // TODO: Allow disabling admin (not enough to just remove this, got to disable the router)
            // TODO: Disable everything BUT admin if we enable admin?
            "admin" => [
                "routers" => [
                    "adminhtml" => [
                        "args" => [
                            "frontName" => "administration",
                        ],
                    ],
                ],
            ],
        ];

        // Set default/dev/template/allow_symlink=1 if we are not production or staging
        if( ! in_array($env[self::ENV] ?? "", ["production", "staging"])) {
            $data["default"] = [
                "dev" => [
                    "template" => [
                        "allow_symlink" => "1",
                    ],
                ],
            ];
        }

        return $data;
    }

    /**
     * @param array{MAGENTO_CACHE?:string} $env
     */
    protected function createCache(array $env): array {
        $config = $env[self::ENV_CACHE] ?? "memory://_";
        $dsn = parse_url($config);

        if( ! $dsn || ! array_key_exists("scheme", $dsn)) {
            throw new RuntimeException(sprintf(
                "%s: Malformed DSN in config-variable '%s'.",
                __METHOD__,
                Awardit_Magento_EnvConfig::ENV_CACHE
            ));
        }

        switch(strtolower($dsn["scheme"])) {
        case "redis":
            try {
                return [
                    "backend" => Awardit_Magento_Cache_Redis::class,
                    "backend_options" => Awardit_Magento_Redis::parseDsn($config, self::REDIS_CACHE_QUERY_DEFAULTS),
                ];
            }
            catch(Exception $e) {
                throw new RuntimeException(sprintf("%s: %s", __METHOD__, $e->getMessage()), 0, $e);
            }

        case "memory":
            return [
                "backend" => Awardit_Magento_Cache_ProcessMemory::class,
                "backend_options" => [],
            ];
        default:
            throw new RuntimeException(sprintf(
                "%s: Unknown cache scheme '%s'",
                __METHOD__,
                $dsn["scheme"]
            ));
        }
    }

    /**
     * @param Array<string, string> $env
     */
    protected function createResources(array $env): array {
        $resources = [
            "db" => [
                "table_prefix" => "",
            ],
        ];

        foreach($env as $key => $value) {
            if(strpos($key, "RESOURCE_") === 0) {
                $query = [];
                $name = strtolower(substr($key, strlen("RESOURCE_")));
                $dsn = parse_url($value);

                if( ! $dsn ||
                    ! array_key_exists("scheme", $dsn) ||
                    ! array_key_exists("host", $dsn)) {
                    throw new Exception(sprintf(
                        "%s: Malformed DSN in config-variable '%s'.",
                        __CLASS__,
                        $key
                    ));
                }

                if($dsn["scheme"] === "use") {
                    $resources[$name] = [
                        "connection" => [
                            "use" => strtolower(trim(rawurldecode($dsn["host"]), " \t/")),
                        ],
                    ];

                    continue;
                }

                if( ! array_key_exists("path", $dsn)) {
                    throw new Exception(sprintf(
                        "%s: Malformed DSN in config-variable '%s'.",
                        __CLASS__,
                        $key
                    ));
                }

                parse_str($dsn["query"] ?? "", $query);

                $host = rawurldecode($dsn["host"]);

                if(array_key_exists("port", $dsn)) {
                    $host .= ":".$dsn["port"];
                }

                $resources[$name] = [
                    "connection" => [
                        "active" => "1",
                        "model" => "mysql4",
                        "type" => "pdo_".strtolower($dsn["scheme"]),
                        "username" => rawurldecode($dsn["user"] ?? ""),
                        "password" => rawurldecode($dsn["pass"] ?? ""),
                        "host" => $host,
                        "dbname" => trim(rawurldecode($dsn["path"]), "/ "),
                        "initStatements" => rawurldecode($query["initStatements"] ?? "SET NAMES utf8mb4"),
                        "profiler" => $query["profiler"] ?? "0",
                    ],
                ];
            }
        }

        return $resources;
    }

    // Replaced to only load config.xml
    public function loadBase(): void {
        $this->_isLocalConfigLoaded = true;

        $this->loadFile(Mage::getBaseDir()."/app/etc/config.xml");
        $this->extend($this->localConfig);
    }

    // Replaced to load local.xml from in-memory
    public function loadModules() {
        Varien_Profiler::start('config/load-modules');
        $this->_loadDeclaredModules();

        $this->loadModulesConfiguration(['config.xml'], $this);
        // Override any module configuration (eg. default database connections)
        $this->extend($this->localConfig);

        $this->applyExtends();
        Varien_Profiler::stop('config/load-modules');
        return $this;
    }
}
