<?php

declare(strict_types=1);

namespace App\Domain\PluginSdk\Services;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Coordinates marketplace install, in-place update, and full uninstall flows.
 *
 * Lifecycle:
 *   install   — download zip → extract to tmp → ManifestValidator → move to
 *               storage/app/plugins/{slug}/ → DB row → return summary
 *   update    — fetch new version → install over existing (preserves config)
 *   uninstall — uninstall hook → delete dir → drop DB row → clear autoload
 *   enabledList — listing for the Installed.vue page
 */
class PluginManager
{
    public function __construct(
        private MarketplaceClient $marketplace,
        private ManifestValidator $validator = new ManifestValidator,
    ) {}

    /**
     * Download and install a plugin from the marketplace.
     *
     * @return array{slug: string, name: string, version: string}
     *
     * @throws \RuntimeException
     */
    public function installFromMarketplace(string $slug, ?string $version = null): array
    {
        $plugin = $this->marketplace->getPlugin($slug);

        if (! $plugin) {
            throw new \RuntimeException("Plugin '{$slug}' marketplace'de bulunamadı");
        }

        $version = $version ?? ($plugin['latest_version'] ?? $plugin['version']);
        $zipPath = $this->marketplace->downloadPlugin($slug, $version);

        return $this->installFromFile($zipPath);
    }

    /**
     * Install a plugin from a local zip file path.
     * Validates manifest BEFORE moving any files.
     *
     * @return array{slug: string, name: string, version: string}
     *
     * @throws \RuntimeException
     */
    public function installFromFile(string $zipPath): array
    {
        if (! file_exists($zipPath)) {
            throw new \RuntimeException("Zip dosyası bulunamadı: {$zipPath}");
        }

        $zip = new \ZipArchive;
        $result = $zip->open($zipPath);

        if ($result !== true) {
            throw new \RuntimeException("Zip dosyası açılamadı (hata kodu: {$result})");
        }

        $tmpDir = storage_path('app/plugin-tmp/'.uniqid('install-', true));

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

        $zip->extractTo($tmpDir);
        $zip->close();

        // Some zips have a top-level folder — detect and unwrap
        $entries = array_diff(scandir($tmpDir), ['.', '..']);
        if (count($entries) === 1) {
            $single = $tmpDir.'/'.array_values($entries)[0];
            if (is_dir($single) && file_exists($single.'/plugin.json')) {
                $tmpDir = $single;
            }
        }

        $manifestPath = $tmpDir.'/plugin.json';

        if (! file_exists($manifestPath)) {
            $this->cleanup($tmpDir);
            throw new \RuntimeException("plugin.json bulunamadı — geçerli bir Onoxsoft plugin zip'i değil");
        }

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

        if (! is_array($manifest)) {
            $this->cleanup($tmpDir);
            throw new \RuntimeException('Geçersiz plugin.json formatı — JSON parse hatası');
        }

        // Validate schema BEFORE touching the destination dir.
        try {
            $this->validator->validate($manifest);
            $this->validator->assertPanelVersion(
                $manifest['minimum_panel_version'],
                config('app.version', '0.18.0')
            );
        } catch (\RuntimeException $e) {
            $this->cleanup($tmpDir);
            throw $e;
        }

        $finalDir = storage_path("app/plugins/{$manifest['slug']}");

        // If already installed: preserve config blob, replace files
        $existingConfig = DB::table('installed_plugins')
            ->where('slug', $manifest['slug'])
            ->value('config');

        if (is_dir($finalDir)) {
            $this->cleanup($finalDir);
        }

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

        rename($tmpDir, $finalDir);

        // Register PSR-4 autoload paths (if declared)
        $autoloadPaths = [];
        if (! empty($manifest['autoload'])) {
            foreach ($manifest['autoload'] as $prefix => $relPath) {
                $autoloadPaths[$prefix] = $relPath;
            }
        }

        // Run migrations if declared
        if (! empty($manifest['migrations'])) {
            $this->runMigrations($manifest['slug'], $manifest['migrations'], $finalDir);
        }

        DB::table('installed_plugins')->updateOrInsert(
            ['slug' => $manifest['slug']],
            [
                'name' => $manifest['name'],
                'version' => $manifest['version'],
                'author' => $manifest['author']['name'] ?? null,
                'description' => $manifest['description_tr'] ?? $manifest['description'] ?? '',
                'manifest' => json_encode($manifest),
                'autoload_paths' => json_encode($autoloadPaths),
                'enabled' => true,
                'config' => $existingConfig, // preserve across update
                'installed_at' => now(),
                'updated_at' => now(),
                'created_at' => now(),
            ]
        );

        return [
            'slug' => $manifest['slug'],
            'name' => $manifest['name'],
            'version' => $manifest['version'],
        ];
    }

    /**
     * In-place update: fetch new version from marketplace and reinstall over
     * the existing install (config + autoload preserved).
     *
     * @return array{slug: string, name: string, from: string, to: string}
     *
     * @throws \RuntimeException
     */
    public function update(string $slug, ?string $newVersion = null): array
    {
        $existing = DB::table('installed_plugins')->where('slug', $slug)->first();

        if (! $existing) {
            throw new \RuntimeException("Plugin yüklü değil — önce kurun: {$slug}");
        }

        $previousVersion = $existing->version;

        $remote = $this->marketplace->getPlugin($slug);
        if (! $remote) {
            throw new \RuntimeException("Plugin marketplace'de bulunamadı: {$slug}");
        }

        $target = $newVersion ?? ($remote['latest_version'] ?? $remote['version']);

        if (version_compare($target, $previousVersion, '<=')) {
            throw new \RuntimeException(
                "Yeni sürüm gerekiyor: mevcut v{$previousVersion}, hedef v{$target}"
            );
        }

        $zipPath = $this->marketplace->downloadPlugin($slug, $target);
        $result = $this->installFromFile($zipPath);

        return [
            'slug' => $result['slug'],
            'name' => $result['name'],
            'from' => $previousVersion,
            'to' => $result['version'],
        ];
    }

    /**
     * Remove a plugin: run uninstall hook, delete files, drop DB row.
     *
     * @throws \RuntimeException
     */
    public function uninstall(string $slug): void
    {
        $plugin = DB::table('installed_plugins')->where('slug', $slug)->first();

        if (! $plugin) {
            throw new \RuntimeException("Plugin yüklü değil: {$slug}");
        }

        $manifest = json_decode($plugin->manifest, true);
        $pluginPath = storage_path("app/plugins/{$slug}");

        // Run uninstall hook if declared
        if (! empty($manifest['uninstall'])) {
            $uninstallFile = $pluginPath.'/'.$manifest['uninstall'];
            if (file_exists($uninstallFile)) {
                try {
                    require_once $uninstallFile;
                } catch (\Throwable $e) {
                    Log::warning(
                        "Plugin uninstall hook failed: {$slug}",
                        ['error' => $e->getMessage()]
                    );
                }
            }
        }

        if (is_dir($pluginPath)) {
            $this->cleanup($pluginPath);
        }

        DB::table('installed_plugins')->where('slug', $slug)->delete();
    }

    /**
     * Return installed plugins, manifest + config decoded for the Installed.vue page.
     *
     * @return Collection<int, object>
     */
    public function enabledList(): Collection
    {
        return DB::table('installed_plugins')
            ->orderByDesc('installed_at')
            ->get()
            ->map(function ($row) {
                $manifest = json_decode($row->manifest, true) ?? [];
                $row->manifest = $manifest;
                $row->config = $row->config ? json_decode($row->config, true) : null;
                $row->autoload_paths = $row->autoload_paths ? json_decode($row->autoload_paths, true) : [];
                $row->category = $manifest['category'] ?? null;
                $row->tags = $manifest['tags'] ?? [];
                $row->icon_url = $manifest['icon_url'] ?? $manifest['icon'] ?? null;
                $row->latest_version = null; // populated by controller if marketplace available

                return $row;
            });
    }

    public function enable(string $slug): void
    {
        DB::table('installed_plugins')
            ->where('slug', $slug)
            ->update(['enabled' => true, 'updated_at' => now()]);
    }

    public function disable(string $slug): void
    {
        DB::table('installed_plugins')
            ->where('slug', $slug)
            ->update(['enabled' => false, 'updated_at' => now()]);
    }

    /**
     * Best-effort plugin migration runner. Plugin migrations live in the
     * plugin's own folder under its declared 'migrations' path.
     *
     * @param  array<int, string>  $relPaths
     */
    private function runMigrations(string $slug, array $relPaths, string $pluginDir): void
    {
        foreach ($relPaths as $rel) {
            $abs = $pluginDir.'/'.ltrim($rel, '/');
            if (! file_exists($abs)) {
                Log::warning("Plugin migration missing: {$slug}", ['path' => $abs]);

                continue;
            }
            try {
                require_once $abs;
            } catch (\Throwable $e) {
                Log::error("Plugin migration failed: {$slug}", [
                    'file' => $abs, 'error' => $e->getMessage(),
                ]);
            }
        }
    }

    /**
     * Recursively remove a directory and all its contents.
     */
    private function cleanup(string $dir): void
    {
        if (! is_dir($dir)) {
            return;
        }

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::CHILD_FIRST
        );

        foreach ($files as $f) {
            $f->isDir() ? rmdir($f->getRealPath()) : unlink($f->getRealPath());
        }

        rmdir($dir);
    }
}
