<?php

namespace App\Http\Controllers\Api\Messaging;

use App\Events\ConversationUpdated;
use App\Http\Controllers\Controller;
use App\Http\Requests\Messaging\ManageConversationParticipantRequest;
use App\Http\Requests\Messaging\RenameConversationRequest;
use App\Http\Requests\Messaging\StoreConversationRequest;
use App\Models\Conversation;
use App\Models\ConversationParticipant;
use App\Models\User;
use App\Services\Messaging\ConversationReadService;
use App\Services\Messaging\ConversationStateService;
use App\Services\Messaging\SendMessageAction;
use App\Services\Messaging\UnreadCounterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class ConversationController extends Controller
{
    public function __construct(
        private readonly ConversationStateService $conversationState,
        private readonly ConversationReadService $conversationReads,
        private readonly SendMessageAction $sendMessage,
        private readonly UnreadCounterService $unreadCounters,
    ) {}

    // ── GET /api/messages/conversations ─────────────────────────────────────

    public function index(Request $request): JsonResponse
    {
        $user = $request->user();
        $page = max(1, (int) $request->integer('page', 1));
        $cacheVersion = (int) Cache::get($this->cacheVersionKey($user->id), 1);
        $cacheKey = $this->conversationListCacheKey($user->id, $page, $cacheVersion);

        $conversations = Cache::remember($cacheKey, now()->addSeconds(20), function () use ($user, $page) {
            $query = Conversation::query()
                ->select('conversations.*')
                ->join('conversation_participants as cp_me', function ($join) use ($user) {
                    $join->on('cp_me.conversation_id', '=', 'conversations.id')
                        ->where('cp_me.user_id', '=', $user->id)
                        ->whereNull('cp_me.left_at');
                })
                ->where('conversations.is_active', true)
                ->with([
                    'allParticipants' => fn ($q) => $q->whereNull('left_at')->with(['user:id,username']),
                    'latestMessage.sender:id,username',
                ])
                ->orderByDesc('cp_me.is_pinned')
                ->orderByDesc('cp_me.pinned_at')
                ->orderByDesc('last_message_at')
                ->orderByDesc('conversations.id');

            $this->unreadCounters->applyUnreadCountSelect($query, $user, 'cp_me');

            return $query->paginate(20, ['conversations.*'], 'page', $page);
        });

        $conversations->through(function ($conv) use ($user) {
            $conv->my_participant = $conv->allParticipants
                ->firstWhere('user_id', $user->id);
            return $conv;
        });

        return response()->json([
            ...$conversations->toArray(),
            'summary' => [
                'unread_total' => $this->unreadCounters->totalUnreadForUser($user),
            ],
        ]);
    }

    // ── GET /api/messages/conversation/{id} ─────────────────────────────────

    public function show(Request $request, int $id): JsonResponse
    {
        $conv = $this->findAuthorized($request, $id);

        $conv->load([
            'allParticipants.user:id,username',
            'creator:id,username',
        ]);

        return response()->json($conv);
    }

    // ── POST /api/messages/conversation ─────────────────────────────────────

    public function store(StoreConversationRequest $request): JsonResponse
    {
        $user = $request->user();
        $data = $request->validated();

        if ($data['type'] === 'direct') {
            return $this->createDirect($request, $user, $data);
        }

        return $this->createGroup($request, $user, $data);
    }

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

    public function markRead(Request $request, int $id): JsonResponse
    {
        $conversation = $this->findAuthorized($request, $id);
        $participant = $this->conversationReads->markConversationRead(
            $conversation,
            $request->user(),
            $request->integer('message_id') ?: null,
        );

        return response()->json([
            'ok' => true,
            'last_read_at' => optional($participant->last_read_at)?->toIso8601String(),
            'last_read_message_id' => $participant->last_read_message_id,
            'unread_total' => $this->unreadCounters->totalUnreadForUser($request->user()),
        ]);
    }

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

    public function archive(Request $request, int $id): JsonResponse
    {
        $conversation = $this->findAuthorized($request, $id);
        $participant = $this->participantRecord($request, $id);
        $participant->update(['is_archived' => ! $participant->is_archived]);
        $this->broadcastConversationUpdate($conversation, 'conversation.archived');

        return response()->json(['is_archived' => $participant->is_archived]);
    }

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

    public function mute(Request $request, int $id): JsonResponse
    {
        $conversation = $this->findAuthorized($request, $id);
        $participant = $this->participantRecord($request, $id);
        $participant->update(['is_muted' => ! $participant->is_muted]);
        $this->broadcastConversationUpdate($conversation, 'conversation.muted');

        return response()->json(['is_muted' => $participant->is_muted]);
    }

    public function pin(Request $request, int $id): JsonResponse
    {
        $conversation = $this->findAuthorized($request, $id);
        $participant = $this->participantRecord($request, $id);
        $participant->update(['is_pinned' => true, 'pinned_at' => now()]);
        $this->broadcastConversationUpdate($conversation, 'conversation.pinned');

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

    public function unpin(Request $request, int $id): JsonResponse
    {
        $conversation = $this->findAuthorized($request, $id);
        $participant = $this->participantRecord($request, $id);
        $participant->update(['is_pinned' => false, 'pinned_at' => null]);
        $this->broadcastConversationUpdate($conversation, 'conversation.unpinned');

        return response()->json(['is_pinned' => false]);
    }

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

    public function leave(Request $request, int $id): JsonResponse
    {
        $conv        = $this->findAuthorized($request, $id);
        $participant = $this->participantRecord($request, $id);
        $participantUserIds = ConversationParticipant::where('conversation_id', $id)
            ->whereNull('left_at')
            ->pluck('user_id')
            ->all();

        if ($conv->isGroup()) {
            // Last admin protection
            $adminCount = ConversationParticipant::where('conversation_id', $id)
                ->where('role', 'admin')
                ->whereNull('left_at')
                ->count();

            if ($adminCount === 1 && $participant->role === 'admin') {
                $otherMember = ConversationParticipant::where('conversation_id', $id)
                    ->where('user_id', '!=', $request->user()->id)
                    ->whereNull('left_at')
                    ->first();

                if ($otherMember) {
                    $otherMember->update(['role' => 'admin']);
                }
            }
        }

        $participant->update(['left_at' => now()]);
        $this->conversationState->touchConversationCachesForUsers($participantUserIds);
        $this->broadcastConversationUpdate($conv, 'conversation.left', $participantUserIds);

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

    // ── POST /api/messages/{conversation_id}/add-user ────────────────────────

    public function addUser(ManageConversationParticipantRequest $request, int $id): JsonResponse
    {
        $conv = $this->findAuthorized($request, $id);
        $this->requireAdmin($request, $id);
        $participantUserIds = ConversationParticipant::where('conversation_id', $id)
            ->whereNull('left_at')
            ->pluck('user_id')
            ->all();

        $data = $request->validated();

        $existing = ConversationParticipant::where('conversation_id', $id)
            ->where('user_id', $data['user_id'])
            ->first();

        if ($existing) {
            if ($existing->left_at) {
                $existing->update(['left_at' => null, 'joined_at' => now()]);
            }
        } else {
            ConversationParticipant::create([
                'conversation_id' => $id,
                'user_id'         => $data['user_id'],
                'role'            => 'member',
                'joined_at'       => now(),
            ]);
        }

        $participantUserIds[] = (int) $data['user_id'];
        $this->conversationState->touchConversationCachesForUsers($participantUserIds);
        $this->broadcastConversationUpdate($conv, 'conversation.participant_added', $participantUserIds);

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

    // ── DELETE /api/messages/{conversation_id}/remove-user ───────────────────

    public function removeUser(ManageConversationParticipantRequest $request, int $id): JsonResponse
    {
        $this->requireAdmin($request, $id);
        $data = $request->validated();

        // Cannot remove the conversation creator
        $conv = Conversation::findOrFail($id);
        abort_if($conv->created_by === (int) $data['user_id'], 403, 'Cannot remove the conversation creator.');

        $targetParticipant = ConversationParticipant::where('conversation_id', $id)
            ->where('user_id', $data['user_id'])
            ->whereNull('left_at')
            ->first();

        if ($targetParticipant && $targetParticipant->role === 'admin') {
            $adminCount = ConversationParticipant::where('conversation_id', $id)
                ->where('role', 'admin')
                ->whereNull('left_at')
                ->count();

            abort_if($adminCount <= 1, 422, 'Cannot remove the last admin from this conversation.');
        }

        $participantUserIds = ConversationParticipant::where('conversation_id', $id)
            ->whereNull('left_at')
            ->pluck('user_id')
            ->all();

        ConversationParticipant::where('conversation_id', $id)
            ->where('user_id', $data['user_id'])
            ->whereNull('left_at')
            ->update(['left_at' => now()]);

        $this->conversationState->touchConversationCachesForUsers($participantUserIds);
        $this->broadcastConversationUpdate($conv, 'conversation.participant_removed', $participantUserIds);

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

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

    public function rename(RenameConversationRequest $request, int $id): JsonResponse
    {
        $conv = $this->findAuthorized($request, $id);
        abort_unless($conv->isGroup(), 422, 'Only group conversations can be renamed.');
        $this->requireAdmin($request, $id);

        $data = $request->validated();
        $conv->update(['title' => $data['title']]);
        $participantUserIds = ConversationParticipant::where('conversation_id', $id)
            ->whereNull('left_at')
            ->pluck('user_id')
            ->all();
        $this->conversationState->touchConversationCachesForUsers($participantUserIds);
        $this->broadcastConversationUpdate($conv, 'conversation.renamed', $participantUserIds);

        return response()->json(['title' => $conv->title]);
    }

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

    private function createDirect(Request $request, User $user, array $data): JsonResponse
    {
        $recipient = User::findOrFail($data['recipient_id']);

        abort_if($recipient->id === $user->id, 422, 'You cannot message yourself.');

        if (! $recipient->allowsMessagesFrom($user)) {
            abort(403, 'This user does not accept messages from you.');
        }

        $this->assertNotBlockedBetween($user, $recipient);

        // Reuse existing conversation if one exists
        $conv = Conversation::findDirect($user->id, $recipient->id);

        if (! $conv) {
            $conv = DB::transaction(function () use ($user, $recipient) {
                $conv = Conversation::create([
                    'uuid' => (string) \Illuminate\Support\Str::uuid(),
                    'type'       => 'direct',
                    'created_by' => $user->id,
                    'is_active' => true,
                ]);

                ConversationParticipant::insert([
                    ['conversation_id' => $conv->id, 'user_id' => $user->id,      'role' => 'admin',  'joined_at' => now()],
                    ['conversation_id' => $conv->id, 'user_id' => $recipient->id, 'role' => 'member', 'joined_at' => now()],
                ]);

                return $conv;
            });
        }

        $this->sendMessage->execute($conv, $user, [
            'body' => $data['body'],
            'client_temp_id' => $data['client_temp_id'] ?? null,
        ]);

        return response()->json($conv->fresh()->load('allParticipants.user:id,username'), 201);
    }

    private function createGroup(Request $request, User $user, array $data): JsonResponse
    {
        $participantIds = array_unique(array_merge([$user->id], $data['participant_ids']));

        $conv = DB::transaction(function () use ($user, $data, $participantIds) {
            $conv = Conversation::create([
                'uuid' => (string) \Illuminate\Support\Str::uuid(),
                'type'       => 'group',
                'title'      => $data['title'],
                'created_by' => $user->id,
                'is_active' => true,
            ]);

            $rows = array_map(fn ($uid) => [
                'conversation_id' => $conv->id,
                'user_id'         => $uid,
                'role'            => $uid === $user->id ? 'admin' : 'member',
                'joined_at'       => now(),
            ], $participantIds);

            ConversationParticipant::insert($rows);

            return $conv;
        });

        $this->sendMessage->execute($conv, $user, [
            'body' => $data['body'],
            'client_temp_id' => $data['client_temp_id'] ?? null,
        ]);

        return response()->json($conv->fresh()->load('allParticipants.user:id,username'), 201);
    }

    private function findAuthorized(Request $request, int $id): Conversation
    {
        $conv = Conversation::findOrFail($id);
        $this->authorize('view', $conv);
        return $conv;
    }

    private function participantRecord(Request $request, int $conversationId): ConversationParticipant
    {
        return ConversationParticipant::where('conversation_id', $conversationId)
            ->where('user_id', $request->user()->id)
            ->whereNull('left_at')
            ->firstOrFail();
    }

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

    private function requireAdmin(Request $request, int $id): void
    {
        $conversation = Conversation::findOrFail($id);
        $this->authorize('manageParticipants', $conversation);
    }

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

    private function cacheVersionKey(int $userId): string
    {
        return "messages:conversations:version:{$userId}";
    }

    private function conversationListCacheKey(int $userId, int $page, int $version): string
    {
        return "messages:conversations:user:{$userId}:page:{$page}:v:{$version}";
    }

    private function broadcastConversationUpdate(Conversation $conversation, string $reason, ?array $participantIds = null): void
    {
        $participantIds ??= $this->conversationState->activeParticipantIds($conversation);
        $this->conversationState->touchConversationCachesForUsers($participantIds);

        foreach ($participantIds as $participantId) {
            event(new ConversationUpdated((int) $participantId, $conversation, $reason));
        }
    }

    private function assertNotBlockedBetween(User $sender, User $recipient): void
    {
        if (! Schema::hasTable('user_blocks')) {
            return;
        }

        $blocked = false;

        if (Schema::hasColumns('user_blocks', ['user_id', 'blocked_user_id'])) {
            $blocked = DB::table('user_blocks')
                ->where(function ($q) use ($sender, $recipient) {
                    $q->where('user_id', $sender->id)->where('blocked_user_id', $recipient->id);
                })
                ->orWhere(function ($q) use ($sender, $recipient) {
                    $q->where('user_id', $recipient->id)->where('blocked_user_id', $sender->id);
                })
                ->exists();
        } elseif (Schema::hasColumns('user_blocks', ['blocker_id', 'blocked_id'])) {
            $blocked = DB::table('user_blocks')
                ->where(function ($q) use ($sender, $recipient) {
                    $q->where('blocker_id', $sender->id)->where('blocked_id', $recipient->id);
                })
                ->orWhere(function ($q) use ($sender, $recipient) {
                    $q->where('blocker_id', $recipient->id)->where('blocked_id', $sender->id);
                })
                ->exists();
        }

        abort_if($blocked, 403, 'Messaging is not available between these users.');
    }
}
