<?php

declare(strict_types=1);

namespace App\Services\Auth;

use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\Auth\WorkspaceRole;
use DomainException;
use Illuminate\Support\Facades\DB;

class WorkspaceMembershipManager
{
    public function __construct(
        public WorkspaceAuditLogger $auditLogger,
        private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver,
        private readonly ManagedEnvironmentAccessScopeResolver $managedEnvironmentAccessScopeResolver,
    ) {}

    public function addMember(
        Workspace $workspace,
        User $actor,
        User $member,
        string $role,
        string $source = 'manual',
    ): WorkspaceMembership {
        $this->assertValidRole($role);
        $this->assertActorCanManage($actor, $workspace);

        try {
            $membership = DB::transaction(function () use ($workspace, $actor, $member, $role, $source): WorkspaceMembership {
                $existing = WorkspaceMembership::query()
                    ->where('workspace_id', (int) $workspace->getKey())
                    ->where('user_id', (int) $member->getKey())
                    ->first();

                if ($existing) {
                    if ($existing->role !== $role) {
                        $fromRole = (string) $existing->role;

                        $this->guardLastOwnerDemotion($workspace, $existing, $role);

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

                        $this->auditLogger->log(
                            workspace: $workspace,
                            action: AuditActionId::WorkspaceMembershipRoleChange->value,
                            context: [
                                'metadata' => [
                                    'member_user_id' => (int) $member->getKey(),
                                    'from_role' => $fromRole,
                                    'to_role' => $role,
                                    'source' => $source,
                                ],
                            ],
                            actor: $actor,
                            status: 'success',
                            resourceType: 'workspace',
                            resourceId: (string) $workspace->getKey(),
                        );
                    }

                    return $existing->refresh();
                }

                $membership = WorkspaceMembership::query()->create([
                    'workspace_id' => (int) $workspace->getKey(),
                    'user_id' => (int) $member->getKey(),
                    'role' => $role,
                ]);

                $this->auditLogger->log(
                    workspace: $workspace,
                    action: AuditActionId::WorkspaceMembershipAdd->value,
                    context: [
                        'metadata' => [
                            'member_user_id' => (int) $member->getKey(),
                            'role' => $role,
                            'source' => $source,
                        ],
                    ],
                    actor: $actor,
                    status: 'success',
                    resourceType: 'workspace',
                    resourceId: (string) $workspace->getKey(),
                );

                return $membership;
            });

            $this->clearAuthorizationCaches();

            return $membership;
        } catch (DomainException $exception) {
            if ($exception->getMessage() === 'You cannot demote the last remaining owner.') {
                $this->auditLastOwnerBlocked(
                    workspace: $workspace,
                    actor: $actor,
                    targetUserId: (int) $member->getKey(),
                    attemptedRole: $role,
                    currentRole: WorkspaceRole::Owner->value,
                    attemptedAction: 'role_change',
                );
            }

            throw $exception;
        }
    }

    public function changeRole(Workspace $workspace, User $actor, WorkspaceMembership $membership, string $newRole): WorkspaceMembership
    {
        $this->assertValidRole($newRole);
        $this->assertActorCanManage($actor, $workspace);

        try {
            $membership = DB::transaction(function () use ($workspace, $actor, $membership, $newRole): WorkspaceMembership {
                $membership->refresh();

                if ($membership->workspace_id !== (int) $workspace->getKey()) {
                    throw new DomainException('Membership belongs to a different workspace.');
                }

                $oldRole = (string) $membership->role;

                if ($oldRole === $newRole) {
                    return $membership;
                }

                $this->guardLastOwnerDemotion($workspace, $membership, $newRole);

                $membership->forceFill([
                    'role' => $newRole,
                ])->save();

                $this->auditLogger->log(
                    workspace: $workspace,
                    action: AuditActionId::WorkspaceMembershipRoleChange->value,
                    context: [
                        'metadata' => [
                            'member_user_id' => (int) $membership->user_id,
                            'from_role' => $oldRole,
                            'to_role' => $newRole,
                        ],
                    ],
                    actor: $actor,
                    status: 'success',
                    resourceType: 'workspace',
                    resourceId: (string) $workspace->getKey(),
                );

                return $membership->refresh();
            });

            $this->clearAuthorizationCaches();

            return $membership;
        } catch (DomainException $exception) {
            if ($exception->getMessage() === 'You cannot demote the last remaining owner.') {
                $this->auditLastOwnerBlocked(
                    workspace: $workspace,
                    actor: $actor,
                    targetUserId: (int) $membership->user_id,
                    attemptedRole: $newRole,
                    currentRole: (string) $membership->role,
                    attemptedAction: 'role_change',
                );
            }

            throw $exception;
        }
    }

    public function removeMember(Workspace $workspace, User $actor, WorkspaceMembership $membership): void
    {
        $this->assertActorCanManage($actor, $workspace);

        try {
            DB::transaction(function () use ($workspace, $actor, $membership): void {
                $membership->refresh();

                if ($membership->workspace_id !== (int) $workspace->getKey()) {
                    throw new DomainException('Membership belongs to a different workspace.');
                }

                $this->guardLastOwnerRemoval($workspace, $membership);

                $memberUserId = (int) $membership->user_id;
                $oldRole = (string) $membership->role;

                ManagedEnvironmentMembership::query()
                    ->where('user_id', $memberUserId)
                    ->whereIn(
                        'managed_environment_id',
                        ManagedEnvironment::query()
                            ->select('id')
                            ->where('workspace_id', (int) $workspace->getKey()),
                    )
                    ->delete();

                $membership->delete();

                $this->auditLogger->log(
                    workspace: $workspace,
                    action: AuditActionId::WorkspaceMembershipRemove->value,
                    context: [
                        'metadata' => [
                            'member_user_id' => $memberUserId,
                            'role' => $oldRole,
                        ],
                    ],
                    actor: $actor,
                    status: 'success',
                    resourceType: 'workspace',
                    resourceId: (string) $workspace->getKey(),
                );
            });

            $this->clearAuthorizationCaches();
        } catch (DomainException $exception) {
            if ($exception->getMessage() === 'You cannot remove the last remaining owner.') {
                $this->auditLastOwnerBlocked(
                    workspace: $workspace,
                    actor: $actor,
                    targetUserId: (int) $membership->user_id,
                    attemptedRole: (string) $membership->role,
                    currentRole: (string) $membership->role,
                    attemptedAction: 'remove',
                );
            }

            throw $exception;
        }
    }

    private function assertActorCanManage(User $actor, Workspace $workspace): void
    {
        if (! $this->workspaceCapabilityResolver->can($actor, $workspace, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE)) {
            throw new DomainException('Forbidden.');
        }
    }

    private function clearAuthorizationCaches(): void
    {
        $this->workspaceCapabilityResolver->clearCache();
        $this->managedEnvironmentAccessScopeResolver->clearCache();
    }

    private function assertValidRole(string $role): void
    {
        $valid = array_map(
            static fn (WorkspaceRole $workspaceRole): string => $workspaceRole->value,
            WorkspaceRole::cases(),
        );

        if (! in_array($role, $valid, true)) {
            throw new DomainException('Invalid role.');
        }
    }

    private function guardLastOwnerDemotion(Workspace $workspace, WorkspaceMembership $membership, string $newRole): void
    {
        if ($membership->role !== WorkspaceRole::Owner->value) {
            return;
        }

        if ($newRole === WorkspaceRole::Owner->value) {
            return;
        }

        $owners = WorkspaceMembership::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('role', WorkspaceRole::Owner->value)
            ->count();

        if ($owners <= 1) {
            throw new DomainException('You cannot demote the last remaining owner.');
        }
    }

    private function guardLastOwnerRemoval(Workspace $workspace, WorkspaceMembership $membership): void
    {
        if ($membership->role !== WorkspaceRole::Owner->value) {
            return;
        }

        $owners = WorkspaceMembership::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('role', WorkspaceRole::Owner->value)
            ->count();

        if ($owners <= 1) {
            throw new DomainException('You cannot remove the last remaining owner.');
        }
    }

    private function auditLastOwnerBlocked(
        Workspace $workspace,
        User $actor,
        int $targetUserId,
        string $attemptedRole,
        string $currentRole,
        string $attemptedAction,
    ): void {
        $this->auditLogger->log(
            workspace: $workspace,
            action: AuditActionId::WorkspaceMembershipLastOwnerBlocked->value,
            context: [
                'metadata' => [
                    'workspace_id' => (int) $workspace->getKey(),
                    'actor_user_id' => (int) $actor->getKey(),
                    'target_user_id' => $targetUserId,
                    'attempted_role' => $attemptedRole,
                    'current_role' => $currentRole,
                    'attempted_action' => $attemptedAction,
                ],
            ],
            actor: $actor,
            status: 'blocked',
            resourceType: 'workspace',
            resourceId: (string) $workspace->getKey(),
        );
    }
}
