<?php

namespace App\Console;

use Carbon\Carbon;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Misc\Mail;
use App\Option;

class Kernel extends ConsoleKernel
{
    const FETCH_MAX_EXECUTION_TIME = 30; // minutes

    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        // It is not clear what for this array
        //\App\Console\Commands\CreateUser::class,
    ];

    /**
     * Define the application's command schedule.
     * If --no-interaction flag is set the script will not run 'queue:work' daemon.
     *
     * @param \Illuminate\Console\Scheduling\Schedule $schedule
     *
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // https://github.com/freescout-helpdesk/freescout/issues/3970
        if (!$this->isScheduleRun() && !\Helper::isRoute('system.cron')) {
            return;
        }

        // Remove failed jobs
        $schedule->command('queue:flush')
            ->weekly();

        // Restart processing queued jobs (just in case)
        $schedule->command('queue:restart')
            ->hourly();

        $schedule->command('freescout:fetch-monitor')
            ->everyMinute()
            ->withoutOverlapping();

        $schedule->command('freescout:send-monitor')
            // Every 10 minutes.
            ->cron('*/10 * * * *')
            ->withoutOverlapping();

        $schedule->command('freescout:update-folder-counters')
            ->hourly();

        $app_key = config('app.key');
        if ($app_key) {
            $crc = crc32($app_key);
            $schedule->command('freescout:module-check-licenses')
                ->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
        }

        // Check if user finished viewing conversation.
        $schedule->command('freescout:check-conv-viewers')
            ->everyMinute()
            ->withoutOverlapping();

        $schedule->command('freescout:clean-send-log')
            ->monthly();

        $schedule->command('freescout:clean-notifications-table')
            ->weekly();

        $schedule->command('freescout:clean-tmp')
            ->daily();

        // Logs monitoring.
        $alert_logs_period = config('app.alert_logs_period');
        if (config('app.alert_logs') && $alert_logs_period) {
            $logs_cron = '';
            switch ($alert_logs_period) {
                case 'hour':
                    $logs_cron = '0 * * * *';
                    break;
                case 'day':
                    $logs_cron = '0 0 * * *';
                    break;
                case 'week':
                    $logs_cron = '0 0 * * 0';
                    break;
                case 'month':
                    $logs_cron = '0 0 1 * *';
                    break;
            }
            if ($logs_cron) {
                $schedule->command('freescout:logs-monitor')
                    ->cron($logs_cron)
                    ->withoutOverlapping();
            }
        }

        $fetch_unseen = (int)config('app.fetch_unseen');
        $fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
        $fetch_command_name = 'freescout:fetch-emails'
            . ' --identifier='.$fetch_command_identifier
            . ' --unseen='.$fetch_unseen;

        // Kill fetch commands running for too long.
        // In shedule:run this code is executed every time $schedule->command() in this function is executed.
        if (function_exists('shell_exec')) {
            $fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);

            // The name of the command here must be exactly the same as below!
            // Otherwise long fetching will be killed and won't run longer than 1 mintue.
            // $schedule->command() creates a new mutex, so we need to store mutex name in cache.
            // $mutex_name = $schedule->command($fetch_command_name)
            //     ->everyMinute() - this also need to be set
            //     ->skip(function () {
            //         return true;
            //     })
            //     ->mutexName();

            $mutex_name = \Cache::get('fetch_mutex_name') ?? '';

            // If there is no cache mutext but there are running fetch commands 
            // it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
            // and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
            if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
                // Kill freescout:fetch-emails commands running for too long
                shell_exec('kill '.implode(' | kill ', $fetch_command_pids));
            } elseif (count($fetch_command_pids) == 0) {
                // Make sure 'ps' command actually works.
                $ps_works = \Helper::getRunningProcesses('schedule:run');

                if (count($ps_works)) {
                    // Previous freescout:fetch-emails may have been killed or errored and did not remove the mutex.
                    // So here we are forcefully removing the mutex. Otherwise mutex will live for 24 hours.
                    if (\Cache::has($mutex_name)) {
                        \Cache::forget($mutex_name);
                    }
                }
            }
        }

        // Fetch emails from mailboxes
        $fetch_command = $schedule->command($fetch_command_name)
            // withoutOverlapping() option creates a mutex in the cache 
            // which by default expires in 24 hours.
            // So we are passing an 'expiresAt' parameter to withoutOverlapping() to
            // prevent fetching from not being executed when fetching command by some reason
            // does not remove the mutex from the cache. 
            ->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
            ->sendOutputTo(storage_path().'/logs/fetch-emails.log');

        switch (config('app.fetch_schedule')) {
            case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
                $fetch_command->everyFiveMinutes();
                break;
            case Mail::FETCH_SCHEDULE_EVERY_TEN_MINUTES:
                $fetch_command->everyTenMinutes();
                break;
            case Mail::FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES:
                $fetch_command->everyFifteenMinutes();
                break;
            case Mail::FETCH_SCHEDULE_EVERY_THIRTY_MINUTES:
                $fetch_command->everyThirtyMinutes();
                break;
            case Mail::FETCH_SCHEDULE_HOURLY:
                $fetch_command->Hourly();
                break;
            default:
                $fetch_command->everyMinute();
                break;
        }
        \Cache::put('fetch_mutex_name', $fetch_command->mutexName(), self::FETCH_MAX_EXECUTION_TIME);

        $schedule = \Eventy::filter('schedule', $schedule);

        // If --no-daemonize flag is passed - do not run 'queue:work' daemon.
        foreach ($_SERVER['argv'] ?? [] as $arg) {
            if ($arg == '--no-interaction') {
                return;
            }
        }

        // Command runs as subprocess and sets cache mutex. If schedule:run command is killed
        // subprocess does not clear the mutex and it stays in the cache until cache:clear is executed.
        // By default, the lock will expire after 24 hours.

        $queue_work_params = Config('app.queue_work_params');
        // Add identifier to avoid conflicts with other FreeScout instances on the same server.
        $queue_work_params['--queue'] .= ','.\Helper::getWorkerIdentifier();

        // $schedule->command('queue:work') command below has withoutOverlapping() option,
        // which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
        // So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
        // and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees 
        // that there are two 'queue:work' processes running and kills them.
        // After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
        if (function_exists('shell_exec')) {
            $running_commands = \Helper::getRunningProcesses();

            if (count($running_commands) > 1) {
                // Stop all queue:work processes.
                // queue:work command is stopped by settings a cache key
                \Helper::queueWorkerRestart();
                // Sometimes processes stuck and just continue running, so we need to kill them.
                // Sleep to let processes stop.
                sleep(1);
                // Check processes again.
                $worker_pids = \Helper::getRunningProcesses();
                
                if (count($worker_pids) > 1) {
                    // Current process also has to be killed, as otherwise it "stucks"
                    // $current_pid = getmypid();
                    // foreach ($worker_pids as $i => $pid) {
                    //     if ($pid == $current_pid) {
                    //         unset($worker_pids[$i]);
                    //         break;
                    //     }
                    // }
                    shell_exec('kill '.implode(' | kill ', $worker_pids));
                }
            } elseif (count($running_commands) == 0) {
                // Make sure 'ps' command actually works.
                $ps_works = \Helper::getRunningProcesses('schedule:run');

                if (count($ps_works)) {
                    // Previous queue:work may have been killed or errored and did not remove the mutex.
                    // So here we are forcefully removing the mutex.
                    $mutex_name = $schedule->command('queue:work', $queue_work_params)
                        ->skip(function () {
                            return true;
                        })
                        ->mutexName();
                    if (\Cache::get($mutex_name)) {
                        \Cache::forget($mutex_name);
                    }
                }
            }
        }

        $schedule->command('queue:work', $queue_work_params)
            ->everyMinute()
            ->withoutOverlapping()
            ->sendOutputTo(storage_path().'/logs/queue-jobs.log');
    }

    /**
     * This function is needed because every time $schedule->command() is executed 
     * the schedule() is executed also.
     */
    public function isScheduleRun()
    {
        if (\Helper::isConsole()) {
            return true;
        } else {
            return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);
        }
    }

    /**
     * Register the commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        $this->load(__DIR__.'/Commands');

        // Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
        // to set the host for EHLO command, if it is empty it uses [127.0.0.1].
        // G Suite sometimes rejects emails with EHLO [127.0.0.1].
        if (empty($_SERVER['SERVER_NAME'])) {
            $_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
        }

        require base_path('routes/console.php');
    }
}
