<?php

declare(strict_types=1);

namespace App\Domain\Plugin;

use App\Domain\Blog\ContentRenderer;
use App\Models\Plugin;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use Mews\Purifier\Facades\Purifier;
use RuntimeException;

class PluginManager
{
    /** @var array<string, array<string, mixed>> */
    protected array $discovered = [];

    public function discover(): Collection
    {
        $pluginPath = config('larablog.plugin_path', base_path('plugins'));

        if (! is_dir($pluginPath)) {
            return collect();
        }

        $plugins = collect();

        foreach (File::directories($pluginPath) as $vendorDirectory) {
            foreach (File::directories($vendorDirectory) as $pluginDirectory) {
                $manifestPath = $pluginDirectory.'/plugin.json';

                if (! is_file($manifestPath)) {
                    continue;
                }

                $manifest = json_decode((string) file_get_contents($manifestPath), true);

                if (! is_array($manifest)) {
                    continue;
                }

                $name = (string) ($manifest['name'] ?? basename($pluginDirectory));
                $relativePath = str_replace(base_path().'/', '', $pluginDirectory);

                $plugins->put($name, array_merge($manifest, [
                    'name' => $name,
                    'path' => $pluginDirectory,
                    'relative_path' => $relativePath,
                    'provider' => $manifest['provider'] ?? null,
                    'requires' => array_values(array_filter(
                        array_map('strval', (array) ($manifest['requires'] ?? [])),
                        fn (string $item): bool => $item !== '',
                    )),
                    'optional' => array_values(array_filter(
                        array_map('strval', (array) ($manifest['optional'] ?? [])),
                        fn (string $item): bool => $item !== '',
                    )),
                    'docs' => (string) ($manifest['docs'] ?? 'README.md'),
                ]));
            }
        }

        $this->discovered = $plugins->all();

        return $plugins;
    }

    public function syncDiscoveredPlugins(): Collection
    {
        $discovered = $this->discover();

        foreach ($discovered as $name => $manifest) {
            Plugin::query()->updateOrCreate(
                ['name' => $name],
                [
                    'version' => (string) ($manifest['version'] ?? '1.0.0'),
                    'path' => (string) ($manifest['relative_path'] ?? $manifest['path']),
                ],
            );
        }

        return $discovered;
    }

    public function isEnabled(string $name): bool
    {
        return Plugin::query()
            ->where('name', $name)
            ->where('enabled', true)
            ->exists();
    }

    /**
     * @return list<string>
     */
    public function missingRequires(string $name): array
    {
        $manifest = $this->discover()->get($name);
        if ($manifest === null) {
            throw new InvalidArgumentException("Plugin [{$name}] is not installed.");
        }

        $missing = [];
        foreach ((array) ($manifest['requires'] ?? []) as $required) {
            if (! $this->isEnabled((string) $required)) {
                $missing[] = (string) $required;
            }
        }

        return $missing;
    }

    public function enable(string $name): Plugin
    {
        $missing = $this->missingRequires($name);
        if ($missing !== []) {
            throw new RuntimeException(
                __('admin.messages.plugin_requires', [
                    'plugin' => $name,
                    'requires' => implode(', ', $missing),
                ])
            );
        }

        $plugin = $this->findPluginRecord($name);
        $plugin->update(['enabled' => true]);

        return $plugin->refresh();
    }

    public function disable(string $name): Plugin
    {
        $dependents = $this->enabledDependentsOf($name);
        if ($dependents !== []) {
            throw new RuntimeException(
                __('admin.messages.plugin_required_by', [
                    'plugin' => $name,
                    'dependents' => implode(', ', $dependents),
                ])
            );
        }

        $plugin = $this->findPluginRecord($name);
        $plugin->update(['enabled' => false]);

        return $plugin->refresh();
    }

    /**
     * @return list<string>
     */
    public function enabledDependentsOf(string $name): array
    {
        $dependents = [];

        foreach ($this->discover() as $candidate => $manifest) {
            if ($candidate === $name || ! $this->isEnabled((string) $candidate)) {
                continue;
            }

            $requires = array_map('strval', (array) ($manifest['requires'] ?? []));
            if (in_array($name, $requires, true)) {
                $dependents[] = (string) $candidate;
            }
        }

        return $dependents;
    }

    public function docsPath(string $name, ?string $locale = null): ?string
    {
        $manifest = $this->discover()->get($name);
        if ($manifest === null) {
            return null;
        }

        $docs = trim((string) ($manifest['docs'] ?? 'README.md'));
        if ($docs === '' || str_starts_with($docs, '/') || preg_match('#(^|[\\\\/])\.\.([\\\\/]|$)#', $docs) === 1) {
            return null;
        }

        $root = realpath((string) $manifest['path']);
        if ($root === false) {
            return null;
        }

        foreach ($this->docsCandidates($docs, $locale ?? app()->getLocale()) as $relative) {
            if (str_starts_with($relative, '/') || preg_match('#(^|[\\\\/])\.\.([\\\\/]|$)#', $relative) === 1) {
                continue;
            }

            $path = realpath($root.DIRECTORY_SEPARATOR.str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relative));
            if ($path === false || ! is_file($path)) {
                continue;
            }

            // Never read outside the plugin directory, even via symlinks.
            if ($path !== $root && ! str_starts_with($path, $root.DIRECTORY_SEPARATOR)) {
                continue;
            }

            return $path;
        }

        return null;
    }

    public function readDocs(string $name, ?string $locale = null): ?string
    {
        $path = $this->docsPath($name, $locale);
        if ($path === null) {
            return null;
        }

        $contents = (string) file_get_contents($path);

        return trim($contents) === '' ? null : $contents;
    }

    public function readDocsHtml(string $name, ?string $locale = null): ?string
    {
        $markdown = $this->readDocs($name, $locale);
        if ($markdown === null) {
            return null;
        }

        $html = app(ContentRenderer::class)->markdownToHtml($markdown);

        return Purifier::clean($html, 'article');
    }

    /**
     * Prefer README.{locale}.md, then README.{lang}.md, then the manifest docs file.
     *
     * @return list<string>
     */
    protected function docsCandidates(string $docs, string $locale): array
    {
        $docs = str_replace('\\', '/', $docs);
        $directory = dirname($docs);
        $basename = basename($docs);
        $extension = pathinfo($basename, PATHINFO_EXTENSION);
        $stem = $extension !== ''
            ? substr($basename, 0, -strlen($extension) - 1)
            : $basename;
        $suffix = $extension !== '' ? '.'.$extension : '';
        $prefix = ($directory === '.' || $directory === '') ? '' : $directory.'/';

        $names = [];
        $normalized = str_replace('-', '_', trim($locale));
        if ($normalized !== '' && preg_match('/^[A-Za-z0-9_]+$/', $normalized) === 1) {
            $names[] = $stem.'.'.$normalized.$suffix;
            if (str_contains($normalized, '_')) {
                $names[] = $stem.'.'.explode('_', $normalized, 2)[0].$suffix;
            }
        }
        $names[] = $basename;

        $candidates = [];
        foreach ($names as $name) {
            $relative = $prefix.$name;
            if (! in_array($relative, $candidates, true)) {
                $candidates[] = $relative;
            }
        }

        return $candidates;
    }

    public function registerEnabledProviders(): void
    {
        $discovered = $this->discover();

        Plugin::query()
            ->where('enabled', true)
            ->get()
            ->each(function (Plugin $plugin) use ($discovered): void {
                $manifest = $discovered->get($plugin->name);

                if ($manifest === null) {
                    return;
                }

                $providerClass = $manifest['provider'] ?? null;

                if (! is_string($providerClass) || ! class_exists($providerClass)) {
                    return;
                }

                if (! is_subclass_of($providerClass, ServiceProvider::class)) {
                    return;
                }

                app()->register($providerClass);
            });
    }

    protected function findPluginRecord(string $name): Plugin
    {
        $plugin = Plugin::query()->where('name', $name)->first();

        if ($plugin === null) {
            throw new InvalidArgumentException("Plugin [{$name}] is not installed.");
        }

        return $plugin;
    }
}
