<?php

namespace App\Http\Controllers\Admin;

use App\Events\ModuleActivated;
use App\Http\Controllers\Controller;
use App\Models\Module;
use App\Models\ModuleUpdateLog;
use App\Services\LanguageSyncService;
use App\Services\System\ModuleLoader;
use App\Services\System\ModuleUpdater;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Laracasts\Flash\Flash;
use ZipArchive;

class AdminModulesController extends Controller
{
    /**
     * List all installed modules, plus any modules found on the filesystem
     * that have not yet been registered in the database (manually copied).
     */
    public function index()
    {
        $title   = 'Module Manager';
        $modules = Module::orderBy('status', 'desc')->orderBy('name')->get();

        $installedSlugs = $modules->pluck('slug')->all();
        $unregistered   = collect(\App\Support\ModuleLoader::discover())
            ->filter(fn ($m) => ! in_array($m['slug'] ?? '', $installedSlugs, true))
            ->values();

        return view('admin.modules.index', compact('title', 'modules', 'unregistered'));
    }

    /**
     * Upload and install one or more modules from a zip file.
     *
     * Supports both single-module ZIPs and bundle ZIPs that contain multiple
     * module directories (each with their own module.json). Bundle ZIPs are
     * used for distributing a paid module together with its free dependencies
     * (e.g. SeoAgents + AiAgentsCore in one download).
     *
     * Install order follows module_requires dependencies (topological sort).
     * Modules already installed at the same or higher version are skipped.
     * On partial failure all newly-copied directories are rolled back.
     */
    public function upload(Request $request)
    {
        $request->validate([
            'module_zip' => ['required', 'file', 'mimes:zip', 'max:51200'], // 50MB
        ]);

        $zip         = new ZipArchive();
        $uploadedZip = $request->file('module_zip');
        $tempPath    = storage_path('app/modules_tmp/' . uniqid('mod_', true));

        File::makeDirectory($tempPath, 0755, true, true);

        if ($zip->open($uploadedZip->getRealPath()) !== true) {
            Flash::error('Could not open the uploaded zip file. Please try again.');
            return redirect()->route('ch-admin.modules.index');
        }

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

        // Collect candidate module directories (depth-1 scan for module.json).
        $moduleDirs = $this->findModuleDirs($tempPath);

        if (empty($moduleDirs)) {
            File::deleteDirectory($tempPath);
            Flash::error('Invalid ZIP: no module.json found. Make sure the zip contains a valid module directory.');
            return redirect()->route('ch-admin.modules.index');
        }

        // Parse and validate each manifest.
        $candidates = [];
        foreach ($moduleDirs as $dir) {
            $result = $this->parseAndValidateManifest($dir);
            if (isset($result['error'])) {
                File::deleteDirectory($tempPath);
                Flash::error($result['error']);
                return redirect()->route('ch-admin.modules.index');
            }
            $candidates[$result['manifest']['slug']] = [
                'src'      => $dir,
                'manifest' => $result['manifest'],
            ];
        }

        // Sort by dependency order (dependencies install first).
        try {
            $ordered = $this->sortByDependencies($candidates);
        } catch (\RuntimeException $e) {
            File::deleteDirectory($tempPath);
            Flash::error('Dependency error: ' . $e->getMessage());
            return redirect()->route('ch-admin.modules.index');
        }

        // Install each module in order.
        $installed    = [];
        $skipped      = [];
        $copiedDirs   = []; // track for rollback

        try {
            foreach ($ordered as $item) {
                $manifest  = $item['manifest'];
                $srcPath   = $item['src'];
                $slug      = $manifest['slug'];
                $newVersion = $manifest['version'] ?? '1.0.0';

                // Skip if already installed at same or higher version.
                $existing = Module::where('slug', $slug)->first();
                if ($existing) {
                    if (version_compare($existing->version, $newVersion, '>=')) {
                        $skipped[] = $manifest['name'] ?? $slug;
                        continue;
                    }
                    // Higher version in ZIP: update the DB record but keep existing files
                    // (files will be overwritten below).
                }

                $module    = ModuleLoader::install($srcPath);
                $directory = $this->getModuleDirectory($module);
                $destPath  = ModuleLoader::modulesPath($directory);

                if (File::exists($destPath)) {
                    File::deleteDirectory($destPath);
                }
                File::copyDirectory($srcPath, $destPath);
                $copiedDirs[] = $destPath;

                ModuleLoader::linkAssets($directory);

                $installed[] = $manifest['name'] ?? $slug;
            }
        } catch (\Throwable $e) {
            // Roll back any directories we already copied.
            foreach ($copiedDirs as $dir) {
                File::deleteDirectory($dir);
            }
            File::deleteDirectory($tempPath);
            Flash::error('Installation failed: ' . $e->getMessage());
            return redirect()->route('ch-admin.modules.index');
        } finally {
            File::deleteDirectory($tempPath);
        }

        // Build a human-friendly flash message.
        $parts = [];
        if (! empty($installed)) {
            $parts[] = 'Installed: ' . implode(', ', $installed) . '.';
        }
        if (! empty($skipped)) {
            $parts[] = 'Already up-to-date (skipped): ' . implode(', ', $skipped) . '.';
        }

        if (empty($installed)) {
            Flash::info(implode(' ', $parts) ?: 'Nothing to install.');
        } else {
            Flash::success(implode(' ', $parts) . ' You can now activate the module(s).');
        }

        return redirect()->route('ch-admin.modules.index');
    }

    /**
     * Register a module that was manually copied to the modules/ directory.
     *
     * Reads its module.json, creates the DB record (status = inactive),
     * and links public assets. The admin can then activate it normally.
     */
    public function registerFromFilesystem(Request $request)
    {
        $request->validate(['slug' => ['required', 'string', 'regex:/^[a-z0-9\-]+$/']]);

        $slug   = $request->input('slug');
        $fsMeta = \App\Support\ModuleLoader::findBySlug($slug);

        if (! $fsMeta) {
            Flash::error("Module [{$slug}] was not found on the filesystem. Make sure the directory contains a valid module.json.");
            return redirect()->route('ch-admin.modules.index');
        }

        $module    = ModuleLoader::install($fsMeta['path']);
        $directory = $this->getModuleDirectory($module);
        ModuleLoader::linkAssets($directory);

        // Warn if required modules are not yet registered.
        $requires    = $fsMeta['module_requires'] ?? [];
        $activeSlugs = Module::active()->pluck('slug')->all();
        $missing     = array_filter((array) $requires, fn ($slug) => ! in_array($slug, $activeSlugs, true));

        if (! empty($missing)) {
            $labels = implode(', ', $missing);
            Flash::warning("Module [{$module->name}] registered, but it requires [{$labels}] to be active before you can activate it.");
        } else {
            Flash::success("Module [{$module->name}] has been registered. You can now activate it.");
        }

        return redirect()->route('ch-admin.modules.index');
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    /**
     * Scan a temp directory for depth-1 subdirectories that contain module.json.
     * Also handles single-module ZIPs where the top-level IS the module dir.
     *
     * @return string[]  Absolute paths to module source directories
     */
    private function findModuleDirs(string $tempPath): array
    {
        $found = [];

        // Check if the tempPath itself is a module dir (single-module zip, no wrapper).
        if (File::exists($tempPath . '/module.json')) {
            return [$tempPath];
        }

        foreach (File::directories($tempPath) as $dir) {
            if (File::exists($dir . '/module.json')) {
                $found[] = $dir;
            }
        }

        return $found;
    }

    /**
     * Parse and validate a module.json in the given directory.
     *
     * Returns ['manifest' => array] on success, or ['error' => string] on failure.
     *
     * @return array{manifest?: array, error?: string}
     */
    private function parseAndValidateManifest(string $dir): array
    {
        $manifestPath = $dir . '/module.json';

        if (! File::exists($manifestPath)) {
            return ['error' => "Invalid module in {$dir}: module.json not found."];
        }

        $manifest = json_decode(File::get($manifestPath), true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return ['error' => "Invalid module.json in {$dir}: malformed JSON."];
        }

        foreach (['name', 'slug', 'version', 'provider'] as $field) {
            if (empty($manifest[$field])) {
                return ['error' => "Invalid module.json: missing required field '{$field}'." ];
            }
        }

        if (! preg_match('/^[a-z0-9\-]+$/', $manifest['slug'])) {
            return ['error' => "Invalid module.json: slug '{$manifest['slug']}' must be lowercase letters, numbers, and hyphens only."];
        }

        return ['manifest' => $manifest];
    }

    /**
     * Topologically sort candidates so dependencies come first (Kahn's algorithm).
     *
     * Only considers intra-bundle dependencies. External dependencies (already-
     * installed modules not in this ZIP) are not re-ordered — they are assumed
     * to be present and ModuleLoader::boot() will warn if they are not.
     *
     * @param  array<string, array{src: string, manifest: array}>  $candidates  Keyed by slug
     * @return array<int, array{src: string, manifest: array}>
     * @throws \RuntimeException on circular dependency
     */
    private function sortByDependencies(array $candidates): array
    {
        $inDegree = array_fill_keys(array_keys($candidates), 0);
        $adjList  = [];

        foreach ($candidates as $slug => $item) {
            $requires = $item['manifest']['module_requires'] ?? [];
            foreach ((array) $requires as $dep) {
                if (! isset($candidates[$dep])) {
                    continue; // dependency not in this bundle, assumed already installed
                }
                $adjList[$dep][] = $slug;
                $inDegree[$slug]++;
            }
        }

        $queue  = array_keys(array_filter($inDegree, fn ($d) => $d === 0));
        $sorted = [];

        while (! empty($queue)) {
            $current  = array_shift($queue);
            $sorted[] = $candidates[$current];

            foreach ($adjList[$current] ?? [] as $dependent) {
                $inDegree[$dependent]--;
                if ($inDegree[$dependent] === 0) {
                    $queue[] = $dependent;
                }
            }
        }

        if (count($sorted) !== count($candidates)) {
            throw new \RuntimeException('Circular dependency detected in the uploaded bundle.');
        }

        return $sorted;
    }

    /**
     * Activate a module.
     *
     * Runs any pending module migrations then marks the module active.
     * Products and plans are created manually in the Product Manager — go to
     * Admin → Products → New Product, then link it in the module's settings.
     */
    public function activate(Module $module)
    {
        if ($module->isActive()) {
            Flash::info("Module [{$module->name}] is already active.");
            return redirect()->route('ch-admin.modules.index');
        }

        // Enforce module_requires: all declared dependencies must be active.
        $requires = $module->meta['module_requires'] ?? [];
        if (! empty($requires)) {
            $activeSlugs = Module::active()->pluck('slug')->all();
            $missing     = array_filter((array) $requires, fn ($slug) => ! in_array($slug, $activeSlugs, true));

            if (! empty($missing)) {
                $names = Module::whereIn('slug', $missing)->pluck('name', 'slug');
                $labels = array_map(fn ($slug) => $names->get($slug) ?? $slug, $missing);
                Flash::error(
                    "Cannot activate [{$module->name}]: the following required module(s) must be active first: "
                    . implode(', ', $labels) . '.'
                );
                return redirect()->route('ch-admin.modules.index');
            }
        }

        $directory = $this->getModuleDirectory($module);

        // Run module migrations (outside any transaction — DDL cannot be rolled back).
        // Check both casing conventions (lowercase preferred, matches AbstractModuleServiceProvider)
        $migrationPath = 'modules/' . $directory . '/database/migrations';
        if (! File::isDirectory(base_path($migrationPath))) {
            $migrationPath = 'modules/' . $directory . '/Database/Migrations';
        }
        if (File::isDirectory(base_path($migrationPath))) {
            Artisan::call('migrate', [
                '--path'  => $migrationPath,
                '--force' => true,
            ]);

            $output = Artisan::output();
            if (! empty(trim($output))) {
                \Log::info("Module [{$module->slug}] migrations: " . trim($output));
            }
        }

        $module->activate();

        // Boot the module's service provider NOW (within this request) so that
        // any event listeners it registers are in place before we fire ModuleActivated.
        // Without this, the provider has not been booted yet (it was inactive at
        // request start, so ModuleLoader skipped it), and the listener is never called.
        if (! empty($module->provider_class) && class_exists($module->provider_class)) {
            app()->register($module->provider_class);
        }

        event(new ModuleActivated($module));

        // Sync language keys so module view translations are immediately available
        // without requiring a manual Languages → Sync step.
        app(LanguageSyncService::class)->run();

        Flash::success("Module [{$module->name}] activated successfully.");
        return redirect()->route('ch-admin.modules.index');
    }

    /**
     * Reinstall a module.
     *
     * Clears the module's setup_complete flag and re-fires ModuleActivated so
     * the setup seeder runs again. Existing data is NOT deleted — seeders use
     * firstOrCreate, so only missing records are created.
     *
     * Useful when:
     *  - The module was activated before its product/plans existed (e.g. fresh install).
     *  - The admin accidentally deleted the module's product and wants to recreate it.
     */
    public function reinstall(Module $module)
    {
        if (! $module->isActive()) {
            Flash::error("Module [{$module->name}] must be active to reinstall.");
            return redirect()->route('ch-admin.modules.index');
        }

        // Clear the setup_complete flag so the seeder runs again.
        // Convention: {slug_with_underscores}.setup_complete
        $settingKey = str_replace('-', '_', $module->slug) . '.setup_complete';
        \App\Models\Setting::where('key', $settingKey)->delete();
        \Illuminate\Support\Facades\Cache::forget('settings');

        // Re-register the provider (it was already booted at request start, but
        // registering again is a no-op for the container — we just need the event
        // listeners it wires up to be in place before we fire ModuleActivated).
        if (! empty($module->provider_class) && class_exists($module->provider_class)) {
            app()->register($module->provider_class);
        }

        event(new ModuleActivated($module));

        Flash::success("Module [{$module->name}] has been reinstalled successfully.");
        return redirect()->route('ch-admin.modules.index');
    }

    /**
     * Deactivate a module.
     *
     * What happens on deactivate:
     *  - ModuleLoader::boot() will no longer register this module's ServiceProvider.
     *  - All module routes, views, and admin menu items disappear on next request.
     *  - Database tables and data are PRESERVED (no rollback).
     *  - Users hitting the module URL will get a 404.
     */
    public function deactivate(Module $module)
    {
        $module->deactivate();

        Flash::success("Module [{$module->name}] has been deactivated.");
        return redirect()->route('ch-admin.modules.index');
    }

    /**
     * Uninstall (delete) a module entirely.
     */
    public function uninstall(Module $module)
    {
        $directory = $this->getModuleDirectory($module);
        $modulePath = ModuleLoader::modulesPath($directory);

        ModuleLoader::unlinkAssets($directory);

        if (File::exists($modulePath)) {
            File::deleteDirectory($modulePath);
        }

        $module->delete();

        Flash::success("Module [{$module->name}] has been removed.");
        return redirect()->route('ch-admin.modules.index');
    }

    // -------------------------------------------------------------------------
    // Auto-update endpoints
    // -------------------------------------------------------------------------

    /**
     * Return cached update status for all installed modules, keyed by slug.
     * Data comes from the last UpdateService::checkForUpdates() response.
     */
    public function checkUpdates(ModuleUpdater $updater)
    {
        $results = $updater->checkAll();
        return response()->json($results);
    }

    /**
     * Apply an available update for a single module.
     *
     * Expects JSON body: { download_url, checksum, version, changelog? }
     */
    public function applyUpdate(Request $request, Module $module, ModuleUpdater $updater)
    {
        $data = $request->validate([
            'download_url' => ['required', 'url'],
            'checksum'     => ['required', 'string', 'regex:/^sha256:[a-f0-9]{64}$/'],
            'version'      => ['required', 'string', 'max:30'],
            'changelog'    => ['nullable', 'string', 'max:5000'],
        ]);

        try {
            $updater->apply(
                module:       $module,
                downloadUrl:  $data['download_url'],
                checksum:     $data['checksum'],
                toVersion:    $data['version'],
                changelog:    $data['changelog'] ?? '',
                initiatedBy:  $request->user()?->id,
            );
        } catch (\RuntimeException $e) {
            if ($request->expectsJson()) {
                return response()->json(['error' => $e->getMessage()], 422);
            }
            Flash::error('Update failed: ' . $e->getMessage());
            return redirect()->route('ch-admin.modules.index');
        }

        if ($request->expectsJson()) {
            return response()->json(['success' => true, 'version' => $data['version']]);
        }

        Flash::success("Module [{$module->name}] updated to v{$data['version']} successfully.");
        return redirect()->route('ch-admin.modules.index');
    }

    /**
     * Show recent update history for a single module.
     */
    public function updateHistory(Module $module)
    {
        $logs = ModuleUpdateLog::forModule($module->slug)
            ->latest()
            ->paginate(20);

        return response()->json($logs);
    }

    /**
     * Derive the filesystem directory name from the module's provider class.
     * e.g. "Modules\AiCvBuilder\Providers\ModuleServiceProvider" → "AiCvBuilder"
     */
    private function getModuleDirectory(Module $module): string
    {
        // Try to extract from provider class namespace first (most reliable)
        if (! empty($module->provider_class)) {
            $parts = explode('\\', $module->provider_class);
            // Provider class is Modules\{Directory}\Providers\...
            if (count($parts) >= 2 && $parts[0] === 'Modules') {
                return $parts[1];
            }
        }

        // Fallback: convert slug to StudlyCase (ai-cv-builder → AiCvBuilder)
        return \Illuminate\Support\Str::studly($module->slug);
    }


}
