<?php

/**
 * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
 * Atom feeds for websites that don't have one.
 *
 * For the full license information, please view the UNLICENSE file distributed
 * with this source code.
 *
 * @package Core
 * @license http://unlicense.org/ UNLICENSE
 * @link    https://github.com/rss-bridge/rss-bridge
 */

/**
 * Configuration module for RSS-Bridge.
 *
 * This class implements a configuration module for RSS-Bridge.
 */
final class Configuration
{
    private const VERSION = 'dev.2023-03-22';

    private static $config = [];

    private function __construct()
    {
    }

    /**
     * Verifies the current installation of RSS-Bridge and PHP.
     *
     * Returns an error message and aborts execution if the installation does
     * not satisfy the requirements of RSS-Bridge.
     *
     * @return void
     */
    public static function verifyInstallation()
    {
        if (version_compare(\PHP_VERSION, '7.4.0') === -1) {
            throw new \Exception('RSS-Bridge requires at least PHP version 7.4.0!');
        }

        $errors = [];

        // OpenSSL: https://www.php.net/manual/en/book.openssl.php
        if (!extension_loaded('openssl')) {
            $errors[] = 'openssl extension not loaded';
        }

        // libxml: https://www.php.net/manual/en/book.libxml.php
        if (!extension_loaded('libxml')) {
            $errors[] = 'libxml extension not loaded';
        }

        // Multibyte String (mbstring): https://www.php.net/manual/en/book.mbstring.php
        if (!extension_loaded('mbstring')) {
            $errors[] = 'mbstring extension not loaded';
        }

        // SimpleXML: https://www.php.net/manual/en/book.simplexml.php
        if (!extension_loaded('simplexml')) {
            $errors[] = 'simplexml extension not loaded';
        }

        // Client URL Library (curl): https://www.php.net/manual/en/book.curl.php
        // Allow RSS-Bridge to run without curl module in CLI mode without root certificates
        if (!extension_loaded('curl') && !(php_sapi_name() === 'cli' && empty(ini_get('curl.cainfo')))) {
            $errors[] = 'curl extension not loaded';
        }

        // JavaScript Object Notation (json): https://www.php.net/manual/en/book.json.php
        if (!extension_loaded('json')) {
            $errors[] = 'json extension not loaded';
        }

        if ($errors) {
            throw new \Exception(sprintf('Configuration error: %s', implode(', ', $errors)));
        }
    }

    public static function loadConfiguration(array $customConfig = [], array $env = [])
    {
        if (!file_exists(__DIR__ . '/../config.default.ini.php')) {
            throw new \Exception('The default configuration file is missing');
        }
        $config = parse_ini_file(__DIR__ . '/../config.default.ini.php', true, INI_SCANNER_TYPED);
        if (!$config) {
            throw new \Exception('Error parsing config');
        }
        foreach ($config as $header => $section) {
            foreach ($section as $key => $value) {
                self::setConfig($header, $key, $value);
            }
        }
        foreach ($customConfig as $header => $section) {
            foreach ($section as $key => $value) {
                self::setConfig($header, $key, $value);
            }
        }
        foreach ($env as $envName => $envValue) {
            $nameParts = explode('_', $envName);
            if ($nameParts[0] === 'RSSBRIDGE') {
                if (count($nameParts) < 3) {
                    // Invalid env name
                    continue;
                }
                $header = $nameParts[1];
                $key = $nameParts[2];
                if ($envValue === 'true' || $envValue === 'false') {
                    $envValue = filter_var($envValue, FILTER_VALIDATE_BOOLEAN);
                }
                self::setConfig($header, $key, $envValue);
            }
        }

        if (
            !is_string(self::getConfig('system', 'timezone'))
            || !in_array(self::getConfig('system', 'timezone'), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC))
        ) {
            self::throwConfigError('system', 'timezone');
        }

        if (!is_string(self::getConfig('proxy', 'url'))) {
            self::throwConfigError('proxy', 'url', 'Is not a valid string');
        }

        if (!is_bool(self::getConfig('proxy', 'by_bridge'))) {
            self::throwConfigError('proxy', 'by_bridge', 'Is not a valid Boolean');
        }

        if (!is_string(self::getConfig('proxy', 'name'))) {
            /** Name of the proxy server */
            self::throwConfigError('proxy', 'name', 'Is not a valid string');
        }

        if (!is_string(self::getConfig('cache', 'type'))) {
            self::throwConfigError('cache', 'type', 'Is not a valid string');
        }

        if (!is_bool(self::getConfig('cache', 'custom_timeout'))) {
            self::throwConfigError('cache', 'custom_timeout', 'Is not a valid Boolean');
        }

        if (!is_bool(self::getConfig('authentication', 'enable'))) {
            self::throwConfigError('authentication', 'enable', 'Is not a valid Boolean');
        }

        if (!is_string(self::getConfig('authentication', 'username'))) {
            self::throwConfigError('authentication', 'username', 'Is not a valid string');
        }

        if (!is_string(self::getConfig('authentication', 'password'))) {
            self::throwConfigError('authentication', 'password', 'Is not a valid string');
        }

        if (
            !empty(self::getConfig('admin', 'email'))
            && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL)
        ) {
            self::throwConfigError('admin', 'email', 'Is not a valid email address');
        }

        if (!is_bool(self::getConfig('admin', 'donations'))) {
            self::throwConfigError('admin', 'donations', 'Is not a valid Boolean');
        }

        if (!is_string(self::getConfig('error', 'output'))) {
            self::throwConfigError('error', 'output', 'Is not a valid String');
        }

        if (
            !is_numeric(self::getConfig('error', 'report_limit'))
            || self::getConfig('error', 'report_limit') < 1
        ) {
            self::throwConfigError('admin', 'report_limit', 'Value is invalid');
        }
    }

    public static function getConfig(string $section, string $key)
    {
        return self::$config[strtolower($section)][strtolower($key)] ?? null;
    }

    private static function setConfig(string $section, string $key, $value): void
    {
        self::$config[strtolower($section)][strtolower($key)] = $value;
    }

    public static function getVersion()
    {
        $headFile = __DIR__ . '/../.git/HEAD';

        if (@is_readable($headFile)) {
            $revisionHashFile = '.git/' . substr(file_get_contents($headFile), 5, -1);
            $parts = explode('/', $revisionHashFile);

            if (isset($parts[3])) {
                $branchName = $parts[3];
                if (file_exists($revisionHashFile)) {
                    return sprintf('%s (git.%s.%s)', self::VERSION, $branchName, substr(file_get_contents($revisionHashFile), 0, 7));
                }
            }
        }
        return self::VERSION;
    }

    private static function throwConfigError($section, $key, $message = '')
    {
        throw new \Exception("Config [$section] => [$key] is invalid. $message");
    }
}