<?php

namespace App\Http\Controllers\Api;

use App\Actions\Permissions\NormalizePermissionsPayloadAction;
use App\Actions\Permissions\PreserveUnauthorizedPrivilegedPermissionsAction;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\FilterRequest;
use App\Http\Requests\SaveUserRequest;
use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\UsersTransformer;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class UsersController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.0]
     */
    public function index(FilterRequest $request): array
    {
        $this->authorize('view', User::class);

        $users = User::select([
            'users.activated',
            'users.address',
            'users.avatar',
            'users.city',
            'users.country',
            'users.created_by',
            'users.created_at',
            'users.updated_at',
            'users.deleted_at',
            'users.department_id',
            'users.email',
            'users.employee_num',
            'users.first_name',
            'users.id',
            'users.jobtitle',
            'users.last_login',
            'users.last_name',
            'users.display_name',
            'users.locale',
            'users.location_id',
            'users.manager_id',
            'users.notes',
            'users.permissions',
            'users.phone',
            'users.mobile',
            'users.state',
            'users.two_factor_enrolled',
            'users.two_factor_optin',
            'users.username',
            'users.zip',
            'users.remote',
            'users.ldap_import',
            'users.start_date',
            'users.end_date',
            'users.vip',
            'users.autoassign_licenses',
            'users.website',

        ])->with('manager')
            ->with('groups')
            ->with('userloc')
            ->with('companies')
            ->with('department')
            ->with('createdBy')
            ->withCount([
                'assets as assets_count' => function (Builder $query) {
                    $query->withoutTrashed();
                },
                'licenses as licenses_count',
                'accessories as accessories_count',
                'consumables as consumables_count',
                'managesUsers as manages_users_count',
                'managedLocations as manages_locations_count',
                // Count of maintenances whose polymorphic checked_out_to points
                // at this user. Used by the users index sort + filter and by
                // the user detail Maintenances tab badge.
                'assignedMaintenances as assigned_maintenances_count',
            ]);

        $allowed_columns =
            [
                'last_name',
                'first_name',
                'display_name',
                'email',
                'jobtitle',
                'username',
                'employee_num',
                'groups',
                'activated',
                'created_at',
                'updated_at',
                'two_factor_enrolled',
                'two_factor_optin',
                'last_login',
                'assets_count',
                'licenses_count',
                'consumables_count',
                'accessories_count',
                'manages_users_count',
                'manages_locations_count',
                'assigned_maintenances_count',
                'phone',
                'mobile',
                'address',
                'city',
                'state',
                'country',
                'zip',
                'id',
                'ldap_import',
                'two_factor_optin',
                'two_factor_enrolled',
                'remote',
                'vip',
                'start_date',
                'end_date',
                'autoassign_licenses',
                'website',
                'locale',
                'notes',
                'employee_num',

                // These are *relationships* so we wouldn't normally include them in this array,
                // since they would normally create a `column not found` error,
                // BUT we account for them in the ordering switch down at the end of this method
                // DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
                'company',
                'location',
                'department',
                'manager',
                'created_by',

            ];

        $filter = [];

        if ($request->filled('filter')) {
            $filter = json_decode($request->input('filter'), true);

            if (is_null($filter)) {
                $filter = [];
            }

            $filter = array_filter($filter, function ($key) use ($allowed_columns) {
                return in_array($key, $allowed_columns);
            }, ARRAY_FILTER_USE_KEY);

        }

        // This invokes the Searchable model trait scopeTextSearch and will handle input by search or by advanced search filter
        if ($request->filled('filter') || $request->filled('search')) {
            $users->TextSearch($request->input('filter') ? $request->input('filter') : $request->input('search'));
        }

        if ($request->filled('activated')) {
            $users = $users->where('users.activated', '=', $request->input('activated'));
        }

        if ($request->input('admins') == 'true') {
            $users = $users->OnlyAdminsAndSuperAdmins();
        }

        if ($request->input('superadmins') == 'true') {
            $users = $users->OnlySuperAdmins();
        }

        if ($request->filled('company_id')) {
            // When the caller is the company show-page (expand_company_hierarchy=1),
            // include users who belong to the company's parent or any of its
            // direct children — they inherit access via the one-level hierarchy.
            // Other callers (select2 dropdowns, etc.) keep exact-id semantics.
            $companyIds = $request->boolean('expand_company_hierarchy')
                ? Company::reachableCompanyIds($request->input('company_id'))
                : [(int) $request->input('company_id')];

            $users = $users->whereHas('companies', fn ($q) => $q->whereIn('companies.id', $companyIds));
        }

        if ($request->filled('phone')) {
            $users = $users->where('users.phone', '=', $request->input('phone'));
        }

        if ($request->filled('mobile')) {
            $users = $users->where('users.mobile', '=', $request->input('mobile'));
        }

        if ($request->filled('location_id')) {
            $users = $users->where('users.location_id', '=', $request->input('location_id'));
        }

        if ($request->filled('created_by')) {
            $users = $users->where('users.created_by', '=', $request->input('created_by'));
        }

        if ($request->filled('email')) {
            $users = $users->where('users.email', '=', $request->input('email'));
        }

        if ($request->filled('username')) {
            $users = $users->where('users.username', '=', $request->input('username'));
        }

        if ($request->filled('first_name')) {
            $users = $users->where('users.first_name', '=', $request->input('first_name'));
        }

        if ($request->filled('last_name')) {
            $users = $users->where('users.last_name', '=', $request->input('last_name'));
        }

        if ($request->filled('display_name')) {
            $users = $users->where('users.display_name', '=', $request->input('display_name'));
        }

        if ($request->filled('employee_num')) {
            $users = $users->where('users.employee_num', '=', $request->input('employee_num'));
        }

        if ($request->filled('state')) {
            $users = $users->where('users.state', '=', $request->input('state'));
        }

        if ($request->filled('country')) {
            $users = $users->where('users.country', '=', $request->input('country'));
        }

        if ($request->filled('website')) {
            $users = $users->where('users.website', '=', $request->input('website'));
        }

        if ($request->filled('zip')) {
            $users = $users->where('users.zip', '=', $request->input('zip'));
        }

        if ($request->filled('group_id')) {
            $users = $users->ByGroup($request->input('group_id'));
        }

        if ($request->filled('department_id')) {
            $users = $users->where('users.department_id', '=', $request->input('department_id'));
        }

        if ($request->filled('manager_id')) {
            $users = $users->where('users.manager_id', '=', $request->input('manager_id'));
        }

        if ($request->filled('ldap_import')) {
            $users = $users->where('ldap_import', '=', $request->input('ldap_import'));
        }

        if ($request->filled('remote')) {
            $users = $users->where('remote', '=', $request->input('remote'));
        }

        if ($request->filled('vip')) {
            $users = $users->where('vip', '=', $request->input('vip'));
        }

        if ($request->filled('two_factor_enrolled')) {
            $users = $users->where('two_factor_enrolled', '=', $request->input('two_factor_enrolled'));
        }

        if ($request->filled('two_factor_optin')) {
            $users = $users->where('two_factor_optin', '=', $request->input('two_factor_optin'));
        }

        if ($request->filled('start_date')) {
            $users = $users->where('users.start_date', '=', $request->input('start_date'));
        }

        if ($request->filled('end_date')) {
            $users = $users->where('users.end_date', '=', $request->input('end_date'));
        }

        if ($request->filled('assets_count')) {
            $users->has('assets', '=', $request->input('assets_count'));
        }

        if ($request->filled('consumables_count')) {
            $users->has('consumables', '=', $request->input('consumables_count'));
        }

        if ($request->filled('licenses_count')) {
            $users->has('licenses', '=', $request->input('licenses_count'));
        }

        if ($request->filled('accessories_count')) {
            $users->has('accessories', '=', $request->input('accessories_count'));
        }

        if ($request->filled('assigned_maintenances_count')) {
            $users->has('assignedMaintenances', '=', $request->input('assigned_maintenances_count'));
        }

        if ($request->filled('manages_users_count')) {
            $users->has('managesUsers', '=', $request->input('manages_users_count'));
        }

        if ($request->filled('manages_locations_count')) {
            $users->has('managedLocations', '=', $request->input('manages_locations_count'));
        }

        if ($request->filled('autoassign_licenses')) {
            $users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
        }

        if ($request->filled('locale')) {
            $users = $users->where('users.locale', '=', $request->input('locale'));
        }

        if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
            $users = $users->onlyTrashed();
        } elseif (($request->filled('all')) && ($request->input('all') == 'true')) {
            $users = $users->withTrashed();
        }

        $order = $request->input('order') === 'asc' ? 'asc' : 'desc';

        switch ($request->input('sort')) {
            case 'manager':
                $users = $users->OrderManager($order);
                break;
            case 'location':
                $users = $users->OrderLocation($order);
                break;
            case 'department':
                $users = $users->OrderDepartment($order);
                break;
            case 'created_by':
                $users = $users->OrderByCreatedBy($order);
                break;
            case 'company':
                $users = $users->OrderCompany($order);
                break;
            case 'first_name':
                $users->orderBy('first_name', $order);
                $users->orderBy('last_name', $order);
                break;
            case 'last_name':
                $users->orderBy('last_name', $order);
                $users->orderBy('first_name', $order);
                break;
            default:
                $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
                $users = $users->orderBy($sort, $order);
                break;
        }

        // Make sure the offset and limit are actually integers and do not exceed system limits
        $total = $users->count();
        $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
        $limit = app('api_limit_value');

        $users = $users->skip($offset)->take($limit)->get();

        return (new UsersTransformer)->transformUsers($users, $total);
    }

    /**
     * Gets a paginated collection for the select2 menus
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.0.16]
     * @see SelectlistTransformer
     */
    public function selectlist(Request $request): array
    {
        $this->authorize('view.selectlists');

        $users = User::select(
            [
                'users.id',
                'users.username',
                'users.employee_num',
                'users.first_name',
                'users.last_name',
                'users.display_name',
                'users.gravatar',
                'users.avatar',
                'users.email',
            ]
        )->where('show_in_list', '=', '1');

        // When FMCS is enabled, automatically scope to companies the acting user belongs to.
        // scopeCompanyables is a no-op for superusers and when FMCS is disabled.
        $users = Company::scopeCompanyables($users, 'company_id', 'users');

        // Allow further narrowing to a specific company passed via data-company-ids on the select.
        // Superusers MUST bypass this filter — they manage across companies and need to see every
        // user on checkout dropdowns. Scoping superusers to the item's company breaks the umbrella-
        // corp / service-provider workflow where one admin checks items out to users in any sub-company.
        // See: https://github.com/snipe/snipe-it/issues/ (v8.6.3 regression report)
        if ((Setting::getSettings()->full_multiple_companies_support == '1')
            && $request->filled('companyId')
            && ! auth()->user()->isSuperUser()) {
            $companyIds = array_values(array_filter(array_map('intval', explode(',', $request->input('companyId')))));
            if (! empty($companyIds)) {
                $users = Company::scopeUsersByCompanyIds($users, $companyIds);
            }
        }

        if ($request->filled('excludeId')) {
            $users->where('users.id', '!=', (int) $request->input('excludeId'));
        }

        if ($request->filled('search')) {
            $users = $users->where(function ($query) use ($request) {
                $query->SimpleNameSearch($request->input('search'))
                    ->orWhere('username', 'LIKE', '%'.$request->input('search').'%')
                    ->orWhere('display_name', 'LIKE', '%'.$request->input('search').'%')
                    ->orWhere('email', 'LIKE', '%'.$request->input('search').'%')
                    ->orWhere('employee_num', 'LIKE', '%'.$request->input('search').'%');
            });
        }

        $users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
        $users = $users->paginate(50);

        foreach ($users as $user) {
            $name_str = $user->display_name;

            if ($user->username != '') {
                $name_str .= ' ('.$user->username.')';
            }

            if ($user->employee_num != '') {
                $name_str .= ' - #'.$user->employee_num;
            }

            $user->use_text = $name_str;
            $user->use_image = ($user->present()->gravatar) ? $user->present()->gravatar : null;
        }

        return (new SelectlistTransformer)->transformSelectlist($users);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.0]
     *
     * @param  Request  $request
     */
    public function store(SaveUserRequest $request): JsonResponse
    {
        $this->authorize('create', User::class);

        $authenticatedUser = auth()->user();

        // Resolve requested company memberships up front, BEFORE any user
        // record or pivot row lands on the DB. Prior behavior filled and
        // saved the user first, then filtered company IDs after the fact
        // (see syncCompaniesWithLogging below). A non-superuser could
        // submit a company_id belonging to another company they were not
        // a member of; the user row was persisted before the filter ran,
        // producing an unauthorized cross-tenant record even when the
        // pivot ended up empty (leaving the account as a floater under
        // null_company_is_floater installs). Reject the whole request if
        // any requested id fails the actor's permitted-companies filter.
        $requestedCompanyIds = array_values(array_filter(array_map(
            'intval',
            (array) ($request->input('company_ids') ?? ($request->filled('company_id') ? [$request->input('company_id')] : [])),
        )));
        $permittedCompanyIds = Company::getIdsForCurrentUser($requestedCompanyIds);

        if (count($requestedCompanyIds) !== count($permittedCompanyIds)) {
            return response()->json(Helper::formatStandardApiResponse(
                'error',
                null,
                trans('admin/users/message.error.company_not_permitted'),
            ), 403);
        }

        // Groups validation runs BEFORE save so the transaction below is a
        // straight-line succeed-or-rollback. Doing it after save (as the
        // pre-refactor version did) meant a bad groups payload persisted a
        // user record even though the response reported "error".
        if (($request->has('groups')) && (auth()->user()->isSuperUser())) {
            $groupsValidator = Validator::make($request->only('groups'), [
                'groups.*' => 'integer|exists:permission_groups,id',
            ]);

            if ($groupsValidator->fails()) {
                return response()->json(Helper::formatStandardApiResponse('error', null, $groupsValidator->errors()));
            }
        }

        $user = new User;
        $user->fill($request->all());
        $user->created_by = auth()->id();

        if ($request->has('permissions')) {
            $user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
                requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
                authenticatedUser: $authenticatedUser,
            ));
        }

        if ($request->filled('password')) {
            $user->password = bcrypt($request->input('password'));
        } else {
            $user->password = $user->noPassword();
        }

        app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');

        // Wrap save + groups + company sync in a single transaction so a
        // failure anywhere in the create sequence rolls back the whole
        // thing. Without this, an exception in syncCompaniesWithLogging()
        // or groups()->sync() would leave a partially-created user record
        // on the DB with unfiltered attribute state from $request->all().
        $saveFailed = false;

        DB::transaction(function () use ($request, $user, $permittedCompanyIds, &$saveFailed) {
            if (! $user->save()) {
                $saveFailed = true;

                return;
            }

            if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {

                try {
                    $user->notify(new WelcomeNotification($user));
                } catch (\Exception $e) {
                    Log::warning('Could not send welcome notification for user: '.$e->getMessage());
                }

            }

            if (($request->has('groups')) && (auth()->user()->isSuperUser())) {
                $user->groups()->sync($request->input('groups'));
            }

            $user->syncCompaniesWithLogging($permittedCompanyIds);
        });

        if ($saveFailed) {
            return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
        }

        return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.create')));
    }

    /**
     * Display the specified resource.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @param  int  $id
     */
    public function show($id): JsonResponse|array
    {
        $this->authorize('view', User::class);

        if ($user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count', 'assignedMaintenances as assigned_maintenances_count')->find($id)) {
            $this->authorize('view', $user);

            return (new UsersTransformer)->transformUser($user);
        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));

    }

    /**
     * Update the specified resource in storage.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.0]
     *
     * @param  Request  $request
     * @param  int  $id
     */
    public function update(SaveUserRequest $request, User $user): JsonResponse
    {
        $this->authorize('update', $user);

        $authenticatedUser = auth()->user();

        /**
         * This is a janky hack to prevent people from changing admin demo user data on the public demo.
         * The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
         *  Thanks, jerks. You are why we can't have nice things. - snipe
         */
        if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) {
            return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
        }

        // Resolve requested company memberships up front, BEFORE any DB
        // write. Same reasoning as store(): the pre-refactor update path
        // filled the user record from $request->all() and saved it, then
        // filtered the company IDs afterward. A non-superuser could
        // therefore relocate an existing user into a different company
        // via a scalar company_id or company_ids[] payload. Reject the
        // whole request if any requested id fails the actor's permitted
        // companies filter.
        if ($request->has('company_ids') || $request->filled('company_id')) {
            $requestedCompanyIds = array_values(array_filter(array_map(
                'intval',
                (array) ($request->input('company_ids') ?? [$request->input('company_id')]),
            )));
            $permittedCompanyIds = Company::getIdsForCurrentUser($requestedCompanyIds);

            if (count($requestedCompanyIds) !== count($permittedCompanyIds)) {
                return response()->json(Helper::formatStandardApiResponse(
                    'error',
                    null,
                    trans('admin/users/message.error.company_not_permitted'),
                ), 403);
            }
        }

        // User::GATED_AUTH_FIELDS enter through the canEditAuthFields branch
        // below. If the caller does not hold that gate against this target
        // but their request nevertheless carries any of those fields, fail
        // loud rather than persisting a partial write while returning a
        // success response. Prior behavior silently dropped the auth-field
        // writes and returned `success`, which misrepresented what
        // actually persisted.
        $requestedAuthFields = array_values(array_intersect(User::GATED_AUTH_FIELDS, array_keys($request->all())));
        $canEditAuthFields = auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo');

        if (! empty($requestedAuthFields) && ! $canEditAuthFields) {
            return response()->json(Helper::formatStandardApiResponse(
                'error',
                null,
                trans('admin/users/message.auth_fields_denied', ['fields' => implode(', ', $requestedAuthFields)]),
            ));
        }

        // Pull out sensitive fields that require extra permission. The
        // GATED_AUTH_FIELDS constant covers user-editable secrets; the
        // additional keys below are internal state (2FA secrets, remember
        // tokens, activation codes) that must never be settable from a
        // request payload regardless of caller privilege.
        $user->fill($request->except(array_merge(User::GATED_AUTH_FIELDS, [
            'activation_code',
            'remember_token',
            'two_factor_secret',
            'two_factor_enrolled',
            'two_factor_optin',
        ])));

        if ($canEditAuthFields) {

            if ($request->filled('password')) {
                $user->password = bcrypt($request->input('password'));
            }

            // We need to use has()  instead of filled()
            // here because we need to overwrite permissions
            // if someone needs to null them out

            if ($request->has('username')) {
                $user->username = $request->input('username');
            }

            if ($request->has('email')) {
                $user->email = $request->input('email');
            }

            if ($request->has('activated')) {
                $user->activated = $request->input('activated');
            }

            if ($request->has('permissions')) {
                // This is going to update the whole thing, not just what was passed.
                $user->permissions = json_encode(PreserveUnauthorizedPrivilegedPermissionsAction::run(
                    requestedPermissions: NormalizePermissionsPayloadAction::run($request->input('permissions')),
                    authenticatedUser: $authenticatedUser,
                    originalPermissions: NormalizePermissionsPayloadAction::run($user->decodePermissions()),
                    targetUser: $user,
                ));
            }

        }

        if ($user->id == $request->input('manager_id')) {
            return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
        }

        if ($request->has('location_id')) {
            // Update the location of any assets checked out to this user
            Asset::where('assigned_type', User::class)
                ->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
        }

        app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');

        if ($user->save()) {
            // Check if the request has groups passed and has a value, AND that the user us a superuser
            if (($request->has('groups')) && (auth()->user()->isSuperUser())) {

                $validator = Validator::make($request->only('groups'), [
                    'groups.*' => 'integer|exists:permission_groups,id',
                ]);

                if ($validator->fails()) {
                    return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()));
                }

                // Sync the groups since the user is a superuser and the groups pass validation
                $user->groups()->sync($request->input('groups'));
            }

            // company_ids (new format) = full replacement sync.
            // Legacy company_id = add without removing other associations.
            if ($request->has('company_ids')) {
                $companyIds = array_filter(array_map('intval', (array) $request->input('company_ids')));
                $user->syncCompaniesWithLogging(Company::getIdsForCurrentUser($companyIds));
            } elseif ($request->filled('company_id')) {
                $filtered = Company::getIdsForCurrentUser([(int) $request->input('company_id')]);
                if (! empty($filtered)) {
                    $user->companies()->syncWithoutDetaching($filtered);
                    $user->syncLegacyCompanyIdMirror();
                }
            }

            return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
        }

        return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
    }

    /**
     * Remove the specified resource from storage.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.0]
     *
     * @param  int  $id
     */
    public function destroy(DeleteUserRequest $request, $id): JsonResponse
    {
        $this->authorize('delete', User::class);

        if ($user = User::withTrashed()->find($id)) {

            $this->authorize('delete', $user);

            if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {

                if ($user->delete()) {

                    // Remove the user's avatar if they have one
                    // @todo This should be done on purge, not here
                    //                    if (Storage::disk('public')->exists('avatars/' . $user->avatar)) {
                    //                        try {
                    //                            Storage::disk('public')->delete('avatars/' . $user->avatar);
                    //                        } catch (\Exception $e) {
                    //                            Log::debug($e);
                    //                        }
                    //                    }

                    return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
                }

                return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
            }

            return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.cannot_delete')));

        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')));

    }

    /**
     * Return JSON containing a list of assets assigned to a user.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v3.0]
     *
     * @param  $userId
     */
    public function assets(Request $request, $id): JsonResponse|array
    {
        $this->authorize('view', User::class);
        $this->authorize('view', Asset::class);

        if ($user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id)) {
            $this->authorize('view', $user);

            $assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');

            // Filter on category ID
            if ($request->filled('category_id')) {
                $assets = $assets->InCategory($request->input('category_id'));
            }

            // Filter on model ID
            if ($request->filled('model_id')) {

                $model_ids = $request->input('model_id');
                if (! is_array($model_ids)) {
                    $model_ids = [$model_ids];
                }
                $assets = $assets->InModelList($model_ids);
            }

            $total = $assets->count();
            $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
            $limit = app('api_limit_value');

            $assets = $assets->skip($offset)->take($limit)->get();

            return (new AssetsTransformer)->transformAssets($assets, $total, $request);
        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));

    }

    /**
     * Notify a specific user via email with all of their assigned assets.
     *
     * @author [Lukas Fehling] [<lukas.fehling@adabay.rocks>]
     *
     * @since [v6.0.13]
     */
    public function emailAssetList(Request $request, $id): JsonResponse
    {
        $this->authorize('update', User::class);

        if ($user = User::find($id)) {
            $this->authorize('update', $user);

            if (empty($user->email)) {
                return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
            }

            $user->notify((new CurrentInventory($user)));

            return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.inventorynotification.success')));
        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));

    }

    /**
     * Return JSON containing a list of consumables assigned to a user.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v3.0]
     *
     * @param  $userId
     */
    public function consumables(Request $request, $id): array
    {
        $this->authorize('view', User::class);
        $this->authorize('view', Consumable::class);
        $user = User::findOrFail($id);
        $this->authorize('view', $user);
        $consumables = $user->consumables;

        return (new ConsumablesTransformer)->transformConsumables($consumables, $consumables->count(), $request);
    }

    /**
     * Return JSON containing a list of accessories assigned to a user.
     *
     * @author [A. Gianotto] [<snipe@snipe.net>]
     *
     * @since [v4.6.14]
     *
     * @param  $userId
     */
    public function accessories(Request $request, $id): array
    {
        $this->authorize('view', User::class);
        $user = User::findOrFail($id);
        $this->authorize('view', $user);
        $this->authorize('view', Accessory::class);

        $accessories = $user->accessories();

        $total = $accessories->count();
        $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
        $limit = app('api_limit_value');

        $accessories = $accessories->skip($offset)->take($limit)->get();

        return (new AccessoriesTransformer)->transformAccessories($accessories, $total);
    }

    /**
     * Return JSON containing a list of licenses assigned to a user.
     *
     * @author [N. Mathar] [<snipe@snipe.net>]
     *
     * @since [v5.0]
     *
     * @param  $userId
     */
    public function licenses(Request $request, $id): JsonResponse|array
    {
        $this->authorize('view', User::class);
        $this->authorize('view', License::class);

        if ($user = User::where('id', $id)->withTrashed()->first()) {
            $licenses = $user->licenses();

            $total = $licenses->count();
            $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
            $limit = app('api_limit_value');

            $licenses = $licenses->skip($offset)->take($limit)->get();

            return (new LicensesTransformer)->transformLicenses($licenses, $total);
        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));

    }

    /**
     * Get info on the current user.
     *
     * @author [Juan Font] [<juanfontalonso@gmail.com>]
     *
     * @since [v4.4.2]
     */
    public function getCurrentUserInfo(Request $request): array
    {
        return (new UsersTransformer)->transformUser($request->user());
    }

    /**
     * Display the EULAs accepted by the user.
     *
     * @return JsonResponse
     *
     *@since [v8.1.16]
     *
     * @author [Godfrey Martinez] [<gmartinez@grokability.com>]
     */
    public function eulas(Request $request, User $user, ActionlogsTransformer $transformer)
    {
        $this->authorize('view', $user);

        $eulas = $user->eulas();

        $total = $eulas->count();
        $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
        $limit = app('api_limit_value');

        $eulas = $eulas->skip($offset)->take($limit)->get();

        return response()->json(
            $transformer->transformActionlogs($eulas, $total)
        );
    }

    /**
     * Restore a soft-deleted user.
     *
     * @author [E. Taylor] [<dev@evantaylor.name>]
     *
     * @param  int  $userId
     *
     * @since [v6.0.0]
     */
    public function restore($userId): JsonResponse
    {
        $this->authorize('delete', User::class);

        if ($user = User::withTrashed()->find($userId)) {

            $this->authorize('delete', $user);

            if ($user->deleted_at == '') {
                return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.user')])), 200);
            }

            if ($user->restore()) {

                $logaction = new Actionlog;
                $logaction->item_type = User::class;
                $logaction->item_id = $user->id;
                $logaction->created_at = date('Y-m-d H:i:s');
                $logaction->created_by = auth()->id();
                $logaction->logaction('restore');

                return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')), 200);
            }

        }

        return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);

    }

    /**
     * Run the LDAP sync command to import users from LDAP via API.
     *
     * @author A. Gianotto <snipe@snipe.net>
     *
     * @since 8.2.2
     *
     * @return JsonResponse
     */
    public function syncLdapUsers(Request $request)
    {
        // Superuser-only: a bulk LDAP sync surfaces users from across
        // the entire directory (all companies, all OUs), so anyone with
        // "users.edit" but no full-directory access shouldn't be able
        // to run it or read the summary that lists them.
        if (! auth()->user()?->isSuperUser()) {
            abort(403);
        }
        // Call Artisan LDAP import command.

        Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);

        // Collect and parse JSON summary.
        $ldap_results_json = Artisan::output();
        $ldap_results = json_decode($ldap_results_json, true);

        if (! $ldap_results) {
            return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.no_results')), 200);
        }

        // Direct user to appropriate status page.
        if ($ldap_results['error']) {
            return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
        }

        return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);

    }

    public function history(Request $request, User $user): JsonResponse|array
    {
        $this->authorize('history', $user);
        $historyQuery = $user->getHistory($request);
        $total = (clone $historyQuery)->count();
        $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
        $limit = app('api_limit_value');
        $history = (clone $historyQuery)->skip($offset)->take($limit)->get();

        return response()->json((new ActionlogsTransformer)->transformActionlogs($history, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
    }
}
