<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Group;
use App\Models\GroupJoinRequest;
use App\Models\GroupMember;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class GroupJoinRequestService
{
    public function __construct(
        private readonly GroupHistoryService $history,
        private readonly GroupMembershipService $memberships,
        private readonly NotificationService $notifications,
    ) {
    }

    public function submit(Group $group, User $actor, array $attributes): GroupJoinRequest
    {
        if (! $group->canRequestJoin($actor)) {
            throw ValidationException::withMessages([
                'group' => 'This group is not accepting join requests.',
            ]);
        }

        if ($group->hasActiveMember($actor)) {
            throw ValidationException::withMessages([
                'group' => 'You are already a member of this group.',
            ]);
        }

        $pendingRequest = GroupJoinRequest::query()
            ->where('group_id', $group->id)
            ->where('user_id', $actor->id)
            ->whereIn('status', [
                GroupJoinRequest::STATUS_PENDING,
            ])
            ->exists();

        if ($pendingRequest) {
            throw ValidationException::withMessages([
                'group' => 'You already have a pending request for this group.',
            ]);
        }

        $request = GroupJoinRequest::query()->create([
            'group_id' => $group->id,
            'user_id' => $actor->id,
            'message' => $attributes['message'] ?? null,
            'portfolio_url' => $attributes['portfolio_url'] ?? null,
            'desired_role' => isset($attributes['desired_role'])
                ? Group::normalizeMemberRole((string) $attributes['desired_role'])
                : null,
            'skills_json' => $attributes['skills_json'] ?? null,
            'status' => GroupJoinRequest::STATUS_PENDING,
            'expires_at' => now()->addDays(max(3, (int) config('groups.join_requests.expires_after_days', 21))),
        ]);

        $this->history->record(
            $group,
            $actor,
            'join_request_submitted',
            sprintf('%s requested to join the group.', $actor->name ?: $actor->username ?: 'A user'),
            'group_join_request',
            (int) $request->id,
            null,
            [
                'desired_role' => $request->desired_role,
                'portfolio_url' => $request->portfolio_url,
            ],
        );

        foreach ($this->reviewRecipients($group, $actor->id) as $recipient) {
            $this->notifications->notifyGroupJoinRequestReceived($recipient, $actor, $group, $request);
        }

        if ($group->membership_policy === Group::MEMBERSHIP_OPEN) {
            GroupMember::query()->updateOrCreate(
                [
                    'group_id' => $group->id,
                    'user_id' => $actor->id,
                ],
                [
                    'invited_by_user_id' => $group->owner_user_id,
                    'role' => Group::normalizeMemberRole((string) ($request->desired_role ?: Group::ROLE_MEMBER)),
                    'status' => Group::STATUS_ACTIVE,
                    'note' => 'Auto-approved by open membership policy.',
                    'invited_at' => now(),
                    'accepted_at' => now(),
                    'revoked_at' => null,
                ],
            );

            $request->forceFill([
                'status' => GroupJoinRequest::STATUS_APPROVED,
                'review_notes' => 'Auto-approved by open membership policy.',
                'reviewed_at' => now(),
            ])->save();

            $this->history->record(
                $group,
                $actor,
                'join_request_auto_approved',
                'Auto-approved join request because the group uses open membership.',
                'group_join_request',
                (int) $request->id,
                ['status' => GroupJoinRequest::STATUS_PENDING],
                ['status' => GroupJoinRequest::STATUS_APPROVED],
            );

            return $request->fresh(['group', 'user.profile']);
        }

        return $request->fresh(['group', 'user.profile']);
    }

    public function approve(GroupJoinRequest $request, User $actor, ?string $role = null, ?string $notes = null): GroupJoinRequest
    {
        $group = $request->group()->with('members')->firstOrFail();

        if (! $group->canReviewJoinRequests($actor)) {
            throw ValidationException::withMessages([
                'request' => 'You are not allowed to review join requests for this group.',
            ]);
        }

        if ($request->status !== GroupJoinRequest::STATUS_PENDING) {
            throw ValidationException::withMessages([
                'request' => 'Only pending join requests can be approved.',
            ]);
        }

        $resolvedRole = Group::normalizeMemberRole((string) ($role ?: $request->desired_role ?: Group::ROLE_MEMBER));
        if (! in_array($resolvedRole, [Group::ROLE_ADMIN, Group::ROLE_EDITOR, Group::ROLE_MEMBER], true)) {
            $resolvedRole = Group::ROLE_MEMBER;
        }

        DB::transaction(function () use ($group, $request, $actor, $resolvedRole, $notes): void {
            GroupMember::query()->updateOrCreate(
                [
                    'group_id' => $group->id,
                    'user_id' => $request->user_id,
                ],
                [
                    'invited_by_user_id' => $actor->id,
                    'role' => $resolvedRole,
                    'status' => Group::STATUS_ACTIVE,
                    'note' => $notes,
                    'invited_at' => now(),
                    'accepted_at' => now(),
                    'revoked_at' => null,
                ],
            );

            $request->forceFill([
                'status' => GroupJoinRequest::STATUS_APPROVED,
                'reviewed_by_user_id' => $actor->id,
                'review_notes' => $notes,
                'reviewed_at' => now(),
            ])->save();
        });

        $request->refresh();

        $this->history->record(
            $group,
            $actor,
            'join_request_approved',
            sprintf('Approved %s to join the group.', $request->user?->name ?: $request->user?->username ?: 'a user'),
            'group_join_request',
            (int) $request->id,
            ['status' => GroupJoinRequest::STATUS_PENDING],
            ['status' => GroupJoinRequest::STATUS_APPROVED, 'role' => $resolvedRole],
        );

        app(GroupActivityService::class)->record(
            $group,
            $actor,
            'member_joined',
            'group_join_request',
            (int) $request->id,
            sprintf('%s joined %s', $request->user?->name ?: $request->user?->username ?: 'A member', $group->name),
            'Membership approved through group join requests.',
            'public',
        );

        $this->notifications->notifyGroupJoinRequestApproved($request->user, $actor, $group, $resolvedRole, $request);

        return $request->fresh(['group', 'user.profile', 'reviewedBy.profile']);
    }

    public function reject(GroupJoinRequest $request, User $actor, ?string $notes = null): GroupJoinRequest
    {
        $group = $request->group()->with('members')->firstOrFail();

        if (! $group->canReviewJoinRequests($actor)) {
            throw ValidationException::withMessages([
                'request' => 'You are not allowed to review join requests for this group.',
            ]);
        }

        if ($request->status !== GroupJoinRequest::STATUS_PENDING) {
            throw ValidationException::withMessages([
                'request' => 'Only pending join requests can be rejected.',
            ]);
        }

        $request->forceFill([
            'status' => GroupJoinRequest::STATUS_REJECTED,
            'reviewed_by_user_id' => $actor->id,
            'review_notes' => $notes,
            'reviewed_at' => now(),
        ])->save();

        $this->history->record(
            $group,
            $actor,
            'join_request_rejected',
            sprintf('Rejected join request from %s.', $request->user?->name ?: $request->user?->username ?: 'a user'),
            'group_join_request',
            (int) $request->id,
            ['status' => GroupJoinRequest::STATUS_PENDING],
            ['status' => GroupJoinRequest::STATUS_REJECTED],
        );

        $this->notifications->notifyGroupJoinRequestRejected($request->user, $actor, $group, $request);

        return $request->fresh(['group', 'user.profile', 'reviewedBy.profile']);
    }

    public function withdraw(GroupJoinRequest $request, User $actor): GroupJoinRequest
    {
        if ((int) $request->user_id !== (int) $actor->id || $request->status !== GroupJoinRequest::STATUS_PENDING) {
            throw ValidationException::withMessages([
                'request' => 'This join request cannot be withdrawn.',
            ]);
        }

        $request->forceFill([
            'status' => GroupJoinRequest::STATUS_WITHDRAWN,
            'reviewed_at' => now(),
        ])->save();

        $this->history->record(
            $request->group,
            $actor,
            'join_request_withdrawn',
            'Join request withdrawn.',
            'group_join_request',
            (int) $request->id,
            ['status' => GroupJoinRequest::STATUS_PENDING],
            ['status' => GroupJoinRequest::STATUS_WITHDRAWN],
        );

        return $request->fresh(['group', 'user.profile']);
    }

    public function pendingCount(Group $group): int
    {
        return (int) GroupJoinRequest::query()
            ->where('group_id', $group->id)
            ->where('status', GroupJoinRequest::STATUS_PENDING)
            ->count();
    }

    public function currentRequestFor(Group $group, ?User $viewer): ?array
    {
        if (! $viewer) {
            return null;
        }

        $request = GroupJoinRequest::query()
            ->where('group_id', $group->id)
            ->where('user_id', $viewer->id)
            ->latest('created_at')
            ->first();

        return $request ? $this->mapRequest($request) : null;
    }

    public function mapRequests(Group $group, ?User $viewer = null, array $filters = []): array
    {
        $bucket = (string) ($filters['bucket'] ?? 'pending');
        $page = max(1, (int) ($filters['page'] ?? 1));
        $perPage = min(max((int) ($filters['per_page'] ?? 20), 10), 50);

        $query = GroupJoinRequest::query()
            ->with(['user.profile', 'reviewedBy.profile'])
            ->where('group_id', $group->id);

        if ($bucket !== 'all') {
            $query->where('status', $bucket);
        }

        $paginator = $query->latest('created_at')->paginate($perPage, ['*'], 'page', $page);

        return [
            'items' => collect($paginator->items())->map(fn (GroupJoinRequest $request): array => $this->mapRequest($request, $group, $viewer))->values()->all(),
            'meta' => [
                'current_page' => $paginator->currentPage(),
                'last_page' => $paginator->lastPage(),
                'per_page' => $paginator->perPage(),
                'total' => $paginator->total(),
            ],
            'filters' => [
                'bucket' => $bucket,
            ],
            'bucket_options' => [
                ['value' => 'pending', 'label' => 'Pending'],
                ['value' => 'approved', 'label' => 'Approved'],
                ['value' => 'rejected', 'label' => 'Rejected'],
                ['value' => 'withdrawn', 'label' => 'Withdrawn'],
                ['value' => 'all', 'label' => 'All'],
            ],
        ];
    }

    public function mapRequest(GroupJoinRequest $request, ?Group $group = null, ?User $viewer = null): array
    {
        $resolvedGroup = $group ?: $request->group;

        return [
            'id' => (int) $request->id,
            'status' => (string) $request->status,
            'message' => $request->message,
            'portfolio_url' => $request->portfolio_url,
            'desired_role' => $request->desired_role,
            'desired_role_label' => Group::displayRole($request->desired_role),
            'skills' => array_values(array_filter($request->skills_json ?? [])),
            'review_notes' => $request->review_notes,
            'created_at' => $request->created_at?->toISOString(),
            'reviewed_at' => $request->reviewed_at?->toISOString(),
            'expires_at' => $request->expires_at?->toISOString(),
            'user' => $request->user ? [
                'id' => (int) $request->user->id,
                'name' => $request->user->name,
                'username' => $request->user->username,
                'avatar_url' => AvatarUrl::forUser((int) $request->user->id, $request->user->profile?->avatar_hash, 72),
                'profile_url' => route('profile.show', ['username' => strtolower((string) $request->user->username)]),
            ] : null,
            'reviewed_by' => $request->reviewedBy ? [
                'id' => (int) $request->reviewedBy->id,
                'name' => $request->reviewedBy->name,
                'username' => $request->reviewedBy->username,
            ] : null,
            'can_approve' => $viewer !== null && $resolvedGroup !== null && $resolvedGroup->canReviewJoinRequests($viewer) && $request->status === GroupJoinRequest::STATUS_PENDING,
            'can_reject' => $viewer !== null && $resolvedGroup !== null && $resolvedGroup->canReviewJoinRequests($viewer) && $request->status === GroupJoinRequest::STATUS_PENDING,
        ];
    }

    private function reviewRecipients(Group $group, int $excludeUserId): array
    {
        return User::query()
            ->whereIn('id', $this->memberships->activeContributorIds($group))
            ->get()
            ->filter(fn (User $member): bool => (int) $member->id !== $excludeUserId && $group->canReviewJoinRequests($member))
            ->values()
            ->all();
    }
}