<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Collection;
use App\Models\CollectionMember;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class CollectionCollaborationService
{
    public function __construct(
        private readonly NotificationService $notifications,
    ) {
    }

    public function ensureOwnerMembership(Collection $collection): void
    {
        CollectionMember::query()
            ->where('collection_id', $collection->id)
            ->where('role', Collection::MEMBER_ROLE_OWNER)
            ->where('user_id', '!=', $collection->user_id)
            ->update([
                'role' => Collection::MEMBER_ROLE_EDITOR,
                'updated_at' => now(),
            ]);

        CollectionMember::query()->updateOrCreate(
            [
                'collection_id' => $collection->id,
                'user_id' => $collection->user_id,
            ],
            [
                'invited_by_user_id' => $collection->user_id,
                'role' => Collection::MEMBER_ROLE_OWNER,
                'status' => Collection::MEMBER_STATUS_ACTIVE,
                'invited_at' => now(),
                'expires_at' => null,
                'accepted_at' => now(),
                'revoked_at' => null,
            ]
        );

        $this->syncCollaboratorsCount($collection);
    }

    public function ensureManagerMembership(Collection $collection, User $manager): void
    {
        if ((int) $collection->user_id === (int) $manager->id) {
            return;
        }

        CollectionMember::query()->updateOrCreate(
            [
                'collection_id' => $collection->id,
                'user_id' => $manager->id,
            ],
            [
                'invited_by_user_id' => $collection->user_id,
                'role' => Collection::MEMBER_ROLE_EDITOR,
                'status' => Collection::MEMBER_STATUS_ACTIVE,
                'invited_at' => now(),
                'expires_at' => null,
                'accepted_at' => now(),
                'revoked_at' => null,
            ]
        );

        $this->syncCollaboratorsCount($collection);
    }

    public function inviteMember(Collection $collection, User $actor, User $invitee, string $role, ?string $note = null, ?int $expiresInDays = null, ?string $expiresAt = null): CollectionMember
    {
        $this->guardManageMembers($collection, $actor);
        $this->expirePendingInvites();

        if (! in_array($role, [Collection::MEMBER_ROLE_EDITOR, Collection::MEMBER_ROLE_CONTRIBUTOR, Collection::MEMBER_ROLE_VIEWER], true)) {
            throw ValidationException::withMessages([
                'role' => 'Choose a valid collaborator role.',
            ]);
        }

        if ($collection->isOwnedBy($invitee)) {
            throw ValidationException::withMessages([
                'username' => 'The collection owner is already a collaborator.',
            ]);
        }

        $member = DB::transaction(function () use ($collection, $actor, $invitee, $role, $note, $expiresInDays, $expiresAt): CollectionMember {
            $inviteExpiresAt = $this->resolveInviteExpiry($expiresInDays, $expiresAt);

            $member = CollectionMember::query()->updateOrCreate(
                [
                    'collection_id' => $collection->id,
                    'user_id' => $invitee->id,
                ],
                [
                    'invited_by_user_id' => $actor->id,
                    'role' => $role,
                    'status' => Collection::MEMBER_STATUS_PENDING,
                    'note' => $note,
                    'invited_at' => now(),
                    'expires_at' => $inviteExpiresAt,
                    'accepted_at' => null,
                    'revoked_at' => null,
                ]
            );

            $this->syncCollaboratorsCount($collection);

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

        $this->notifications->notifyCollectionInvite($invitee, $actor, $collection, $role);

        return $member;
    }

    public function acceptInvite(CollectionMember $member, User $user): CollectionMember
    {
        $this->expireMemberIfNeeded($member);

        if ((int) $member->user_id !== (int) $user->id || $member->status !== Collection::MEMBER_STATUS_PENDING) {
            throw ValidationException::withMessages([
                'member' => 'This invitation cannot be accepted.',
            ]);
        }

        $member->forceFill([
            'status' => Collection::MEMBER_STATUS_ACTIVE,
            'expires_at' => null,
            'accepted_at' => now(),
            'revoked_at' => null,
        ])->save();

        $this->syncCollaboratorsCount($member->collection);

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

    public function declineInvite(CollectionMember $member, User $user): CollectionMember
    {
        $this->expireMemberIfNeeded($member);

        if ((int) $member->user_id !== (int) $user->id || $member->status !== Collection::MEMBER_STATUS_PENDING) {
            throw ValidationException::withMessages([
                'member' => 'This invitation cannot be declined.',
            ]);
        }

        $member->forceFill([
            'status' => Collection::MEMBER_STATUS_REVOKED,
            'accepted_at' => null,
            'revoked_at' => now(),
        ])->save();

        $this->syncCollaboratorsCount($member->collection);

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

    public function updateMemberRole(CollectionMember $member, User $actor, string $role): CollectionMember
    {
        $this->guardManageMembers($member->collection, $actor);

        if ($member->role === Collection::MEMBER_ROLE_OWNER) {
            throw ValidationException::withMessages([
                'member' => 'The collection owner role cannot be changed.',
            ]);
        }

        $member->forceFill(['role' => $role])->save();

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

    public function revokeMember(CollectionMember $member, User $actor): void
    {
        $this->guardManageMembers($member->collection, $actor);

        if ($member->role === Collection::MEMBER_ROLE_OWNER) {
            throw ValidationException::withMessages([
                'member' => 'The collection owner cannot be removed.',
            ]);
        }

        $member->forceFill([
            'status' => Collection::MEMBER_STATUS_REVOKED,
            'expires_at' => null,
            'revoked_at' => now(),
        ])->save();

        $this->syncCollaboratorsCount($member->collection);
    }

    public function transferOwnership(Collection $collection, CollectionMember $member, User $actor): Collection
    {
        if (! $collection->isOwnedBy($actor) && ! $actor->isAdmin()) {
            throw ValidationException::withMessages([
                'member' => 'Only the collection owner can transfer ownership.',
            ]);
        }

        if ((int) $member->collection_id !== (int) $collection->id) {
            throw ValidationException::withMessages([
                'member' => 'This collaborator does not belong to the selected collection.',
            ]);
        }

        if ($member->status !== Collection::MEMBER_STATUS_ACTIVE) {
            throw ValidationException::withMessages([
                'member' => 'Only active collaborators can become the new owner.',
            ]);
        }

        if ($collection->type === Collection::TYPE_EDITORIAL && $collection->editorial_owner_mode !== Collection::EDITORIAL_OWNER_CREATOR) {
            throw ValidationException::withMessages([
                'member' => 'System-owned and staff-account editorials cannot be transferred through collaborator controls.',
            ]);
        }

        return DB::transaction(function () use ($collection, $member, $actor): Collection {
            $previousOwnerId = (int) $collection->user_id;

            CollectionMember::query()
                ->where('collection_id', $collection->id)
                ->where('user_id', $previousOwnerId)
                ->update([
                    'role' => Collection::MEMBER_ROLE_EDITOR,
                    'updated_at' => now(),
                ]);

            $member->forceFill([
                'role' => Collection::MEMBER_ROLE_OWNER,
                'status' => Collection::MEMBER_STATUS_ACTIVE,
                'expires_at' => null,
                'accepted_at' => $member->accepted_at ?? now(),
            ])->save();

            $collection->forceFill([
                'user_id' => $member->user_id,
                'managed_by_user_id' => (int) $actor->id === (int) $member->user_id ? null : $actor->id,
                'editorial_owner_mode' => $collection->type === Collection::TYPE_EDITORIAL ? Collection::EDITORIAL_OWNER_CREATOR : $collection->editorial_owner_mode,
                'editorial_owner_user_id' => $collection->type === Collection::TYPE_EDITORIAL ? null : $collection->editorial_owner_user_id,
                'editorial_owner_label' => $collection->type === Collection::TYPE_EDITORIAL ? null : $collection->editorial_owner_label,
            ])->save();

            $this->ensureOwnerMembership($collection->fresh());
            $this->syncCollaboratorsCount($collection->fresh());

            return $collection->fresh(['user.profile']);
        });
    }

    public function expirePendingInvites(): int
    {
        return CollectionMember::query()
            ->where('status', Collection::MEMBER_STATUS_PENDING)
            ->whereNotNull('expires_at')
            ->where('expires_at', '<=', now())
            ->update([
                'status' => Collection::MEMBER_STATUS_REVOKED,
                'revoked_at' => now(),
                'updated_at' => now(),
            ]);
    }

    public function activeContributorIds(Collection $collection): array
    {
        $activeIds = $collection->members()
            ->where('status', Collection::MEMBER_STATUS_ACTIVE)
            ->whereIn('role', [
                Collection::MEMBER_ROLE_OWNER,
                Collection::MEMBER_ROLE_EDITOR,
                Collection::MEMBER_ROLE_CONTRIBUTOR,
            ])
            ->pluck('user_id')
            ->map(static fn ($id) => (int) $id)
            ->all();

        if (! in_array((int) $collection->user_id, $activeIds, true)) {
            $activeIds[] = (int) $collection->user_id;
        }

        return array_values(array_unique($activeIds));
    }

    public function mapMembers(Collection $collection, ?User $viewer = null): array
    {
        $this->expirePendingInvites();

        $members = $collection->members()
            ->with(['user.profile', 'invitedBy.profile'])
            ->orderByRaw("CASE role WHEN 'owner' THEN 0 WHEN 'editor' THEN 1 WHEN 'contributor' THEN 2 ELSE 3 END")
            ->orderBy('created_at')
            ->get();

        return $members->map(function (CollectionMember $member) use ($collection, $viewer): array {
            $user = $member->user;

            return [
                'id' => (int) $member->id,
                'user_id' => (int) $member->user_id,
                'role' => (string) $member->role,
                'status' => (string) $member->status,
                'note' => $member->note,
                'invited_at' => $member->invited_at?->toISOString(),
                'expires_at' => $member->expires_at?->toISOString(),
                'accepted_at' => $member->accepted_at?->toISOString(),
                'is_expired' => $member->status === Collection::MEMBER_STATUS_REVOKED && $member->expires_at !== null && $member->expires_at->lte(now()) && $member->accepted_at === null,
                'can_accept' => $viewer !== null && (int) $member->user_id === (int) $viewer->id && $member->status === Collection::MEMBER_STATUS_PENDING,
                'can_decline' => $viewer !== null && (int) $member->user_id === (int) $viewer->id && $member->status === Collection::MEMBER_STATUS_PENDING,
                'can_revoke' => $viewer !== null && $collection->canManageMembers($viewer) && $member->role !== Collection::MEMBER_ROLE_OWNER,
                'can_transfer' => $viewer !== null
                    && $collection->isOwnedBy($viewer)
                    && $member->status === Collection::MEMBER_STATUS_ACTIVE
                    && $member->role !== Collection::MEMBER_ROLE_OWNER,
                'user' => [
                    'id' => (int) $user->id,
                    'name' => $user->name,
                    'username' => $user->username,
                    'avatar_url' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash, 72),
                    'profile_url' => route('profile.show', ['username' => strtolower((string) $user->username)]),
                ],
                'invited_by' => $member->invitedBy ? [
                    'id' => (int) $member->invitedBy->id,
                    'username' => $member->invitedBy->username,
                    'name' => $member->invitedBy->name,
                ] : null,
            ];
        })->all();
    }

    public function syncCollaboratorsCount(Collection $collection): void
    {
        $count = (int) $collection->members()
            ->where('status', Collection::MEMBER_STATUS_ACTIVE)
            ->count();

        $collection->forceFill([
            'collaborators_count' => $count,
        ])->save();
    }

    private function guardManageMembers(Collection $collection, User $actor): void
    {
        if (! $collection->canManageMembers($actor)) {
            throw ValidationException::withMessages([
                'collection' => 'You are not allowed to manage collaborators for this collection.',
            ]);
        }
    }

    private function expireMemberIfNeeded(CollectionMember $member): void
    {
        if ($member->status !== Collection::MEMBER_STATUS_PENDING || ! $member->expires_at || $member->expires_at->isFuture()) {
            return;
        }

        $member->forceFill([
            'status' => Collection::MEMBER_STATUS_REVOKED,
            'revoked_at' => now(),
        ])->save();
    }

    private function resolveInviteExpiry(?int $expiresInDays, ?string $expiresAt): Carbon
    {
        if ($expiresAt !== null && $expiresAt !== '') {
            return Carbon::parse($expiresAt);
        }

        if ($expiresInDays !== null) {
            return now()->addDays(max(1, $expiresInDays));
        }

        return now()->addDays(max(1, (int) config('collections.invites.expires_after_days', 7)));
    }
}
