<?php
/**
 * Created by PhpStorm.
 * User: Exodus
 * Date: 28.05.2016
 * Time: 16:05
 */

namespace Exodus4D\Pathfinder\Lib;

use Exodus4D\Pathfinder\Lib\Db\Pool;
use Exodus4D\Pathfinder\Lib\Api\CcpClient;
use Exodus4D\Pathfinder\Lib\Api\GitHubClient;
use Exodus4D\Pathfinder\Lib\Api\SsoClient;
use Exodus4D\Pathfinder\Lib\Api\EveScoutClient;
use Exodus4D\Pathfinder\Lib\Socket\AbstractSocket;
use Exodus4D\Pathfinder\Lib\Socket\SocketInterface;
use Exodus4D\Pathfinder\Lib\Socket\TcpSocket;

class Config extends \Prefab {

    /**
     * prefix for custom Pathfinder env vars
     */
    const PREFIX_KEY                                = 'PF';

    /**
     * delimiter for custom Pathfinder env vars
     */
    const ARRAY_DELIMITER                           = '-';

    /**
     * Hive key for all Pathfinder config vars (*.ini files)
     */
    const HIVE_KEY_PATHFINDER                       = 'PATHFINDER';

    /**
     * Hive key for all environment config vars (*.ini files)
     */
    const HIVE_KEY_ENVIRONMENT                      = 'ENVIRONMENT';

    /**
     * Hive key for custom plugins (js map modules)
     */
    const HIVE_KEY_PLUGIN                           = 'PLUGIN';

    /**
     * Hive key for Socket validation check
     */
    const CACHE_KEY_SOCKET_VALID                    = 'CACHED_SOCKET_VALID';

    /**
     * Cache time for Socket validation check
     */
    const CACHE_TTL_SOCKET_VALID                    = 60;

    // ================================================================================================================
    // Redis
    // ================================================================================================================

    /**
     * Redis connect timeout (seconds)
     */
    const REDIS_OPT_TIMEOUT                         = 2;

    /**
     * Redis read timeout (seconds)
     */
    const REDIS_OPT_READ_TIMEOUT                    = 10;

    /**
     * redis retry interval (milliseconds)
     */
    const REDIS_OPT_RETRY_INTERVAL                  = 200;

    // ================================================================================================================
    // EVE downtime
    // ================================================================================================================

    /**
     * SSO downtime length (estimation), minutes
     */
    const DOWNTIME_LENGTH                           = 8;

    /**
     * SSO downtime buffer length extends downtime length, minutes
     */
    const DOWNTIME_BUFFER                           = 1;

    // ================================================================================================================
    // ESI API id´s
    // ================================================================================================================

    /**
     * ESI categoryId or 'Structure's
     */
    const ESI_CATEGORY_STRUCTURE_ID                 = 65;

    /**
     * ESI categoryId or 'Ship's
     */
    const ESI_CATEGORY_SHIP_ID                      = 6;

    /**
     * ESI groupId or 'Wormhole's
     */
    const ESI_GROUP_WORMHOLE_ID                     = 988;

    /**
     * ESI dogmaAttributeId for 'scanWormholeStrength's
     */
    const ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID     = 1908;

    /**
     * error message for missing Composer dependency class
     */
    const ERROR_CLASS_NOT_EXISTS_COMPOSER           = 'Class "%s" not found. → Check installed Composer packages';

    /**
     * error message for missing Composer dependency method
     */
    const ERROR_METHOD_NOT_EXISTS_COMPOSER          = 'Method "%s()" not found in class "%s". → Check installed Composer packages';


    /**
     * environment config keys that should be parsed as array
     * -> use "," as delimiter in config files/data
     */
    const ARRAY_KEYS                                = ['CCP_ESI_SCOPES', 'CCP_ESI_SCOPES_ADMIN'];

    /**
     * custom HTTP status codes
     */
    const
        HTTP_422 = 'Unprocessable Entity';

    /**
     * all environment data
     * @var array
     */
    private $serverConfigData                       = [];

    /**
     * Config constructor.
     * @param \Base $f3
     */
    public function __construct(\Base $f3){
        // set server data
        // -> CGI params (Nginx)
        // -> .htaccess (Apache)
        $this->setServerData();
        // set environment data
        $this->setAllEnvironmentData($f3);
        // set hive configuration variables
        // -> overwrites default configuration
        $this->setHiveVariables($f3);

        // set global getter for \DateTimeZone
        $f3->set('getTimeZone', function() use ($f3) : \DateTimeZone {
            return new \DateTimeZone( $f3->get('TZ') );
        });

        // set global getter for new \DateTime
        $f3->set('getDateTime', function(string $time = 'now', ?\DateTimeZone $timeZone = null) use ($f3) : \DateTime {
            $timeZone = $timeZone ? : $f3->get('getTimeZone')();
           return new \DateTime($time, $timeZone);
        });

        // database connection pool -----------------------------------------------------------------------------------
        $f3->set(Pool::POOL_NAME, Pool::instance(
            function(string $alias) use ($f3) : array {
                // get DB config by alias for new connections
                return self::getDatabaseConfig($f3, $alias);
            },
            function(string $schema) use ($f3) : array {
                // get DB requirement vars from requirements.ini
                return self::getRequiredDbVars($f3, $schema);
            }
        ));

        // lazy init Web Api clients ----------------------------------------------------------------------------------
        $f3->set(SsoClient::CLIENT_NAME, SsoClient::instance());
        $f3->set(CcpClient::CLIENT_NAME, CcpClient::instance());
        $f3->set(GitHubClient::CLIENT_NAME, GitHubClient::instance());
        $f3->set(EveScoutClient::CLIENT_NAME, EveScoutClient::instance());

        // Socket connectors ------------------------------------------------------------------------------------------
        $f3->set(TcpSocket::SOCKET_NAME, function(array $options = ['timeout' => 1]) : SocketInterface {
            return AbstractSocket::factory(TcpSocket::class, self::getSocketUri(), $options);
        });
    }

    /**
     * get environment configuration data
     * @param \Base $f3
     * @return array|null
     */
    protected function getAllEnvironmentData(\Base $f3){
        if(!$f3->exists(self::HIVE_KEY_ENVIRONMENT, $environmentData)){
            $environmentData =  $this->setAllEnvironmentData($f3);
        }

        return $environmentData;
    }

    /**
     * set/overwrite some global framework variables original set in config.ini
     * -> can be  overwritten in environments.ini OR ENV-Vars
     * -> see: https://github.com/exodus4d/pathfinder/issues/175
     * that depend on environment settings
     * @param \Base $f3
     */
    protected function setHiveVariables(\Base $f3){
        // hive keys that can be overwritten
        $hiveKeys = ['BASE', 'URL', 'DEBUG', 'CACHE'];

        foreach($hiveKeys as $key){
            if( !is_null( $var = self::getEnvironmentData($key)) ){
                $f3->set($key,$var);
            }
        }
    }

    /**
     * set all environment configuration data
     * @param \Base $f3
     * @return array|mixed|null
     */
    protected function setAllEnvironmentData(\Base $f3){
        $environmentData = null;

        if( !empty($this->serverConfigData['ENV']) ){
            // get environment config from $_SERVER data
            $environmentData = (array)$this->serverConfigData['ENV'];

            // some environment variables should be parsed as array
            array_walk($environmentData, function(&$item, $key){
                $item = (in_array($key, self::ARRAY_KEYS)) ? explode(',', $item) : $item;
            });

            $environmentData['ENVIRONMENT_CONFIG'] = 'PHP: environment variables';
        }else{
            // get environment data from *.ini file config
            $customConfDir = $f3->get('CONF');

            // check "custom" ini dir, of not found check default ini dir
            foreach($customConfDir as $type => $path){
                $envConfFile = $path . 'environment.ini';
                $f3->config($envConfFile, true);

                if(
                    $f3->exists(self::HIVE_KEY_ENVIRONMENT) &&
                    ($environment = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.SERVER')) &&
                    ($environmentData = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.' . $environment))
                ){
                    $environmentData['ENVIRONMENT_CONFIG'] = 'Config: ' . $envConfFile;
                    break;
                }
            }
        }

        if( !is_null($environmentData) ){
            ksort($environmentData);
            $f3->set(self::HIVE_KEY_ENVIRONMENT, $environmentData);
        }

        return $environmentData;
    }

    /**
     * get/extract all server data passed to PHP
     * this can be done by either:
     * OS Environment variables:
     *  -> add to /etc/environment
     * OR:
     * Nginx (server config):
     * -> FastCGI syntax
     *      fastcgi_param PF-ENV-DEBUG 3;
     */
    protected function setServerData(){
        $data = [];
        foreach($_SERVER as $key => $value){
            if(strpos($key, self::PREFIX_KEY . self::ARRAY_DELIMITER) === 0){
                $path = explode( self::ARRAY_DELIMITER, $key);
                // remove prefix
                array_shift($path);

                $tmp = &$data;
                foreach($path as $segment){
                    $tmp[$segment] = (array)$tmp[$segment];
                    $tmp = &$tmp[$segment];
                }

                // type cast values
                // (e.g. '1.2' => (float); '4' => (int),...)
                $tmp = is_numeric($value) ? $value + 0 : $value;
            }
        }

        $this->serverConfigData = $data;
    }

    /**
     * get a environment variable by hive key
     * @param $key
     * @return string|null
     */
    static function getEnvironmentData($key){
        $hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key;
        \Base::instance()->exists($hiveKey, $data);
        return $data;
    }

    /**
     * get database config values
     * @param \Base $f3
     * @param string $alias
     * @return array
     */
    static function getDatabaseConfig(\Base $f3, string $alias) : array {
        $alias = strtoupper($alias);

        $config = [
            'ALIAS'     => $alias,
            'SCHEME'    => 'mysql',
            'HOST'      => 'localhost',
            'PORT'      => 3306,
            'SOCKET'    => null,
            'NAME'      => self::getEnvironmentData('DB_' . $alias . '_NAME'),
            'USER'      => self::getEnvironmentData('DB_' . $alias . '_USER'),
            'PASS'      => self::getEnvironmentData('DB_' . $alias . '_PASS')
        ];

        $pdoReg = '/^(?<SCHEME>[[:alpha:]]+):((host=(?<HOST>[a-zA-Z0-9-_\.]*))|(unix_socket=(?<SOCKET>[a-zA-Z0-9\/]*\.sock)))((;dbname=(?<NAME>\w*))|(;port=(?<PORT>\d*))){0,2}/';
        if(preg_match($pdoReg, self::getEnvironmentData('DB_' . $alias . '_DNS'), $matches)){
            // remove unnamed matches
            $matches = array_intersect_key($matches, $config);
            // remove empty matches
            $matches = array_filter($matches);
            // merge matches with default config
            $config = array_merge($config, $matches);
        }

        // connect options --------------------------------------------------------------------------------------------
        $options = [
            \PDO::ATTR_ERRMODE          => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_TIMEOUT          => $f3->get('REQUIREMENTS.MYSQL.PDO_TIMEOUT')
        ];

        if($config['SCHEME'] == 'mysql'){
            $options[\PDO::MYSQL_ATTR_COMPRESS]     = true;
            $options[\PDO::MYSQL_ATTR_INIT_COMMAND] = implode(',', [
                "SET NAMES " . self::getRequiredDbVars($f3, $config['SCHEME'])['CHARACTER_SET_CONNECTION'] . " COLLATE " . self::getRequiredDbVars($f3, $config['SCHEME'])['COLLATION_CONNECTION'],
                "@@session.time_zone = '+00:00'",
                "@@session.default_storage_engine = " . self::getRequiredDbVars($f3, $config['SCHEME'])['DEFAULT_STORAGE_ENGINE']
            ]);
        }

        if(self::getPathfinderData('experiments.persistent_db_connections')){
            $options[\PDO::ATTR_PERSISTENT] = true;
        }

        $config['OPTIONS'] = $options;

        return $config;
    }

    /**
     * get required MySQL variables from requirements.ini
     * @param \Base $f3
     * @param string $schema
     * @return array
     */
    static function getRequiredDbVars(\Base $f3, string $schema) : array {
        return $f3->exists('REQUIREMENTS[' . strtoupper($schema) . '][VARS]', $vars) ? $vars : [];
    }

    /**
     * get SMTP config values
     * @return \stdClass
     */
    static function getSMTPConfig() : \stdClass{
        $config             = new \stdClass();
        $config->host       = self::getEnvironmentData('SMTP_HOST');
        $config->port       = self::getEnvironmentData('SMTP_PORT');
        $config->scheme     = self::getEnvironmentData('SMTP_SCHEME');
        $config->username   = self::getEnvironmentData('SMTP_USER');
        $config->password   = self::getEnvironmentData('SMTP_PASS');
        $config->from       = [
            self::getEnvironmentData('SMTP_FROM') => self::getPathfinderData('name')
        ];
        return $config;
    }

    /**
     * validates an SMTP config
     * @param \stdClass $config
     * @return bool
     */
    static function isValidSMTPConfig(\stdClass $config) : bool {
        // validate email from either an configured array or plain string
        $validateMailConfig = function($mailConf = null) : bool {
            $email = null;
            if(is_array($mailConf)){
                reset($mailConf);
                $email = key($mailConf);
            }elseif(is_string($mailConf)){
                $email = $mailConf;
            }
            return \Audit::instance()->email($email);
        };

        return (
            !empty($config->host) &&
            !empty($config->username) &&
            $validateMailConfig($config->from) &&
            $validateMailConfig($config->to)
        );
    }

    /**
     * get email for notifications by hive key
     * @param $key
     * @return mixed
     */
    static function getNotificationMail($key){
        return self::getPathfinderData('notification' . ($key ? '.' . $key : ''));
    }

    /**
     * get map default config values for map types (private/corp/ally)
     * -> read from pathfinder.ini
     * @param string $mapType
     * @return mixed
     */
    static function getMapsDefaultConfig($mapType = ''){
        if( $mapConfig = self::getPathfinderData('map' . ($mapType ? '.' . $mapType : '')) ){
            $mapConfig = Util::arrayChangeKeyCaseRecursive($mapConfig);
        }

        return $mapConfig;
    }

    /**
     * get Plugin config from `plugin.ini`
     * @param string|null $key
     * @param bool $checkEnabled
     * @return array|null
     */
    static function getPluginConfig(?string $key, bool $checkEnabled = true) : ?array {
        $isEnabled = $checkEnabled ?
            filter_var(\Base::instance()->get(
                self::HIVE_KEY_PLUGIN . '.' . strtoupper($key) . '_ENABLED'),
                FILTER_VALIDATE_BOOLEAN
            ) :
            true;

        $data = null;
        if($isEnabled){
            $hiveKey = self::HIVE_KEY_PLUGIN . '.' . strtoupper($key);
            $data = (array)\Base::instance()->get($hiveKey);
        }
        return $data;
    }

    /**
     * use this function to "validate" the socket connection.
     * The result will be CACHED for a few seconds!
     * This function is intended to pre-check a Socket connection if it MIGHT exists.
     * No data will be send to the Socket, this function just validates if a socket is available
     * -> see pingDomain()
     * @param string $uri
     * @return bool
     */
    static function validSocketConnect(string $uri) : bool{
        $valid = false;
        $f3 = \Base::instance();

        if( !$f3->exists(self::CACHE_KEY_SOCKET_VALID, $valid) ){
            if( $socketUrl = self::getSocketUri() ){
                // get socket URI parts -> not elegant...
                $domain = parse_url( $socketUrl, PHP_URL_SCHEME) . '://' . parse_url( $socketUrl, PHP_URL_HOST);
                $port = parse_url( $socketUrl, PHP_URL_PORT);
                // check connection -> get ms
                $status = self::pingDomain($domain, $port);
                if($status >= 0){
                    // connection OK
                    $valid = true;
                }else{
                    // connection error/timeout
                    $valid = false;
                }
            }else{
                // requirements check failed or URL not valid
                $valid = false;
            }

            $f3->set(self::CACHE_KEY_SOCKET_VALID, $valid, self::CACHE_TTL_SOCKET_VALID);
        }

        return $valid;
    }

    /**
     * get response time for a host in ms or -1 on error/timeout
     * @param string $domain
     * @param int $port
     * @param int $timeout
     * @return int
     */
    static function pingDomain(string $domain, int $port, $timeout = 1) : int {
        $startTime = microtime(true);
        $file      = @fsockopen ($domain, $port, $errno, $errstr, $timeout);
        $stopTime  = microtime(true);

        if (!$file){
            // Site is down
            $status = -1;
        }else {
            fclose($file);
            $status = ($stopTime - $startTime) * 1000;
            $status = floor($status);
        }
        return $status;
    }

    /**
     * get URI for TCP socket
     * @return bool|string
     */
    static function getSocketUri(){
        $uri = false;

        if(
            ( $ip = self::getEnvironmentData('SOCKET_HOST') ) &&
            ( $port = self::getEnvironmentData('SOCKET_PORT') )
        ){
            $uri = 'tcp://' . $ip . ':' . $port;
        }
        return $uri;
    }

    /**
     * @param string $key
     * @return null|mixed
     */
    static function getPathfinderData($key = ''){
        $hiveKey = self::HIVE_KEY_PATHFINDER . ($key ? '.' . strtoupper($key) : '');
        if( !\Base::instance()->exists($hiveKey, $data) ){
            $data = null;
        }
        return $data;
    }

    /**
     * get HTTP status by HTTP return code
     * -> either from F3 or from self::Config constants
     * @param int $code
     * @return string
     */
    static function getHttpStatusByCode(int $code) : string {
        if(empty($status = @constant('Base::HTTP_' .  $code))){
            $status = @constant('self::HTTP_' .  $code);
        }
        return $status;
    }

    /**
     * parse [D]ata [S]ource [N]ame string from *.ini into $conf parts
     * -> $dsn = redis=localhost:6379:2
     *    $conf = ['type' => 'redis', 'host' => 'localhost', 'port' => 6379, 'db' => 2]
     * -> some $conf values might be NULL if not found in $dsn!
     * -> some missing values become defaults
     * @param string $dsn
     * @param array|null $conf
     * @return bool
     */
    static function parseDSN(string $dsn, ?array &$conf = []) : bool {
        // reset reference
        if($matches = (bool)preg_match('/^(\w+)\h*=\h*(.+)/', trim($dsn), $parts)){
            $conf['type'] = $parts[1];
            if($conf['type'] == 'redis'){
                [$conf['host'], $conf['port'], $conf['db'], $conf['auth']] = explode(':', $parts[2]) + [1 => 6379, 2 => null, 3 => null];
            }elseif($conf['type'] == 'folder'){
                $conf['folder'] = $parts[2];
            }
            // int cast numeric values
            $conf = array_map(function($val){
                return is_numeric($val) ? intval($val) : $val;
            }, $conf);
        }
        return $matches;
    }

    /**
     * check if a given DateTime() is within downTime range: downtime + 10m
     * -> can be used for prevent logging errors during downTime
     * @param \DateTime|null $dateCheck
     * @return bool
     */
    static function inDownTimeRange(\DateTime $dateCheck = null) : bool {
        $inRange = false;
        // default daily downtime 00:00am
        $downTimeParts = [0, 0];
        if( !empty($downTime = (string)self::getEnvironmentData('CCP_SSO_DOWNTIME')) ){
            $parts = array_map('intval', explode(':', $downTime));
            if(count($parts) === 2){
                // well formatted DOWNTIME found in config files
                $downTimeParts = $parts;
            }
        }

        try{
            // downTime Range is 10m
            $downtimeLength = self::DOWNTIME_LENGTH + (2 * self::DOWNTIME_BUFFER);
            $timezone = \Base::instance()->get('getTimeZone')();

            // if not set -> use current time
            $dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck;
            $dateDowntimeStart = new \DateTime('now', $timezone);
            $dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]);
            $dateDowntimeStart->sub(new \DateInterval('PT' . self::DOWNTIME_BUFFER . 'M'));

            $dateDowntimeEnd = clone $dateDowntimeStart;
            $dateDowntimeEnd->add(new \DateInterval('PT' . $downtimeLength . 'M'));

            $dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd);
            $inRange = $dateRange->inRange($dateCheck);
        }catch(\Exception $e){
            $f3 = \Base::instance();
            $f3->error(500, $e->getMessage(), $e->getTrace());
        }

        return $inRange;
    }

    /**
     * format timeInterval in seconds into human readable string
     * @param int $seconds
     * @return string
     * @throws \Exception
     */
    static function formatTimeInterval(int $seconds = 0) : string {
        $dtF = new \DateTime('@0');
        $dtT = new \DateTime("@" . $seconds);
        $diff = $dtF->diff($dtT);

        $format = ($d = $diff->format('%d')) ? $d . 'd ' : '';
        $format .= ($h = $diff->format('%h')) ? $h . 'h ' : '';
        $format .= ($i = $diff->format('%i')) ? $i . 'm ' : '';
        $format .= ($s = $diff->format('%s')) ? $s . 's' : '';
        return $format;
    }

    /**
     * @param $fromExists
     * @param int $ttlMax
     * @return int
     */
    static function ttlLeft($fromExists, int $ttlMax) : int {
        $ttlMax = max($ttlMax, 0);
        if($fromExists){
            // == true || array
            if(is_array($fromExists)){
                return max(min((int)ceil(round(array_sum($fromExists) - microtime(true), 4)), $ttlMax), 0);
            }else{
                return 0;
            }
        }else{
            // == false
            return $ttlMax;
        }
    }

    /**
     * @param string|null $class
     * @return string
     */
    static function withNamespace(?string $class) : string {
        $path = [\Base::instance()->get('NAMESPACE')];
        if($class){
            $path[] = $class;
        }
        return implode('\\', $path);
    }
}