<?php
/**
 * @package    Proxim
 * @author     Dominic Tanui <dominic@codeworldweb.com.dev | https://codeworldweb.com.dev>
 * @copyright  2019 Proxim
 * @version    1.5.0
 * @since      File available since Release 1.0.0
 */

namespace Proxim;

use Db;
use Proxim\Database\DbQuery;
use \VisualAppeal\AutoUpdate;
use \Smarty;
use Proxim\User\Employee;
use Proxim\Order\Order;
use Proxim\Util\ArrayUtils;
use Proxim\Util\DateUtils;
use Proxim\Slim\BaseRequest;
use Proxim\Slim\BaseResponse;
use Proxim\Slim\Environment;
use Slim\Slim;

/**
 * Application
 */
class Application extends Slim {

    const VERSION = '1.0.2';

    const DEFAULT_CHECK_VERSION_DELAY_HOURS = 24;

    /** @var string */
    public $fingerprint;

    /** @var string */
    public $timezone;

    /** @var string */
    public $base_uri;

    /** @var string */
    public $modules_uri;

    /**
     * @var Application
     */
    public static $instance = null;

    /**
     * @var bool
     */
    protected $booted = false;

    /**
     * Constructor
     */
    public function __construct( array $appSettings  ) {
        parent::__construct($appSettings);

        $this->container->singleton('environment', function () {
            return Environment::getInstance();
        });

        $this->container->singleton('response', function () {
            return new BaseResponse();
        });

        // Default request
        $this->container->singleton('request', function ($c) {
            return new BaseRequest($c['environment']);
        });

        $featureList = array(
            'user-agent=' . $this->request->getUserAgent(),
            'ip-address=' . $this->request->getIp() 
        );

        $this->fingerprint = sprintf("Proxim (%s)", implode('; ', $featureList));
 
        $base_uri = $this->request->getUrl();

        $this->base_uri = $base_uri;
        $this->modules_uri = $base_uri . '/content/modules';

        $smarty = new Smarty();

        $smarty->setCompileDir( PROX_DIR_CACHE . 'compile');
        $smarty->setCacheDir( PROX_DIR_CACHE );
        $smarty->use_sub_dirs = true;
        $smarty->caching = false;
        $smarty->setTemplateDir( PROX_DIR_TEMPLATES );
        $smarty->escape_html = true;

        smartyRegisterFunction($smarty, 'modifier', 'classname', 'smartyClassname');
        smartyRegisterFunction($smarty, 'modifier', 'classnames', 'smartyClassnames');
        smartyRegisterFunction($smarty, 'modifier', 'json_encode', array('Proxim\Tools', 'jsonEncode'));
        smartyRegisterFunction($smarty, 'modifier', 'json_decode', array('Proxim\Tools', 'jsonDecode'));
        smartyRegisterFunction($smarty, 'function', 'hook', 'smartyHook');

        $this->container->smarty = $smarty;
        
        $this->container->cookie = new Cookie(
            'Proxim-user', '', time()+30*86400, 
            array($this->base_uri), 
            $this->request->getScheme() === "https"
        );

        self::$instance = $this;

        $this->booted = true;
    }

    public function getFingerprint() {
        return $this->fingerprint;
    }

    public function updateUser(Employee $employee)
    {
        $cookie = $this->container->cookie;
        
        $cookie->user_id = $employee->id;
        $cookie->email = $employee->email;
        $cookie->password = $employee->password;
        $cookie->logged = 1;

        $cookie->write();
    }

    public function getNotificationEmails() 
    {
        $receivers = array();

        $notification_emails = Configuration::get('NOTIFICATION_EMAILS');
        if(!empty($notification_emails)) {
            $notification_emails = explode(',', $notification_emails);
            foreach($notification_emails as $notification_email) {
                $notification_email = trim($notification_email);
                if(Validate::isEmail($notification_email)) {
                    $receivers[] = $notification_email;
                }
            }
        }

        return $receivers;
    }

    public function runCronjob() {
        $smarty = $this->container->smarty;

        Hook::exec('actionBeforeCron');

        $next_cron_date = Configuration::get("NEXT_CRONJOB_RUN");
        if ($next_cron_date && !DateUtils::hasPassed($next_cron_date)) {
            return;
        }

        // approve orders after x days
        $sql = new DbQuery();
        $sql->select('o.*');
        $sql->from('order', 'o');
        $sql->where('status_id = ' . Order::DELIVERED);
        $autoApproveTime = Configuration::get('ORDER_AUTOAPPROVE_TIME', null, 168);
        $sql->where( sprintf('delivered_at <= DATE_SUB(NOW(), INTERVAL %s HOUR)', $autoApproveTime));
        $result = Db::getInstance(PROX_USE_SQL_SLAVE)->executeS($sql);
        foreach($result as $order) {
            $o = new Order();
            $order = $o->getObject( (array) $order );
            if(Validate::isLoadedObject($order)) {
                $order->status_id = Order::FINISHED;
                $order->approved_at = DateUtils::now();
                $order->update();
            }
        }

        Hook::exec('actionAfterCron');

        Configuration::updateValue("NEXT_CRONJOB_RUN", DateUtils::date(null, '+1 hour') );
    }

    public function cloudStorageEnabled() {
        $s3_enabled = Configuration::get('S3_ENABLED');
        $digitalocean_enabled = Configuration::get('DIGITALOCEAN_ENABLED');

        if($s3_enabled || $digitalocean_enabled) {
            return true;
        }

        return false;
    }

    /**
     * checkVersion ask to proxim.craftyworks.co if there is a new version. return an array if yes, false otherwise.
     *
     * @return mixed
     */
    public function checkVersion( $force = false )
    {
        $current_version = Configuration::get('PROX_VERSION');
        $lastCheck = Configuration::get('PROX_LAST_VERSION_CHECK');
        if(!$lastCheck) {
            $lastCheck = 0;
        }

        if ($force || ($lastCheck < time() - (3600 * self::DEFAULT_CHECK_VERSION_DELAY_HOURS))) {
            $update = new AutoUpdate();
            $update->setCurrentVersion( $current_version );
            $update->setUpdateUrl(PROX_UPDATES_URL);

            if ($update->checkUpdate() === false) {
                return false;
            }

            Configuration::updateValue('PROX_LAST_VERSION_CHECK', time());

            if ($update->newVersionAvailable()) {
                Configuration::updateValue('PROX_NEW_VERSION', true );
                return array('version' => $update->getLatestVersion() );
            } else {
                Configuration::updateValue('PROX_NEW_VERSION', false );
                return false;
            }
        }

        return false;
    }

    /**
     * @inheritdoc
     */
    protected function mapRoute($args)
    {
        $pattern = array_shift($args);
        $callable = $this->resolveCallable(array_pop($args));
        $route = new \Slim\Route($pattern, $callable);
        $this->router->map($route);
        if (count($args) > 0) {
            $route->setMiddleware($args);
        }

        return $route;
    }

    /**
     * Resolve toResolve into a closure that that the router can dispatch.
     *
     * If toResolve is of the format 'class:method', then try to extract 'class'
     * from the container otherwise instantiate it and then dispatch 'method'.
     *
     * @param mixed $toResolve
     *
     * @return callable
     *
     * @throws \RuntimeException if the callable does not exist
     * @throws \RuntimeException if the callable is not resolvable
     */
    public function resolveCallable($toResolve)
    {
        $resolved = $toResolve;

        if (!is_callable($toResolve) && is_string($toResolve)) {
            // check for slim callable as "class:method"
            $callablePattern = '!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!';
            if (preg_match($callablePattern, $toResolve, $matches)) {
                $class = $matches[1];
                $method = $matches[2];

                if ($this->container->has($class)) {
                    $resolved = [$this->container->get($class), $method];
                } else {
                    if (!class_exists($class)) {
                        throw new \RuntimeException(sprintf('Callable %s does not exist', $class));
                    }
                    $resolved = [new $class($this), $method];
                }
            } else {
                // check if string is something in the DIC that's callable or is a class name which
                // has an __invoke() method
                $class = $toResolve;
                if ($this->container->has($class)) {
                    $resolved = $this->container->get($class);
                } else {
                    if (!class_exists($class)) {
                        throw new \RuntimeException(sprintf('Callable %s does not exist', $class));
                    }
                    $resolved = new $class($this);
                }
            }
        }

        if (!is_callable($resolved)) {
            throw new \RuntimeException(sprintf(
                '%s is not resolvable',
                is_array($toResolve) || is_object($toResolve) ? json_encode($toResolve) : $toResolve
            ));
        }

        return $resolved;
    }

    public function sendResponse( $data )
    {
        $response = parent::response();

        if (count($data)) {
            $response->header('Content-Type', 'application/json; charset=utf-8');
            $response->setBody(json_encode($data, JSON_UNESCAPED_UNICODE));
        }

        return $response;
    }
}