<?php

namespace Ignite\Core\Console\Dev;

use Illuminate\Console\Command;
use Illuminate\Support\Str;

/**
 * There's still quite a bit of work to do on this. No warranty right now :).
 */
class OverrideClass extends Command
{
    /**
     * @var string
     */
    protected $signature = 'ignite:dev:override-class {classFQN?} {--force}';

    /**
     * @var string
     */
    protected $description = 'Override an Ignite class in your program module';

    /**
     * @var \ReflectionClass
     */
    protected $reflector;

    /**
     * Handle the command.
     */
    public function handle()
    {
        if (! $this->haveProgramModule()) {
            $this->error('This app does not have a program module.');
            return;
        }

        $classToOverride = $this->argument('classFQN') ?? $this->ask('Class FQN');
        $this->info('Overriding '.$classToOverride);

        if (! class_exists($classToOverride)) {
            $this->error($classToOverride.' does not exist');
            return;
        }

        $this->reflector = new \ReflectionClass($classToOverride);

        if (! $this->isOverridable()) {
            $this->error('Cannot generate override class for this class: '.$classToOverride);
            return;
        }

        try {
            $this->copyToProgram();
            $this->updateContainerBindings();
        } catch (\RuntimeException $e) {
            $this->error($e->getMessage());
            return;
        }
    }

    /**
     * Get a path in the program directory.
     *
     * @param string|null $path
     * @return string
     */
    protected static function programPath(?string $path = null): string
    {
        $path = $path ? '/'.ltrim($path, '/') : '';

        return base_path('modules/Program'.$path);
    }

    /**
     * Does this app have a Program module?
     *
     * @return bool
     */
    protected function haveProgramModule(): bool
    {
        return is_dir(self::programPath());
    }

    /**
     * Is this class something we can override?
     * Currently, only supporting Ignite classes.
     *
     * @return bool
     */
    protected function isOverridable(): bool
    {
        return Str::startsWith($this->reflector->getNamespaceName(), 'Ignite');
    }

    /**
     * Get the namespace we should use for this file when it is in the program module.
     *
     * @return string
     */
    protected function classProgramNamespace(): string
    {
        $namespaceParts = explode('\\', $this->reflector->getNamespaceName());
        $namespaceParts[1] = 'Program';

        return join('\\', $namespaceParts);
    }

    /**
     * Get the namespace we should use for the Program version of this class.
     *
     * @return string
     */
    protected function classProgramFQN(): string
    {
        return $this->classProgramNamespace().'\\'.$this->reflector->getShortName();
    }

    /**
     * Get the path we should put this file when it is in the program module.
     *
     * @return string
     */
    protected function classProgramPath(): string
    {
        $path = collect(explode('\\', $this->reflector->getNamespaceName()))
            ->skip(2)
            ->join('/');

        return self::programPath('src/'.$path.'/'.basename($this->reflector->getFileName()));
    }

    /**
     * Get the alias that we'll use for the overridden class.
     *
     * @return string
     */
    protected function overriddenClassAlias(): string
    {
        $ignitePackage = collect(explode('\\', $this->reflector->getName()))
            ->skip(1)
            ->first();

        return $ignitePackage.class_basename($this->reflector->getName());
    }

    /**
     * Get the use statement for the overridden class (this aliases the class).
     *
     * @return string
     */
    protected function overriddenUseStatement(): string
    {
        return "use {$this->reflector->getName()} as {$this->overriddenClassAlias()};";
    }

    /**
     * Generate the source of the file for this class that we'll use in the Program directory.
     *
     * @return string
     */
    protected function generateProgramSource(): string
    {
        return <<<PHP
<?php

namespace {$this->classProgramNamespace()};

{$this->overriddenUseStatement()}

class {$this->reflector->getShortName()} extends {$this->overriddenClassAlias()}
{
}
PHP;
    }

    /**
     * Copy the file for the class we're overriding to its equivalent Program path.
     *
     * @return void
     * @throws \RuntimeException
     */
    protected function copyToProgram(): void
    {
        $programPath = $this->classProgramPath();

        if (file_exists($programPath) && ! $this->option('force')) {
            throw new \RuntimeException('Cannot copy. File already exists: '.$programPath);
        }

        $this->ensureProgramDirectoryExists();

        file_put_contents($programPath, $this->generateProgramSource());

        $this->info('Copied to: '.$programPath);
    }

    /**
     * Ensure the directory that we'll place this file in exists and is a directory.
     *
     * @return void
     * @throws \RuntimeException
     */
    protected function ensureProgramDirectoryExists(): void
    {
        $programDir = dirname($this->classProgramPath());

        if (file_exists($programDir)) {
            if (! is_dir($programDir)) {
                throw new \RuntimeException('Destination directory is a file: '.$programDir);
            }
        } else {
            mkdir($programDir, 0755, true);
        }
    }

    /**
     * Update Laravel's container, so it looks for the override class instead of the overridden one.
     *
     * @return void
     */
    protected function updateContainerBindings(): void
    {
        $serviceProviderClass = '\Ignite\Program\Providers\ProgramServiceProvider';

        if (! class_exists($serviceProviderClass)) {
            $this->warn('Not updating service provider. Not found: '.$serviceProviderClass);
            return;
        }

        $reflector = new \ReflectionClass($serviceProviderClass);

        $providerContents = file_get_contents($reflector->getFileName());

        $providerContents = preg_replace(
            '/;\s+^class/m',
            ';'.PHP_EOL.$this->overriddenUseStatement().PHP_EOL.
            '// TODO Add this to the bindings array:'.PHP_EOL.
            "{$this->overriddenClassAlias()}::class => \\{$this->classProgramFQN()}::class,".
            PHP_EOL.PHP_EOL.'class',
            $providerContents
        );

        file_put_contents($reflector->getFileName(), $providerContents);

        $this->info('Updated service provider. Make sure to review changes. '.$serviceProviderClass);
    }
}
