<?php

namespace CleantalkSP\SpbctWP\Scanner\OSCron;

use CleantalkSP\SpbctWP\Scanner\OSCron\Objects\OSCronTask;
use CleantalkSP\SpbctWP\Scanner\OSCron\Shell\OSCronGetEnvCron;
use CleantalkSP\SpbctWP\Scanner\OSCron\Shell\OSCronSetEnvCron;
use CleantalkSP\SpbctWP\Scanner\OSCron\Storages\OsCronTasksStorage;
use CleantalkSP\SpbctWP\Scanner\OSCron\View\OSCronLocale;
use CleantalkSP\SpbctWP\Scanner\Controller;

class OSCronModel
{
    private static $spbc_marker = '# Disabled by Security by CleanTalk #';

    /**
     * Runs the cron model to update tasks and save the cron file.
     *
     * @return string|true The result of the operation or an error message.
     */
    public static function run()
    {
        try {
            // Retrieve the current environment crontab
            $env_cron = static::getEnvTasks();

            // Parse the tasks from the cron file
            $tasks = static::parseEnvTasks($env_cron);

            // Analyze the tasks to determine if they are dangerous
            $tasks = static::analyzeEnvTasks($tasks);

            // Save the tasks to storage
            OsCronTasksStorage::set($tasks);
            //FYI This is end of scanner stage
        } catch (\Exception $error) {
            return $error->getMessage();
        }
        return true;
    }

    /**
     * Sync the crontab data rewriting storage data.
     * @return string The content of the cron file IN STORAGE.
     * @throws \Exception
     */
    public static function getEnvTasks()
    {
        // Retrieve the current environment crontab
        $cron_content = static::getEnvCron();
        if (empty($cron_content)) {
            throw new \Exception(OSCronLocale::getInstance()->error__load_env_cron);
        }
        return $cron_content;
    }


    /**
     *  -- TASKS --
     */

    /**
     * Parses the cron file into tasks.
     *
     * @param string $cron_file The content of the cron file.
     * @return OSCronTask[] The parsed tasks.
     * @throws \Exception If an error occurs during parsing.
     */
    public static function parseEnvTasks($cron_file)
    {
        // Split the cron file content into lines and trim each line
        $task_text_from_env = explode("\n", $cron_file);

        $env_tasks = array();

        $line_number = 0;

        // Iterate over each line in the cron file
        foreach ($task_text_from_env as $whole_task) {
            $line_number++;
            $status = 'enabled';

            // Skip empty lines and comments, mark disabled tasks
            if (
                empty($whole_task) ||
                (
                    strpos($whole_task, '#') === 0 &&
                    strpos($whole_task, static::$spbc_marker) === false
                )
            ) {
                continue;
            }

            $whole_task = trim($whole_task);

            if ( strpos($whole_task, static::$spbc_marker) !== false ) {
                // Clear from mark to get correct result of repeats
                $whole_task = str_replace(static::$spbc_marker, '', $whole_task);
                $status     = 'disabled';
            }

            // Split the line into parts and extract the time pattern and command
            $parts = preg_split('/\s/', $whole_task, 6);
            $time_pattern = implode(' ', array_slice($parts, 0, 5));
            if (empty($parts[5])) {
                // can not parse the command
                continue;
            }
            $command = $parts[5];

            // Create a new task and set its properties
            $task = new OSCronTask($whole_task);
            $env_tasks[] = $task->setCommand($command)
                ->setStatus($status)
                ->setLineNumber($line_number)
                ->setRepeatsPattern($time_pattern)
                ->validate();
        }

        return $env_tasks;
    }

    /**
     * Analyzes the tasks, set analysis verdict and set statuses of previous checks.
     *
     * @param OSCronTask[] $tasks The tasks to analyze.
     *
     * @return OSCronTask[] The analyzed tasks.
     * @throws \Exception If an error occurs during analysis.
     */
    public static function analyzeEnvTasks($tasks)
    {
        $tasks_in_storage = OsCronTasksStorage::get();

        if ( $tasks_in_storage ) {
            $i = 0;
            foreach ( $tasks as $env_task ) {
                foreach ( $tasks_in_storage as $storage_task ) {
                    if ( $env_task->hash === $storage_task->hash && $storage_task->verdict !== 'unchecked' ) {
                        // Task was analysed early
                        $tasks[$i] = $storage_task;
                    }
                }
                $i++;
            }
        }

        $task_analyser  = new OSCronTaskAnalyser();
        $analysed_tasks = array();
        $signatures = Controller::getSignatures();
        foreach ( $tasks as $env_task ) {
            if ( $env_task->verdict === 'unchecked' ) {
                $task_is_dangerous = $task_analyser->check($env_task->command, $signatures)->getVerdict();
                $analysis_verdict  = $task_is_dangerous
                    ? 'critical'
                    : 'checked';
                $task_status       = $task_is_dangerous
                    ? 'critical'
                    : $env_task->status;
                $env_task->setVerdict($analysis_verdict);
                $env_task->setStatus($task_status);

                $env_task->signature_matches = $task_analyser->getSignatureMatches();
            }
            $analysed_tasks[] = $env_task;
        }

        return $analysed_tasks;
    }

    /**
     * Updates a task by its unique identifier.
     *
     * @param string $uid The unique identifier of the task.
     * @param OSCronTask $new_task The new task data.
     * @return string|false The index of the updated task or false if not found.
     * @throws \Exception If an error occurs during update.
     */
    public static function updateTaskOfStorageById($uid, $new_task)
    {
        $new_task->validate();
        $tasks = OsCronTasksStorage::get();
        foreach ($tasks as $key => $task) {
            if ($task->id === $uid) {
                $tasks[$key] = $new_task;
                OsCronTasksStorage::set($tasks);
                return $uid;
            }
        }
        return false;
    }

    /**
     * Prepares new crontab content and writes the tasks to the environment crontab.
     *
     * @return true
     * @throws \Exception
     */
    public static function writeEnvCron()
    {
        // New tasks to write to the crontab
        $tasks          = OsCronTasksStorage::get();
        $crontab_string = '';
        foreach ( $tasks as $task ) {
            if ( $task->status === 'disabled' ) {
                $crontab_string .= self::$spbc_marker;
            }
            $crontab_string .= $task->whole_line . "\n";
        }
        // Save the updated cron file to the environment crontab
        static::setEnvCron($crontab_string);

        return true;
    }

    /**
     * -- THE SHELL --
     */

    /**
     * Retrieves the current environment crontab.
     *
     * @return string The current environment crontab.
     * @throws \Exception If an error occurs during retrieval.
     */
    private static function getEnvCron()
    {
        // Check if the operating system is Windows
        if (static::isWindows()) {
            throw new \Exception(OSCronLocale::getInstance()->no_windows_support);
        }

        return OSCronGetEnvCron::get();
    }

    /**
     * Saves the environment crontab.
     *
     * @param string $file_content The content to save to the crontab.
     *
     * @throws \Exception If an error occurs during saving.
     */
    private static function setEnvCron($file_content)
    {
        if (static::isWindows()) {
            throw new \Exception(OSCronLocale::getInstance()->no_windows_support);
        }
        OSCronSetEnvCron::set(rtrim($file_content));
    }

    /**
     *  -- MISC --
     */

    /**
     * Checks if the operating system is Windows.
     *
     * @return bool True if the operating system is Windows, false otherwise.
     */
    public static function isWindows()
    {
        return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
    }
}
