<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Services\UpdaterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
use Illuminate\Support\Facades\Log;

/**
 * Admin → System Update screen.  ── NO-LICENSE VARIANT ──
 *
 * This is the AddonLicenseService-stripped build of UpdateController for
 * customers we ship without the Envato purchase-code gate (grandfathered
 * accounts, internal staging boxes, white-label resells where the license
 * step is contractually disabled).
 *
 * Differences vs. the standard build:
 *   - No license gate in the middleware: every wizard step runs unconditionally
 *     once the super-admin check passes.
 *   - No `license()` action: the route /admin/update/license is intentionally
 *     left undefined on this build; remove it from routes/web.php too.
 *   - `index()` no longer passes a `$platformLicense` payload to the view.
 *     The blade variant for this build must hide its Step 1 panel.
 *
 * Everything else (backup → upload → apply → migrate → finalize → rollback)
 * is identical to the standard controller.
 *
 * Drop-in usage:
 *   1. Rename this file from "UpdateController copy.php" → "UpdateController.php"
 *      (replace the original file on the target server).
 *   2. Edit routes/web.php and remove the `admin.update.license` route entry.
 *   3. Run `php artisan optimize:clear && sudo systemctl restart php8.5-fpm`.
 *
 * Super-admin resolution still supports all three auth contexts:
 *   - admin guard  (App\Models\Admin with role='super_admin')
 *   - web guard    (Spatie "Super Admin" role)
 *   - web guard    (legacy User.is_admin + User.admin_role='super_admin')
 */
class UpdateController extends Controller implements HasMiddleware
{
    public static function middleware(): array
    {
        return [
            new Middleware(function ($request, $next) {
                // [DIAG] Stamp every entry to the updater middleware with
                // a request id so we can correlate every log line in one
                // request. Use the X-Request-ID header if a reverse proxy
                // set it; otherwise generate a short random id ourselves.
                $rid = $request->header('X-Request-ID')
                    ?: substr(bin2hex(random_bytes(4)), 0, 8);

                // [DIAG] Step 1 — what does Laravel see about the request?
                Log::info('UPDATER.middleware: ENTRY', [
                    'rid'         => $rid,
                    'method'      => $request->method(),
                    'route_name'  => $request->route()?->getName(),
                    'route_uri'   => $request->route()?->uri(),
                    'path'        => $request->path(),
                    'auth_guard'  => config('auth.defaults.guard'),
                    'session_id'  => $request->hasSession() ? $request->session()->getId() : null,
                    'has_session' => $request->hasSession(),
                    'ip'          => $request->ip(),
                    'ua'          => substr((string) $request->userAgent(), 0, 120),
                ]);

                $user = $request->user();

                // [DIAG] Step 2 — who, if anyone, did Laravel resolve from
                // the session? Null here = unauthenticated.
                Log::info('UPDATER.middleware: AUTH RESOLVED', [
                    'rid'         => $rid,
                    'user_id'     => $user?->id,
                    'user_email'  => $user?->email,
                    'user_class'  => $user ? get_class($user) : null,
                    'is_admin'    => $user?->is_admin,
                    'admin_role'  => $user?->admin_role,
                    'is_admin_type'   => $user ? gettype($user->is_admin) : null,
                    'admin_role_type' => $user ? gettype($user->admin_role) : null,
                    'active_workspace_id' => $user?->active_workspace_id,
                    'status'      => $user?->status,
                ]);

                if (!$user) {
                    Log::warning('UPDATER.middleware: DENIED — no user', ['rid' => $rid]);
                    abort(403);
                }

                // Super-admin resolution across BOTH auth models:
                //   - App\Models\Admin (admin guard)  — has `role` column +
                //     isSuperAdmin() helper. This is what /admin/* routes
                //     resolve to in production.
                //   - App\Models\User (web guard)     — has the legacy
                //     `is_admin` + `admin_role` columns OR a Spatie
                //     "Super Admin" role.
                $adminGuard   = $user instanceof \App\Models\Admin;
                $adminRoleCol = $adminGuard ? ($user->role ?? null) : null;
                $adminSuper   = $adminGuard
                    && (method_exists($user, 'isSuperAdmin')
                        ? $user->isSuperAdmin()
                        : $adminRoleCol === 'super_admin');

                $hasMethod    = method_exists($user, 'hasRole');
                $spatieRole   = $hasMethod ? (bool) $user->hasRole('Super Admin') : false;
                $legacyAdmin  = (bool) (isset($user->is_admin)
                    && $user->is_admin
                    && ($user->admin_role ?? '') === 'super_admin');

                $isSuper      = $adminSuper || $spatieRole || $legacyAdmin;

                Log::info('UPDATER.middleware: SUPER ADMIN CHECK', [
                    'rid'             => $rid,
                    'user_class'      => get_class($user),
                    'admin_guard_model' => $adminGuard,
                    'admin_role_col'  => $adminRoleCol,
                    'admin_super'     => $adminSuper,
                    'has_role_method' => $hasMethod,
                    'spatie_role_super_admin'    => $spatieRole,
                    'legacy_is_admin_plus_role'  => $legacyAdmin,
                    'is_admin_raw'   => $user->is_admin ?? null,
                    'admin_role_raw' => $user->admin_role ?? null,
                    'final_is_super' => $isSuper,
                ]);

                if (!$isSuper) {
                    Log::warning('UPDATER.middleware: DENIED — not super admin', [
                        'rid'        => $rid,
                        'user_id'    => $user->id,
                        'email'      => $user->email,
                        'is_admin'   => $user->is_admin ?? null,
                        'admin_role' => $user->admin_role ?? null,
                    ]);
                    abort(403, 'Only Super Admins can access the system updater.');
                }

                // ── No license gate on this build ─────────────────────
                // The standard controller calls AddonLicenseService::isLicensed()
                // here and refuses POST wizard steps until the Envato purchase
                // code has been verified. That whole block is intentionally
                // stripped from this variant — wizard steps run as soon as the
                // super-admin check passes.
                // ──────────────────────────────────────────────────────

                // [DIAG] We're through.
                Log::info('UPDATER.middleware: ALLOWED', [
                    'rid'     => $rid,
                    'user_id' => $user->id,
                    'route'   => $request->route()?->getName(),
                ]);

                return $next($request);
            }),
        ];
    }

    public function __construct(private UpdaterService $updater)
    {
    }

    public function index()
    {
        $currentVersion = $this->updater->currentVersion();
        $currentBuild = $this->updater->currentBuild();
        $backups = $this->updater->listBackups();

        // ── No platform-license payload on this build ─────────────────
        // Standard controller resolves AddonLicenseService::readPlatformLicense()
        // here and passes it to the view so the wizard's Step 1 "Verify
        // Envato purchase code" panel can show the buyer name. This build
        // ships without that step, so the view doesn't get the variable —
        // the matching blade in this distribution must omit Step 1.
        // ──────────────────────────────────────────────────────────────

        return view('admin.update.index', compact(
            'currentVersion',
            'currentBuild',
            'backups',
        ));
    }

    // license() removed in this variant — see file docblock.

    public function backup(): JsonResponse
    {
        // Pre-flight checks so the admin sees a clear cause instead of
        // a generic "An unexpected error occurred" from the global 500 handler.
        $issues = $this->preflightBackupChecks();
        if (! empty($issues)) {
            return response()->json([
                'success' => false,
                'message' => 'Backup failed pre-flight: ' . implode(' | ', $issues),
            ], 500);
        }

        try {
            $result = $this->updater->createBackup();
            return response()->json([
                'success' => true,
                'message' => 'Backup created successfully.',
                'backup' => $result,
            ]);
        } catch (\Throwable $e) {
            Log::error('[Updater] Backup step failed', [
                'class' => get_class($e),
                'message' => $e->getMessage(),
                'file' => $e->getFile() . ':' . $e->getLine(),
            ]);

            return response()->json([
                'success' => false,
                'message' => sprintf(
                    'Backup failed (%s): %s @ %s:%d',
                    class_basename($e),
                    $e->getMessage(),
                    basename($e->getFile()),
                    $e->getLine()
                ),
            ], 500);
        }
    }

    private function preflightBackupChecks(): array
    {
        $issues = [];

        if (! class_exists(\ZipArchive::class)) {
            $issues[] = 'PHP ZipArchive extension is not installed (run: sudo apt install php-zip && restart php-fpm)';
        }

        $backupBase = storage_path('app/backups');
        if (! is_dir($backupBase)) {
            @mkdir($backupBase, 0755, true);
        }
        if (! is_dir($backupBase) || ! is_writable($backupBase)) {
            $issues[] = sprintf('Backup directory not writable: %s (run: sudo chown -R www-data:www-data storage && sudo chmod -R 775 storage)', $backupBase);
        }

        $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
        if (in_array('exec', $disabled, true) && in_array('shell_exec', $disabled, true) && in_array('proc_open', $disabled, true)) {
            // Not fatal — Updater falls back to PHP-based dump — but log it.
            $issues[] = 'PHP exec/shell_exec/proc_open are all disabled; mysqldump will fall back to slower PHP-based dump';
        }

        $freeBytes = @disk_free_space($backupBase) ?: 0;
        if ($freeBytes > 0 && $freeBytes < 200 * 1024 * 1024) {
            $issues[] = sprintf('Less than 200 MB free disk space on backup volume (%s MB available)', round($freeBytes / 1048576));
        }

        // Soft warnings (non-fatal) are excluded from the returned list so the wizard proceeds.
        return array_filter($issues, fn ($i) => ! str_starts_with($i, 'PHP exec'));
    }

    public function upload(Request $request): JsonResponse
    {
        $request->validate([
            'file' => ['required', 'file', 'mimes:zip', 'max:512000'],
        ]);

        try {
            $path = $this->updater->saveUploadedZip($request->file('file'));

            $zipVersion = $this->updater->getZipVersion($path);
            if (!$zipVersion) {
                $this->updater->cleanup();
                return response()->json([
                    'success' => false,
                    'message' => 'Invalid update package. No version info found in ZIP (config/version.php missing).',
                ], 422);
            }

            $currentVersion = $this->updater->currentVersion();
            if (version_compare($zipVersion, $currentVersion, '<=')) {
                $this->updater->cleanup();
                return response()->json([
                    'success' => false,
                    'message' => "ZIP contains v{$zipVersion} but you already have v{$currentVersion}. Upload a newer version.",
                ], 422);
            }

            return response()->json([
                'success' => true,
                'message' => "Update ZIP uploaded — v{$zipVersion} ready to install.",
                'zip_version' => $zipVersion,
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Upload failed: ' . $e->getMessage(),
            ], 500);
        }
    }

    public function apply(Request $request): JsonResponse
    {
        $zipPath = storage_path('app/temp/updater/update.zip');

        if (!file_exists($zipPath)) {
            return response()->json([
                'success' => false,
                'message' => 'No update ZIP found. Please upload first.',
            ], 400);
        }

        try {
            $updated = $this->updater->applyUpdate($zipPath);
            return response()->json([
                'success' => true,
                'message' => 'Files updated successfully.',
                'updated' => $updated,
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Apply failed: ' . $e->getMessage(),
            ], 500);
        }
    }

    public function migrate(): JsonResponse
    {
        try {
            $output = $this->updater->runMigrations();
            return response()->json([
                'success' => true,
                'message' => 'Migrations completed.',
                'output' => $output,
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Migration failed: ' . $e->getMessage(),
            ], 500);
        }
    }

    public function finalize(): JsonResponse
    {
        try {
            $this->updater->clearCaches();
            $health = $this->updater->healthCheck();
            $this->updater->cleanup();

            $allGood = !in_array(false, $health, true);

            return response()->json([
                'success' => $allGood,
                'message' => $allGood ? 'Update completed successfully!' : 'Update done but health check has warnings.',
                'health' => $health,
                'new_version' => config('version.version'),
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Finalize failed: ' . $e->getMessage(),
            ], 500);
        }
    }

    public function rollback(Request $request): JsonResponse
    {
        $request->validate([
            'backup_dir' => ['required', 'string'],
        ]);

        $dir = $request->input('backup_dir');

        // Security: ensure the path is within our backups directory
        $backupBase = realpath(storage_path('app/backups'));
        $targetDir = realpath($dir);
        if (!$targetDir || !str_starts_with($targetDir, $backupBase)) {
            return response()->json(['success' => false, 'message' => 'Invalid backup path.'], 403);
        }

        try {
            $this->updater->rollback($targetDir);
            return response()->json([
                'success' => true,
                'message' => 'Rollback completed. Version restored.',
            ]);
        } catch (\Throwable $e) {
            return response()->json([
                'success' => false,
                'message' => 'Rollback failed: ' . $e->getMessage(),
            ], 500);
        }
    }
}
