<?php

/**
 * @package    Grav\Console\Cli
 *
 * @copyright  Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Console\Cli;

use Grav\Common\Filesystem\Folder;
use Grav\Common\Utils;
use Grav\Console\GravCommand;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function count;

/**
 * Class SandboxCommand
 * @package Grav\Console\Cli
 */
class SandboxCommand extends GravCommand
{
    /** @var array */
    protected $directories = [
        '/assets',
        '/backup',
        '/cache',
        '/images',
        '/logs',
        '/tmp',
        '/user/accounts',
        '/user/config',
        '/user/data',
        '/user/pages',
        '/user/plugins',
        '/user/themes',
    ];

    /** @var array */
    protected $files = [
        '/.dependencies',
        '/.htaccess',
        '/user/config/site.yaml',
        '/user/config/system.yaml',
    ];

    /** @var array */
    protected $mappings = [
        '/.gitignore'           => '/.gitignore',
        '/.editorconfig'        => '/.editorconfig',
        '/CHANGELOG.md'         => '/CHANGELOG.md',
        '/LICENSE.txt'          => '/LICENSE.txt',
        '/README.md'            => '/README.md',
        '/CONTRIBUTING.md'      => '/CONTRIBUTING.md',
        '/index.php'            => '/index.php',
        '/composer.json'        => '/composer.json',
        '/bin'                  => '/bin',
        '/system'               => '/system',
        '/vendor'               => '/vendor',
        '/webserver-configs'    => '/webserver-configs',
    ];

    /** @var string */
    protected $source;
    /** @var string */
    protected $destination;

    /**
     * @return void
     */
    protected function configure(): void
    {
        $this
            ->setName('sandbox')
            ->setDescription('Setup of a base Grav system in your webroot, good for development, playing around or starting fresh')
            ->addArgument(
                'destination',
                InputArgument::REQUIRED,
                'The destination directory to symlink into'
            )
            ->addOption(
                'symlink',
                's',
                InputOption::VALUE_NONE,
                'Symlink the base grav system'
            )
            ->setHelp("The <info>sandbox</info> command help create a development environment that can optionally use symbolic links to link the core of grav to the git cloned repository.\nGood for development, playing around or starting fresh");

        $source = getcwd();
        if ($source === false) {
            throw new RuntimeException('Internal Error');
        }
        $this->source = $source;
    }

    /**
     * @return int
     */
    protected function serve(): int
    {
        $input = $this->getInput();

        $this->destination = $input->getArgument('destination');

        // Create Some core stuff if it doesn't exist
        $error = $this->createDirectories();
        if ($error) {
            return $error;
        }

        // Copy files or create symlinks
        $error = $input->getOption('symlink') ? $this->symlink() : $this->copy();
        if ($error) {
            return $error;
        }

        $error = $this->pages();
        if ($error) {
            return $error;
        }

        $error = $this->initFiles();
        if ($error) {
            return $error;
        }

        $error = $this->perms();
        if ($error) {
            return $error;
        }

        return 0;
    }

    /**
     * @return int
     */
    private function createDirectories(): int
    {
        $io = $this->getIO();

        $io->newLine();
        $io->writeln('<comment>Creating Directories</comment>');
        $dirs_created = false;

        if (!file_exists($this->destination)) {
            Folder::create($this->destination);
        }

        foreach ($this->directories as $dir) {
            if (!file_exists($this->destination . $dir)) {
                $dirs_created = true;
                $io->writeln('    <cyan>' . $dir . '</cyan>');
                Folder::create($this->destination . $dir);
            }
        }

        if (!$dirs_created) {
            $io->writeln('    <red>Directories already exist</red>');
        }

        return 0;
    }

    /**
     * @return int
     */
    private function copy(): int
    {
        $io = $this->getIO();

        $io->newLine();
        $io->writeln('<comment>Copying Files</comment>');


        foreach ($this->mappings as $source => $target) {
            if ((string)(int)$source === (string)$source) {
                $source = $target;
            }

            $from = $this->source . $source;
            $to = $this->destination . $target;

            $io->writeln('    <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
            @Folder::rcopy($from, $to);
        }

        return 0;
    }

    /**
     * @return int
     */
    private function symlink(): int
    {
        $io = $this->getIO();

        $io->newLine();
        $io->writeln('<comment>Resetting Symbolic Links</comment>');

        // Symlink also tests if using git.
        if (is_dir($this->source . '/tests')) {
            $this->mappings['/tests'] = '/tests';
        }

        foreach ($this->mappings as $source => $target) {
            if ((string)(int)$source === (string)$source) {
                $source = $target;
            }

            $from = $this->source . $source;
            $to = $this->destination . $target;

            $io->writeln('    <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);

            if (is_dir($to)) {
                @Folder::delete($to);
            } else {
                @unlink($to);
            }
            symlink($from, $to);
        }

        return 0;
    }

    /**
     * @return int
     */
    private function pages(): int
    {
        $io = $this->getIO();

        $io->newLine();
        $io->writeln('<comment>Pages Initializing</comment>');

        // get pages files and initialize if no pages exist
        $pages_dir = $this->destination . '/user/pages';
        $pages_files = array_diff(scandir($pages_dir), ['..', '.']);

        if (count($pages_files) === 0) {
            $destination = $this->source . '/user/pages';
            Folder::rcopy($destination, $pages_dir);
            $io->writeln('    <cyan>' . $destination . '</cyan> <comment>-></comment> Created');
        }

        return 0;
    }

    /**
     * @return int
     */
    private function initFiles(): int
    {
        if (!$this->check()) {
            return 1;
        }

        $io = $this->getIO();
        $io->newLine();
        $io->writeln('<comment>File Initializing</comment>');
        $files_init = false;

        // Copy files if they do not exist
        foreach ($this->files as $source => $target) {
            if ((string)(int)$source === (string)$source) {
                $source = $target;
            }

            $from = $this->source . $source;
            $to = $this->destination . $target;

            if (!file_exists($to)) {
                $files_init = true;
                copy($from, $to);
                $io->writeln('    <cyan>' . $target . '</cyan> <comment>-></comment> Created');
            }
        }

        if (!$files_init) {
            $io->writeln('    <red>Files already exist</red>');
        }

        return 0;
    }

    /**
     * @return int
     */
    private function perms(): int
    {
        $io = $this->getIO();
        $io->newLine();
        $io->writeln('<comment>Permissions Initializing</comment>');

        $dir_perms = 0755;

        $binaries = glob($this->destination . DS . 'bin' . DS . '*');

        foreach ($binaries as $bin) {
            chmod($bin, $dir_perms);
            $io->writeln('    <cyan>bin/' . Utils::basename($bin) . '</cyan> permissions reset to ' . decoct($dir_perms));
        }

        $io->newLine();

        return 0;
    }

    /**
     * @return bool
     */
    private function check(): bool
    {
        $success = true;
        $io = $this->getIO();

        if (!file_exists($this->destination)) {
            $io->writeln('    file: <red>' . $this->destination . '</red> does not exist!');
            $success = false;
        }

        foreach ($this->directories as $dir) {
            if (!file_exists($this->destination . $dir)) {
                $io->writeln('    directory: <red>' . $dir . '</red> does not exist!');
                $success = false;
            }
        }

        foreach ($this->mappings as $target => $link) {
            if (!file_exists($this->destination . $target)) {
                $io->writeln('    mappings: <red>' . $target . '</red> does not exist!');
                $success = false;
            }
        }

        if (!$success) {
            $io->newLine();
            $io->writeln('<comment>install should be run with --symlink|--s to symlink first</comment>');
        }

        return $success;
    }
}