<?php

namespace App\Services\Meet;

use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Models\User;
use App\Services\Meet\Media\MediaProviderInterface;
use App\Support\EventsRegistrationSession;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class SessionService
{
    public function __construct(
        protected MediaProviderInterface $media,
        protected RoomService $rooms,
    ) {}

    public function start(Room $room, User $host): Session
    {
        $existing = $room->activeSession();
        if ($existing) {
            return $existing;
        }

        app(LicenseService::class)->assertCanStartSession($room->organization, $room->branch_id, $room);

        $session = Session::create([
            'owner_ref' => $room->owner_ref,
            'room_id' => $room->id,
            'media_room_name' => $this->rooms->mediaRoomName($room),
            'status' => 'live',
            'started_at' => now(),
        ]);

        $room->update(['status' => 'live']);

        $this->media->createRoom($session->media_room_name);

        $this->joinParticipant($session, $host, 'host');

        if ($room->setting('auto_record', false)) {
            app(RecordingService::class)->start($session, $host);
        }

        app(MeetNotificationService::class)->meetingStarted($session, $host);

        app(\App\Services\Integrations\WebhookDispatcher::class)->meetingStarted($session);

        AuditLogger::record($room->owner_ref, 'session.started', $room->organization_id, $host->ownerRef(), Session::class, $session->id);

        return $session;
    }

    public function end(Session $session, string $actorRef): Session
    {
        $session->update([
            'status' => 'ended',
            'ended_at' => now(),
        ]);

        $session->room->update(['status' => 'ended']);

        $session->participants()
            ->whereIn('status', ['joined', 'waiting'])
            ->update(['status' => 'left', 'left_at' => now()]);

        foreach ($session->recordings()->where('status', 'recording')->get() as $recording) {
            app(RecordingService::class)->stop($recording, $actorRef);
        }

        if ($session->room->isRecurring()) {
            app(RecurringMeetingService::class)->spawnNextOccurrences($session->room, 1);
        }

        AuditLogger::record($session->owner_ref, 'session.ended', $session->room->organization_id, $actorRef, Session::class, $session->id);

        app(\App\Services\Integrations\WebhookDispatcher::class)->meetingEnded($session);

        app(UsageMeteringService::class)->recordSessionUsage($session);

        return $session;
    }

    public function joinParticipant(
        Session $session,
        ?User $user,
        string $role,
        ?string $displayName = null,
        ?string $email = null,
        string $status = 'joined',
    ): Participant {
        $userRef = $user?->ownerRef();
        $existing = $this->findExistingParticipant($session, $userRef, $email);

        if ($existing) {
            $updates = [
                'left_at' => null,
                'display_name' => $displayName ?? $existing->display_name,
                'ip_address' => request()?->ip(),
            ];

            if ($existing->status === 'waiting') {
                $updates['status'] = 'waiting';
            } else {
                $updates['status'] = 'joined';
                $updates['joined_at'] = $existing->joined_at ?? now();
            }

            if ($this->shouldPromoteParticipantRole($existing->role, $role)) {
                $updates['role'] = $role;
            }

            $existing->update($updates);

            return $existing->fresh();
        }

        $joinedAt = $status === 'joined' ? now() : null;

        $participant = Participant::create([
            'owner_ref' => $session->owner_ref,
            'session_id' => $session->id,
            'user_ref' => $userRef,
            'display_name' => $displayName ?? $user?->name ?? 'Guest',
            'email' => $email,
            'role' => $role,
            'status' => $status,
            'joined_at' => $joinedAt,
            'ip_address' => request()?->ip(),
        ]);

        if ($status === 'joined') {
            $this->updatePeakParticipants($session);
        }

        if ($status === 'joined') {
            AuditLogger::record(
                $session->owner_ref,
                'participant.joined',
                $session->room->organization_id,
                $userRef,
                Participant::class,
                $participant->id,
            );
        }

        return $participant;
    }

    public function admitParticipant(Participant $participant): Participant
    {
        abort_unless($participant->status === 'waiting', 422);

        $participant->update([
            'status' => 'joined',
            'joined_at' => now(),
            'left_at' => null,
        ]);

        $this->updatePeakParticipants($participant->session);

        AuditLogger::record(
            $participant->owner_ref,
            'participant.joined',
            $participant->session->room->organization_id,
            $participant->user_ref,
            Participant::class,
            $participant->id,
        );

        return $participant->fresh();
    }

    public function denyParticipant(Participant $participant): Participant
    {
        abort_unless($participant->status === 'waiting', 422);

        $participant->update([
            'status' => 'removed',
            'left_at' => now(),
        ]);

        AuditLogger::record(
            $participant->owner_ref,
            'participant.removed',
            $participant->session->room->organization_id,
            $participant->user_ref,
            Participant::class,
            $participant->id,
        );

        return $participant->fresh();
    }

    public function syncEventsSpeakerRole(Request $request, Participant $participant): Participant
    {
        $participant->loadMissing('session.room');
        $room = $participant->session->room;

        if (! $room->isEventsLinked() || ! EventsRegistrationSession::isSpeakerVerified($request, $room)) {
            return $participant;
        }

        return $this->promoteParticipantRole($participant, 'panelist');
    }

    private function promoteParticipantRole(Participant $participant, string $role): Participant
    {
        if ($this->shouldPromoteParticipantRole($participant->role, $role)) {
            $participant->update(['role' => $role]);

            return $participant->fresh();
        }

        return $participant;
    }

    private function shouldPromoteParticipantRole(string $currentRole, string $newRole): bool
    {
        static $rank = [
            'guest' => 10,
            'attendee' => 20,
            'participant' => 25,
            'panelist' => 40,
            'co_host' => 50,
            'host' => 60,
        ];

        return ($rank[$newRole] ?? 0) > ($rank[$currentRole] ?? 0);
    }

    protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant
    {
        $active = fn ($q) => $q->whereIn('status', ['invited', 'waiting', 'joined']);

        if ($userRef) {
            return $session->participants()
                ->where('user_ref', $userRef)
                ->where($active)
                ->first();
        }

        if ($email) {
            return $session->participants()
                ->whereNull('user_ref')
                ->where('email', $email)
                ->where($active)
                ->first();
        }

        $guestUuid = (string) request()?->session()?->get("meet.participant.{$session->uuid}", '');
        if ($guestUuid === '') {
            return null;
        }

        return $session->participants()
            ->where('uuid', $guestUuid)
            ->whereNull('user_ref')
            ->where($active)
            ->first();
    }

    public function rememberParticipantSession(Request $request, Participant $participant, ?Session $session = null): void
    {
        $session ??= $participant->session;
        $request->session()->put("meet.participant.{$session->uuid}", $participant->uuid);
    }

    public function ensureJoinedParticipant(
        Request $request,
        Session $session,
        ?User $user,
        string $role,
        ?string $displayName = null,
        ?string $email = null,
    ): Participant {
        if ($user) {
            $existing = $this->joinedParticipantForUser($session, $user);
            if ($existing) {
                $existing = $this->promoteParticipantRole($existing, $role);
                $this->rememberParticipantSession($request, $existing, $session);

                return $existing;
            }
        }

        $participant = $this->joinParticipant($session, $user, $role, $displayName, $email);
        $this->rememberParticipantSession($request, $participant, $session);

        return $participant;
    }

    public function transitionGreenRoomParticipant(
        Request $request,
        Participant $greenParticipant,
        Session $liveSession,
    ): ?Participant {
        $presenters = (array) $liveSession->presenter_refs;
        $isPresenter = in_array($greenParticipant->role, ['host', 'co_host', 'panelist'], true)
            || ($greenParticipant->user_ref && in_array($greenParticipant->user_ref, $presenters, true));

        if (! $isPresenter) {
            return null;
        }

        $user = $greenParticipant->user_ref
            ? User::where('public_id', $greenParticipant->user_ref)->first()
            : null;

        $role = $greenParticipant->role;
        if ($user && $user->ownerRef() === $liveSession->room->host_user_ref) {
            $role = 'host';
        } elseif (! in_array($role, ['host', 'co_host', 'panelist'], true)) {
            $role = 'panelist';
        }

        $greenSession = $greenParticipant->session;
        $participant = $this->ensureJoinedParticipant(
            $request,
            $liveSession,
            $user,
            $role,
            $greenParticipant->display_name,
            $greenParticipant->email,
        );

        $request->session()->forget("meet.participant.{$greenSession->uuid}");

        return $participant;
    }

    public function joinedParticipantForUser(Session $session, User $user): ?Participant
    {
        return $session->participants()
            ->where('user_ref', $user->ownerRef())
            ->whereIn('status', ['invited', 'waiting', 'joined'])
            ->first();
    }

    public function leaveParticipant(Participant $participant): Participant
    {
        $participant->update([
            'status' => 'left',
            'left_at' => now(),
            'hand_raised' => false,
        ]);

        AuditLogger::record(
            $participant->owner_ref,
            'participant.left',
            $participant->session->room->organization_id,
            $participant->user_ref,
            Participant::class,
            $participant->id,
        );

        return $participant;
    }

    public function generateMediaToken(Participant $participant): string
    {
        $participant->loadMissing('breakoutRoom');
        $state = $this->mediaSessionState($participant);
        $mediaRoom = app(BreakoutService::class)->mediaRoomForParticipant($participant);

        return $this->media->generateToken(
            $mediaRoom,
            $participant->uuid,
            $participant->display_name,
            [
                'can_publish' => $state['can_publish'],
                'can_subscribe' => true,
                'can_publish_data' => true,
                'room_admin' => $participant->isHost(),
            ],
        );
    }

    /** @return array{can_publish: bool, audio_only_publish: bool, speak_granted: bool, speak_requested: bool, uses_stage_layout: bool, in_breakout: bool, breakout_room_id: ?int, breakout: ?array{uuid: string, name: string, closes_at: ?string}} */
    public function mediaSessionState(Participant $participant): array
    {
        $participant->loadMissing(['breakoutRoom', 'session.room']);
        $room = $participant->session->room;
        $breakoutService = app(BreakoutService::class);
        $inBreakout = $breakoutService->isInActiveBreakout($participant);
        $usesStageLayout = app(StageLayoutService::class)->usesStageLayout($room) && ! $inBreakout;
        $canPublish = $this->canParticipantPublish($participant, $inBreakout);
        $audioOnlyPublish = $canPublish
            && $participant->isAttendee()
            && (bool) $participant->speak_granted
            && ! $inBreakout;

        $breakout = null;
        if ($inBreakout && $participant->breakoutRoom) {
            $breakout = [
                'uuid' => $participant->breakoutRoom->uuid,
                'name' => $participant->breakoutRoom->name,
                'closes_at' => $participant->breakoutRoom->closes_at?->toIso8601String(),
            ];
        }

        return [
            'can_publish' => $canPublish,
            'audio_only_publish' => $audioOnlyPublish,
            'speak_granted' => (bool) $participant->speak_granted,
            'speak_requested' => (bool) $participant->speak_requested,
            'uses_stage_layout' => $usesStageLayout,
            'in_breakout' => $inBreakout,
            'breakout_room_id' => $inBreakout ? $participant->breakout_room_id : null,
            'breakout' => $breakout,
        ];
    }

    public function canParticipantPublish(Participant $participant, ?bool $inBreakout = null): bool
    {
        $breakoutService = app(BreakoutService::class);
        $inBreakout ??= $breakoutService->isInActiveBreakout($participant);

        if ($inBreakout) {
            return true;
        }

        $room = $participant->session->room;

        if ($room->isSpace()) {
            return app(\App\Services\Meet\SpaceService::class)->canPublish($room, $participant)
                && $participant->canPublishMedia();
        }

        return app(WebinarService::class)->canPublish($room, $participant)
            && $participant->canPublishMedia();
    }

    public function getOrStartSession(Room $room, User $host): Session
    {
        if ($room->activeSession()) {
            return $room->activeSession();
        }

        return $this->start($room, $host);
    }

    public function lock(Session $session, string $actorRef): Session
    {
        $session->update(['is_locked' => true, 'locked_at' => now()]);
        AuditLogger::record($session->owner_ref, 'meeting.locked', $session->room->organization_id, $actorRef, Session::class, $session->id);

        return $session;
    }

    public function unlock(Session $session, string $actorRef): Session
    {
        $session->update(['is_locked' => false, 'locked_at' => null]);
        AuditLogger::record($session->owner_ref, 'meeting.unlocked', $session->room->organization_id, $actorRef, Session::class, $session->id);

        return $session;
    }

    protected function updatePeakParticipants(Session $session): void
    {
        $count = $session->participants()->where('status', 'joined')->count();
        if ($count > $session->peak_participants) {
            $session->update(['peak_participants' => $count]);
        }
    }
}
