<?php

namespace Ignite\Core\Providers;

use Ignite\Core\Auth\Authorization;
use Ignite\Core\Auth\Guard;
use Ignite\Core\Auth\Impersonation;
use Ignite\Core\Auth\UserProvider;
use Ignite\Core\Contracts\Repositories\BlockRepository;
use Ignite\Core\Contracts\Repositories\GroupRepository;
use Ignite\Core\Contracts\Repositories\ImportRepository;
use Ignite\Core\Contracts\Repositories\PageRepository;
use Ignite\Core\Contracts\Repositories\ParticipantRepository;
use Ignite\Core\Contracts\Repositories\PermissionRepository;
use Ignite\Core\Contracts\Repositories\TransactionRepository;
use Ignite\Core\Contracts\Repositories\UserRepository;
use Ignite\Core\Helpers\Format;
use Ignite\Core\Helpers\MimeTypes;
use Ignite\Core\Ignite;
use Ignite\Core\Models\Menu\AdminMenu;
use Ignite\Core\Models\Menu\FooterNavigationMenu;
use Ignite\Core\Models\Menu\NavigationMenu;
use Ignite\Core\Models\Menu\ParticipantMenu;
use Ignite\Core\Models\Menu\UserNavigationMenu;
use Ignite\Core\Models\Source\Manager;
use Ignite\Core\Program;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Foundation\Application;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Symfony\Component\Mime\Address;

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

    /**
     * @var string
     */
    protected $moduleNameLower = '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 = true;

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

        \Spatie\Ignition\Ignition::make()
            // ->applicationPath(base_path())
            // ->shouldDisplayException(!app()->environment('production'))
            ->register();

        setlocale(LC_MONETARY, config('core.locale.money', 'en_US.UTF-8'));
    }

    /**
     * Resolve theming options from presets and explicit overrides.
     *
     * @return void
     */
    protected function bootThemingConfig()
    {
        $theming = config('core.theming', []);

        if (! is_array($theming)) {
            return;
        }

        $preset = $theming['preset'] ?? 'default';
        $presets = is_array($theming['presets'] ?? null) ? $theming['presets'] : [];
        $overrides = is_array($theming['options'] ?? null) ? $theming['options'] : [];

        $base = Arr::except($theming, ['preset', 'presets', 'options']);
        $presetValues = $presets[$preset] ?? ($presets['default'] ?? []);
        $presetValues = is_array($presetValues) ? $presetValues : [];

        $resolvedOptions = array_replace_recursive($base, $presetValues, $overrides);

        config([
            'core.theming' => array_replace($theming, $resolvedOptions),
        ]);
    }

    /**
     * Register views.
     *
     * @return void
     */
    public function bootViews()
    {
        $viewPath = resource_path("views/modules/{$this->moduleNameLower}");
        $sourcePath = module_path($this->moduleName, 'resources/views');

        if ($this->canPublishViews) {
            $this->publishes([
                $sourcePath => $viewPath
            ], ['views', "ignite-{$this->moduleNameLower}-views"]);
        }

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleName);
    }

    /**
     * The publishable view paths.
     *
     * @return array
     */
    protected function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach ($this->app->config->get('view.paths') as $path) {
            if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
                $paths[] = $path . '/modules/' . $this->moduleNameLower;
            }
        }
        return $paths;
    }

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

    /**
     * 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') && $this->app->runningInConsole()) {
            $this->loadFactoriesFrom(module_path($this->moduleName, '$FACTORIES_PATH$'));
        }
    }

    /**
     * Register the directory to load migrations.
     *
     * @return void
     */
    protected function bootMigrations()
    {
        $this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
    }

    /**
     * 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);

        $this->registerConfigs();
        $this->registerCustomUserProvider();
        $this->registerProgramSingleton();
        $this->registerIgniteSingleton();
        $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();
        $this->registerSourceModels();
    }

    /**
     * Register and publish core configurations.
     *
     * @return void
     */
    protected function registerConfigs()
    {
        $configPath = __DIR__.'/../../config/config.php';
        if (file_exists($configPath)) {
            $this->mergeConfigFrom($configPath, $this->moduleNameLower);
        }

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

        $configs = [
            // Third-party
            'audit', 'laravel-form-builder', 'datatables-buttons', 'datatables-html',
            // Second-party
            'auth', 'filesystems', 'mail', 'services', 'session',
        ];

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

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

    /**
     * Initialize custom validation extensions.
     *
     * @return void
     */
    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.'
        );

        Validator::extend(
            'ReCaptchaV2Robot',
            'Ignite\Core\Validators\RecaptchaV2RobotRule@passes',
            'You must prove you are not a robot!'
        );
    }

    /**
     * Register and boot macros.
     *
     * @return void
     */
    protected function bootMacros()
    {
        Str::macro('extension', function ($path) {
            return mb_strtolower(pathinfo($path, PATHINFO_EXTENSION));
        });

        Stringable::macro('extension', function () {
            return new Stringable(mb_strtolower(pathinfo($this->value, PATHINFO_EXTENSION)));
        });

        Str::macro('mimeType', function ($filename) {
            return MimeTypes::fromFilename($filename);
        });

        Stringable::macro('mimeType', function () {
            return new Stringable(MimeTypes::fromFilename($this->value));
        });
    }

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

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

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

        $this->app->alias(Ignite::class, 'ignite');
    }

    /**
     * Register the authorization class.
     *
     * @return void
     */
    protected 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');
    }

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

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

    /**
     * Register the authentication driver.
     *
     * @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.
     *
     * @return void
     */
    protected function registerUserRepository()
    {
        $this->app->bind(UserRepository::class, function () {
            return new \Ignite\Core\Repositories\UserRepository();
        });

        $this->app->alias(UserRepository::class, 'user');
    }

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

        $this->app->alias(GroupRepository::class, 'group');
    }

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

        $this->app->alias(PermissionRepository::class, 'permission');
    }

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

        $this->app->alias(ParticipantRepository::class, 'participant');
    }

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

        $this->app->alias(ImportRepository::class, 'import');
    }

    /**
     * Register the concrete Page implementation for the page repo contract.
     *
     * @return void
     */
    protected function registerPageRepository()
    {
        $this->app->singleton(PageRepository::class, function () {
            return new \Ignite\Core\Repositories\PageRepository();
        });

        $this->app->alias(PageRepository::class, 'page');
    }

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

        $this->app->alias(BlockRepository::class, 'block');
    }

    /**
     * Register the concrete Transaction repo implementation for the Transaction repo contract.
     *
     * @return void
     */
    protected function registerTransactionRepository()
    {
        $this->app->singleton(TransactionRepository::class, function ($app) {
            return new \Ignite\Core\Repositories\TransactionRepository(
                $app->make(\Ignite\Core\Models\Import\Hashers\TransactionHasher::class)
            );
        });

        $this->app->alias(TransactionRepository::class, 'transaction');
    }

    /**
     * Register the admin menu as a singleton.
     *
     * @return void
     */
    protected function registerMenus()
    {
        $this->app->singleton(NavigationMenu::class, function () {
            return new NavigationMenu();
        });

        $this->app->singleton(UserNavigationMenu::class, function () {
            return new UserNavigationMenu();
        });

        // Footer navigation uses a dedicated menu manager so its precompile event stays isolated from the header menu.
        $this->app->singleton(FooterNavigationMenu::class, function () {
            return new FooterNavigationMenu();
        });

        $this->app->singleton(AdminMenu::class, function () {
            return new AdminMenu();
        });

        $this->app->singleton(ParticipantMenu::class, function () {
            return new ParticipantMenu();
        });
    }

    /**
     * Register the Format helper as a singleton and provide an alias.
     *
     * @return void
     */
    protected function registerFormatHelper()
    {
        $this->app->singleton(Format::class, function ($app) {
            return new Format($app);
        });

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

    /**
     * Register the source model manager class.
     *
     * @return void
     */
    public function registerSourceModels()
    {
        $this->app->singleton(Manager::class, function ($app) {
            return new Manager($app['config']->get('core.source.models', []));
        });
    }

    /**
     * Listen for the MessageSending event and intercept the To address and name.
     *
     * @return void
     */
    protected function addEmailInterceptListeners()
    {
        if (!config('mail.intercept.enabled')) {
            return;
        }

        Event::listen(MessageSending::class, function (MessageSending $event) {
            $message = $event->message;
            $headers = $message->getHeaders();

            $originalTo = $message->getTo();
            $contentHtml = $message->getHtmlBody();
            $contentText = $message->getTextBody();
            $isAppendingOriginals = config('mail.intercept.append_originals');

            if ($isAppendingOriginals) {
                if (!empty($headers->get('to'))) {
                    $new = 'ORIGINAL-TO: ' . str_replace('To: ', '', $headers->get('to')->toString());
                    $contentHtml .= $contentHtml ? "<BR/>" . htmlentities($new) : '';
                    $contentText .= $contentText ? "\n" . $new : '';
                }
                if (!empty($headers->get('cc'))) {
                    $new = 'ORIGINAL-CC: ' . str_replace('Cc: ', '', $headers->get('cc')->toString());
                    $contentHtml .= $contentHtml ? "<BR/>" . htmlentities($new) : '';
                    $contentText .= $contentText ? "\n" . $new : '';
                }
                if (!empty($headers->get('bcc'))) {
                    $new = 'ORIGINAL-BCC: ' . str_replace('Bcc: ', '', $headers->get('bcc')->toString());
                    $contentHtml .= $contentHtml ? "<BR/>" . htmlentities($new) : '';
                    $contentText .= $contentText ? "\n" . $new : '';
                }
            }

            $headers->remove('to');
            $headers->remove('cc');
            $headers->remove('bcc');
            $message->setHeaders($headers);

            $message->replyTo($originalTo[0]);
            if ($message->getHtmlBody()) {
                $message->html($contentHtml);
            }
            if ($message->getTextBody()) {
                $message->text($contentText);
            }

            $intercept = explode(',', config('mail.intercept.address'));
            $interceptNames = explode(',', config('mail.intercept.name'));
            foreach ($intercept as $key => $email) {
                if (!isset($interceptNames[$key])) {
                    $interceptNames[$key] = 'Unknown';
                }
                $message->addTo(new Address($email, $interceptNames[$key]));
            }
        });
    }

    /**
     * Listen for the MessageSending event and add the SES tracking header.
     *
     * @return void
     */
    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',
        ];
    }
}
