<?php

namespace App\Http\Controllers\Api\Messaging;

use App\Events\ConversationUpdated;
use App\Events\MessageDeleted;
use App\Events\MessageUpdated;
use App\Http\Controllers\Controller;
use App\Http\Requests\Messaging\StoreMessageRequest;
use App\Http\Requests\Messaging\ToggleMessageReactionRequest;
use App\Http\Requests\Messaging\UpdateMessageRequest;
use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\Message;
use App\Models\MessageReaction;
use App\Services\Messaging\ConversationDeltaService;
use App\Services\Messaging\ConversationStateService;
use App\Services\Messaging\MessagingPayloadFactory;
use App\Services\Messaging\MessageSearchIndexer;
use App\Services\Messaging\SendMessageAction;
use App\Services\Messaging\UnreadCounterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class MessageController extends Controller
{
    private const PAGE_SIZE = 30;

    public function __construct(
        private readonly ConversationDeltaService $conversationDelta,
        private readonly ConversationStateService $conversationState,
        private readonly MessagingPayloadFactory $payloadFactory,
        private readonly SendMessageAction $sendMessage,
        private readonly UnreadCounterService $unreadCounters,
    ) {}

    // ── GET /api/messages/{conversation_id} ──────────────────────────────────

    public function index(Request $request, int $conversationId): JsonResponse
    {
        $conversation = $this->findConversationOrFail($conversationId);
        $cursor = $request->integer('cursor') ?: $request->integer('before_id');
        $afterId = $request->integer('after_id');

        if ($afterId) {
            $messages = $this->conversationDelta->messagesAfter($conversation, $request->user(), $afterId);

            return response()->json([
                'data' => $messages,
                'next_cursor' => null,
            ]);
        }

        $query = Message::withTrashed()
            ->where('conversation_id', $conversationId)
            ->with(['sender:id,username', 'reactions', 'attachments'])
            ->orderByDesc('created_at')
            ->orderByDesc('id');

        if ($cursor) {
            $query->where('id', '<', $cursor);
        }

        $chunk = $query->limit(self::PAGE_SIZE + 1)->get();
        $hasMore = $chunk->count() > self::PAGE_SIZE;
        $messages = $chunk->take(self::PAGE_SIZE)->reverse()->values();
        $nextCursor = $hasMore && $messages->isNotEmpty() ? (int) $messages->first()->id : null;

        return response()->json([
            'data' => $messages->map(fn (Message $message) => $this->payloadFactory->message($message, (int) $request->user()->id))->values(),
            'next_cursor' => $nextCursor,
        ]);
    }

    public function delta(Request $request, int $conversationId): JsonResponse
    {
        $conversation = $this->findConversationOrFail($conversationId);
        $afterMessageId = max(0, (int) $request->integer('after_message_id'));

        abort_if($afterMessageId < 1, 422, 'after_message_id is required.');

        return response()->json([
            'data' => $this->conversationDelta->messagesAfter($conversation, $request->user(), $afterMessageId),
            'conversation' => $this->payloadFactory->conversationSummary($conversation->fresh(), (int) $request->user()->id),
            'summary' => [
                'unread_total' => $this->unreadCounters->totalUnreadForUser($request->user()),
            ],
        ]);
    }

    // ── POST /api/messages/{conversation_id} ─────────────────────────────────

    public function store(StoreMessageRequest $request, int $conversationId): JsonResponse
    {
        $conversation = $this->findConversationOrFail($conversationId);
        $data = $request->validated();
        $data['attachments'] = $request->file('attachments', []);

        $body = trim((string) ($data['body'] ?? ''));
        abort_if($body === '' && empty($data['attachments']), 422, 'Message body or attachment is required.');

        $message = $this->sendMessage->execute($conversation, $request->user(), $data);

        return response()->json($this->payloadFactory->message($message, (int) $request->user()->id), 201);
    }

    // ── POST /api/messages/{conversation_id}/react ───────────────────────────

    public function react(ToggleMessageReactionRequest $request, int $conversationId, int $messageId): JsonResponse
    {
        $this->findConversationOrFail($conversationId);
        $data = $request->validated();
        $this->assertAllowedReaction($data['reaction']);

        $existing = MessageReaction::where([
            'message_id' => $messageId,
            'user_id'    => $request->user()->id,
            'reaction'   => $data['reaction'],
        ])->first();

        if ($existing) {
            $existing->delete();
        } else {
            MessageReaction::create([
                'message_id' => $messageId,
                'user_id'    => $request->user()->id,
                'reaction'   => $data['reaction'],
            ]);
        }

        return response()->json($this->reactionSummary($messageId, (int) $request->user()->id));
    }

    // ── DELETE /api/messages/{conversation_id}/react ─────────────────────────

    public function unreact(ToggleMessageReactionRequest $request, int $conversationId, int $messageId): JsonResponse
    {
        $this->findConversationOrFail($conversationId);
        $data = $request->validated();
        $this->assertAllowedReaction($data['reaction']);

        MessageReaction::where([
            'message_id' => $messageId,
            'user_id'    => $request->user()->id,
            'reaction'   => $data['reaction'],
        ])->delete();

        return response()->json($this->reactionSummary($messageId, (int) $request->user()->id));
    }

    public function reactByMessage(ToggleMessageReactionRequest $request, int $messageId): JsonResponse
    {
        $message = Message::query()->findOrFail($messageId);
        $this->findConversationOrFail((int) $message->conversation_id);
        $data = $request->validated();
        $this->assertAllowedReaction($data['reaction']);

        $existing = MessageReaction::where([
            'message_id' => $messageId,
            'user_id' => $request->user()->id,
            'reaction' => $data['reaction'],
        ])->first();

        if ($existing) {
            $existing->delete();
        } else {
            MessageReaction::create([
                'message_id' => $messageId,
                'user_id' => $request->user()->id,
                'reaction' => $data['reaction'],
            ]);
        }

        return response()->json($this->reactionSummary($messageId, (int) $request->user()->id));
    }

    public function unreactByMessage(ToggleMessageReactionRequest $request, int $messageId): JsonResponse
    {
        $message = Message::query()->findOrFail($messageId);
        $this->findConversationOrFail((int) $message->conversation_id);
        $data = $request->validated();
        $this->assertAllowedReaction($data['reaction']);

        MessageReaction::where([
            'message_id' => $messageId,
            'user_id' => $request->user()->id,
            'reaction' => $data['reaction'],
        ])->delete();

        return response()->json($this->reactionSummary($messageId, (int) $request->user()->id));
    }

    // ── PATCH /api/messages/message/{messageId} ───────────────────────────────

    public function update(UpdateMessageRequest $request, int $messageId): JsonResponse
    {
        $message = Message::findOrFail($messageId);

        $this->authorize('update', $message);

        abort_if($message->deleted_at !== null, 422, 'Cannot edit a deleted message.');

        $data = $request->validated();

        $message->update([
            'body'      => $data['body'],
            'edited_at' => now(),
        ]);
        app(MessageSearchIndexer::class)->updateMessage($message);

        $participantUserIds = $this->conversationState->activeParticipantIds((int) $message->conversation_id);
        $this->conversationState->touchConversationCachesForUsers($participantUserIds);

        DB::afterCommit(function () use ($message, $participantUserIds): void {
            event(new MessageUpdated($message->fresh(['sender:id,username,name', 'attachments', 'reactions'])));

            $conversation = Conversation::find($message->conversation_id);
            if ($conversation) {
                foreach ($participantUserIds as $participantId) {
                    event(new ConversationUpdated((int) $participantId, $conversation, 'message.updated'));
                }
            }
        });

        return response()->json($this->payloadFactory->message($message->fresh(['sender:id,username,name', 'attachments', 'reactions']), (int) $request->user()->id));
    }

    // ── DELETE /api/messages/message/{messageId} ──────────────────────────────

    public function destroy(Request $request, int $messageId): JsonResponse
    {
        $message = Message::findOrFail($messageId);

        $this->authorize('delete', $message);

        $participantUserIds = $this->conversationState->activeParticipantIds((int) $message->conversation_id);
        app(MessageSearchIndexer::class)->deleteMessage($message);
        $message->delete();
        $this->conversationState->touchConversationCachesForUsers($participantUserIds);

        DB::afterCommit(function () use ($message, $participantUserIds): void {
            $message->refresh();
            event(new MessageDeleted($message));

            $conversation = Conversation::find($message->conversation_id);
            if ($conversation) {
                foreach ($participantUserIds as $participantId) {
                    event(new ConversationUpdated((int) $participantId, $conversation, 'message.deleted'));
                }
            }
        });

        return response()->json(['ok' => true]);
    }

    // ── Private helpers ──────────────────────────────────────────────────────

    private function assertParticipant(Request $request, int $conversationId): void
    {
        abort_unless(
            ConversationParticipant::where('conversation_id', $conversationId)
                ->where('user_id', $request->user()->id)
                ->whereNull('left_at')
                ->exists(),
            403,
            'You are not a participant of this conversation.'
        );
    }

    private function touchConversationCachesForUsers(array $userIds): void
    {
        $this->conversationState->touchConversationCachesForUsers($userIds);
    }

    private function assertAllowedReaction(string $reaction): void
    {
        $allowed = (array) config('messaging.reactions.allowed', []);
        abort_unless(in_array($reaction, $allowed, true), 422, 'Reaction is not allowed.');
    }

    private function reactionSummary(int $messageId, int $userId): array
    {
        $rows = MessageReaction::query()
            ->selectRaw('reaction, count(*) as aggregate_count')
            ->where('message_id', $messageId)
            ->groupBy('reaction')
            ->get();

        $summary = [];
        foreach ($rows as $row) {
            $summary[(string) $row->reaction] = (int) $row->aggregate_count;
        }

        $mine = MessageReaction::query()
            ->where('message_id', $messageId)
            ->where('user_id', $userId)
            ->pluck('reaction')
            ->values()
            ->all();

        $summary['me'] = $mine;

        return $summary;
    }

    private function findConversationOrFail(int $conversationId): Conversation
    {
        $conversation = Conversation::query()->findOrFail($conversationId);
        $this->authorize('view', $conversation);

        return $conversation;
    }
}
