<?php

namespace App\Livewire\Settings;

use App\Enums\Role;
use App\Models\AuditEvent;
use App\Models\User;
use App\Services\SessionService;
use App\Support\Confirm\ConfirmToken;
use App\Support\Confirm\InvalidConfirmToken;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * Multi-user account management. Accounts now carry an RBAC role (admin > operator >
 * viewer); the mutating actions here are gated behind the `manage-users` ability (admin
 * only). Destructive actions (remove, force-logout) and role changes go through the R5
 * confirm modal, which writes one AuditEvent and re-dispatches the apply event back here
 * (mirrors Settings\Security / Settings\Sessions).
 *
 * The last-admin invariant is enforced everywhere: the sole remaining admin can neither
 * be removed nor demoted, so the panel can never be stranded with zero administrators.
 *
 * The current operator is always recorded as the actor on each audit row.
 */
class Users extends Component
{
    /** Open the CreateUser modal; it dispatches `usersChanged` on success. */
    public function create(): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        $this->dispatch('openModal', component: 'modals.create-user');
    }

    /**
     * Destructive (R5): open the confirm modal for removing an account. Guards
     * (self / last account) are enforced here AND again in the apply handler, so
     * a stale event can never slip past after the modal is open.
     */
    public function confirmRemove(int $userId): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        $user = User::find($userId);
        if (! $user || ! $this->canRemove($user)) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('accounts.remove_heading'),
                'body' => __('accounts.remove_body'),
                'confirmLabel' => __('accounts.remove'),
                'danger' => true,
                'icon' => 'trash',
                // Defer the toast: the apply handler reports the real outcome.
                'notify' => '',
                'token' => ConfirmToken::issue(
                    'userRemoved',
                    ['userId' => $userId],
                    auditTarget: $user->email,
                ),
            ],
        );
    }

    #[On('userRemoved')]
    public function remove(string $confirmToken, SessionService $sessions): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'userRemoved');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $userId = $payload['params']['userId'];

        $self = Auth::id();

        // Atomic: lock the user rows + recount inside the transaction so two concurrent
        // deletes can't both pass the "more than one account" check and strand the
        // install with zero users (a lock-out). The last account is never removable.
        // Sentinel: null = silent no-op (self / last account / gone); 'last_admin' =
        // refused because it would strand zero admins → surface the guard toast.
        $email = DB::transaction(function () use ($userId, $self, $sessions): ?string {
            if (User::lockForUpdate()->count() <= 1) {
                return null;
            }

            $user = User::find($userId);
            if (! $user || $user->getKey() === $self) {
                return null;
            }

            // Last-admin invariant: removing the sole administrator would leave the panel
            // with zero admins (no one can manage users again). Lock + recount admins in
            // the same transaction so concurrent removes can't both slip past the check.
            if ($user->role === Role::Admin
                && User::where('role', Role::Admin->value)->lockForUpdate()->count() <= 1) {
                return 'last_admin';
            }

            $email = $user->email;

            // Kill every session + rotate the remember_token BEFORE deleting, so a
            // stolen remember-me cookie can never resurrect the account between the
            // session wipe and the row delete.
            $sessions->logoutUserEverywhere($user);
            $user->delete();

            return $email;
        });

        if ($email === null) {
            return;
        }

        if ($email === 'last_admin') {
            $this->dispatch('notify', message: __('accounts.last_admin_guard'), level: 'error');

            return;
        }

        $this->audit('user.delete', $email);
        $this->dispatch('notify', message: __('accounts.remove_notify'));
    }

    /**
     * Destructive (R5): open the confirm modal for signing another account out
     * of every device. Never offered for self (self uses Settings → Sessions).
     */
    public function confirmLogout(int $userId): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        $user = User::find($userId);
        if (! $user || $user->is(Auth::user())) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('accounts.logout_heading'),
                'body' => __('accounts.logout_body'),
                'confirmLabel' => __('accounts.logout'),
                'danger' => true,
                'icon' => 'logout',
                'notify' => '',
                'token' => ConfirmToken::issue(
                    'userLoggedOut',
                    ['userId' => $userId],
                    auditTarget: $user->email,
                ),
            ],
        );
    }

    #[On('userLoggedOut')]
    public function logout(string $confirmToken, SessionService $sessions): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'userLoggedOut');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $userId = $payload['params']['userId'];

        $user = User::find($userId);
        if (! $user || $user->is(Auth::user())) {
            return;
        }

        $sessions->logoutUserEverywhere($user);

        $this->audit('user.logout', $user->email);
        $this->dispatch('notify', message: __('accounts.logout_notify'));
    }

    /**
     * Sensitive (R5): open the confirm modal for changing another account's role. The
     * new role must be a valid Role case; the apply handler re-checks the last-admin
     * invariant so a stale event can never demote the sole admin after the modal opens.
     */
    public function confirmRoleChange(int $userId, string $role): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        $user = User::find($userId);
        if (! $user || ! in_array($role, array_column(Role::cases(), 'value'), true)) {
            return;
        }

        $this->dispatch('openModal',
            component: 'modals.confirm-action',
            arguments: [
                'heading' => __('accounts.role_change_confirm_heading'),
                'body' => __('accounts.role_change_confirm_body', [
                    'name' => $user->name,
                    'role' => Role::from($role)->label(),
                ]),
                'confirmLabel' => __('accounts.role_change_confirm'),
                'danger' => false,
                'icon' => 'shield',
                // Defer the toast: the apply handler reports the real outcome.
                'notify' => '',
                'token' => ConfirmToken::issue(
                    'userRoleChanged',
                    ['userId' => $userId, 'role' => $role],
                    auditTarget: $user->email,
                ),
            ],
        );
    }

    #[On('userRoleChanged')]
    public function applyRoleChange(string $confirmToken): void
    {
        abort_unless(auth()->user()?->can('manage-users'), 403);

        try {
            $payload = ConfirmToken::consume($confirmToken, 'userRoleChanged');
        } catch (InvalidConfirmToken) {
            return; // forged / replayed / direct-bypass attempt — no-op
        }
        $userId = $payload['params']['userId'];
        $role = $payload['params']['role'];

        // Atomic: lock the target + recount admins inside the transaction so two concurrent
        // demotions can't both pass the last-admin check and strand zero administrators.
        // Sentinel: null = silent no-op (gone / same role); 'last_admin' = refused because
        // it would demote the sole admin → surface the guard toast.
        $result = DB::transaction(function () use ($userId, $role): ?string {
            $user = User::whereKey($userId)->lockForUpdate()->first();
            if (! $user) {
                return null;
            }

            $newRole = Role::from($role);

            // Last-admin invariant: never demote the sole administrator away from admin.
            if ($user->role === Role::Admin
                && $newRole !== Role::Admin
                && User::where('role', Role::Admin->value)->lockForUpdate()->count() <= 1) {
                return 'last_admin';
            }

            $user->role = $newRole;
            $user->save();

            AuditEvent::create([
                'user_id' => Auth::id(),
                'actor' => Auth::user()?->name,
                'action' => 'user.role_change',
                'target' => $user->email,
                'ip' => request()->ip(),
                'meta' => ['to' => $role],
            ]);

            return $user->email;
        });

        if ($result === null) {
            return;
        }

        if ($result === 'last_admin') {
            $this->dispatch('notify', message: __('accounts.last_admin_guard'), level: 'error');

            return;
        }

        $this->dispatch('usersChanged');
        $this->dispatch('notify', message: __('accounts.role_changed_toast'));
    }

    /** Reload the list after the CreateUser modal adds an account. */
    #[On('usersChanged')]
    public function refreshList(): void
    {
        // The list is re-fetched in render(); this handler just forces the round trip.
    }

    /**
     * A user may be offered the remove control unless it is the current operator or the
     * last remaining account (no lock-out). This is only the UI gate; the removal itself
     * is restricted to admins (`manage-users`) and the apply handler additionally refuses
     * to strand zero administrators (last-admin invariant).
     */
    private function canRemove(User $user): bool
    {
        return ! $user->is(Auth::user()) && User::count() > 1;
    }

    private function audit(string $action, ?string $target): void
    {
        AuditEvent::create([
            'user_id' => Auth::id(),
            'actor' => Auth::user()?->name ?? 'system',
            'action' => $action,
            'target' => $target,
            'ip' => request()->ip(),
        ]);
    }

    public function render()
    {
        return view('livewire.settings.users', [
            'users' => User::query()->orderBy('name')->get(),
            'currentId' => Auth::id(),
        ]);
    }
}
