<?php

declare(strict_types=1);

namespace core\handler;

use Exception;
use ParseError;
use Phar;
use utils\CommandInterface;

final class LoadLibraryHandler extends AbstractHandler
{
    private const REQUIRED_ARGUMENTS_COUNT = 1;
    private const MAX_DIRECTORY_DEPTH = 10;
    private static array $loadedLibraries = [];
    private static array $loadedAutoloaders = [];
    private static array $registeredPsr4Autoloaders = [];
    private array $loadedClassPaths = [];

    /**
     * @throws Exception
     */
    public function process(CommandInterface $command): int
    {
        $this->validate($command, self::REQUIRED_ARGUMENTS_COUNT, self::class);

        $assemblyName = (string) $command->getPayload()[0];

        if (!file_exists($assemblyName)) {
            throw new Exception('File not found: ' . $assemblyName);
        }

        $canonicalPath = realpath($assemblyName);
        if ($canonicalPath === false) {
            throw new Exception('Cannot resolve path: ' . $assemblyName);
        }

        if (in_array($canonicalPath, self::$loadedLibraries, true)) {
            return 0;
        }

        if (is_dir($canonicalPath)) {
            $this->loadDirectory($canonicalPath);
        } elseif (strtolower(pathinfo($canonicalPath, PATHINFO_EXTENSION)) === 'phar') {
            $this->loadPharFile($canonicalPath);
        } else {
            $this->loadPhpFile($canonicalPath);
        }

        self::$loadedLibraries[] = $canonicalPath;

        return 0;
    }

    /**
     * @throws Exception
     */
    private function loadDirectory(string $directoryPath): void
    {
        if (!is_readable($directoryPath)) {
            throw new Exception('Directory is not readable: ' . $directoryPath);
        }

        $realPath = realpath($directoryPath);
        if ($realPath === false) {
            throw new Exception('Cannot resolve directory path: ' . $directoryPath);
        }

        $currentIncludePath = get_include_path();
        if (strpos($currentIncludePath, $realPath) === false) {
            set_include_path($currentIncludePath . PATH_SEPARATOR . $realPath);
            $this->loadedClassPaths[] = $realPath;
        }

        if ($this->tryLoadComposerAutoloaderFromDirectory($realPath)) {
            return;
        }

        $this->registerSimpleAutoloader($realPath);
    }

    /**
     * Tries to load Composer autoloader from the given directory.
     * Checks for vendor/autoload.php first, then falls back to composer.json.
     */
    private function tryLoadComposerAutoloaderFromDirectory(string $directory): bool
    {
        $vendorAutoloader = $directory . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
        $composerJson = $directory . DIRECTORY_SEPARATOR . 'composer.json';

        if (file_exists($vendorAutoloader)) {
            $this->loadVendorAutoloader($vendorAutoloader);

            return true;
        }

        if (file_exists($composerJson)) {
            $this->registerAutoloaderFromComposerJson($composerJson, $directory);

            return true;
        }

        return false;
    }

    /**
     * Registers a simple PSR-4 style autoloader based on directory structure.
     * This is used as a fallback when no Composer autoloader is available.
     * The autoloader maps class namespaces directly to file paths.
     */
    private function registerSimpleAutoloader(string $baseDir): void
    {
        if (in_array($baseDir, self::$registeredPsr4Autoloaders, true)) {
            return;
        }

        spl_autoload_register(function (string $class) use ($baseDir): void {
            // Convert namespace to file path (PSR-4 style)
            $relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
            $file = $baseDir . DIRECTORY_SEPARATOR . $relativePath;

            if (file_exists($file)) {
                include_once $file;
            }
        });

        self::$registeredPsr4Autoloaders[] = $baseDir;
    }

    /**
     * @throws Exception
     */
    private function loadPharFile(string $pharPath): void
    {
        if (!is_readable($pharPath)) {
            throw new Exception('PHAR file is not readable: ' . $pharPath);
        }

        $realPath = realpath($pharPath);
        if ($realPath === false) {
            throw new Exception('Cannot resolve PHAR path: ' . $pharPath);
        }

        try {
            $phar = new Phar($realPath);

            $currentIncludePath = get_include_path();
            if (strpos($currentIncludePath, $realPath) === false) {
                set_include_path($currentIncludePath . PATH_SEPARATOR . $realPath);
                $this->loadedClassPaths[] = $realPath;
            }

            if (isset($phar['bootstrap.php'])) {
                include_once 'phar://' . $realPath . '/bootstrap.php';
            }
        } catch (Exception $e) {
            throw new Exception(sprintf('Error loading PHAR file %s : %s', $pharPath, $e->getMessage()));
        }
    }

    private static array $loadedPhpFiles = [];

    /**
     * @throws Exception
     */
    private function loadPhpFile(string $filePath): void
    {
        if (!is_readable($filePath)) {
            throw new Exception('PHP file is not readable: ' . $filePath);
        }

        $realPath = realpath($filePath);
        if ($realPath === false) {
            throw new Exception('Cannot resolve PHP file path: ' . $filePath);
        }

        $fileExtension = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
        if ($fileExtension !== 'php') {
            throw new Exception('File is not a valid PHP file: ' . $realPath);
        }

        if (in_array($realPath, self::$loadedPhpFiles, true)) {
            return;
        }

        $code = file_get_contents($realPath);
        if ($code === false) {
            throw new Exception(sprintf('Failed to read php file: %s', $realPath));
        }

        try {
            /** @noinspection PhpExpressionResultUnusedInspection */
            token_get_all($code, TOKEN_PARSE);
        } catch (ParseError $e) {
            throw new Exception(sprintf('PHP syntax error in file %s: %s', $realPath, $e->getMessage()));
        }

        $lastError = error_get_last();
        if ($lastError && $lastError['type'] === E_PARSE) {
            throw new Exception(sprintf('PHP syntax error in file %s: %s', $realPath, $lastError['message']));
        }

        $this->findAndLoadComposerAutoloader($realPath);

        include_once $realPath;

        self::$loadedPhpFiles[] = $realPath;
    }

    /**
     * Searches for Composer autoloader in the directory hierarchy and loads it.
     * Falls back to registering PSR-4 autoloader from composer.json if vendor/autoload.php is not available.
     */
    private function findAndLoadComposerAutoloader(string $filePath): void
    {
        $directory = dirname($filePath);

        for ($i = 0; $i < self::MAX_DIRECTORY_DEPTH; $i++) {
            $vendorAutoloader = $directory . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
            $composerJson = $directory . DIRECTORY_SEPARATOR . 'composer.json';

            if (file_exists($vendorAutoloader)) {
                $this->loadVendorAutoloader($vendorAutoloader);

                return;
            }

            if (file_exists($composerJson)) {
                $this->registerAutoloaderFromComposerJson($composerJson, $directory);

                return;
            }

            $parentDirectory = dirname($directory);
            // Reached filesystem root
            if ($parentDirectory === $directory) {
                break;
            }

            $directory = $parentDirectory;
        }
    }

    /**
     * Loads vendor autoloader if not already loaded.
     */
    private function loadVendorAutoloader(string $autoloaderPath): void
    {
        $realAutoloaderPath = realpath($autoloaderPath);

        if ($realAutoloaderPath === false) {
            return;
        }

        if (in_array($realAutoloaderPath, self::$loadedAutoloaders, true)) {
            return;
        }

        include_once $realAutoloaderPath;

        self::$loadedAutoloaders[] = $realAutoloaderPath;
    }

    /**
     * Registers PSR-4 autoloader based on composer.json configuration.
     * This is a fallback when vendor/autoload.php is not available.
     */
    private function registerAutoloaderFromComposerJson(string $composerJsonPath, string $baseDir): void
    {
        $realComposerJsonPath = realpath($composerJsonPath);

        if ($realComposerJsonPath === false) {
            return;
        }

        if (in_array($realComposerJsonPath, self::$registeredPsr4Autoloaders, true)) {
            return;
        }

        $composerContent = file_get_contents($realComposerJsonPath);
        if ($composerContent === false) {
            return;
        }

        $composerConfig = json_decode($composerContent, true);
        if ($composerConfig === null || !isset($composerConfig['autoload']['psr-4'])) {
            return;
        }

        $psr4Map = $composerConfig['autoload']['psr-4'];
        $realBaseDir = realpath($baseDir) ?: $baseDir;

        spl_autoload_register(function (string $class) use ($psr4Map, $realBaseDir): void {
            foreach ($psr4Map as $prefix => $path) {
                $prefixLength = strlen($prefix);

                if (strncmp($class, $prefix, $prefixLength) !== 0) {
                    continue;
                }

                $relativeClass = substr($class, $prefixLength);
                $file = $realBaseDir . DIRECTORY_SEPARATOR . $path . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';

                if (file_exists($file)) {
                    include_once $file;
                    return;
                }
            }
        });

        self::$registeredPsr4Autoloaders[] = $realComposerJsonPath;
    }

    public static function getLoadedLibraries(): array
    {
        return self::$loadedLibraries;
    }

    /**
     * @throws Exception
     */
    public function __destruct()
    {
        foreach ($this->loadedClassPaths as $path) {
            if (strpos($path, sys_get_temp_dir()) === 0 && is_dir($path)) {
                $this->removeDirectoryOrFile($path);
            }
        }
    }

    /**
     * @throws Exception
     */
    private function removeDirectoryOrFile(string $dir): void
    {
        if (!file_exists($dir)) {
            return;
        }

        if (is_file($dir) || is_link($dir)) {
            if (!unlink($dir)) {
                throw new Exception('Cannot delete file path: ' . $dir);
            }

            return;
        }
        $files = scandir($dir);
        foreach ($files as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }
            $this->removeDirectoryOrFile($dir . DIRECTORY_SEPARATOR . $file);
        }

        if(!rmdir($dir)) {
            throw new Exception('Cannot delete dir path: '. $dir);
        }
    }
}
