<?php

namespace App\Traits;

use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

/**
 * Provides workspace-scoped authorization and operation locking for Livewire components.
 *
 * Role hierarchy: owner (4) > admin (3) > agent (2) > viewer (1)
 * Permission levels:
 *   - view:      viewer+   (read dashboards, reports, data)
 *   - interact:  agent+    (reply, tag, assign, create contacts/deals)
 *   - create:    agent+    (alias for interact)
 *   - manage:    admin+    (settings, billing, send campaigns, AI config)
 *   - dangerous: owner only (delete workspace, remove members, dangerous operations)
 */
trait AuthorizesWorkspaceActions
{
    /**
     * Get the current user's role in the active workspace (cached 60s).
     */
    protected function getWorkspaceRole(): string
    {
        $user = Auth::user();

        if (! $user || ! $user->active_workspace_id) {
            return 'viewer';
        }

        $cacheKey = "ws_role:{$user->id}:{$user->active_workspace_id}";

        // FIX-005/006: Reduced from 60s to 10s to minimize privilege escalation window
        return Cache::remember($cacheKey, 10, function () use ($user) {
            $membership = $user->workspaces()
                ->where('workspaces.id', $user->active_workspace_id)
                ->first();

            return $membership?->pivot?->role ?? 'viewer';
        });
    }

    /**
     * Authorize the current action. Returns false and flashes error if unauthorized.
     *
     * Usage in Livewire methods:
     *   if (! $this->authorizeWorkspaceAction('manage')) return;
     */
    protected function authorizeWorkspaceAction(string $level): bool
    {
        $role = $this->getWorkspaceRole();

        $roleWeight = match ($role) {
            'owner' => 4,
            'admin' => 3,
            'agent' => 2,
            'viewer' => 1,
            default => 0,
        };

        $requiredWeight = match ($level) {
            'view' => 1,
            'interact' => 2,
            'create' => 2,
            'manage' => 3,
            'dangerous' => 4,
            default => 4,
        };

        if ($roleWeight < $requiredWeight) {
            session()->flash('error', 'You do not have permission to perform this action.');
            return false;
        }

        return true;
    }

    /**
     * Check if the user has at least a given role (for conditional UI rendering).
     */
    protected function isAtLeast(string $minimumRole): bool
    {
        $roleWeight = match ($this->getWorkspaceRole()) {
            'owner' => 4, 'admin' => 3, 'agent' => 2, 'viewer' => 1, default => 0,
        };

        $requiredWeight = match ($minimumRole) {
            'owner' => 4, 'admin' => 3, 'agent' => 2, 'viewer' => 1, default => 4,
        };

        return $roleWeight >= $requiredWeight;
    }

    /**
     * Execute a callback inside an operation lock to prevent duplicate actions.
     * Returns null and flashes error if lock cannot be acquired.
     *
     * Usage:
     *   return $this->withOperationLock("send-campaign-{$id}", function () {
     *       // ... send logic
     *   });
     */
    /**
     * Run $callback under a per-user cache lock so duplicate UI clicks
     * can't trigger the side effect twice. Idempotent across:
     *   - the same user double-clicking Send before the first response
     *     comes back (most common case — Livewire fires twice on flaky
     *     networks)
     *   - two PHP-FPM workers handling near-simultaneous Livewire
     *     requests for the same component
     *
     * Failure modes the old implementation silently dropped (causing the
     * "I clicked Send and nothing happened, ended up writing a script
     * to re-dispatch jobs" bug report):
     *
     *   1. Lock genuinely held by another in-flight request → we now
     *      `block()` for $blockWaitSeconds before giving up, so the
     *      second click almost always waits briefly and succeeds
     *      instead of failing instantly with a session flash that
     *      Livewire never renders.
     *   2. `cache_locks` table missing OR cache driver doesn't support
     *      atomic locks (some shared-hosting configs default to `array`
     *      or misconfigured `database` cache) → we catch
     *      BadMethodCallException + RuntimeException and execute the
     *      callback anyway, after logging a warning. Better a non-locked
     *      send than a silently-dropped one.
     *   3. Genuine timeout after $blockWaitSeconds → log enough context
     *      (operation, user, key, ttl, driver) so a developer can
     *      diagnose stuck locks from production logs.
     */
    protected function withOperationLock(
        string $operation,
        callable $callback,
        int $ttlSeconds = 30,
        int $blockWaitSeconds = 3
    ): mixed {
        $key  = "op_lock:{$operation}:" . Auth::id();
        $ctx  = ['operation' => $operation, 'user_id' => Auth::id(), 'key' => $key];

        try {
            $lock = Cache::lock($key, $ttlSeconds);
        } catch (\Throwable $e) {
            // Cache store doesn't support locking at all (e.g. `null`
            // driver on a misconfigured install). Degrade gracefully —
            // running the callback once unlocked is still safer than
            // refusing all sends until the cache config is fixed.
            Log::warning('withOperationLock: cache driver does not support locks; running callback unlocked', $ctx + [
                'driver' => config('cache.default'),
                'error'  => $e->getMessage(),
            ]);
            return $callback();
        }

        try {
            // `block()` waits up to N seconds for the lock and auto-
            // releases when the callback returns or throws. This handles
            // the rapid-double-click race without bouncing the user
            // with a useless "already in progress" error.
            return $lock->block($blockWaitSeconds, $callback);
        } catch (LockTimeoutException $e) {
            // Another request really IS holding the lock for longer than
            // we waited. Surface a clear, actionable message AND log so
            // an admin can correlate stuck locks with a stuck send.
            Log::warning('withOperationLock: lock timeout — callback not executed', $ctx + [
                'wait_seconds' => $blockWaitSeconds,
                'ttl_seconds'  => $ttlSeconds,
            ]);
            session()->flash('error', 'This operation is already in progress. Please wait a moment and try again.');
            return null;
        } catch (\BadMethodCallException | \RuntimeException $e) {
            // The cache driver returned a Lock object whose backing
            // implementation later threw (e.g. table missing). Same
            // degradation as the constructor path above — run unlocked
            // rather than swallow the user's click.
            Log::warning('withOperationLock: lock backend failed; running callback unlocked', $ctx + [
                'driver' => config('cache.default'),
                'error'  => $e->getMessage(),
            ]);
            return $callback();
        }
    }

    /**
     * Flush cached role when workspace membership changes.
     */
    protected function flushRoleCache(): void
    {
        $user = Auth::user();
        if ($user && $user->active_workspace_id) {
            Cache::forget("ws_role:{$user->id}:{$user->active_workspace_id}");
        }
    }
}
