<?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.
 *
 * Drives a 5-step wizard:
 *   1. Backup files + DB     → POST /admin/update/backup
 *   2. Upload update ZIP     → POST /admin/update/upload
 *   3. Apply (extract + copy)→ POST /admin/update/apply
 *   4. Run new migrations    → POST /admin/update/migrate
 *   5. Clear caches + verify → POST /admin/update/finalize
 *
 *   Rollback (anytime)       → POST /admin/update/rollback
 *
 * Only Super Admins (Spatie role OR is_admin + admin_role=super_admin)
 * can access. Each step returns JSON; the blade is a single-page Alpine
 * wizard that drives them sequentially.
 */
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. Sudhir's been
                // testing as a customer; this catches "wrong user logged in".
                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.
                // Previously this check only handled User, so customers
                // logging into the admin panel via the admin guard hit
                // 403 even when their admins.role was 'super_admin'.
                $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,
                        'admin_role' => $user->admin_role,
                    ]);
                    abort(403, 'Only Super Admins can access the system updater.');
                }

                // License gate — only the PRE-APPLY wizard steps
                // (backup / upload / apply) refuse to run until the
                // platform Envato purchase code has been verified.
                //
                // The POST-APPLY steps — migrate, finalize, rollback —
                // are intentionally whitelisted. Reason: those routes
                // can ONLY be reached after a successful apply() call,
                // which is itself gated by this same license check.
                // If the wizard mid-flight is asked to re-verify the
                // license after apply(), the customer is stuck: files
                // are already at the new version on disk, but the
                // migration step is blocked, the version stamp says
                // "new" while the DB schema is still old, and a
                // customer who reload the page sees a "successfully
                // updated" UI on top of a half-broken install.
                //
                // The risk of allowing these three routes through
                // without a license re-check is bounded: super-admin
                // is still required (gate above), and the upstream
                // apply() step already proved the license was valid
                // moments ago. We also explicitly log every bypass
                // so an audit trail exists.
                //
                // GET /admin/update is intentionally allowed through
                // so the wizard view can render its Step 0 "Verify
                // purchase code" panel inline; the verify route
                // (POST /admin/update/license) is whitelisted below
                // so the admin can actually submit the code from there.
                /** @var \App\Services\Addons\AddonLicenseService $licenses */
                $licenses = app(\App\Services\Addons\AddonLicenseService::class);
                $isLicenseSubmit = $request->routeIs('admin.update.license');
                $isViewLoad      = $request->isMethod('GET');
                $isLicensed      = $licenses->isLicensed();
                $isPostApplyStep = $request->routeIs(
                    'admin.update.migrate',
                    'admin.update.finalize',
                    'admin.update.rollback',
                );

                // [DIAG] Step 4 — license gate decision. Even when the user
                // passes the super-admin check, an unlicensed install on a
                // pre-apply POST step will return 403 JSON. This logs the
                // booleans that produce that decision so a stuck update
                // can be debugged from the log alone.
                Log::info('UPDATER.middleware: LICENSE GATE', [
                    'rid'                => $rid,
                    'is_licensed'        => $isLicensed,
                    'is_license_submit'  => $isLicenseSubmit,
                    'is_view_load'       => $isViewLoad,
                    'is_post_apply_step' => $isPostApplyStep,
                    'will_block'         => !$isLicensed && !$isLicenseSubmit && !$isViewLoad && !$isPostApplyStep,
                ]);

                if (!$isLicensed && !$isLicenseSubmit && !$isViewLoad && !$isPostApplyStep) {
                    Log::warning('UPDATER.middleware: DENIED — license required', ['rid' => $rid]);
                    return response()->json([
                        'success' => false,
                        'message' => 'Verify your MailTrixy Envato purchase code in Step 1 before running update steps.',
                    ], 403);
                }

                if (!$isLicensed && $isPostApplyStep) {
                    Log::notice('UPDATER.middleware: ALLOWED (post-apply bypass) — license not yet verified but request is for migrate/finalize/rollback. The pre-apply gate already validated the license; letting the update finish prevents a half-broken install.', [
                        'rid'    => $rid,
                        'route'  => $request->route()?->getName(),
                    ]);
                }

                // [DIAG] Step 5 — we're through. Logged at INFO so the
                // "allowed" path appears in the same stream as the denies,
                // making it grep-friendly for ops.
                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();

        // Platform license payload feeds the wizard's Step 1 "Verify
        // Envato purchase code" panel. Null → form shown; non-null →
        // step renders as a completed checkmark with the buyer name.
        $platformLicense = app(\App\Services\Addons\AddonLicenseService::class)
            ->readPlatformLicense();

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

    /**
     * Step 1 of the wizard — verify the platform Envato purchase code.
     * Mirrors AddonController::verifyPlatformLicense so admins can
     * complete licensing inline from either page without bouncing.
     * Whitelisted in the controller middleware so it runs even when
     * the install isn't licensed yet (catch-22 otherwise).
     */
    public function license(Request $request): JsonResponse
    {
        $code = trim((string) $request->input('purchase_code', ''));
        if ($code === '') {
            return response()->json([
                'success' => false,
                'message' => 'Please paste your Envato purchase code.',
            ], 422);
        }

        try {
            $payload = app(\App\Services\Addons\AddonLicenseService::class)
                ->verifyPlatformLicense($code);
        } catch (\Throwable $e) {
            Log::warning('[Updater] License verify failed', [
                'error' => $e->getMessage(),
            ]);
            return response()->json([
                'success' => false,
                'message' => $e->getMessage(),
            ], 422);
        }

        $buyer = $payload['envato_buyer'] ?: 'Envato buyer';
        return response()->json([
            'success' => true,
            'message' => "MailTrixy licensed to {$buyer}. You can now run the update steps below.",
            'license' => [
                'envato_buyer' => $payload['envato_buyer'] ?? '',
                'verified_at'  => $payload['verified_at'] ?? '',
            ],
        ]);
    }

    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);
        }
    }
}
