<?php

declare(strict_types=1);

final class MyronExtensionLoader
{
    private bool $booted = false;

    /** @var array<string, MyronExtensionManifest> */
    private array $manifests = [];

    /** @var array<string, string> */
    private array $loadFailures = [];

    /** @var array<string, string> */
    private array $adminRoutes = [];

    public function __construct(
        private readonly string $extensionsRoot,
        private readonly PDO $pdo,
        private readonly MyronActionRegistry $registry,
        private readonly MyronExtensionScopeGrantRepository $grants,
        private readonly MyronExtensionMigrationRunner $migrationRunner,
    ) {
    }

    public function boot(bool $force = false): void
    {
        if ($this->booted && !$force) {
            return;
        }

        $this->booted = true;
        $this->manifests = [];
        $this->loadFailures = [];
        $this->adminRoutes = [];

        if (!is_dir($this->extensionsRoot)) {
            return;
        }

        $manifestPaths = glob($this->extensionsRoot . '/*/manifest.json') ?: [];
        sort($manifestPaths, SORT_STRING);

        foreach ($manifestPaths as $manifestPath) {
            $directoryName = basename(dirname((string) $manifestPath));
            $vendor = str_replace('-', '_', $directoryName);
            try {
                $manifest = MyronExtensionManifest::fromFile((string) $manifestPath, $vendor);
                $this->manifests[$manifest->vendor] = $manifest;
                if (!$this->isGranted($manifest)) {
                    continue;
                }

                if ($manifest->hasMigrations) {
                    $this->migrationRunner->run($manifest->vendor, $this->pdo, $this->loadMigrations($manifest));
                }

                $payload = $this->requireBootstrap($manifest);
                $actions = is_array($payload['actions'] ?? null) ? $payload['actions'] : [];
                foreach ($actions as $action) {
                    $this->registry->registerExtension($manifest->vendor, $action);
                }
                $micTypes = is_array($payload['micElementTypes'] ?? null) ? $payload['micElementTypes'] : [];
                foreach ($micTypes as $slug => $resolver) {
                    if ($resolver instanceof MicElementResolverInterface) {
                        myronMicElementRegistry()->registerExtension($manifest->vendor, (string) $slug, $resolver);
                    }
                }
                $mailTransports = is_array($payload['mailTransports'] ?? null) ? $payload['mailTransports'] : [];
                foreach ($mailTransports as $slug => $transport) {
                    if ($transport instanceof MailTransportInterface) {
                        myronMailTransportRegistry()->registerExtension($manifest->vendor, (string) $slug, $transport);
                    }
                }
                foreach ($manifest->adminRoutes as $route) {
                    $handler = $manifest->directory . '/' . $route['path'];
                    if (is_file($handler)) {
                        $this->adminRoutes[$this->adminRoutePath($manifest->vendor, $route['key'])] = $handler;
                    }
                }
                $this->grants->clearLoadErrors($manifest->vendor);
            } catch (Throwable $exception) {
                $message = $exception->getMessage();
                $this->loadFailures[$vendor] = $message;
                error_log('[MyronCMS extension] ' . $vendor . ' load failed: ' . $message);
                $this->grants->recordLoadError($vendor, $message);
            }
        }
    }

    /**
     * @return array<string, string>
     */
    public function getAdminRoutes(): array
    {
        return $this->adminRoutes;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function statuses(): array
    {
        $latestErrors = $this->grants->latestLoadErrorsByVendor();
        $statuses = [];
        foreach ($this->grants->all() as $grant) {
            $vendor = $grant['vendor'];
            $manifest = $this->manifests[$vendor] ?? null;
            $statuses[$vendor] = [
                'vendor' => $vendor,
                'name' => $grant['name'],
                'version' => $grant['version'],
                'enabled' => (int) $grant['enabled'] === 1,
                'grantedScopes' => MyronApiScopes::parseList($grant['granted_scopes']),
                'directoryExists' => is_dir($this->extensionsRoot . '/' . $vendor) || is_dir($this->extensionsRoot . '/' . str_replace('_', '-', $vendor)),
                'manifestFound' => $manifest instanceof MyronExtensionManifest,
                'loaded' => $manifest instanceof MyronExtensionManifest && !isset($this->loadFailures[$vendor]) && (int) $grant['enabled'] === 1,
                'lastError' => (string) ($latestErrors[$vendor]['error_message'] ?? $this->loadFailures[$vendor] ?? ''),
            ];
        }

        return $statuses;
    }

    /**
     * @return array{actions:list<MyronAction>,micElementTypes:array<string, MicElementResolverInterface>,mailTransports:array<string, MailTransportInterface>}
     */
    private function requireBootstrap(MyronExtensionManifest $manifest): array
    {
        $bootstrap = $manifest->directory . '/bootstrap.php';
        if (!is_file($bootstrap)) {
            return ['actions' => [], 'micElementTypes' => [], 'mailTransports' => []];
        }

        $sdk = new MyronExtensionSDK($manifest->vendor);
        $result = (static function (string $bootstrap, MyronExtensionSDK $sdk, MyronExtensionManifest $manifest): mixed {
            return require $bootstrap;
        })($bootstrap, $sdk, $manifest);

        $actions = [];
        if ($result instanceof MyronAction) {
            $actions[] = $result;
        } elseif (is_array($result)) {
            $rawActions = is_array($result['actions'] ?? null) ? $result['actions'] : $result;
            foreach ($rawActions as $action) {
                if ($action instanceof MyronAction) {
                    $actions[] = $action;
                }
            }
        }

        $micTypes = [];
        if (is_array($result) && is_array($result['micElementTypes'] ?? null)) {
            foreach ($result['micElementTypes'] as $slug => $resolver) {
                if ($resolver instanceof MicElementResolverInterface) {
                    $micTypes[(string) $slug] = $resolver;
                }
            }
        }

        $mailTransports = [];
        if (is_array($result) && is_array($result['mailTransports'] ?? null)) {
            foreach ($result['mailTransports'] as $slug => $transport) {
                if ($transport instanceof MailTransportInterface) {
                    $mailTransports[(string) $slug] = $transport;
                }
            }
        }
        if (is_array($result) && (isset($result['paymentProviders']) || isset($result['paymentProviderCredentials']))) {
            throw new RuntimeException('Extensions cannot register or own payment-provider credentials; configure payment providers through site administration.');
        }

        return ['actions' => $actions, 'micElementTypes' => $micTypes, 'mailTransports' => $mailTransports];
    }

    private function isGranted(MyronExtensionManifest $manifest): bool
    {
        if (!$this->grants->schemaReady()) {
            return true;
        }

        $grant = $this->grants->findByVendor($manifest->vendor);
        if ($grant === null) {
            error_log('[MyronCMS extension] ' . $manifest->vendor . ' is not registered; skipping.');
            return false;
        }
        if ((int) $grant['enabled'] !== 1) {
            return false;
        }

        $grantedScopes = MyronApiScopes::parseList($grant['granted_scopes']);
        foreach ($manifest->requiredScopes as $scope) {
            if (!in_array($scope, $grantedScopes, true)) {
                throw new RuntimeException('Extension requires ungranted scope: ' . $scope . '.');
            }
        }

        return true;
    }

    /**
     * @return array<string, callable(PDO): void|list<string>>
     */
    private function loadMigrations(MyronExtensionManifest $manifest): array
    {
        $migrationFiles = glob($manifest->directory . '/migrations/*.php') ?: [];
        sort($migrationFiles, SORT_STRING);

        $migrations = [];
        foreach ($migrationFiles as $file) {
            $loaded = require (string) $file;
            if (is_array($loaded)) {
                foreach ($loaded as $version => $migration) {
                    $migrations[(string) $version] = $migration;
                }
                continue;
            }
            if (is_callable($loaded)) {
                $version = preg_replace('/[^A-Za-z0-9_]+/', '_', basename((string) $file, '.php')) ?: basename((string) $file, '.php');
                $migrations[$version] = $loaded;
            }
        }

        return $migrations;
    }

    private function adminRoutePath(string $vendor, string $key): string
    {
        $slug = str_replace('_', '-', $vendor);
        $tail = $key;
        if (str_starts_with($tail, $slug . '-')) {
            $tail = substr($tail, strlen($slug) + 1);
        }
        $vendorParts = explode('_', $vendor);
        $short = (string) end($vendorParts);
        if (str_starts_with($tail, $short . '-')) {
            $tail = substr($tail, strlen($short) + 1);
        }
        return '/x/' . $vendor . '/' . trim($tail, '-');
    }
}
