<?php

declare(strict_types=1);

namespace App\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Dependency\Dependency;
use App\Framework\DI\Dependency\DependencyType;
use App\Framework\Reflection\ReflectionProvider;

/**
 * Compiles a Container into optimized PHP code for production use
 */
final readonly class ContainerCompiler
{
    public function __construct(
        private ReflectionProvider $reflectionProvider,
        private DependencyResolver $dependencyResolver
    ) {
    }

    /**
     * Compile container to PHP file with all bindings and dependencies
     */
    public function compile(DefaultContainer $container, string $outputPath, array $options = []): void
    {
        $code = $this->generateContainerCode($container, $options);

        $this->writeToFile($outputPath, $code);
    }

    /**
     * Generate the complete compiled container PHP code
     */
    private function generateContainerCode(DefaultContainer $container, array $options): string
    {
        $timestamp = date('Y-m-d H:i:s');
        $hash = $this->generateContainerHash($container);

        $bindings = $this->compileBindings($container);
        $singletons = $this->compileSingletons($container);
        $dependencies = $this->compileDependencyMethods($container);

        return <<<PHP
<?php

declare(strict_types=1);

/**
 * Compiled Container
 * Generated: {$timestamp}
 * Hash: {$hash}
 *
 * WARNING: This file is auto-generated. Do not edit manually!
 */

namespace App\Framework\DI\Compiled;

use App\Framework\DI\Container;

final class CompiledContainer implements Container
{
    private array \$instances = [];
    private array \$singletons = [];

    public function __construct()
    {
        \$this->initializeSingletons();
    }

    public function get(string \$class): object
    {
        // Singleton check
        if (isset(\$this->singletons[\$class])) {
            if (isset(\$this->instances[\$class])) {
                return \$this->instances[\$class];
            }

            \$instance = \$this->createInstance(\$class);
            \$this->instances[\$class] = \$instance;
            return \$instance;
        }

        return \$this->createInstance(\$class);
    }

    public function has(string \$class): bool
    {
        return method_exists(\$this, 'create' . str_replace('\\\\', '_', \$class));
    }

    public function bind(string \$class, callable|string|object \$concrete): void
    {
        throw new \\RuntimeException('Cannot bind to compiled container. Recompile instead.');
    }

    public function singleton(string \$class, callable|string|object \$concrete): void
    {
        throw new \\RuntimeException('Cannot add singletons to compiled container. Recompile instead.');
    }

    public function instance(string \$class, object \$instance): void
    {
        \$this->instances[\$class] = \$instance;
    }

    private function initializeSingletons(): void
    {
{$singletons}
    }

    private function createInstance(string \$class): object
    {
        return match (\$class) {
{$bindings}
            default => throw new \\InvalidArgumentException("Class {\$class} is not bound in the container")
        };
    }

{$dependencies}
}
PHP;
    }

    /**
     * Compile all container bindings to optimized creation calls
     */
    private function compileBindings(DefaultContainer $container): string
    {
        $bindings = [];
        $bindingRegistry = $this->getBindingRegistry($container);

        foreach ($bindingRegistry->getAllBindings() as $class => $binding) {
            $methodName = $this->getMethodName($class);
            $bindings[] = "            '{$class}' => \$this->{$methodName}()";
        }

        // Add trailing comma if bindings exist (for match expression syntax)
        return empty($bindings) ? '' : implode(",\n", $bindings) . ',';
    }

    /**
     * Compile singleton registrations
     */
    private function compileSingletons(DefaultContainer $container): string
    {
        $code = [];
        $instanceRegistry = $this->getInstanceRegistry($container);

        foreach ($instanceRegistry->getSingletons() as $class) {
            $code[] = "        \$this->singletons['{$class}'] = true;";
        }

        return implode("\n", $code);
    }

    /**
     * Compile dependency creation methods
     */
    private function compileDependencyMethods(DefaultContainer $container): string
    {
        $methods = [];
        $bindingRegistry = $this->getBindingRegistry($container);

        foreach ($bindingRegistry->getAllBindings() as $class => $binding) {
            $methods[] = $this->compileDependencyMethod($class);
        }

        return implode("\n\n", $methods);
    }

    /**
     * Compile a single dependency creation method
     */
    private function compileDependencyMethod(string $class): string
    {
        $methodName = $this->getMethodName($class);
        $className = ClassName::create($class);
        $dependencies = $this->dependencyResolver->resolveDependencies($className);

        if ($dependencies->isEmpty()) {
            return "    private function {$methodName}(): \\{$class}\n    {\n        return new \\{$class}();\n    }";
        }

        $dependencyCode = $this->compileDependencyInjection($dependencies);

        return "    private function {$methodName}(): \\{$class}\n    {\n        return new \\{$class}({$dependencyCode});\n    }";
    }

    /**
     * Compile dependency injection parameters
     */
    private function compileDependencyInjection($dependencies): string
    {
        $args = [];

        foreach ($dependencies as $dependency) {
            // Handle Dependency value objects from new DI system
            if ($dependency instanceof Dependency) {
                if ($dependency->getType() === DependencyType::DEPENDENCY) {
                    $className = $dependency->getClassName()?->getFullyQualified();
                    if ($className) {
                        $args[] = "\$this->get('{$className}')";
                    } else {
                        $args[] = 'null';
                    }
                } elseif ($dependency->getType() === DependencyType::NULL) {
                    $args[] = 'null';
                } else {
                    $args[] = var_export($dependency->getValue(), true);
                }
            } elseif (is_object($dependency)) {
                $args[] = "\$this->get('" . $dependency::class . "')";
            } elseif (is_string($dependency) && ! empty($dependency) && trim($dependency) !== '' && ClassName::create($dependency)->exists()) {
                $args[] = "\$this->get('{$dependency}')";
            } else {
                $args[] = var_export($dependency, true);
            }
        }

        return implode(', ', $args);
    }

    /**
     * Generate method name from class name
     */
    private function getMethodName(string $class): string
    {
        return 'create' . str_replace('\\', '_', $class);
    }

    /**
     * Generate container hash for cache invalidation
     */
    private function generateContainerHash(DefaultContainer $container): string
    {
        $bindingRegistry = $this->getBindingRegistry($container);

        $data = [
            'bindings' => $bindingRegistry->getAllKeys(),
            'cache_stats' => $this->dependencyResolver->getCacheStats(),
            'php_version' => PHP_VERSION,
            'framework_version' => '1.0.0', // Should come from VersionInfo
        ];

        return hash('sha256', serialize($data));
    }

    /**
     * Write compiled code to file
     */
    private function writeToFile(string $path, string $code): void
    {
        $directory = dirname($path);

        if (! is_dir($directory)) {
            mkdir($directory, 0755, true);
        }

        $result = file_put_contents($path, $code, LOCK_EX);

        if ($result === false) {
            throw new \RuntimeException("Failed to write compiled container to: {$path}");
        }

        chmod($path, 0644);
    }

    /**
     * Get binding registry from DefaultContainer (using reflection for private access)
     */
    private function getBindingRegistry(DefaultContainer $container): BindingRegistry
    {
        $reflection = new \ReflectionObject($container);
        $property = $reflection->getProperty('bindings');

        return $property->getValue($container);
    }

    /**
     * Get instance registry from DefaultContainer (using reflection for private access)
     */
    private function getInstanceRegistry(DefaultContainer $container): InstanceRegistry
    {
        $reflection = new \ReflectionObject($container);
        $property = $reflection->getProperty('instances');

        return $property->getValue($container);
    }

    /**
     * Check if compiled container is valid and up-to-date
     */
    public function isCompiledContainerValid(DefaultContainer $container, string $compiledPath): bool
    {
        if (! file_exists($compiledPath)) {
            return false;
        }

        // Check if file is readable
        if (! is_readable($compiledPath)) {
            return false;
        }

        try {
            // Extract hash from compiled file
            $content = file_get_contents($compiledPath);
            if (! preg_match('/\* Hash: ([a-f0-9]{64})/', $content, $matches)) {
                return false; // No hash found, file is invalid
            }

            $compiledHash = $matches[1];
            $currentHash = $this->generateContainerHash($container);

            // Hash mismatch = container changed, recompile needed
            return $compiledHash === $currentHash;

        } catch (\Exception $e) {
            // Any error means invalid
            return false;
        }
    }

    /**
     * Get compiled container path based on environment and configuration
     */
    public static function getCompiledContainerPath(?string $cacheDir = null): string
    {
        $cacheDir = $cacheDir ?? sys_get_temp_dir() . '/framework-cache';

        if (! is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }

        return $cacheDir . '/compiled-container.php';
    }

    /**
     * Load compiled container from file
     */
    public static function load(string $compiledPath): Container
    {
        if (! file_exists($compiledPath)) {
            throw new \RuntimeException("Compiled container not found: {$compiledPath}");
        }

        require_once $compiledPath;

        $className = 'App\\Framework\\DI\\Compiled\\CompiledContainer';

        if (! ClassName::create($className)->exists()) {
            throw new \RuntimeException("Compiled container class not found: {$className}");
        }

        return new $className();
    }

    /**
     * Compile container asynchronously (for production environments)
     */
    public function compileAsync(DefaultContainer $container, string $outputPath, array $options = []): void
    {
        // In production, we might want to compile in background
        // For now, just compile synchronously
        $this->compile($container, $outputPath, $options);
    }
}
