<?php

namespace Ignite\Core\Providers;

use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Database\Eloquent\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Ignite\Core\Auth\Authorization;
use Ignite\Core\Auth\Guard;
use Ignite\Core\Auth\Impersonation;
use Ignite\Core\Auth\UserProvider;
use Ignite\Core\Program;

class CoreServiceProvider extends ServiceProvider
{
    /** @var string */
    protected $moduleName = 'Core';

    /**
     * The path to the module files.
     * @var string
     */
    protected $modulePath = '';

    /**
     * A flag to determine if the module can publish configuration files.
     * @var bool
     */
    protected $canPublishConfig = true;

    /**
     * A flag to determine if the module can publish view files.
     * @var bool
     */
    protected $canPublishViews = false;

    /**
     * Bootstrap any module services.
     *
     * @return void
     */
    public function boot()
    {
        $this->bootTranslations();
        $this->bootConfig();
        $this->publishConfig();
        $this->bootViews();
        $this->bootAssets();
        $this->bootFactories();
        $this->bootMigrations();
        $this->bootValidationExtensions();
        $this->app['authorization']->registerPermissions();
    }

    /**
     * Register config.
     *
     * @return void
     */
    protected function bootConfig()
    {
        $configPath = __DIR__ . '/../Config/config.php';
        $configName = strtolower($this->moduleName);

        if ($this->canPublishConfig) {
            $this->publishes(["$configPath" => config_path($configName . '.php')], 'ignite-core');
        }

        if (file_exists($configPath)) {
            $this->mergeConfigFrom($configPath, $configName);
        }
    }

    /**
     * Register views.
     *
     * @return void
     */
    public function bootViews()
    {
        $sourcePath = __DIR__ . '/../Resources/views';

        $paths = [$sourcePath];

        if ($this->canPublishViews) {
            $paths = array_merge($paths, array_map(function ($path) {
                return $path . '/modules/' . $this->moduleName;
            }, $this->app['config']->get('view.paths')));
            $this->publishes([$sourcePath => base_path('resources/views/modules/' . $this->moduleName)]);
        }

        $this->loadViewsFrom($paths, $this->moduleName);
    }

    /**
     * Publish assets for ignite.
     */
    protected function bootAssets()
    {
        $this->publishes([
            __DIR__.'/../Assets/ignite' => public_path('vendor/ignite'),
        ], 'ignite-core');
    }

    /**
     * Boot translations.
     *
     * @return void
     */
    public function bootTranslations()
    {
        $langPath = base_path('resources/lang/modules/' . strtolower($this->moduleName));

        if (is_dir($langPath)) {
            $this->loadTranslationsFrom($langPath, $this->moduleName);
        } else {
            $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', $this->moduleName);
        }
    }

    /**
     * Register an additional directory of factories.
     *
     * @return void
     */
    public function bootFactories()
    {
        if (! app()->environment('production')) {
            app(Factory::class)->load(__DIR__ . '/../Database/Factories');
        }
    }

    protected function bootMigrations()
    {
        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
    }

    /**
     * Publish configuration for Ignite.
     */
    protected function publishConfig()
    {
        $configs = [
            // Third-party
            'adminlte', 'audit', 'bugsnag', 'laravel-form-builder', 'modules',
            // Second-party
            'auth', 'filesystems', 'mail', 'services',
            // First-party
            'sources'
        ];

        foreach ($configs as $item) {
            $configName = $item . '.php';
            $configPath = __DIR__ . '/../Config/' . $configName;
            $this->publishes(["$configPath" => config_path($configName)], 'ignite-core');
            if (file_exists($configPath)) {
                $this->mergeConfigFrom("$configPath", $item);
            }
        }
    }

    /**
     * Register any module services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->register(RouteServiceProvider::class);
        $this->app->register(ConsoleServiceProvider::class);
        $this->app->register(EventServiceProvider::class);
        $this->app->register(ViewServiceProvider::class);

        // Bugsnag
        //$this->app->alias('bugsnag.multi', \Illuminate\Contracts\Logging\Log::class);
        //$this->app->alias('bugsnag.multi', \Psr\Log\LoggerInterface::class);

        $this->registerCustomUserProvider();
        $this->registerProgramSingleton();
        $this->registerAuthorizationSingleton();
        $this->registerImpersonationSingleton();
        $this->registerAuthDriver();
        $this->registerUserRepository();
        $this->registerGroupRepository();
        $this->registerPermissionRepository();
        $this->registerParticipantRepository();
        $this->registerImportRepository();
        $this->registerPageRepository();
        $this->registerBlockRepository();
        $this->registerTransactionRepository();
        $this->registerMenus();
        $this->registerFormatHelper();
    }

    /**
     * Initialize our custom User Provider.
     *
     * @return $this
     */
    protected function registerCustomUserProvider()
    {
        Auth::provider('ignite', function ($app) {
            $model = $app['config']['auth.providers.users.model'];
            return new UserProvider($app['hash'], $model);
        });

        return $this;
    }

    /**
     * Initialize custom validation extensions.
     *
     * @return $this
     */
    protected function bootValidationExtensions()
    {
        Validator::extend(
            'amount',
            'Ignite\Core\Validators\Amount@validate',
            'The :attribute value must be a numerical amount without a currency symbol.'
        );

        Validator::extend(
            'emaildomain',
            'Ignite\Core\Validators\EmailDomain@validate',
            'The :attribute must end with ":domain".'
        );
        Validator::replacer('emaildomain', 'Ignite\Core\Validators\EmailDomain@replaceDomain');

        Validator::extend(
            'dontrepeat',
            'Ignite\Core\Validators\NoRepeatCharacters@validate',
            'The :attribute value looks incorrect, it contains the same character too many times.'
        );

        return $this;
    }

    /**
     * Register the Program class.
     *
     * @return $this
     */
    private function registerProgramSingleton()
    {
        $this->app->singleton(Program::class, function ($app) {
            return new Program($app);
        });

        $this->app->alias(Program::class, 'program');

        return $this;
    }

    /**
     * Register the authorization class.
     *
     * @return $this
     */
    private function registerAuthorizationSingleton()
    {
        $this->app->singleton(Authorization::class, function ($app) {
            return new Authorization(
                $app->make(Gate::class),
                $app['cache.store']
            );
        });

        $this->app->alias(Authorization::class, 'authorization');

        return $this;
    }

    /**
     * Register the authorization class.
     *
     * @return $this
     */
    private function registerImpersonationSingleton()
    {
        $this->app->singleton(Impersonation::class, function ($app) {
            return new Impersonation($app);
        });

        $this->app->alias(Impersonation::class, 'impersonation');

        return $this;
    }

    /**
     * @param   void
     * @return  void
     */
    protected function registerAuthDriver()
    {
        /** @var AuthManager $auth */
        $auth = $this->app['auth'];
        $auth->extend('session', function (Application $app, $name, array $config) use ($auth) {
            $provider = $auth->createUserProvider($config['provider']);
            $guard = new Guard($name, $provider, $app['session.store']);
            if (method_exists($guard, 'setCookieJar')) {
                $guard->setCookieJar($app['cookie']);
            }
            if (method_exists($guard, 'setDispatcher')) {
                $guard->setDispatcher($app['events']);
            }
            if (method_exists($guard, 'setRequest')) {
                $guard->setRequest($app->refresh('request', $guard, 'setRequest'));
            }
            return $guard;
        });
    }

    /**
     * Register the concrete User Repository implementation for the User Repository contract.
     */
    private function registerUserRepository()
    {
        $this->app->bind(\Ignite\Core\Contracts\Repositories\UserRepository::class, function () {
            return new \Ignite\Core\Repositories\UserRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\UserRepository::class, 'user');
    }

    /**
     * Register the concrete Group Repository implementation for the Group Repository contract.
     */
    private function registerGroupRepository()
    {
        $this->app->bind(\Ignite\Core\Contracts\Repositories\GroupRepository::class, function () {
            return new \Ignite\Core\Repositories\GroupRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\GroupRepository::class, 'group');
    }

    /**
     * Register the concrete Permission Repository implementation for the Permission Repository contract.
     */
    private function registerPermissionRepository()
    {
        $this->app->bind(\Ignite\Core\Contracts\Repositories\PermissionRepository::class, function ($app) {
            return new \Ignite\Core\Repositories\PermissionRepository($app->make(Authorization::class));
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\PermissionRepository::class, 'permission');
    }

    /**
     * Register the concrete Participant implementation for the Participant repo contract.
     */
    private function registerParticipantRepository()
    {
        $this->app->bind(\Ignite\Core\Contracts\Repositories\ParticipantRepository::class, function () {
            return new \Ignite\Core\Repositories\ParticipantRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\ParticipantRepository::class, 'participant');
    }

    /**
     * Register the concrete Import implementation for the import repo contract.
     */
    private function registerImportRepository()
    {
        $this->app->singleton(\Ignite\Core\Contracts\Repositories\ImportRepository::class, function () {
            return new \Ignite\Core\Repositories\ImportRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\ImportRepository::class, 'import');
    }
    /**
     * Register the concrete Page implementation for the page repo contract.
     */
    private function registerPageRepository()
    {
        $this->app->singleton(\Ignite\Core\Contracts\Repositories\PageRepository::class, function () {
            return new \Ignite\Core\Repositories\PageRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\PageRepository::class, 'page');
    }

    /**
     * Register the concrete Block repo implementation for the Block repo contract.
     */
    private function registerBlockRepository()
    {
        $this->app->singleton(\Ignite\Core\Contracts\Repositories\BlockRepository::class, function () {
            return new \Ignite\Core\Repositories\BlockRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\BlockRepository::class, 'block');
    }

    /**
     * Register the concrete Transaction repo implementation for the Transaction repo contract.
     */
    private function registerTransactionRepository()
    {
        $this->app->singleton(\Ignite\Core\Contracts\Repositories\TransactionRepository::class, function ($app) {
            return new \Ignite\Core\Repositories\TransactionRepository();
        });

        $this->app->alias(\Ignite\Core\Contracts\Repositories\TransactionRepository::class, 'transaction');
    }

    /**
     * Register the admin menu as a singleton.
     */
    private function registerMenus()
    {
        $this->app->singleton(\Ignite\Core\Models\Menu\NavigationMenu::class, function () {
            return new \Ignite\Core\Models\Menu\NavigationMenu();
        });

        $this->app->singleton(\Ignite\Core\Models\Menu\UserNavigationMenu::class, function () {
            return new \Ignite\Core\Models\Menu\UserNavigationMenu();
        });

        $this->app->singleton(\Ignite\Core\Models\Menu\AdminMenu::class, function () {
            return new \Ignite\Core\Models\Menu\AdminMenu();
        });

        $this->app->singleton(\Ignite\Core\Models\Menu\ParticipantMenu::class, function () {
            return new \Ignite\Core\Models\Menu\ParticipantMenu();
        });
    }

    /**
     * Register the Format helper as a singleton and provide an alias.
     */
    private function registerFormatHelper()
    {
        $this->app->singleton(\Ignite\Core\Helpers\Format::class, function ($app) {
            return new \Ignite\Core\Helpers\Format($app);
        });

        $this->app->alias(\Ignite\Core\Helpers\Format::class, 'format');
    }

    /**
     * Listen for the MessageSending event and intercept the To address and name.
     */
    protected function addEmailInterceptListeners()
    {
        Event::listen(MessageSending::class, function (MessageSending $event) {
            if (config('mail.intercept.enabled')) {
                $event->message->setReplyTo($event->message->getTo());
                $event->message->setTo(config('mail.intercept.address'), config('mail.intercept.name'));
            }
        });
    }

    /**
     * Listen for the MessageSending event and add the SES tracking header.
     */
    protected function addEmailSesTrackingHeaderListener()
    {
        Event::listen(MessageSending::class, function (MessageSending $event) {
            $event->message->getHeaders()->addTextHeader('X-SES-CONFIGURATION-SET', 'Engagement');
        });
    }

    /**
     * Get the services provided by the provider.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'program',
            'format',
            'authorization',
            'impersonation',
            'group',
            'permission',
            'participant',
            'import',
            'page',
            'block'
        ];
    }
}
