<?php

namespace Concrete\Core\Application;

use Concrete\Core\Cache\Command\ClearCacheCommand;
use Concrete\Core\Cache\Page\PageCache;
use Concrete\Core\Cache\Page\PageCacheRecord;
use Concrete\Core\Entity\Site\Site;
use Concrete\Core\Foundation\ClassAutoloader;
use Concrete\Core\Foundation\EnvironmentDetector;
use Concrete\Core\Foundation\Runtime\DefaultRuntime;
use Concrete\Core\Foundation\Runtime\RuntimeInterface;
use Concrete\Core\Http\DispatcherInterface;
use Concrete\Core\Http\Request;
use Concrete\Core\Localization\Localization;
use Concrete\Core\Logging\Channels;
use Concrete\Core\Logging\LoggerAwareInterface;
use Concrete\Core\Messenger\MessageBusManager;
use Concrete\Core\Package\BrokenPackage;
use Concrete\Core\Package\PackageList;
use Concrete\Core\Package\PackageService;
use Concrete\Core\Routing\RedirectResponse;
use Concrete\Core\System\Mutex\MutexInterface;
use Concrete\Core\Updater\Update;
use Concrete\Core\Url\Url;
use Concrete\Core\Url\UrlImmutable;
use Config;
use Exception;
use Illuminate\Container\Container;
use Job;
use JobSet;
use Page;
use Psr\Log\LoggerAwareInterface as PsrLoggerAwareInterface;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use View;

class Application extends Container
{
    protected $installed = null;
    protected $environment = null;
  
    /**
     * @var \Concrete\Core\Package\Package[]
     */
    protected $packages = [];

    /**
     * Dispatches a command/message on the message bus. If the command is executed immediately, the result is returned.
     * This is a convenience method for mostly synchronous commands, and it uses the command bus.
     *
     * @param object $command
     * @param string $onBus
     * @return mixed
     */
    public function executeCommand($command, $onBus = 'default')
    {
        $messageBus = $this->make(MessageBusManager::class)->getBus($onBus);
        $envelope = $messageBus->dispatch($command);
        $handled = $envelope->last(HandledStamp::class);
        if ($handled instanceof HandledStamp) {
            return $handled->getResult();
        }
    }

    /**
     * Turns off the lights.
     *
     * @param array $options Array of options for disabling certain things during shutdown
     *      Add `'jobs' => true` to disable scheduled jobs
     *      Add `'log_queries' => true` to disable query logging
     */
    public function shutdown($options = [])
    {
        \Events::dispatch('on_shutdown');

        if ($this->isInstalled()) {
            if (!isset($options['jobs']) || $options['jobs'] == false) {
                $this->handleScheduledJobs();
            }

            foreach (\Database::getConnections() as $connection) {
                $connection->close();
            }
        }
        exit;
    }

    /**
     * @param \Concrete\Core\Http\Request $request
     *
     * @deprecated Use the dispatcher object to dispatch
     */
    public function dispatch(Request $request)
    {
        /** @var DispatcherInterface $dispatcher */
        $dispatcher = $this->make(DispatcherInterface::class);

        return $dispatcher->dispatch($request);
    }

    /**
     * Utility method for clearing all application caches.
     */
    public function clearCaches()
    {
        $command = new ClearCacheCommand();
        $this->executeCommand($command);
    }

    /**
     * If we have job scheduling running through the site, we check to see if it's time to go for it.
     */
    protected function handleScheduledJobs()
    {
        $config = $this['config'];

        if ($config->get('concrete.jobs.enable_scheduling')) {
            $c = Page::getCurrentPage();
            if ($c instanceof Page && !$c->isAdminArea()) {
                // check for non dashboard page
                $jobs = Job::getList(true);
                $auth = Job::generateAuth();
                $url = '';
                // jobs
                if (count($jobs)) {
                    foreach ($jobs as $j) {
                        if ($j->isScheduledForNow()) {
                            $url = View::url(
                                                  '/ccm/system/jobs/run_single?auth=' . $auth . '&jID=' . $j->getJobID(
                                                  )
                                );
                            break;
                        }
                    }
                }

                // job sets
                if (!strlen($url)) {
                    $jSets = JobSet::getList(true);
                    if (is_array($jSets) && count($jSets)) {
                        foreach ($jSets as $set) {
                            if ($set->isScheduledForNow()) {
                                $url = View::url(
                                                      '/ccm/system/jobs?auth=' . $auth . '&jsID=' . $set->getJobSetID(
                                                      )
                                    );
                                break;
                            }
                        }
                    }
                }

                if (strlen($url)) {
                    try {
                        $this->make('http/client')->get($url);
                    } catch (Exception $x) {
                    }
                }
            }
        }
    }

    /**
     * Returns true if Concrete is installed, false if it has not yet been.
     */
    public function isInstalled()
    {
        if ($this->installed === null) {
            if (!$this->isAlias('config')) {
                throw new Exception('Attempting to check install status before application initialization.');
            }

            $this->installed = $this->make('config')->get('concrete.installed');
        }

        return $this->installed;
    }

    /**
     * Checks to see whether we should deliver a Concrete response from the page cache.
     *
     * @param \Concrete\Core\Http\Request $request
     */
    public function checkPageCache(\Concrete\Core\Http\Request $request)
    {
        $library = PageCache::getLibrary();
        if ($library->shouldCheckCache($request)) {
            $record = $library->getRecord($request);
            if ($record instanceof PageCacheRecord) {
                if ($record->validate($request)) {
                    return $library->deliver($record);
                }
            }
        }

        return false;
    }

    /**
     * Check if the core needs to be updated, and if so, updates it.
     *
     * @throws \Concrete\Core\System\Mutex\MutexBusyException throws a MutexBusyException exception if there upgrade process is already running
     * @throws \Concrete\Core\Updater\Migrations\MigrationIncompleteException throws a MigrationIncompleteException exception if there's still some migration pending
     */
    public function handleAutomaticUpdates()
    {
        $update = false;
        $config = $this['config'];
        $installedDb = $config->get('concrete.version_db_installed');
        $coreDb = $config->get('concrete.version_db');
        if ($installedDb < $coreDb) {
            $update = true;
        } else {
            $installedVersion = $config->get('concrete.version_installed');
            $coreVersion = $config->get('concrete.version');
            if (version_compare($installedVersion, $coreVersion, '<')) {
                $update = true;
            }
        }
        if ($update) {
            $this->make(MutexInterface::class)->execute(Update::MUTEX_KEY, function () {
                Update::updateToCurrentVersion();
            });
        }
    }

    /**
     * Register package autoloaders. Has to come BEFORE session calls.
     */
    public function setupPackageAutoloaders()
    {
        $packageEntities = PackageList::get()->getPackages();
        if ($packageEntities === []) {
            return;
        }
        $packageService = $this->make(PackageService::class);
        $config = $this->make('config');
        foreach ($packageEntities as $packageEntity) {
            $packageController = $packageService->getClass($packageEntity->getPackageHandle());
            if (!$packageController instanceof BrokenPackage) {
                $config->package($packageController);
                $this->packages[] = $packageController;
            }
        }
    }

    /**
     * Run startup and localization events on any installed packages.
     */
    public function setupPackages()
    {
        $config = $this->make('config');
        $loc = Localization::getInstance();
        if ($config->get('concrete.updates.enable_auto_update_packages')) {
            foreach ($this->packages as $pkg) {
                $dbPkg = \Package::getByHandle($pkg->getPackageHandle());
                /** @var \Concrete\Core\Entity\Package $dbPkg */
                $pkgInstalledVersion = $dbPkg->getPackageVersion();
                $pkgFileVersion = $pkg->getPackageVersion();
                if (version_compare($pkgFileVersion, $pkgInstalledVersion, '>')) {
                    $loc->pushActiveContext(Localization::CONTEXT_SYSTEM);
                    $dbPkg->upgradeCoreData();
                    $dbPkg->upgrade();
                    $loc->popActiveContext();
                }
            }
        }
        $packagesWithOnAfterStart = [];
        $service = $this->make(PackageService::class);
        foreach ($this->packages as $pkg) {
            if (method_exists($pkg, 'on_start')) {
                $pkg->on_start();
            }
            $service->bootPackageEntityManager($pkg);
            if (method_exists($pkg, 'on_after_packages_start')) {
                $packagesWithOnAfterStart[] = $pkg;
            }
        }
        foreach ($packagesWithOnAfterStart as $pkg) {
            $pkg->on_after_packages_start();
        }
    }

    /**
     * Ensure we have a cache directory.
     */
    public function setupFilesystem()
    {
        $config = $this['config'];

        if (!is_dir($config->get('concrete.cache.directory'))) {
            @mkdir($config->get('concrete.cache.directory'), $config->get('concrete.filesystem.permissions.directory'));
            @touch($config->get('concrete.cache.directory') . '/index.html', $config->get('concrete.filesystem.permissions.file'));
        }
    }

    /**
     * Returns true if the app is run through the command line.
     */
    public static function isRunThroughCommandLineInterface()
    {
        return PHP_SAPI == 'cli' || PHP_SAPI === 'phpdbg';
    }

    /**
     * Using the configuration value, determines whether we need to redirect to a URL with
     * a trailing slash or not.
     *
     * @param SymfonyRequest $request
     * @param Site $site
     *
     * @return \Concrete\Core\Routing\RedirectResponse
     */
    public function handleURLSlashes(SymfonyRequest $request, Site $site)
    {
        $path = $request->getPathInfo();
        // If this isn't the homepage
        if ($path && $path != '/') {
            $config = $this->make('config');
            $trailing_slashes = $config->get('concrete.seo.trailing_slash');
            // If the trailing slash doesn't match the config, return a redirect response
            if (($trailing_slashes && substr($path, -1) != '/') ||
                (!$trailing_slashes && substr($path, -1) == '/')) {
                $parsed_url = Url::createFromUrl($request->getUri(),
                $trailing_slashes ? Url::TRAILING_SLASHES_ENABLED : Url::TRAILING_SLASHES_DISABLED);

                $response = new RedirectResponse($parsed_url, 301);
                $response->setRequest($request);

                return $response;
            }
        }
    }

    /**
     * If we have redirect to canonical host enabled, we need to honor it here.
     *
     * @param SymfonyRequest $r
     * @param Site $site
     *
     * @return \Concrete\Core\Routing\RedirectResponse|null
     */
    public function handleCanonicalURLRedirection(SymfonyRequest $r, Site $site)
    {
        $globalConfig = $this['config'];
        $siteConfig = $site->getConfigRepository();

        if ($globalConfig->get('concrete.seo.redirect_to_canonical_url') && $siteConfig->get('seo.canonical_url')) {
            $requestUri = $r->getUri();

            $path = parse_url($requestUri, PHP_URL_PATH);
            $trailingSlash = substr($path, -1) === '/';

            $url = UrlImmutable::createFromUrl($requestUri, $trailingSlash);

            $mainCanonical = null;
            foreach (['seo.canonical_url', 'seo.canonical_url_alternative'] as $key) {
                $canonicalUrlString = $siteConfig->get($key);
                if (!$canonicalUrlString) {
                    continue;
                }
                $canonicalUrl = UrlImmutable::createFromUrl(
                    $canonicalUrlString,
                    (bool) $globalConfig->get('concrete.seo.trailing_slash')
                );
                // Set the parts of the current URL that are specified in the canonical URL, including host,
                // scheme, port. Set scheme first so that our port can use the magic "set if necessary" method.
                $canonical = $url
                    ->setScheme($canonicalUrl->getScheme()->get())
                    ->setHost($canonicalUrl->getHost()->get())
                    ->setPort($canonicalUrl->getPort()->get())
                ;
                // Now we have our current url, swapped out with the important parts of the canonical URL.
                // If it matches, we're good.
                if ($canonical == $url) {
                    return null;
                }
                if ($mainCanonical === null) {
                    $mainCanonical = $canonical;
                }
            }

            $response = new RedirectResponse($mainCanonical, '301');

            return $response;
        }
    }

    /**
     * Get or check the current application environment.
     *
     * @param  mixed
     *
     * @return string|bool
     */
    public function environment()
    {
        if (count(func_get_args()) > 0) {
            return in_array($this->environment, func_get_args());
        }

        return $this->environment;
    }

    /**
     * Detect the application's current environment.
     *
     * @param  array|string|callable  $environments
     *
     * @return string
     */
    public function detectEnvironment($environments)
    {
        $r = Request::getInstance();
        $relativePath = '';
        $scriptName = $r->server->get('SCRIPT_NAME');
        // First normalize the directory separator
        $scriptName = str_replace(DIRECTORY_SEPARATOR, '/', $scriptName);
        if ($scriptName === DISPATCHER_FILENAME) {
            $relativePath = '';
        } else {
            // Remove duplicate slashes
            $scriptName = preg_replace('~/{2,}~', '/', $scriptName);
            $indexPhpLocation = strpos($scriptName, '/' . DISPATCHER_FILENAME);
            $relativePath = '/' . trim(substr($scriptName, 0, $indexPhpLocation), '/');
        }
        $this['app_relative_path'] = $relativePath === '/' ? '' : $relativePath;

        $args = $this->isRunThroughCommandLineInterface() && isset($_SERVER['argv']) ? $_SERVER['argv'] : null;

        $detector = new EnvironmentDetector();

        return $this->environment = $detector->detect($environments, $args);
    }

    /**
     * Instantiate a concrete instance of the given type.
     *
     * @param  string $concrete
     * @param  array $parameters
     *
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     *
     * @return mixed
     */
    public function build($concrete)
    {
        $object = parent::build($concrete);
        if (is_object($object)) {
            if ($object instanceof ApplicationAwareInterface) {
                $object->setApplication($this);
            }

            if ($object instanceof LoggerAwareInterface) {
                $logger = $this->make('log/factory')->createLogger($object->getLoggerChannel());
                $object->setLogger($logger);
            } elseif ($object instanceof PsrLoggerAwareInterface) {
                $logger = $this->make('log/factory')->createLogger(Channels::CHANNEL_APPLICATION);
                $object->setLogger($logger);
            }
        }

        return $object;
    }

    /**
     * @return RuntimeInterface
     */
    public function getRuntime()
    {
        // Set the runtime to a singleton
        $runtime_class = 'Concrete\Core\Foundation\Runtime\DefaultRuntime';
        if (!$this->isShared($runtime_class)) {
            $this->singleton($runtime_class);
        }

        /** @var DefaultRuntime $runtime */
        $runtime = $this->make($runtime_class);

        // If we're in CLI, lets set the runner to the CLI runner
        if ($this->isRunThroughCommandLineInterface()) {
            $runtime->setRunClass('Concrete\Core\Foundation\Runtime\Run\CLIRunner');
        }

        return $runtime;
    }

    /**
     * Get the list of registered aliases.
     *
     * @return string[]
     */
    public function getRegisteredAliases()
    {
        return array_keys($this->aliases);
    }

    /**
     * Get the list of registered instances.
     *
     * @return string[]
     */
    public function getRegisteredInstances()
    {
        return array_keys($this->instances);
    }

    /**
     * @deprecated Use the singleton method
     *
     * @param $abstract
     * @param $concrete
     */
    public function bindShared($abstract, $concrete)
    {
        return $this->singleton($abstract, $concrete);
    }
    
}
