<?php

declare(strict_types=1);

namespace SPC\command;

use Laravel\Prompts\ConfirmPrompt;
use Laravel\Prompts\Prompt;
use Psr\Log\LogLevel;
use SPC\ConsoleApplication;
use SPC\exception\ExceptionHandler;
use SPC\exception\SPCException;
use SPC\util\AttributeMapper;
use SPC\util\GlobalEnvManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZM\Logger\ConsoleLogger;

abstract class BaseCommand extends Command
{
    protected bool $no_motd = false;

    protected InputInterface $input;

    protected OutputInterface $output;

    public function __construct(?string $name = null)
    {
        parent::__construct($name);
        $this->addOption('debug', null, null, 'Enable debug mode');
        $this->addOption('no-motd', null, null, 'Disable motd');
        $this->addOption('preserve-log', null, null, 'Preserve log files, do not delete them on initialized');
    }

    public function initialize(InputInterface $input, OutputInterface $output): void
    {
        if ($input->getOption('no-motd')) {
            $this->no_motd = true;
        }

        set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) {
            $tips = [
                E_WARNING => ['PHP Warning: ', 'warning'],
                E_NOTICE => ['PHP Notice: ', 'notice'],
                E_USER_ERROR => ['PHP Error: ', 'error'],
                E_USER_WARNING => ['PHP Warning: ', 'warning'],
                E_USER_NOTICE => ['PHP Notice: ', 'notice'],
                E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'],
                E_DEPRECATED => ['PHP Deprecated: ', 'notice'],
                E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'],
            ];
            $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error'];
            $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
            logger()->{$level_tip[1]}($error);
            // 如果 return false 则错误会继续递交给 PHP 标准错误处理
            return true;
        });
        $version = ConsoleApplication::VERSION;
        if (!$this->no_motd) {
            echo "     _        _   _                 _
 ___| |_ __ _| |_(_) ___      _ __ | |__  _ __
/ __| __/ _` | __| |/ __|____| '_ \\| '_ \\| '_ \\
\\__ \\ || (_| | |_| | (_|_____| |_) | | | | |_) |
|___/\\__\\__,_|\\__|_|\\___|    | .__/|_| |_| .__/   v{$version}
                             |_|         |_|
";
        }
    }

    abstract public function handle(): int;

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->input = $input;
        $this->output = $output;

        // init log
        $this->initLogFiles();

        // init logger
        $this->initConsoleLogger();

        // load attribute maps
        AttributeMapper::init();

        // init windows fallback
        $this->initWindowsPromptFallback($input, $output);

        // init GlobalEnv
        if (!$this instanceof BuildCommand) {
            GlobalEnvManager::init();
            f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
        }

        try {
            // show raw argv list for logger()->debug
            logger()->debug('argv: ' . implode(' ', $_SERVER['argv']));
            return $this->handle();
        } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) {
            // Handle SPCException and log it
            ExceptionHandler::handleSPCException($e);
            return static::FAILURE;
        } catch (\Throwable $e) {
            // Handle any other exceptions
            ExceptionHandler::handleDefaultException($e);
            return static::FAILURE;
        }
    }

    protected function getOption(string $name): mixed
    {
        return $this->input->getOption($name);
    }

    protected function getArgument(string $name): mixed
    {
        return $this->input->getArgument($name);
    }

    protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int
    {
        if ($result) {
            logger()->info($success_msg);
            return static::SUCCESS;
        }
        logger()->error($fail_msg);
        return static::FAILURE;
    }

    /**
     * Parse extension list from string, replace alias and filter internal extensions.
     *
     * @param array|string $ext_list Extension string list, e.g. "mbstring,posix,sockets" or array
     */
    protected function parseExtensionList(array|string $ext_list): array
    {
        // replace alias
        $ls = array_map(function ($x) {
            $lower = strtolower(trim($x));
            if (isset(SPC_EXTENSION_ALIAS[$lower])) {
                logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.');
                return SPC_EXTENSION_ALIAS[$lower];
            }
            return $lower;
        }, is_array($ext_list) ? $ext_list : array_filter(explode(',', $ext_list)));

        // filter internals
        return array_values(array_filter($ls, function ($x) {
            if (in_array($x, SPC_INTERNAL_EXTENSIONS)) {
                logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored.");
                return false;
            }
            return true;
        }));
    }

    /**
     * Initialize spc log files.
     */
    private function initLogFiles(): void
    {
        $log_dir = SPC_LOGS_DIR;
        if (!file_exists($log_dir)) {
            mkdir($log_dir, 0755, true);
        } elseif (!$this->getOption('preserve-log')) {
            // Clean up old log files
            $files = glob($log_dir . '/*.log');
            foreach ($files as $file) {
                if (is_file($file)) {
                    unlink($file);
                }
            }
        }
    }

    /**
     * Initialize console logger.
     */
    private function initConsoleLogger(): void
    {
        global $ob_logger;
        if ($this->input->getOption('debug') || $this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
            $ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$this->input->getOption('no-ansi'));
            define('DEBUG_MODE', true);
        } else {
            $ob_logger = new ConsoleLogger(decorated: !$this->input->getOption('no-ansi'));
        }
        $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
        $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
            if ($log_file_fd) {
                fwrite($log_file_fd, strip_ansi_colors($output) . "\n");
            }
            return true;
        });
    }

    /**
     * Initialize Windows prompt fallback for laravel-prompts.
     */
    private function initWindowsPromptFallback(InputInterface $input, OutputInterface $output): void
    {
        Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
        ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {
            $helper = new QuestionHelper();
            $case = $prompt->default ? ' [Y/n] ' : ' [y/N] ';
            $question = new ConfirmationQuestion($prompt->label . $case, $prompt->default);
            return $helper->ask($input, $output, $question);
        });
    }
}
