<?php

namespace IZON\Admin;

use Exception;
use ReflectionClass;

use function IZON\DI\get;
use function IZON\DI\factoryBean;
use IZON\DB\DaoFactoryBean;

use IZON\Logs\Logger;

use IZON\DB\Dao;

use IZON\MVC\Exceptions\Handlers\PageNotFoundExceptionHandler;
use IZON\MVC\Exceptions\Handlers\AccessForbiddenExceptionHandler;
use IZON\MVC\Exceptions\Handlers\InternalServerErrorExceptionHandler;
use IZON\Admin\MVC\Exceptions\Handlers\AdminPageNotFoundExceptionHandler;

use IZON\Admin\DI\ModulesAwareContainerBuilder;

use IZON\Admin\Services\AdminService;

use IZON\Admin\Domain\AdminModule;

use \IZON\MVC\Config as MVCConfig;

/**
 * slouzi k nacteni konfigurace i pro admin
 */
class Config extends MVCConfig {

    /**
     * do jakeho souboru se maji zapsat informce o nainstalovanych specificky modulech
     */
    const INSTALLED_MODULES_FILENAME = "installed-modules.php";
    
    /**
     * jak se jmenuje soubor obsahujici konfiguraci admina v modulu admina
     * 
     */
    const ADMIN_CONFIG_FILENAME = "admin-config.php";
    
    /**
     * jak se jmenuje spolecna konfigurace pro modul
     */
    const MODULE_COMMON_CONFIG_FILENAME = "config.php";
    
    /**
     * jak se jmenuje soubor obsahujici konfiguraci modulu pro praci v adminu
     */
    const MODULE_CONFIG_FILENAME = "module-config.php";
    
    /**
     * name of setting file module actions are kept
     */
    const MODULES_ACTION_CONFIG_FILE_NAME = "module-actions-config.php";
    
    /**
     * identigikator defaultni db
     */
    const DEFAULT_DB_CONNECTION_IDENTIFIER = 'db.connection';

    /**
     * identifier of all module locations
     */
    const MODULES_DIRS_IDENTIFIER = 'admin.modules.dirs';

    /**
     * defaultni servis admina
     * @var string 
     */
    const ADMIN_SERVICE_IDENTIFIER = 'izon.admin.services.adminService';

    /**
     * controller ktery slouzi k prihlaseni k aplikaci 
     * @var string 
     */
    const LOGIN_CONTROLLER_IDENTIFIER = 'izon.admin.controller.loginController';
    
    /**
     * controller ktery slouzi k zobrazeni uvodni obrazovky admina
     * @var string 
     */
    const DASHBOARD_CONTROLLER_IDENTIFIER = 'izon.admin.controller.dashboardController';
    
    /**
     * controller ktery slouzi k prihlaseni k aplikaci 
     * @var string
     */
    const LOGOUT_CONTROLLER_IDENTIFIER = 'izon.admin.controller.logoutController';
    
    /**
     * controller ktery slouzi k prihlaseni k aplikaci 
     * @var string
     */
    const SWITCH_COUNTRY_CONTROLLER_IDENTIFIER = 'izon.admin.controller.switchCountryController';
    
    /**
     * controller that switches bewtween supported locales for admin
     * @var string
     */
    const SWITCH_INTERFACE_LOCALE_CONTROLLER_IDENTIFIER = 'izon.admin.controller.switchInterfaceLocaleController';
    
    /**
     * controller ktery slouzi k prihlaseni k aplikaci 
     * @var string
     */
    const NOTIFICATIONS_CONTROLLER_IDENTIFIER = 'izon.admin.controller.notificationsController';
    
    /**
     * interceptor pro uvereni prihlaseneho uzivatele
     */
    const LOGGED_USER_CHECK_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.loggedUserCheckInterceptor';
    
    /**
     * interceptor that sets locale and getext based on user selected interface locale
     */
    const HANDLE_LOGGED_USER_INTERFACE_LOCALE_INTERCEPTOR = 'izon.admin.interceptors.handleLoggedUserInterfaceLocaleInterceptor';


    /**
     * interceptor pro nahrani informaci o prihlasenem uzivateli
     */
    const LOGGED_USER_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.loggedUserInterceptor';
    
    /**
     * interceptor pro nacitani akci modulu
     */
    const MODULES_ACTIONS_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.modulesActionsInterceptor';
    
    /**
     * interceptor pro nacitani zemi admina
     */
    const MODULES_SUPPORTED_COUNTRIES_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.supportedCountriesInterceptor';
    
    /**
     * interceptor pro nacitani akci modulu
     */
    const ADMIN_VERSION_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.loadAdminVersionInterceptor';
    
    /**
     * interceptor pro nacitaní max velikosti upload souboru
     */
    const ADMIN_MAX_FILE_SIZE_INTERCEPTOR_IDENTIFIER = 'izon.admin.interceptors.loadMaxFileSizeInterceprtor';
    
    /**
     * pouziva se pro pedani informace o nacitanych konfiguracich modulu
     * obsahuje typ modulu
     * @var string 
     */
    public static $moduleType;
    
    /**
     * pouziva se pro pedani informace o nacitanych konfiguracich modulu
     * obsahuje unikatni identifikator modulu
     * @var string 
     */
    public static $moduleIdentifier;
    
    /**
     * true pokud prave probiha nacitani modulu
     */
    public static $moduleLoading;
    
    /**
     * cesta k souboru se zakladni konfiguraci admina
     * @var string|null
     */
    protected $adminConfigFileDir = null;

    /**
     * servis pro administraci
     * @var AdminService|null
     */
    protected $adminService = null;

    /**
     * @var array obsahuje mista kam se maji dat moduly 
     */
    protected $librariesConfigs = [];

    /**
     * @var Logger|null
     */
    protected $log = null;
    
    
    public function __construct($baseDir, 
                                $containerBuilderClass = ModulesAwareContainerBuilder::class) {
        parent::__construct($baseDir, $containerBuilderClass);
        
        // konfigurace pro servisy a controllery admina 
        // TODO: zmenit, aby se bralo nejak inteligentneji
        $this->adminConfigFileDir = dirname(dirname(dirname(__DIR__))) ."/". self::ADMIN_CONFIG_FILENAME;
    }

    /**
     * TODO: upravit init, aby provdel inicializaci v podtride
     */
    public function init() {
        $containerBuilderClass = $this->containerBuilderClass;
        
        // pole, do ktereho se ukladaji definice
        $definitionsArray = [];
        // nastavuje zakladni cesty v aplikaci
        $pathDefinitions = [
            self::APP_DIR_IDENTIFIER => $this->baseDir,
            self::CONFIG_DIR_IDENTIFIER => \IZON\DI\MVC\appSubDir("/". $this->configDir),
            self::TMP_DIR_IDENTIFIER => \IZON\DI\MVC\appSubDir("/". $this->tmpDir),
            self::CACHE_DIR_IDENTIFIER => \IZON\DI\MVC\appSubDir("/". $this->cacheDir),
        ];
        $definitionsArray = array_merge($definitionsArray, $pathDefinitions);
        
        // vrati cestu ke konfigu
        $configDir = $this->getAbsoluteConfigDir();

        // nacte zakladni konfiguraci
        $rootConfig = $configDir ."/". $this->configFilePrefix .".php";
        if( !file_exists($rootConfig) ) {
            $message = "Root config $rootConfig doesn't exist.";
            throw new Exception($message);
        }
        // docasny konfig dokud se nevytvori di kontainer
        $tempConfig = require $rootConfig;

        // nacte specificke konfigurace podle MACHINE_ID
        if( getenv($this->machineIdKey) != '' ) {
            $machineinfo = explode('-', getenv($this->machineIdKey));
          
            for($i = 1; $i <= count($machineinfo); $i++) {
                $tmp = '';
                for ($j = 0; $j < $i; $j++) {
                    $tmp .= '-'. $machineinfo[$j];
                }
                $filePath = $configDir . "/" . $this->configFilePrefix . $tmp . '.php';
                if( file_exists($filePath) ) {
//                    $this->log->info( "Adding config ". $filePath);
                    $develConfigArray = require $filePath;
                    // slit s korenovou konfiguraci
                    $tempConfig = array_merge($tempConfig, $develConfigArray);
                }
            }
        }
        
        // pouziti cache pokud je povolena a vytvorena
        $useConfigCache = false;
        if( array_key_exists(self::USE_CONFIG_CACHE_IDENTIFIER, $tempConfig) ) { 
            $useConfigCache = $tempConfig[self::USE_CONFIG_CACHE_IDENTIFIER];
        }
        $cacheDir = $this->baseDir ."/". $this->cacheDir;
        $definitionsCacheFileName = $cacheDir ."/". $this->configCacheFileName;
        if( $useConfigCache  // ma se pouzit cache definic
            && file_exists($definitionsCacheFileName) // existuje soubor s cache
            && is_file($definitionsCacheFileName) ) { // a je to soubor
            // podivej se jestli mas cache a pokud ano tak ji pouzij

            $fileContent = file_get_contents($definitionsCacheFileName);
            $unserialized = unserialize($fileContent);
            $definitions = $unserialized["definitions"];
            $definitions[self::APP_DIR] = $this->baseDir; // set app dir as sent in current request

            // nastavuje devel server promennou
            self::$develserver = $definitions["develserver"];

            $cachedBuilder = new $containerBuilderClass();
            $cachedBuilder->addDefinitions($definitions);
            $container = $cachedBuilder->build();

            // inits array with definitions
            $this->definitionsArray = $definitions;
            
            self::$extConfig = $container;      
            // init logger
            $container->get(self::INIT_LOGGER_IDENTIFIER);
            // set logger
            $this->log = Logger::getLogger(self::class);

            $this->container = $container;

            return;
        }
        
        // nastavuje develserver podle konfigu
        self::$develserver = $tempConfig["develserver"];
        self::$extConfig = $tempConfig;
        
        // zjisti jestli existuje config admina
        $definitionsArray = array_merge($definitionsArray, $tempConfig);
        if( !file_exists($this->adminConfigFileDir)
            || !is_file($this->adminConfigFileDir) ) {
            throw new Exception("Neexistuje soubor ". $this->adminConfigFileDir ." s konfiguraci admina");
        }
        // nacita konfig admina
        $adminConfig = require $this->adminConfigFileDir;
        $definitionsArray = array_merge($definitionsArray, $adminConfig);
        
        
        // vytvorit builder pro spravu definic
        $builder = new $containerBuilderClass();
        $builder->addDefinitions($definitionsArray);
        // inits array with definitions
        $this->definitionsArray = $definitionsArray;
        
        // locations of modules
        $this->modulesLocations = require $this->getAbsoluteConfigDir() ."/". self::INSTALLED_MODULES_FILENAME;
        $modulesDirs = [];
        foreach($this->modulesLocations as $moduleType => $moduleLocation) {
            $modulesDirs[$moduleType] = $moduleLocation["path"];
        }
        // add them to config
        $modulesDirs = [self::MODULES_DIRS_IDENTIFIER =>  $modulesDirs];
        $builder->addDefinitions($modulesDirs);
        // inits array with definitions
        $definitionsArray = array_merge($definitionsArray, $modulesDirs);
        
        // vytvari include daa
        // nacte vsechny daa aplikacce
        // nacita daa modulu
        foreach($this->modulesLocations as $moduleLocation) {
            if( isset($moduleLocation['daoDirs']) ) {
                foreach( $moduleLocation['daoDirs'] as $daoDir) {
                    // NOTICE: mabe use better path creation or content of self::INSTALLED_MODULES_FILENAME
                    $daoDir = $this->getBaseDir() ."/vendor/". $daoDir;
                    foreach( glob($daoDir ."/*Dao.php") as $file ) {
                        if (in_array($file, array('.', '..'))) {
                            continue;
                        }
                        require_once $file;
                    }
                }
            }
        }
        $appSrcDir = $this->getBaseDir() ."/app/src";
        // prochazet do hloubky 1 a 2
        $daoFiles = array_merge(glob($appSrcDir ."/*/*Dao.php"), glob($appSrcDir ."/*/*/*Dao.php"));
        foreach($daoFiles  as $file) {
            if (in_array($file, array('.', '..'))) {
                continue;
            }
            require_once $file;
        }
        // vytvori includovaci array
        $daoInterfacesArray = [];
        foreach(get_declared_interfaces() as $interface) {
            $reflector = new ReflectionClass($interface);
            if( $reflector->isSubclassOf(Dao::class) ) {
                $daoClass = $interface;
                $daoInterfacesArray[$daoClass] = factoryBean(DaoFactoryBean::class)
                        ->constructor(get(self::DEFAULT_DB_CONNECTION_IDENTIFIER), $daoClass);
            }
        }
        $builder->addDefinitions($daoInterfacesArray);
        $definitionsArray = array_merge($definitionsArray, $daoInterfacesArray);
        
        // docasne vytvori container, aby mohl vytahnout servis pro praci s adminem
        $this->container = $builder->build();
        if( !$this->container->has(self::ADMIN_SERVICE_IDENTIFIER) ) {
            throw new Exception("Nema servis pro praci s adminem");
        }
        
        // zpristupnuje config pres volani Config::getConfig()
        self::$extConfig = $this->container;
        $this->container->get(self::INIT_LOGGER_IDENTIFIER);
        // now we can use normal logger
        $this->log = Logger::getLogger(self::class);
        
        // TODO: kontrolovat jestli se zmenilo neco v konfiguraci modulu
        // vytahne si servis pro admina
        $this->adminService = $this->container->get(self::ADMIN_SERVICE_IDENTIFIER);
        
        // nacist konfigurace modulu
        $modulesActions = []; // akce modulu co se maji zobrazovat v pravem sloupci
        $modules = $this->adminService->getActiveModules();
        
        // builder needs to be reinitialized
        $builder = new $containerBuilderClass();
        $builder->addDefinitions($definitionsArray);
        
        foreach ($modules as $module) {
            self::$moduleType = $module->getType();
            self::$moduleIdentifier = $module->getIdentifier();
            self::$moduleLoading = true;
            
            if( !isset($this->modulesLocations[$module->getType()]) ) {
                throw new Exception("Module ". $module->getType() ." not installed.");
            }
            // adresar ve kterem se modul nachazi specificky pro web
            $moduleDir = $configDir .'/'. $this->modulesLocations[$module->getType()]["path"];

            // existuje adresar takoveho modulu
            if( file_exists($moduleDir)
                && is_readable($moduleDir) ) {
                // prida konfigurace modulu
                $deff = [];
                $moduleActions = $this->loadAdminModuleConfig($builder, $module, $moduleDir, $deff);
                if( $moduleActions !== NULL
                    && isset($moduleActions["actions"])
                    && isset($moduleActions["menuActions"]) 
                    && isset($moduleActions["defaultAction"]) ) { // no actions defined, dont add to possible modules
                    
                    $modulesActions[$module->getIdentifier()] = [
                        "name" => $module->getName(),
                        "type" => $module->getType(),
                        "identifier" => $module->getIdentifier(),
                        "icon" => $module->getIcon(),
                        "actions" => $moduleActions["actions"],
                        "defaultAction" => $moduleActions["defaultAction"],
                        "menuActions" => $moduleActions["menuActions"],
                    ];

                } else {
                    $eMsg = "Module ". $module->getType() ." doesn't have moduleActions array or one of moduleActions[actions], moduleActions[menuActions],  moduleActions[defaultAction]";
                    $this->log->warning($eMsg);
                }
            } else {
                $eMsg = "Dir $moduleDir for module ". $module->getType() ." doesn't not exist.";
                $this->log->error($eMsg);
                throw new Exception($eMsg);
            }
            
            $definitionsArray  = array_merge($definitionsArray, $deff);
            self::$moduleLoading = false;
        }
        
         // TODO: brat identifier nekce z neceho inteligentnejsiho
        $modulesActionsDefinition = ["admin.modulesActions" => $modulesActions];
        $builder->addDefinitions($modulesActionsDefinition);
        $definitionsArray = array_merge($definitionsArray, $modulesActionsDefinition);
        
        // nacist ostatni konfigurace webu
        $webDir = $this->getAbsoluteConfigDir() . "/web";
        foreach(scandir($webDir) as $file) {
            if( \IZON\String\endsWith($file, ".php") ) {
                $controllerDefinitions = require $webDir . "/" . $file;
                $builder->addDefinitions($controllerDefinitions);
                $definitionsArray = array_merge($definitionsArray, $controllerDefinitions);
            }
        }

        // vytbori di container
        $this->container = $builder->build();
        // inits array with definitions
        $this->definitionsArray = $definitionsArray;
        
        // vytvor cache pokud je to povoleno
        if( $this->container->has(self::USE_CONFIG_CACHE_IDENTIFIER)
            && $this->container->get(self::USE_CONFIG_CACHE_IDENTIFIER) ) { // ma se pouzit cache definic
            
            // vytvorit cache definic
            $fileContent = ["definitions" => $definitionsArray, "modulesDirs" => self::$moduleDirs];
            $serialized = serialize($fileContent);
            if( !file_exists($cacheDir) ) { // vytvorit cache dir
                mkdir($cacheDir, 0777, true);
            }
            // zapis data do souboru
            file_put_contents($definitionsCacheFileName, $serialized, LOCK_EX);
        } else { // nema se cachovat konfig
            // smazat predchozi konfig pokud exitruje
            if(file_exists($definitionsCacheFileName)) {
                unlink($definitionsCacheFileName);
            }
        }
    }
    
    /**
     * returns exception handlers for aplication
     */
    public function getExceptionHandlers() {
        $container = $this->container;
        if ( $container->has(self::EXCEPTION_HANDLERS_INDENTIFIER) ) {
            $handlers = $container->get(self::EXCEPTION_HANDLERS_INDENTIFIER);
        } else {
            // use default exeption handlers
            $handlers = [
                new PageNotFoundExceptionHandler($this->baseDir ."/www/errors/404.php"),
                new AdminPageNotFoundExceptionHandler($this->baseDir ."/www/errors/404.php"),
                new AccessForbiddenExceptionHandler($this->baseDir ."/www/errors/403.php"),
                new InternalServerErrorExceptionHandler($this->baseDir ."/www/errors/500.php")
            ];
        }
        
        return $handlers;
    }
    
    /**
     * nacte konfiguraci pro admina daneho modulu
     * @param ModulesAwareContainerBuilder $builder builder pro di
     * @param string $moduleDir v jakem adresari se modul nachazi
     */
    protected function loadAdminModuleConfig(ModulesAwareContainerBuilder $builder,
                                        AdminModule $module,
                                        $moduleDir,
                                        &$definitionsArray) {
        // nacist zakladni konfiguraci modulu
        $configFileDir = $moduleDir ."/". self::MODULE_COMMON_CONFIG_FILENAME;
        if( file_exists($configFileDir) ) { // TODO: resit ze modul nema konfig
            $moduleConfArray = include $configFileDir;
            
            $newModuleConfArray = [];
            
            foreach($moduleConfArray as $key => $value) {
                $propertyNewName = self::composeModuleEntryName($key, 
                                                                $module->getIdentifier());
                $newModuleConfArray[$propertyNewName] = $value;
            }
            $builder->addDefinitions($newModuleConfArray);
            $definitionsArray = array_merge($definitionsArray, $newModuleConfArray);
        }
        
        // nacist konfiguraci pro admina
        $configFileDir = $moduleDir ."/". self::MODULE_CONFIG_FILENAME;
        if( file_exists($configFileDir) ) { // TODO: resit ze modul nema konfig
            $moduleConfArray = include $configFileDir;
            
            $newModuleConfArray = [];
            
            // vytvori identifikatory nezavisle pro jednotlive nactene moduly
            foreach($moduleConfArray as $key => $value) {
                $propertyNewName = self::composeModuleEntryName($key,
                                                            $module->getIdentifier());
                
                $newModuleConfArray[$propertyNewName] = $value;
            }
            $builder->addDefinitions($newModuleConfArray);
            $definitionsArray = array_merge($definitionsArray, $newModuleConfArray);
            
            // nacist akce modulu
            if( !isset($moduleConfArray["moduleActions"]) ) {
                throw new Exception("Module ". $module->getType() ." doesn't have actions definition [array key: moduleActions]");
            }
            
            return $moduleConfArray["moduleActions"];
        }
    }

    
    /**
     * vrati identifikator hodnoty z modulu
     * @param string $entryName
     * @param string $moduleIdentifier
     */
    public static function getModuleEntryName($entryName,
                                                $moduleIdentifier) {
//        echo "Calling getModuleEntryName with parameters: [$entryName, $moduleType, $moduleIdentifier]";
        
        if($moduleIdentifier == NULL
           && !self::$moduleLoading) { // neprovadi se nacitani neceho z modulu v modulu
            $message = "pri odkazovani se na zaznam z modulu je traba zadat \$moduleIdentifier";
            throw new Exception($message);
        }
 
        if( $moduleIdentifier == NULL ) {
            $moduleIdentifier = self::$moduleIdentifier;
        }
        
        return self::composeModuleEntryName($entryName,
                                                $moduleIdentifier);
    } 

    /**
     * slozi identifikator pro zaznam v konfiguraci v adminu unikatni pro nacitani jednotlivych modulu
     * @param string $entryName zaznam v konfiguraci modulu
     * @param string $moduleIdentifier identifikator 
     * @return string konecny identifikator pod ktery se ulozi do DI kontejneru
     */
    protected static function composeModuleEntryName($entryName, 
                                                    $moduleIdentifier) {
        return "MODULE#$moduleIdentifier#$entryName";
    }
}