<?php

namespace App\Services\Addons;

use App\Models\Addon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

/**
 * Loads and registers installed addons at application boot.
 *
 * Each addon lives at /addons/{slug}/ and must contain an addon.json manifest.
 * On boot, this manager:
 *   1. Queries the `addons` table for rows with status='active'
 *   2. For each one, requires the addon's PHP files, registers its
 *      service provider (if declared), and mounts its routes/views.
 *
 * The manager is intentionally fault-tolerant: a single broken addon must
 * NEVER take down the whole admin panel. Every step is wrapped in a try/catch
 * and surfaces an admin-visible error on the addons page instead of crashing.
 */
class AddonManager
{
    /**
     * Root folder where addons are extracted. Returned as an absolute path
     * so File::* helpers don't get confused by CWD changes during installs.
     */
    public static function root(): string
    {
        return base_path('addons');
    }

    public static function ensureRootExists(): void
    {
        $root = self::root();
        if (!File::isDirectory($root)) {
            File::makeDirectory($root, 0755, true);
        }
    }

    /**
     * Boot every active addon. Called from AppServiceProvider::boot()
     * after the schema is ready (so the addons table query doesn't fail
     * during a fresh install).
     */
    public function bootActiveAddons(): void
    {
        // Skip during installer / before DB exists. Without this the first
        // page load of a fresh install crashes on the addons-table query.
        if (!self::tableReady()) {
            return;
        }

        $active = Addon::where('status', 'active')->get();
        foreach ($active as $addon) {
            try {
                $this->bootSingle($addon);
            } catch (\Throwable $e) {
                Log::error("AddonManager: failed to boot {$addon->slug}: " . $e->getMessage(), [
                    'trace' => $e->getTraceAsString(),
                ]);
            }
        }
    }

    /**
     * Cache the table-existence check for the request lifetime —
     * Schema::hasTable hits the information_schema on every call.
     */
    protected static function tableReady(): bool
    {
        static $cached = null;
        if ($cached !== null) {
            return $cached;
        }
        try {
            return $cached = Schema::hasTable('addons');
        } catch (\Throwable $e) {
            return $cached = false;
        }
    }

    /**
     * Wire one active addon into the framework: load PHP files, register
     * service provider, mount routes, register view namespace.
     */
    protected function bootSingle(Addon $addon): void
    {
        $manifest = $addon->manifest ?? [];
        $path = $addon->path();

        if (!File::isDirectory($path)) {
            Log::warning("AddonManager: folder missing for active addon {$addon->slug}", ['expected' => $path]);
            return;
        }

        // License gate — paired with AddonController::activate's check
        // but ALSO enforced here so a manually-flipped status='active'
        // row (DB edit, SQL injection, restored backup) still can't
        // boot the addon without a valid license file on disk.
        //
        // Resolved via the container instead of being injected so this
        // class stays compatible with existing call sites that
        // construct AddonManager directly.
        try {
            /** @var \App\Services\Addons\AddonLicenseService $licenses */
            $licenses = app(\App\Services\Addons\AddonLicenseService::class);
            if (!$licenses->isLicensed($addon)) {
                Log::warning("AddonManager: skipping boot of {$addon->slug} — no license file. "
                    . 'Admin must enter the Envato purchase code from Settings → Addons.');
                return;
            }
        } catch (\Throwable $e) {
            // Don't take down the addons subsystem if the license
            // helpers themselves error — log and refuse to boot, which
            // is the same outcome as "license missing".
            Log::error("AddonManager: license check failed for {$addon->slug}: " . $e->getMessage());
            return;
        }

        // 1. Composer-style PSR-4 autoload: register the addon's `src/`
        //    directory under whatever namespace the manifest declares so
        //    `use App\Addons\Instagram\…` resolves without composer.json edits.
        $namespace = $manifest['namespace'] ?? null;
        if ($namespace && File::isDirectory($path . '/src')) {
            spl_autoload_register(function ($class) use ($namespace, $path) {
                $prefix = rtrim($namespace, '\\') . '\\';
                if (str_starts_with($class, $prefix)) {
                    $relative = substr($class, strlen($prefix));
                    $file = $path . '/src/' . str_replace('\\', '/', $relative) . '.php';
                    if (File::exists($file)) {
                        require_once $file;
                    }
                }
            });
        }

        // 2. Service provider — if the manifest declares one, register it.
        $providerClass = $manifest['provider'] ?? null;
        if ($providerClass && class_exists($providerClass)) {
            app()->register($providerClass);
        }

        // 3. Views — addons can ship blades under views/ and reference them
        //    via {{-- @include('{slug}::name') --}} in their own templates.
        if (File::isDirectory($path . '/views')) {
            view()->addNamespace($addon->slug, $path . '/views');
        }

        // 4. Routes — load routes.php if present, wrapping in a group so
        //    the addon doesn't need to repeat middleware. The addon picks
        //    its own URL prefix inside that file.
        $routesFile = $path . '/routes.php';
        if (File::exists($routesFile)) {
            Route::middleware('web')->group($routesFile);
        }
    }

    /**
     * Reads an addon.json from a folder and returns the manifest array,
     * or throws if it's missing/invalid. Used during install validation.
     */
    public static function readManifest(string $folder): array
    {
        $file = rtrim($folder, '/\\') . '/addon.json';
        if (!File::exists($file)) {
            throw new \RuntimeException('addon.json is missing in the uploaded ZIP.');
        }
        $raw = File::get($file);
        $data = json_decode($raw, true);
        if (!is_array($data)) {
            throw new \RuntimeException('addon.json is not valid JSON: ' . json_last_error_msg());
        }
        foreach (['slug', 'name', 'version'] as $required) {
            if (empty($data[$required])) {
                throw new \RuntimeException("addon.json is missing required field: {$required}");
            }
        }
        // Slug must be filesystem-safe — no `..`, no slashes, no spaces.
        // Without this guard a malicious ZIP could escape /addons/ by
        // setting slug='../../app/Http/Controllers'.
        if (!preg_match('/^[a-z0-9][a-z0-9\-_]{1,99}$/i', $data['slug'])) {
            throw new \RuntimeException('addon.json slug must be alphanumeric (dashes/underscores allowed).');
        }
        return $data;
    }

    /**
     * Run the addon's migrations (in addons/{slug}/migrations/) using the
     * regular `migrate` artisan command pointed at that folder.
     */
    public function runMigrations(Addon $addon): void
    {
        $folder = $addon->path('migrations');
        if (!File::isDirectory($folder)) {
            return;
        }
        // --realpath because Laravel's migrate path resolver chokes on
        // back-slashes on Windows when given a relative addon path.
        Artisan::call('migrate', [
            '--path' => str_replace(base_path() . DIRECTORY_SEPARATOR, '', $folder),
            '--realpath' => false,
            '--force' => true,
        ]);
    }

    /**
     * Activate an addon: run migrations first, then flip status + boot.
     *
     * Order matters: previously we flipped `status=active` BEFORE running
     * migrations, so a migration failure left the addon visibly Active in
     * the admin panel even though its tables / columns didn't exist. The
     * UI's idempotency check (`if status === 'active' return`) then made
     * re-clicking Activate a no-op — the only escape was Deactivate → Activate.
     *
     * Now we migrate first; only on success do we flip the row. Any DDL
     * exception bubbles up to the AddonController which already catches it
     * and shows an inline error toast — no half-activated state lingers.
     */
    public function activate(Addon $addon): void
    {
        if ($addon->status === 'active') {
            return;
        }
        $this->runMigrations($addon);
        $addon->update([
            'status' => 'active',
            'activated_at' => now(),
        ]);
        // Boot the freshly-activated addon for the current request so the
        // admin can immediately use it without a page reload + re-boot.
        $this->bootSingle($addon->fresh());
    }

    public function deactivate(Addon $addon): void
    {
        $addon->update(['status' => 'inactive']);
        // We don't undo Route::group() / view namespaces here — they're
        // process-local and disappear on the next request. The DB row
        // flip is enough to prevent re-boot.
    }

    /**
     * Uninstall = deactivate + delete the addon folder. Migrations are
     * NOT rolled back: most addons store live data (Instagram conversations
     * etc.) and silently dropping a customer's messages would be worse than
     * leaving an inert column behind. Admin can drop the table manually.
     */
    public function uninstall(Addon $addon): void
    {
        $this->deactivate($addon);
        $folder = $addon->path();
        if (File::isDirectory($folder) && Str::startsWith(realpath($folder) ?: '', realpath(self::root()) ?: 'NEVER_MATCH')) {
            // Belt-and-braces: realpath check ensures we only delete a folder
            // that is actually under /addons/. Without it a corrupted slug
            // could in theory point File::deleteDirectory at the project root.
            File::deleteDirectory($folder);
        }
        $addon->delete();
    }
}
