<?php

namespace App\Http\Controllers\Widget;

use App\Events\Conversations\HumanRequestedEvent;
use App\Models\Agent;
use App\Models\Conversation;
use App\Models\Workspace;
use App\Services\LiveChat\BusinessHours;
use App\Services\LiveChat\WorkspacePresence;
use App\Services\Widget\WidgetJwt;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

/**
 * Visitor-side "I want to talk to a human" signal.
 *
 * Fired when the visitor clicks the escalation pill (or any future UI
 * surface that wants to flag a conversation for operator attention).
 * Sets `conversations.human_requested_at` so the operator-side queue
 * can surface it under the "Needs human" filter, broadcasts a
 * `HumanRequestedEvent` so an admin tab gets the live ping, and
 * causes `MessageStreamController` to serve a holding bubble instead
 * of an LLM reply on subsequent turns until an operator claims.
 *
 * Idempotent: a second click during the same waiting window doesn't
 * reset the timestamp (so the waiting indicator's "X min ago" stays
 * truthful) and doesn't re-broadcast.
 */
class RequestHumanController
{
    public function __construct(
        private WidgetJwt $jwt,
        private WorkspacePresence $presence,
        private BusinessHours $businessHours,
    ) {}

    public function __invoke(Request $request): JsonResponse
    {
        $token = $request->bearerToken() ?? $request->header('X-Widget-Token');
        if (! is_string($token)) {
            return response()->json(['error' => ['code' => 'missing_token']], 401);
        }
        try {
            $claims = $this->jwt->verify($token);
        } catch (\Throwable) {
            return response()->json(['error' => ['code' => 'invalid_token']], 401);
        }

        $conversationId = (string) ($claims['conversation_id'] ?? '');
        if ($conversationId === '') {
            return response()->json(['error' => ['code' => 'no_conversation']], 422);
        }

        $conversation = Conversation::query()
            ->withoutGlobalScopes()
            ->find($conversationId);
        if ($conversation === null) {
            return response()->json(['error' => ['code' => 'conversation_not_found']], 404);
        }

        // Workspace context for smart routing. We need it before
        // we decide whether to flag the conversation: if the routing
        // decides "we're offline, leave email", we should NOT set
        // `human_requested_at` (otherwise the holding-bubble branch in
        // MessageStreamController fires on the next visitor turn).
        $agent = Agent::query()->withoutGlobalScopes()->find($conversation->agent_id);
        $workspace = $agent !== null
            ? Workspace::query()->withoutGlobalScopes()->find($agent->workspace_id)
            : null;

        $status = $this->resolveStatus($conversation, $workspace);

        // Already requested + still waiting → idempotent no-op. Returning
        // the same shape so the widget can render a consistent "queued"
        // state without branching on first-vs-subsequent click.
        $alreadyWaiting = $conversation->human_requested_at !== null
            && $conversation->claimed_by_user_id === null;

        if (
            $status === 'queued'
            && ! $alreadyWaiting
            && $conversation->claimed_by_user_id === null
        ) {
            $conversation->forceFill(['human_requested_at' => now()])->save();
            HumanRequestedEvent::dispatch(
                $conversation->id,
                (string) $conversation->agent_id,
            );
        }

        $next_open_at = $status === 'offline_after_hours' && $workspace !== null
            ? $this->businessHours->nextOpenAt($workspace)
            : null;

        return response()->json([
            'data' => [
                'conversation_id' => $conversation->id,
                'human_requested_at' => $conversation->human_requested_at?->toIso8601String(),
                'claimed' => $conversation->claimed_by_user_id !== null,
                'status' => $status,
                'next_open_at' => $next_open_at,
            ],
        ]);
    }

    /**
     * Decision tree:
     *   - already claimed → already_claimed (rare; visitor clicked
     *     the pill while an operator was claiming. Treat as "queued"
     *     for UX simplicity.)
     *   - business hours configured + currently closed →
     *     offline_after_hours (with next_open_at)
     *   - 0 active operators → offline_no_operators
     *   - else → queued
     */
    private function resolveStatus(Conversation $conversation, ?Workspace $workspace): string
    {
        if ($conversation->claimed_by_user_id !== null) {
            return 'queued';
        }
        if ($workspace === null) {
            return 'queued';
        }
        if (! $this->businessHours->isOpen($workspace)) {
            return 'offline_after_hours';
        }
        if ($this->presence->activeOperatorCount($workspace) === 0) {
            return 'offline_no_operators';
        }

        return 'queued';
    }
}
