<?php

declare(strict_types=1);

namespace StaticPHP\Doctor;

use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Registry\DoctorLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use ZM\Logger\ConsoleColor;

readonly class Doctor
{
    public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true)
    {
        // debug shows all loaded doctor items
        $items = DoctorLoader::getDoctorItems();
        $names = array_map(fn ($i) => $i->item_name, array_map(fn ($x) => $x[0], $items));
        logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names));
    }

    /**
     * Returns true if doctor was previously passed with the current SPC version.
     */
    public static function isHealthy(): bool
    {
        $lock = self::getLockPath();
        return file_exists($lock) && trim((string) @file_get_contents($lock)) === \StaticPHP\ConsoleApplication::VERSION;
    }

    /**
     * Write current SPC version to the lock file, marking doctor as passed.
     */
    public static function markPassed(): void
    {
        $primary = self::getLockPath();
        if (!is_dir(dirname($primary))) {
            @mkdir(dirname($primary), 0755, true);
        }
        if (@file_put_contents($primary, \StaticPHP\ConsoleApplication::VERSION) === false) {
            @file_put_contents((getcwd() ?: '.') . DIRECTORY_SEPARATOR . '.spc-doctor.lock', \StaticPHP\ConsoleApplication::VERSION);
        }
    }

    /**
     * Check all valid check items.
     * @return bool true if all checks passed, false otherwise
     */
    public function checkAll(): bool
    {
        if ($this->interactive) {
            InteractiveTerm::notice('Starting doctor checks ...');
        }
        foreach ($this->getValidCheckList() as $check) {
            if (!$this->checkItem($check)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check a single check item.
     *
     * @param  CheckItem|string $check The check item to be checked
     * @return bool             True if the check passed or was fixed, false otherwise
     */
    public function checkItem(CheckItem|string $check): bool
    {
        if (is_string($check)) {
            $found = null;
            foreach (DoctorLoader::getDoctorItems() as $item) {
                if ($item[0]->item_name === $check) {
                    $found = $item[0];
                    break;
                }
            }
            if ($found === null) {
                $this->output?->writeln("<error>Check item '{$check}' not found.</error>");
                return false;
            }
            $check = $found;
        }
        $prepend = $this->interactive ? '  - ' : '';
        $this->output?->write("{$prepend}Checking <comment>{$check->item_name}</comment> ... ");

        // call check
        $result = call_user_func($check->callback);

        if ($result === null) {
            $this->output?->writeln('skipped');
            return true;
        }
        if (!$result instanceof CheckResult) {
            $this->output?->writeln('<error>Skipped due to invalid return value</error>');
            return true;
        }
        if ($result->isOK()) {
            /* @phpstan-ignore-next-line */
            $this->output?->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓'));
            return true;
        }
        $this->output?->writeln('<error>' . $result->getMessage() . '</error>');

        // if the check item is not fixable, fail immediately
        if ($result->getFixItem() === '') {
            $this->output?->writeln('This check item can not be fixed automatically !');
            return false;
        }
        // unknown fix item
        if (!DoctorLoader::getFixItem($result->getFixItem())) {
            $this->output?->writeln("<error>Internal error: Unknown fix item: {$result->getFixItem()}</error>");
            return false;
        }
        // skip fix
        if ($this->auto_fix === FIX_POLICY_DIE) {
            $this->output?->writeln('<comment>Auto-fix is disabled. Please fix this issue manually.</comment>');
            return false;
        }
        // prompt for fix
        if ($this->auto_fix === FIX_POLICY_PROMPT) {
            $helper = new QuestionHelper();
            $input = ApplicationContext::has(InputInterface::class) ? ApplicationContext::get(InputInterface::class) : new ArrayInput([]);
            $output = ApplicationContext::has(OutputInterface::class) ? ApplicationContext::get(OutputInterface::class) : $this->output ?? new ConsoleOutput();
            if (!$helper->ask($input, $output, new ConfirmationQuestion('Do you want to try to fix this issue now? [Y/n] ', true))) {
                $this->output?->writeln('<comment>You canceled fix.</comment>');
                return false;
            }
        }
        // perform fix
        InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... ");
        Shell::passthruCallback(function () {
            InteractiveTerm::advance();
        });
        // $this->output?->writeln("Fixing <comment>{$check->item_name}</comment> ... ");
        if ($this->emitFix($result->getFixItem(), $result->getFixParams())) {
            InteractiveTerm::finish('Fix applied successfully!');
            return true;
        }
        InteractiveTerm::finish('Failed to apply fix!', false);
        return false;
    }

    private static function getLockPath(): string
    {
        if (SystemTarget::getTargetOS() === 'Windows') {
            $trial_ls = [
                getenv('LOCALAPPDATA') ?
                    (getenv('LOCALAPPDATA') . '\.spc-doctor.lock') :
                    (((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock'),
                sys_get_temp_dir() . '\.spc-doctor.lock',
                WORKING_DIR . '\.spc-doctor.lock',
            ];
        } else {
            $trial_ls = [
                getenv('XDG_CACHE_HOME') ?
                    (getenv('XDG_CACHE_HOME') . '/.spc-doctor.lock')
                    : (((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock'),
                sys_get_temp_dir() . '/.spc-doctor.lock',
                WORKING_DIR . '/.spc-doctor.lock',
            ];
        }
        foreach ($trial_ls as $path) {
            if (is_writable(dirname($path))) {
                return $path;
            }
        }
        // fallback to current directory
        return WORKING_DIR . DIRECTORY_SEPARATOR . '.spc-doctor.lock';
    }

    private function emitFix(string $fix_item, array $fix_item_params = []): bool
    {
        keyboard_interrupt_register(function () {
            $this->output?->writeln('<error>You cancelled fix</error>');
        });
        try {
            return ApplicationContext::invoke(DoctorLoader::getFixItem($fix_item), $fix_item_params);
        } catch (SPCException $e) {
            $this->output?->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
            return false;
        } catch (\Throwable $e) {
            logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
            $this->output?->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
            return false;
        } finally {
            keyboard_interrupt_unregister();
        }
    }

    /**
     * Get a list of valid check items for current environment.
     */
    private function getValidCheckList(): iterable
    {
        foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) {
            /* @var CheckItem $item */
            // optional check
            /* @phpstan-ignore-next-line */
            if (is_callable($optional) && !call_user_func($optional)) {
                continue; // skip this when the optional check is false
            }
            // limit_os check
            if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) {
                continue;
            }
            // skipped items by env
            $skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: ''));
            if (in_array($item->item_name, $skip_items)) {
                continue; // skip this item
            }
            yield $item;
        }
    }
}
