<?php

namespace Ignite\Core\Console\Dev;

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

/**
 * Command to override Ignite classes in the program module.
 *
 * This command allows developers to create program-specific overrides of Ignite framework classes.
 * It handles the following tasks:
 * 1. Validates the target class exists and is overridable
 * 2. Creates a new class in the program module that extends the original
 * 3. Updates service provider bindings to use the overridden class
 */
class OverrideClass extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ignite:dev:override-class {classFQN?} {--force}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Override an Ignite class in your program module. WIP - No warranty right now.';

    /**
     * Reflection instance of the class being overridden.
     *
     * @var \ReflectionClass
     */
    protected $reflector;

    /**
     * Execute the console command.
     *
     * This method:
     * 1. Validates the program module exists
     * 2. Gets the target class to override
     * 3. Validates the class exists and is overridable
     * 4. Copies the class to the program module
     * 5. Updates container bindings
     *
     * @return void
     */
    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 the absolute path to the program module directory.
     *
     * @param string|null $path Optional subpath within the program module
     * @return string Absolute path to the program module or subdirectory
     */
    protected static function programPath(?string $path = null): string
    {
        $path = $path ? '/'.ltrim($path, '/') : '';

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

    /**
     * Check if the application has a Program module.
     *
     * @return bool True if the program module directory exists
     */
    protected function haveProgramModule(): bool
    {
        return is_dir(self::programPath());
    }

    /**
     * Determine if the target class can be overridden.
     * Currently only allows overriding classes in the Ignite namespace.
     *
     * @return bool True if the class can be overridden
     */
    protected function isOverridable(): bool
    {
        return Str::startsWith($this->reflector->getNamespaceName(), 'Ignite');
    }

    /**
     * Generate the namespace for the overridden class in the program module.
     * Replaces the 'Core' namespace segment with 'Program'.
     *
     * @return string The namespace for the program version of the class
     */
    protected function classProgramNamespace(): string
    {
        $namespaceParts = explode('\\', $this->reflector->getNamespaceName());
        $namespaceParts[1] = 'Program';

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

    /**
     * Get the fully qualified name for the program version of the class.
     *
     * @return string The FQN including namespace and class name
     */
    protected function classProgramFQN(): string
    {
        return $this->classProgramNamespace().'\\'.$this->reflector->getShortName();
    }

    /**
     * Get the filesystem path where the overridden class should be stored.
     *
     * @return string Absolute path to the program class file
     */
    protected function classProgramPath(): string
    {
        $path = collect(explode('\\', $this->reflector->getNamespaceName()))
            ->skip(2)
            ->join('/');

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

    /**
     * Generate an alias for the original class to avoid naming conflicts.
     * Format: {PackageName}{ClassName}
     *
     * @return string The alias to use for the original class
     */
    protected function overriddenClassAlias(): string
    {
        $ignitePackage = collect(explode('\\', $this->reflector->getName()))
            ->skip(1)
            ->first();

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

    /**
     * Generate the use statement that aliases the original class.
     *
     * @return string The use statement with alias
     */
    protected function overriddenUseStatement(): string
    {
        return "use {$this->reflector->getName()} as {$this->overriddenClassAlias()};";
    }

    /**
     * Generate the source code for the overridden class.
     * Creates a new class that extends the original with an alias.
     *
     * @return string The PHP source code for the overridden class
     */
    protected function generateProgramSource(): string
    {
        return <<<PHP
<?php

namespace {$this->classProgramNamespace()};

{$this->overriddenUseStatement()}

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

    /**
     * Copy the overridden class to the program module.
     * Creates necessary directories and writes the generated source code.
     *
     * @return void
     * @throws \RuntimeException If the file already exists and --force is not used
     */
    protected function copyToProgram(): void
    {
        $programPath = $this->classProgramPath();
        // unlink($programPath);

        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 target directory exists in the program module.
     * Creates the directory structure if it doesn't exist.
     *
     * @return void
     * @throws \RuntimeException If the target path exists but is not a directory
     */
    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 the service provider to bind the original class to the overridden version.
     * Modifies the ProgramServiceProvider to:
     * 1. Add the use statement for the original class with alias
     * 2. Add the binding in the $bindings array
     *
     * @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());

        // add 'use'
        $providerContents = preg_replace(
            '/;\s+^class/m',
            ';' . PHP_EOL . $this->overriddenUseStatement() . PHP_EOL
            . PHP_EOL . 'class',
            $providerContents
        );

        // add to 'bindings' array
        $providerContents = preg_replace(
            '/public \$bindings = \[/m',
            'public \$bindings = [' . PHP_EOL .
            "\t\t{$this->overriddenClassAlias()}::class => \\{$this->classProgramFQN()}::class,",
            $providerContents
        );

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

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