using Remotely.Shared.Models;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Remotely.Server.Data;
using Remotely.Server.Extensions;
using Remotely.Server.Models;
using Remotely.Shared;
using Remotely.Shared.Dtos;
using Remotely.Shared.Entities;
using Remotely.Shared.Utilities;
using Remotely.Shared.ViewModels;
using System.Text.Json;

namespace Remotely.Server.Services;

// TODO: Separate this into domain-specific services.
public interface IDataService
{
    Task AddAlert(string deviceId, string organizationId, string alertMessage, string? details = null);

    Task<Result<DeviceGroup>> AddDeviceGroup(string orgId, DeviceGroup deviceGroup);
    Task<Result> AddDeviceToGroup(string deviceId, string groupId);
    Task<Result<InviteLink>> AddInvite(string orgId, InviteViewModel invite);

    Task<Result<Device>> AddOrUpdateDevice(DeviceClientDto device);

    Task<Result> AddOrUpdateSavedScript(SavedScript script, string userId);

    Task AddOrUpdateScriptSchedule(ScriptSchedule schedule);

    Task<Result<ScriptResult>> AddScriptResult(ScriptResultDto dto);
    Task<Result> AddScriptResultToScriptRun(string scriptResultId, int scriptRunId);

    Task AddScriptRun(ScriptRun scriptRun);

    Task<string> AddSharedFile(IBrowserFile file, string organizationId, Action<double, string> progressCallback);

    Task<string> AddSharedFile(IFormFile file, string organizationId);

    bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage);

    Task ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin);

    Task CleanupOldRecords();

    Task<Result<ApiToken>> CreateApiToken(string userName, string tokenName, string secretHash);

    Task<Result<Device>> CreateDevice(DeviceSetupOptions options);

    Task<Result> CreateUser(string userEmail, bool isAdmin, string organizationId);

    Task DeleteAlert(Alert alert);

    Task DeleteAllAlerts(string orgId, string? userName = null);

    Task<Result> DeleteApiToken(string userName, string tokenId);

    Task<Result> DeleteDeviceGroup(string orgId, string deviceGroupId);

    Task<Result> DeleteInvite(string orgId, string inviteId);

    Task DeleteSavedScript(Guid scriptId);

    Task DeleteScriptSchedule(int scriptScheduleId);

    Task<Result> DeleteUser(string orgId, string targetUserId);

    void DeviceDisconnected(string deviceId);

    bool DoesUserExist(string userName);

    bool DoesUserHaveAccessToDevice(string deviceId, RemotelyUser remotelyUser);

    bool DoesUserHaveAccessToDevice(string deviceId, string remotelyUserId);

    string[] FilterDeviceIdsByUserPermission(string[] deviceIds, RemotelyUser remotelyUser);

    string[] FilterUsersByDevicePermission(IEnumerable<string> userIds, string deviceId);

    Task<Result<Alert>> GetAlert(string alertId);

    Alert[] GetAlerts(string userId);

    ApiToken[] GetAllApiTokens(string userId);

    ScriptResult[] GetAllCommandResults(string orgId);

    ScriptResult[] GetAllCommandResultsForUser(string orgId, string userName, string deviceId);

    Device[] GetAllDevices(string orgId);

    InviteLink[] GetAllInviteLinks(string organizationId);

    ScriptResult[] GetAllScriptResults(string orgId, string deviceId);

    ScriptResult[] GetAllScriptResultsForUser(string orgId, string userName);

    RemotelyUser[] GetAllUsersForServer();

    Task<RemotelyUser[]> GetAllUsersInOrganization(string orgId);

    Task<Result<ApiToken>> GetApiKey(string keyId);

    Task<Result<BrandingInfo>> GetBrandingInfo(string organizationId);

    Task<Result<Organization>> GetDefaultOrganization();

    Task<Result<Device>> GetDevice(
          string deviceId,
          Action<IQueryable<Device>>? queryBuilder = null);

    Task<Result<Device>> GetDevice(string orgId, string deviceId);

    int GetDeviceCount();

    int GetDeviceCount(RemotelyUser user);

    Task<Result<DeviceGroup>> GetDeviceGroup(
        string deviceGroupId,
        bool includeDevices = false,
        bool includeUsers = false);

    DeviceGroup[] GetDeviceGroups(string username);

    DeviceGroup[] GetDeviceGroupsForOrganization(string organizationId);

    List<Device> GetDevices(IEnumerable<string> deviceIds);

    Device[] GetDevicesForUser(string userName);

    Task<Result<Organization>> GetOrganizationById(string organizationId);

    Task<Result<Organization>> GetOrganizationByUserName(string userName);

    int GetOrganizationCount();
    Task<int> GetOrganizationCountAsync();

    Task<Result<string>> GetOrganizationNameById(string organizationId);

    Task<Result<string>> GetOrganizationNameByUserName(string userName);

    Task<IEnumerable<ScriptRun>> GetPendingScriptRuns(string deviceId);

    Task<List<SavedScript>> GetQuickScripts(string userId);

    Task<Result<SavedScript>> GetSavedScript(Guid scriptId);

    Task<Result<SavedScript>> GetSavedScript(string userId, Guid scriptId);

    Task<List<SavedScript>> GetSavedScriptsWithoutContent(string userId, string organizationId);

    Task<Result<ScriptResult>> GetScriptResult(string resultId);

    Task<Result<ScriptResult>> GetScriptResult(string resultId, string orgId);

    Task<List<ScriptSchedule>> GetScriptSchedules(string organizationId);

    Task<List<ScriptSchedule>> GetScriptSchedulesDue();

    List<string> GetServerAdmins();

    Task<SettingsModel> GetSettings();

    Task<Result<SharedFile>> GetSharedFiled(string fileId);

    int GetTotalDevices();

    Task<Result<RemotelyUser>> GetUserById(string userId);

    Task<Result<RemotelyUser>> GetUserByName(
        string userName, 
        Action<IQueryable<RemotelyUser>>? queryBuilder = null);

    Task<Result<RemotelyUserOptions>> GetUserOptions(string userName);

    Task<Result> JoinViaInvitation(string userName, string inviteId);

    void RemoveDevices(string[] deviceIds);

    Task<bool> RemoveUserFromDeviceGroup(string orgId, string groupId, string userId);

    Task<Result> RenameApiToken(string userName, string tokenId, string tokenName);

    Task ResetBranding(string organizationId);

    Task SaveSettings(SettingsModel settings);

    Task SetAllDevicesNotOnline();

    Task SetDisplayName(RemotelyUser user, string displayName);

    Task SetIsDefaultOrganization(string orgId, bool isDefault);

    Task SetIsServerAdmin(string targetUserId, bool isServerAdmin, string callerUserId);

    void SetServerVerificationToken(string deviceId, string verificationToken);

    Task<bool> TempPasswordSignIn(string email, string password);

    Task UpdateBrandingInfo(
        string organizationId,
        string productName,
        byte[] iconBytes);

    Task<Result<Device>> UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId);

    Task UpdateDevice(string deviceId, string? tag, string? alias, string? deviceGroupId, string? notes);

    Task<Result> UpdateOrganizationName(string orgId, string newName);

    Task UpdateTags(string deviceID, string tags);

    Task<Result> UpdateUserOptions(string userName, RemotelyUserOptions options);
    Task<bool> ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP);
}

public class DataService : IDataService
{
    private readonly IAppDbFactory _appDbFactory;
    private readonly IHostEnvironment _hostEnvironment;
    private readonly ILogger<DataService> _logger;
    private readonly SemaphoreSlim _settingsLock = new(1, 1);

    public DataService(
        IHostEnvironment hostEnvironment,
        IAppDbFactory appDbFactory,
        ILogger<DataService> logger)
    {
        _hostEnvironment = hostEnvironment;
        _appDbFactory = appDbFactory;
        _logger = logger;
    }

    public async Task AddAlert(string deviceId, string organizationId, string alertMessage, string? details = null)
    {
        using var dbContext = _appDbFactory.GetContext();

        var users = dbContext.Users
           .Include(x => x.Alerts)
           .Where(x => x.OrganizationID == organizationId);

        if (!string.IsNullOrWhiteSpace(deviceId))
        {
            var filteredUserIDs = FilterUsersByDevicePermissionInternal(
                dbContext,
                users.Select(x => x.Id),
                deviceId);

            users = users.Where(x => filteredUserIDs.Contains(x.Id));
        }

        await users.ForEachAsync(x =>
        {
            var alert = new Alert()
            {
                CreatedOn = DateTimeOffset.Now,
                DeviceID = deviceId,
                Message = alertMessage,
                OrganizationID = organizationId,
                Details = details
            };
            x.Alerts ??= new List<Alert>();
            x.Alerts.Add(alert);
        });

        await dbContext.SaveChangesAsync();
    }

    public async Task<Result<DeviceGroup>> AddDeviceGroup(string orgId, DeviceGroup deviceGroup)
    {
        using var dbContext = _appDbFactory.GetContext();

        var organization = dbContext.Organizations
            .Include(x => x.DeviceGroups)
            .FirstOrDefault(x => x.ID == orgId);

        if (organization is null)
        {
            return Result.Fail<DeviceGroup>("Organization not found.");
        }

        if (dbContext.DeviceGroups.Any(x =>
            x.OrganizationID == orgId &&
            x.Name.ToLower() == deviceGroup.Name.ToLower()))
        {
            return Result.Fail<DeviceGroup>("Device group already exists.");
        }

        dbContext.Attach(deviceGroup);
        deviceGroup.Organization = organization;
        deviceGroup.OrganizationID = orgId;

        organization.DeviceGroups ??= new List<DeviceGroup>();
        organization.DeviceGroups.Add(deviceGroup);
        await dbContext.SaveChangesAsync();
        return Result.Ok(deviceGroup);
    }

    public async Task<Result> AddDeviceToGroup(string deviceId, string groupId)
    {
        using var context = _appDbFactory.GetContext();
        var device = await context.Devices.FirstOrDefaultAsync(x => x.ID == deviceId);

        if (device is null)
        {
            return Result.Fail("Device not found.");
        }

        var group = await context.DeviceGroups.FirstOrDefaultAsync(x => 
            x.OrganizationID == device.OrganizationID &&
            x.ID == groupId);

        if (group is null)
        {
            return Result.Fail("Group not found.");
        }

        group.Devices ??= new();
        group.Devices.Add(device);
        device.DeviceGroup = group;
        device.DeviceGroupID = group.ID;
        await context.SaveChangesAsync();

        return Result.Ok();
    }

    public async Task<Result<InviteLink>> AddInvite(string orgId, InviteViewModel invite)
    {
        using var dbContext = _appDbFactory.GetContext();

        var organization = dbContext.Organizations
            .Include(x => x.InviteLinks)
            .FirstOrDefault(x => x.ID == orgId);

        if (organization is null)
        {
            return Result.Fail<InviteLink>("Organization not found.");
        }

        var inviteLink = new InviteLink()
        {
            InvitedUser = invite.InvitedUser?.ToLower(),
            DateSent = DateTimeOffset.Now,
            IsAdmin = invite.IsAdmin,
            Organization = organization,
            OrganizationID = organization.ID,
        };

        organization.InviteLinks ??= new List<InviteLink>();
        organization.InviteLinks.Add(inviteLink);
        await dbContext.SaveChangesAsync();
        return Result.Ok(inviteLink);
    }

    public async Task<Result<Device>> AddOrUpdateDevice(DeviceClientDto deviceDto)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices.FindAsync(deviceDto.ID);

        if (device is null)
        {
            device = new Device
            {
                OrganizationID = deviceDto.OrganizationID,
                ID = deviceDto.ID,
            };
            await dbContext.Devices.AddAsync(device);
        }

        device.CurrentUser = deviceDto.CurrentUser;
        device.DeviceName = deviceDto.DeviceName;
        device.Drives = deviceDto.Drives;
        device.CpuUtilization = deviceDto.CpuUtilization;
        device.UsedMemory = deviceDto.UsedMemory;
        device.UsedStorage = deviceDto.UsedStorage;
        device.Is64Bit = deviceDto.Is64Bit;
        device.IsOnline = true;
        device.OSArchitecture = deviceDto.OSArchitecture;
        device.OSDescription = deviceDto.OSDescription;
        device.Platform = deviceDto.Platform;
        device.ProcessorCount = deviceDto.ProcessorCount;
        device.PublicIP = deviceDto.PublicIP;
        device.TotalMemory = deviceDto.TotalMemory;
        device.TotalStorage = deviceDto.TotalStorage;
        device.AgentVersion = deviceDto.AgentVersion;
        device.MacAddresses = deviceDto.MacAddresses ?? Array.Empty<string>();
        device.LastOnline = DateTimeOffset.Now;

        if (_hostEnvironment.IsDevelopment() && dbContext.Organizations.Any())
        {
            var org = await dbContext.Organizations.FirstAsync();
            device.Organization = org;
            device.OrganizationID = org.ID;
        }

        if (!await dbContext.Organizations.AnyAsync(x => x.ID == device.OrganizationID))
        {
            _logger.LogInformation(
                "Unable to add device {deviceName} because organization {organizationID}" +
                "does not exist.  Device ID: {ID}.",
                device.DeviceName,
                device.OrganizationID,
                device.ID);

            return Result.Fail<Device>("Organization does not exist.");
        }

        await dbContext.SaveChangesAsync();
        return Result.Ok(device);
    }

    public async Task<Result> AddOrUpdateSavedScript(SavedScript script, string userId)
    {
        using var dbContext = _appDbFactory.GetContext();

        dbContext.SavedScripts.Update(script);

        if (script.Creator is null)
        {
            var user = await dbContext.Users.FindAsync(userId);
            if (user is null)
            {
                return Result.Fail("User not found.");
            }

            script.CreatorId = user.Id;
            script.Creator = user;
            script.OrganizationID = user.OrganizationID;
        }

        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task AddOrUpdateScriptSchedule(ScriptSchedule schedule)
    {
        using var dbContext = _appDbFactory.GetContext();

        var existingSchedule = await dbContext.ScriptSchedules
            .Include(x => x.Creator)
            .Include(x => x.Devices)
            .Include(x => x.DeviceGroups)
            .FirstOrDefaultAsync(x => x.Id == schedule.Id);

        if (existingSchedule is null)
        {
            dbContext.Update(schedule);
        }
        else
        {
            var entry = dbContext.Entry(existingSchedule);
            entry.CurrentValues.SetValues(schedule);

            existingSchedule.Devices.Clear();
            if (schedule.Devices?.Any() == true)
            {
                var deviceIds = schedule.Devices.Select(x => x.ID);
                var newDevices = await dbContext.Devices
                    .Where(x => deviceIds.Contains(x.ID))
                    .ToListAsync();
                existingSchedule.Devices.AddRange(newDevices);
            }

            existingSchedule.DeviceGroups.Clear();
            if (schedule.DeviceGroups?.Any() == true)
            {
                var deviceGroupIds = schedule.DeviceGroups.Select(x => x.ID);
                var newDeviceGroups = await dbContext.DeviceGroups
                    .Where(x => deviceGroupIds.Contains(x.ID))
                    .ToListAsync();
                existingSchedule.DeviceGroups.AddRange(newDeviceGroups);
            }
        }

        await dbContext.SaveChangesAsync();
    }

    public async Task<Result<ScriptResult>> AddScriptResult(ScriptResultDto dto)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = dbContext.Devices.Find(dto.DeviceID);

        if (device is null)
        {
            return Result.Fail<ScriptResult>("Device not found.");
        }

        var scriptResult = new ScriptResult
        {
            DeviceID = dto.DeviceID,
            OrganizationID = device.OrganizationID
        };

        var entry = dbContext.Attach(scriptResult);
        entry.CurrentValues.SetValues(dto);
        entry.State = EntityState.Added;
        await dbContext.ScriptResults.AddAsync(scriptResult);
        await dbContext.SaveChangesAsync();
        return Result.Ok(scriptResult);
    }

    public async Task<Result> AddScriptResultToScriptRun(string scriptResultId, int scriptRunId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var run = await dbContext.ScriptRuns
            .Include(x => x.Results)
            .FirstOrDefaultAsync(x => x.Id == scriptRunId);

        if (run is null)
        {
            return Result.Fail("Run not found.");
        }

        var result = await dbContext.ScriptResults.FindAsync(scriptResultId);

        if (result is null)
        {
            return Result.Fail("Results not found.");
        }

        run.Results ??= new List<ScriptResult>();
        run.Results.Add(result);

        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task AddScriptRun(ScriptRun scriptRun)
    {
        using var dbContext = _appDbFactory.GetContext();

        dbContext.Attach(scriptRun);
        dbContext.ScriptRuns.Add(scriptRun);
        await dbContext.SaveChangesAsync();
    }

    public async Task<string> AddSharedFile(IBrowserFile file, string organizationId, Action<double, string> progressCallback)
    {
        var fileSize = file.Size;
        var fileName = file.Name;

        var fileContents = new byte[fileSize];
        var stream = file.OpenReadStream(AppConstants.MaxUploadFileSize);

        var bytesRead = 0;
        while (bytesRead < fileSize)
        {
            var segmentEnd = Math.Min(50_000, fileSize - bytesRead);
            var read = await stream.ReadAsync(fileContents.AsMemory(bytesRead, (int)segmentEnd));
            if (read == 0)
            {
                break;
            }
            bytesRead += read;
            progressCallback.Invoke((double)bytesRead / fileSize, fileName);
        }

        progressCallback.Invoke(1, fileName);

        return await AddSharedFileImpl(file.Name, fileContents, file.ContentType, organizationId);
    }

    public async Task<string> AddSharedFile(IFormFile file, string organizationId)
    {
        var fileContents = new byte[file.Length];
        using var stream = file.OpenReadStream();
        await stream.ReadAsync(fileContents.AsMemory(0, (int)file.Length));

        return await AddSharedFileImpl(file.Name, fileContents, file.ContentType, organizationId);
    }

    public bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage)
    {
        using var dbContext = _appDbFactory.GetContext();

        resultMessage = string.Empty;

        var deviceGroup = dbContext.DeviceGroups
            .Include(x => x.Users)
            .FirstOrDefault(x =>
                x.ID == groupId &&
                x.OrganizationID == orgId);

        if (deviceGroup == null)
        {
            resultMessage = "Device group not found.";
            return false;
        }

        userName = userName.Trim().ToLower();

        var user = dbContext.Users
            .Include(x => x.DeviceGroups)
            .FirstOrDefault(x =>
                x.UserName!.ToLower() == userName &&
                x.OrganizationID == orgId);

        if (user == null)
        {
            resultMessage = "User not found.";
            return false;
        }

        deviceGroup.Devices ??= new List<Device>();
        user.DeviceGroups ??= new List<DeviceGroup>();

        if (deviceGroup.Users.Any(x => x.Id == user.Id))
        {
            resultMessage = "User already in group.";
            return false;
        }

        deviceGroup.Users.Add(user);
        user.DeviceGroups.Add(deviceGroup);
        dbContext.SaveChanges();
        resultMessage = user.Id;
        return true;
    }

    public async Task ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin)
    {
        using var dbContext = _appDbFactory.GetContext();

        var targetUser = await dbContext.Users.FirstOrDefaultAsync(x =>
                            x.OrganizationID == organizationId &&
                            x.Id == targetUserId);

        if (targetUser != null)
        {
            targetUser.IsAdministrator = isAdmin;
            dbContext.SaveChanges();
        }
    }

    public async Task CleanupOldRecords()
    {
        var settings = await GetSettings();
        using var dbContext = _appDbFactory.GetContext();

        if (settings.DataRetentionInDays < 0)
        {
            return;
        }

        var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(settings.DataRetentionInDays);

        var scriptRuns = await dbContext.ScriptRuns
            .Include(x => x.Results)
            .Include(x => x.Devices)
            .Where(x => x.RunAt < expirationDate)
            .ToArrayAsync();

        foreach (var run in scriptRuns)
        {
            run.Devices?.Clear();
            run.Results?.Clear();
        }

        dbContext.RemoveRange(scriptRuns);

        var commandResults = dbContext.ScriptResults
                                .Where(x => x.TimeStamp < expirationDate);

        dbContext.RemoveRange(commandResults);

        var sharedFiles = dbContext.SharedFiles
                                .Where(x => x.Timestamp < expirationDate);

        dbContext.RemoveRange(sharedFiles);

        await dbContext.SaveChangesAsync();
    }

    public async Task<Result<ApiToken>> CreateApiToken(string userName, string tokenName, string secretHash)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail<ApiToken>("User not found.");
        }
        
        var newToken = new ApiToken()
        {
            Name = tokenName,
            OrganizationID = user.OrganizationID,
            Secret = secretHash
        };
        dbContext.ApiTokens.Add(newToken);
        await dbContext.SaveChangesAsync();
        return Result.Ok(newToken);
    }

    public async Task<Result<Device>> CreateDevice(DeviceSetupOptions options)
    {
        using var dbContext = _appDbFactory.GetContext();

        try
        {
            if (options is null ||
                string.IsNullOrWhiteSpace(options.DeviceID) ||
                string.IsNullOrWhiteSpace(options.OrganizationID) ||
                dbContext.Devices.Any(x => x.ID == options.DeviceID))
            {
                return Result.Fail<Device>("Required parameters are missing or incorrect.");
            }

            var device = new Device()
            {
                ID = options.DeviceID,
                OrganizationID = options.OrganizationID
            };

            if (!string.IsNullOrWhiteSpace(options.DeviceAlias))
            {
                device.Alias = options.DeviceAlias;
            }

            if (!string.IsNullOrWhiteSpace(options.DeviceGroupName))
            {
                var group = dbContext.DeviceGroups.FirstOrDefault(x =>
                    x.Name.ToLower() == options.DeviceGroupName.ToLower() &&
                    x.OrganizationID == device.OrganizationID);
                device.DeviceGroup = group;
            }

            dbContext.Devices.Add(device);

            await dbContext.SaveChangesAsync();

            return Result.Ok(device);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while creating device for organization {id}.", options.OrganizationID);
            return Result.Fail<Device>("An error occurred while creating the device.");
        }
    }

    public async Task<Result> CreateUser(string userEmail, bool isAdmin, string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        try
        {
            var user = new RemotelyUser()
            {
                UserName = userEmail.Trim().ToLower(),
                Email = userEmail.Trim().ToLower(),
                IsAdministrator = isAdmin,
                OrganizationID = organizationId,
                UserOptions = new RemotelyUserOptions(),
                LockoutEnabled = true
            };
            var org = dbContext.Organizations
                .Include(x => x.RemotelyUsers)
                .FirstOrDefault(x => x.ID == organizationId);

            if (org is null)
            {
                return Result.Fail("Organization not found.");
            }

            dbContext.Users.Add(user);
            org.RemotelyUsers.Add(user);
            await dbContext.SaveChangesAsync();
            return Result.Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while creating user for organization {id}.", organizationId);
            return Result.Fail("An error occurred while creating user.");
        }
    }

    public async Task DeleteAlert(Alert alert)
    {
        using var dbContext = _appDbFactory.GetContext();

        dbContext.Alerts.Remove(alert);
        await dbContext.SaveChangesAsync();
    }

    public async Task DeleteAllAlerts(string orgId, string? userName = null)
    {
        using var dbContext = _appDbFactory.GetContext();

        var alerts = dbContext.Alerts.Where(x => x.OrganizationID == orgId);

        if (!string.IsNullOrWhiteSpace(userName))
        {
            var userResult = await GetUserByName(userName);

            if (userResult.IsSuccess)
            {
                var userId = userResult.Value.Id;
                alerts = alerts.Where(x => x.UserID == userId);
            }
        }

        dbContext.Alerts.RemoveRange(alerts);
        await dbContext.SaveChangesAsync();
    }

    public async Task<Result> DeleteApiToken(string userName, string tokenId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail("User not found.");
        }

        var token = dbContext.ApiTokens.FirstOrDefault(x =>
            x.OrganizationID == user.OrganizationID &&
            x.ID == tokenId);

        if (token is null)
        {
            return Result.Fail("Token not found.");
        }

        dbContext.ApiTokens.Remove(token);
        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task<Result> DeleteDeviceGroup(string orgId, string deviceGroupID)
    {
        using var dbContext = _appDbFactory.GetContext();

        var deviceGroup = dbContext.DeviceGroups
            .Include(x => x.Devices)
            .Include(x => x.Users)
            .ThenInclude(x => x.DeviceGroups)
            .FirstOrDefault(x =>
                x.ID == deviceGroupID &&
                x.OrganizationID == orgId);

        if (deviceGroup is null)
        {
            return Result.Fail("Device group not found.");
        }

        deviceGroup.Devices.ForEach(x =>
        {
            x.DeviceGroup = null;
            x.DeviceGroupID = null;
        });

        deviceGroup.Users.ForEach(x =>
        {
            x.DeviceGroups.Remove(deviceGroup);
        });

        deviceGroup.Devices.Clear();
        deviceGroup.Users.Clear();

        dbContext.DeviceGroups.Remove(deviceGroup);

        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task<Result> DeleteInvite(string orgId, string inviteID)
    {
        using var dbContext = _appDbFactory.GetContext();

        var invite = dbContext.InviteLinks.FirstOrDefault(x =>
            x.OrganizationID == orgId &&
            x.ID == inviteID);

        if (invite is null)
        {
            return Result.Fail("Invite not found.");
        }

        var user = dbContext.Users.FirstOrDefault(x => x.UserName == invite.InvitedUser);

        if (user is null)
        {
            return Result.Fail("User not found.");
        }

        if (string.IsNullOrWhiteSpace(user.PasswordHash))
        {
            dbContext.Remove(user);
        }

        dbContext.Remove(invite);
        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task DeleteSavedScript(Guid scriptId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var schedules = await dbContext.ScriptSchedules
            .Where(x => x.SavedScriptId == scriptId)
            .ToListAsync();

        if (schedules.Count > 0)
        {
            dbContext.ScriptSchedules.RemoveRange(schedules);
        }

        var script = await dbContext.SavedScripts
            .Include(x => x.ScriptResults)
            .Include(x => x.ScriptRuns)
            .FirstOrDefaultAsync(x => x.Id == scriptId);

        if (script is not null)
        {
            dbContext.SavedScripts.Remove(script);
        }

        await dbContext.SaveChangesAsync();
    }

    public async Task DeleteScriptSchedule(int scriptScheduleId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var schedule = await dbContext.ScriptSchedules
            .Include(x => x.ScriptRuns)
            .ThenInclude(x => x.Results)
            .Include(x => x.Devices)
            .Include(x => x.DeviceGroups)
            .FirstOrDefaultAsync(x => x.Id == scriptScheduleId);

        if (schedule is not null)
        {
            dbContext.ScriptSchedules.Remove(schedule);
            await dbContext.SaveChangesAsync();
        }
    }

    public async Task<Result> DeleteUser(string orgId, string targetUserId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var org = dbContext
            .Organizations
            .Include(x => x.RemotelyUsers)
            .FirstOrDefault(x => x.ID == orgId);

        if (org is null)
        {
            return Result.Fail("Organization not found.");
        }

        // All the joins are necessary for client-side cascade delete.
        // This method will be called rarely, so I'm not concerned
        // about the performance.
        var target = dbContext.Users
            .Include(x => x.DeviceGroups)
            .ThenInclude(x => x.Devices)
            .Include(x => x.Organization)
            .Include(x => x.Alerts)
            .Include(x => x.SavedScripts)
            .ThenInclude(x => x.ScriptRuns)
            .Include(x => x.SavedScripts)
            .ThenInclude(x => x.ScriptResults)
            .Include(x => x.ScriptSchedules)
            .ThenInclude(x => x.ScriptRuns)
            .ThenInclude(x => x.Results)
            .FirstOrDefault(x =>
                x.Id == targetUserId &&
                x.OrganizationID == orgId);

        if (target is null)
        {
            return Result.Fail("User not found.");
        }

        dbContext.Users.Remove(target);

        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public void DeviceDisconnected(string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = dbContext.Devices.Find(deviceId);
        if (device != null)
        {
            device.LastOnline = DateTimeOffset.Now;
            device.IsOnline = false;
            dbContext.SaveChanges();
        }
    }

    public bool DoesUserExist(string userName)
    {
        using var dbContext = _appDbFactory.GetContext();

        if (string.IsNullOrWhiteSpace(userName))
        {
            return false;
        }

        return dbContext.Users
            .Where(x => x.UserName != null)
            .Any(x => x.UserName!.Trim().ToLower() == userName.Trim().ToLower());
    }

    public bool DoesUserHaveAccessToDevice(string deviceId, RemotelyUser remotelyUser)
    {
        if (remotelyUser is null)
        {
            return false;
        }

        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices
            .Include(x => x.DeviceGroup)
            .ThenInclude(x => x!.Users)
            .Any(device => 
                device.OrganizationID == remotelyUser.OrganizationID &&
                device.ID == deviceId &&
                (
                    remotelyUser.IsAdministrator ||
                    device.DeviceGroup!.Users.Any(user => user.Id == remotelyUser.Id
                )));
    }

    public bool DoesUserHaveAccessToDevice(string deviceId, string remotelyUserId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var remotelyUser = dbContext.Users.Find(remotelyUserId);

        if (remotelyUser is null)
        {
            return false;
        }

        return DoesUserHaveAccessToDevice(deviceId, remotelyUser);
    }

    public string[] FilterDeviceIdsByUserPermission(string[] deviceIds, RemotelyUser remotelyUser)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices
            .Include(x => x.DeviceGroup)
            .ThenInclude(x => x!.Users)
            .Where(device =>
                device.OrganizationID == remotelyUser.OrganizationID &&
                deviceIds.Contains(device.ID) &&
                (
                    remotelyUser.IsAdministrator ||
                    device.DeviceGroup!.Users.Any(user => user.Id == remotelyUser.Id
                )))
            .Select(x => x.ID)
            .ToArray();
    }

    public string[] FilterUsersByDevicePermission(IEnumerable<string> userIds, string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return FilterUsersByDevicePermissionInternal(dbContext, userIds, deviceId);
    }

    public async Task<Result<Alert>> GetAlert(string alertId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var alert = await dbContext.Alerts
            .AsNoTracking()
            .Include(x => x.Device)
            .Include(x => x.User)
            .FirstOrDefaultAsync(x => x.ID == alertId);

        if (alert is null)
        {
            return Result.Fail<Alert>("Alert not found.");
        }

        return Result.Ok(alert);
    }

    public Alert[] GetAlerts(string userId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Alerts
            .AsNoTracking()
            .Include(x => x.Device)
            .Include(x => x.User)
            .Where(x => x.UserID == userId)
            .OrderByDescending(x => x.CreatedOn)
            .ToArray();
    }

    public ApiToken[] GetAllApiTokens(string userId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = dbContext.Users.FirstOrDefault(x => x.Id == userId);

        if (user is null)
        {
            return Array.Empty<ApiToken>();
        }

        return dbContext.ApiTokens
            .AsNoTracking()
            .Where(x => x.OrganizationID == user.OrganizationID)
            .OrderByDescending(x => x.LastUsed)
            .ToArray();
    }

    public ScriptResult[] GetAllCommandResults(string orgId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.ScriptResults
            .AsNoTracking()
            .Where(x => x.OrganizationID == orgId)
            .OrderByDescending(x => x.TimeStamp)
            .ToArray();
    }

    public ScriptResult[] GetAllCommandResultsForUser(string orgId, string userName, string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.ScriptResults
            .AsNoTracking()
            .Where(x => x.OrganizationID == orgId &&
                x.SenderUserName == userName &&
                x.DeviceID == deviceId)
            .OrderByDescending(x => x.TimeStamp)
            .ToArray();
    }

    public Device[] GetAllDevices(string orgId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices
            .AsNoTracking()
            .Where(x => x.OrganizationID == orgId)
            .ToArray();
    }

    public InviteLink[] GetAllInviteLinks(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.InviteLinks
            .AsNoTracking()
            .Where(x => x.OrganizationID == organizationId)
            .ToArray();
    }

    public ScriptResult[] GetAllScriptResults(string orgId, string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.ScriptResults
            .AsNoTracking()
            .Where(x => x.OrganizationID == orgId && x.DeviceID == deviceId)
            .OrderByDescending(x => x.TimeStamp)
            .ToArray();
    }

    public ScriptResult[] GetAllScriptResultsForUser(string orgId, string userName)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.ScriptResults
            .AsNoTracking()
            .Where(x => x.OrganizationID == orgId && x.SenderUserName == userName)
            .OrderByDescending(x => x.TimeStamp)
            .ToArray();
    }

    public RemotelyUser[] GetAllUsersForServer()
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Users
            .AsNoTracking()
            .ToArray();
    }

    public async Task<RemotelyUser[]> GetAllUsersInOrganization(string orgId)
    {
        if (string.IsNullOrWhiteSpace(orgId))
        {
            return Array.Empty<RemotelyUser>();
        }

        using var dbContext = _appDbFactory.GetContext();

        var organization = await dbContext.Organizations
            .AsNoTracking()
            .Include(x => x.RemotelyUsers)
            .FirstOrDefaultAsync(x => x.ID == orgId);

        if (organization is null)
        {
            return Array.Empty<RemotelyUser>();
        }

        return organization.RemotelyUsers.ToArray();
    }

    public async Task<Result<ApiToken>> GetApiKey(string keyId)
    {
        if (string.IsNullOrWhiteSpace(keyId))
        {
            return Result.Fail<ApiToken>("Key ID cannot be empty.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var token = await dbContext.ApiTokens
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.ID == keyId);

        if (token is null)
        {
            return Result.Fail<ApiToken>("API key not found.");
        }

        return Result.Ok(token);
    }

    public async Task<Result<BrandingInfo>> GetBrandingInfo(string organizationId)
    {
        if (string.IsNullOrWhiteSpace(organizationId))
        {
            return Result.Fail<BrandingInfo>("Organization ID cannot be empty.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var organization = await dbContext.Organizations
          .AsNoTracking()
          .Include(x => x.BrandingInfo)
          .FirstOrDefaultAsync(x => x.ID == organizationId);

        if (organization is null)
        {
            return Result.Fail<BrandingInfo>("Organization not found.");
        }

        if (organization.BrandingInfo is null)
        {
            var brandingInfo = new BrandingInfo()
            {
                OrganizationId = organizationId
            };

            dbContext.BrandingInfos.Add(brandingInfo);
            organization.BrandingInfo = brandingInfo;

            await dbContext.SaveChangesAsync();
        }
        return Result.Ok(organization.BrandingInfo);
    }

    public async Task<Result<Organization>> GetDefaultOrganization()
    {
        using var dbContext = _appDbFactory.GetContext();

        var org = await dbContext.Organizations
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.IsDefaultOrganization);
        
        if (org is null)
        {
            return Result.Fail<Organization>("Organization not found.");
        }

        return Result.Ok(org);
    }

    public async Task<Result<Device>> GetDevice(string orgId, string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices
            .AsNoTracking()
            .FirstOrDefaultAsync(x =>
                x.OrganizationID == orgId &&
                x.ID == deviceId);

        if (device is null)
        {
            return Result.Fail<Device>("Device not found.");
        }
        return Result.Ok(device);
    }

    public async Task<Result<Device>> GetDevice(
        string deviceId,
        Action<IQueryable<Device>>? queryBuilder = null)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices
            .AsNoTracking()
            .Apply(queryBuilder)
            .FirstOrDefaultAsync(x => x.ID == deviceId);

        if (device is null)
        {
            return Result.Fail<Device>("Device not found.");
        }
        return Result.Ok(device);
    }

    public int GetDeviceCount()
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices.Count();
    }

    public int GetDeviceCount(RemotelyUser user)
    {
        using var dbContext = _appDbFactory.GetContext();

        if (user.IsAdministrator)
        {
            return GetDeviceCount();
        }

        return dbContext.Users
            .AsNoTracking()
            .Include(x => x.DeviceGroups)
            .ThenInclude(x => x.Devices)
            .Where(x => x.Id == user.Id)
            .SelectMany(x => x.DeviceGroups)
            .SelectMany(x => x.Devices)
            .Count();
    }

    public async Task<Result<DeviceGroup>> GetDeviceGroup(
        string deviceGroupId,
        bool includeDevices = false,
        bool includeUsers = false)
    {
        using var dbContext = _appDbFactory.GetContext();

        var query = dbContext.DeviceGroups
            .AsNoTracking()
            .AsQueryable();

        if (includeDevices)
        {
            query = query.Include(x => x.Devices);
        }
        if (includeUsers)
        {
            query = query.Include(x => x.Users);
        }

        var group = await query.FirstOrDefaultAsync(x => x.ID == deviceGroupId);

        if (group is null)
        {
            return Result.Fail<DeviceGroup>("Device group not found.");
        }
        return Result.Ok(group);
    }

    public DeviceGroup[] GetDeviceGroups(string username)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = dbContext.Users
            .AsNoTracking()
            .FirstOrDefault(x => x.UserName == username);

        if (user is null)
        {
            return Array.Empty<DeviceGroup>();
        }
        var userId = user.Id;

        var groupIds = dbContext.DeviceGroups
            .AsNoTracking()
            .Include(x => x.Users)
            .ThenInclude(x => x.DeviceGroups)
            .Where(x =>
                x.OrganizationID == user.OrganizationID &&
                (
                    user.IsAdministrator ||
                    x.Users.Any(x => x.Id == userId)
                )
            )
            .Select(x => x.ID)
            .ToHashSet();

        if (groupIds.Any())
        {
            return dbContext.DeviceGroups
                .AsNoTracking()
                .Where(x => groupIds.Contains(x.ID))
                .OrderBy(x => x.Name)
                .ToArray();
        }

        return Array.Empty<DeviceGroup>();
    }

    public DeviceGroup[] GetDeviceGroupsForOrganization(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.DeviceGroups
            .AsNoTracking()
            .Include(x => x.Users)
            .Where(x => x.OrganizationID == organizationId)
            .OrderBy(x => x.Name)
            .ToArray();
    }

    public List<Device> GetDevices(IEnumerable<string> deviceIds)
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices
            .AsNoTracking()
            .Where(x => deviceIds.Contains(x.ID))
            .ToList();
    }

    public Device[] GetDevicesForUser(string userName)
    {
        using var dbContext = _appDbFactory.GetContext();

        if (string.IsNullOrWhiteSpace(userName))
        {
            return Array.Empty<Device>();
        }

        var user = dbContext.Users
            .AsNoTracking()
            .FirstOrDefault(x => x.UserName == userName);

        if (user is null)
        {
            return Array.Empty<Device>();
        }

        if (user.IsAdministrator)
        {
            return dbContext.Devices
                .AsNoTracking()
                .Where(x => x.OrganizationID == user.OrganizationID)
                .ToArray();
        }

        return dbContext.Users
            .AsNoTracking()
            .Include(x => x.DeviceGroups)
            .ThenInclude(x => x.Devices)
            .Where(x => x.UserName == userName)
            .SelectMany(x => x.DeviceGroups)
            .SelectMany(x => x.Devices)
            .ToArray();
    }

    public async Task<Result<Organization>> GetOrganizationById(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var org = await dbContext.Organizations.FindAsync(organizationId);

        if (org is null)
        {
            return Result.Fail<Organization>("Organization not found.");
        }
        return Result.Ok(org);
    }


    public async Task<Result<Organization>> GetOrganizationByUserName(string userName)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            return Result.Fail<Organization>("User name is required.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users
            .AsNoTracking()
            .Include(x => x.Organization)
            .FirstOrDefaultAsync(x => x.UserName!.ToLower() == userName.ToLower());

        if (user?.Organization is null)
        {
            return Result.Fail<Organization>("User not found.");
        }

        return Result.Ok(user.Organization);
    }

    public int GetOrganizationCount()
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Organizations.Count();
    }

    public async Task<int> GetOrganizationCountAsync()
    {
        using var dbContext = _appDbFactory.GetContext();

        return await dbContext.Organizations.CountAsync();
    }

    public async Task<Result<string>> GetOrganizationNameById(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var org = await dbContext.Organizations
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.ID == organizationId);

        if (org is null)
        {
            return Result.Fail<string>("Organization not found.");
        }

        return Result.Ok(org.OrganizationName);
    }

    public async Task<Result<string>> GetOrganizationNameByUserName(string userName)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            return Result.Fail<string>("Username cannot be empty.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users
            .AsNoTracking()
            .Include(x => x.Organization)
            .FirstOrDefaultAsync(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail<string>("User not found.");
        }

        var orgName = $"{user.Organization?.OrganizationName}";
        return Result.Ok(orgName);
    }

    public async Task<IEnumerable<ScriptRun>> GetPendingScriptRuns(string deviceId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices
            .AsNoTracking()
            .Include(x => x.ScriptRuns)
            .ThenInclude(x => x.Results)
            .Include(x => x.ScriptResults)
            .FirstOrDefaultAsync(x => x.ID == deviceId);

        if (device is null)
        {
            return Enumerable.Empty<ScriptRun>();
        }

        device.ScriptResults ??= new();
        var scriptResultsLookup = device.ScriptResults
            .Select(x => x.ScriptRunId)
            .Distinct()
            .ToHashSet();

        return device.ScriptRuns
            .OrderByDescending(x => x.RunAt)
            .DistinctBy(x => x.SavedScriptId)
            .Where(x => !scriptResultsLookup.Contains(x.Id))
            .ToArray();
    }

    public async Task<List<SavedScript>> GetQuickScripts(string userId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return await dbContext.SavedScripts
            .Where(x => x.CreatorId == userId && x.IsQuickScript)
            .ToListAsync();
    }

    public async Task<Result<SavedScript>> GetSavedScript(string userId, Guid scriptId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var script = await dbContext.SavedScripts
            .AsNoTracking()
            .Include(x => x.Creator)
            .FirstOrDefaultAsync(x =>
                x.Id == scriptId &&
                (x.IsPublic || x.CreatorId == userId));

        if (script is null)
        {
            return Result.Fail<SavedScript>("Script not found.");
        }
        return Result.Ok(script);
    }

    public async Task<Result<SavedScript>> GetSavedScript(Guid scriptId)
    {
        using var dbContext = _appDbFactory.GetContext();
        var script = await dbContext.SavedScripts
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == scriptId);

        if (script is null)
        {
            return Result.Fail<SavedScript>("Script not found.");
        }
        return Result.Ok(script);
    }

    public async Task<List<SavedScript>> GetSavedScriptsWithoutContent(string userId, string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        return await dbContext.SavedScripts
            .AsNoTracking()
            .Include(x => x.Creator)
            .Where(x => 
                x.Creator!.OrganizationID == organizationId &&
                (x.IsPublic || x.CreatorId == userId))
            .Select(x => new SavedScript()
            {
                Creator = x.Creator,
                CreatorId = x.CreatorId,
                FolderPath = x.FolderPath,
                Id = x.Id,
                IsPublic = x.IsPublic,
                IsQuickScript = x.IsQuickScript,
                Name = x.Name,
                OrganizationID = x.OrganizationID
            })
            .ToListAsync();
    }

    public async Task<Result<ScriptResult>> GetScriptResult(string resultId, string orgId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var result = await dbContext.ScriptResults
            .AsNoTracking()
            .FirstOrDefaultAsync(x =>
                x.OrganizationID == orgId &&
                x.ID == resultId);

        if (result is null)
        {
            return Result.Fail<ScriptResult>("Script result not found.");
        }
        return Result.Ok(result);
    }

    public async Task<Result<ScriptResult>> GetScriptResult(string resultId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var result = await dbContext.ScriptResults.FindAsync(resultId);

        if (result is null)
        {
            return Result.Fail<ScriptResult>("Script result not found.");
        }
        return Result.Ok(result);
    }

    public async Task<List<ScriptSchedule>> GetScriptSchedules(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();
        return await dbContext.ScriptSchedules
            .AsNoTracking()
            .Include(x => x.Creator)
            .Include(x => x.Devices)
            .Include(x => x.DeviceGroups)
            .Where(x => x.OrganizationID == organizationId)
            .ToListAsync();
    }

    public async Task<List<ScriptSchedule>> GetScriptSchedulesDue()
    {
        using var dbContext = _appDbFactory.GetContext();

        var now = Time.Now;

        return await dbContext.ScriptSchedules
            .AsNoTracking()
            .Include(x => x.Devices)
            .Include(x => x.DeviceGroups)
            .ThenInclude(x => x.Devices)
            .Where(x => x.NextRun < now)
            .ToListAsync();
    }

    public List<string> GetServerAdmins()
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Users
            .AsNoTracking()
            .Where(x => x.IsServerAdmin && x.UserName != null)
            .Select(x => x.UserName!)
            .ToList();
    }

    public async Task<SettingsModel> GetSettings()
    {
        await _settingsLock.WaitAsync();
        try
        {
            using var dbContext = _appDbFactory.GetContext();
            return await dbContext.GetAppSettings();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while getting settings from database.");
            return new();
        }
        finally
        {
            _settingsLock.Release();
        }
    }

    public async Task<Result<SharedFile>> GetSharedFiled(string fileId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var file = await dbContext.SharedFiles.FindAsync(fileId);

        if (file is null)
        {
            return Result.Fail<SharedFile>("File not found.");
        }
        return Result.Ok(file);
    }

    public int GetTotalDevices()
    {
        using var dbContext = _appDbFactory.GetContext();

        return dbContext.Devices.Count();
    }

    public async Task<Result<RemotelyUser>> GetUserById(string userId)
    {
        if (string.IsNullOrWhiteSpace(userId))
        {
            return Result.Fail<RemotelyUser>("User ID cannot be empty.");
        }
        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == userId);

        if (user is null)
        {
            return Result.Fail<RemotelyUser>("User not found.");
        }
        return Result.Ok(user);
    }

    public async Task<Result<RemotelyUser>> GetUserByName(
        string userName,
        Action<IQueryable<RemotelyUser>>? queryBuilder = null)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            return Result.Fail<RemotelyUser>("Username cannot be empty.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users
            .AsNoTracking()
            .Apply(queryBuilder)
            .FirstOrDefaultAsync(x =>
                x.UserName!.ToLower().Trim() == userName.ToLower().Trim());

        if (user is null)
        {
            return Result.Fail<RemotelyUser>("User not found.");
        }
        return Result.Ok(user);
    }

    public async Task<Result<RemotelyUserOptions>> GetUserOptions(string userName)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail<RemotelyUserOptions>("User not found.");
        }
        return Result.Ok(user.UserOptions ?? new());
    }

    public async Task<Result> JoinViaInvitation(string userName, string inviteId)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            return Result.Fail("Username cannot be empty.");
        }
        if (string.IsNullOrWhiteSpace(inviteId))
        {
            return Result.Fail("Invite ID cannot be empty.");
        }

        using var dbContext = _appDbFactory.GetContext();

        var invite = await dbContext.InviteLinks
            .FirstOrDefaultAsync(x =>
                x.InvitedUser!.ToLower() == userName.ToLower() &&
                x.ID == inviteId);

        if (invite is null)
        {
            return Result.Fail("Invite not found.");
        }

        var user = await dbContext.Users
            .FirstOrDefaultAsync(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail("User not found.");
        }

        var organization = await dbContext.Organizations
            .Include(x => x.RemotelyUsers)
            .FirstOrDefaultAsync(x => x.ID == invite.OrganizationID);

        if (organization is null)
        {
            return Result.Fail("Organization not found.");
        }

        user.Organization = organization;
        user.OrganizationID = organization.ID;
        user.IsAdministrator = invite.IsAdmin;
        organization.RemotelyUsers.Add(user);

        await dbContext.SaveChangesAsync();

        dbContext.InviteLinks.Remove(invite);
        dbContext.SaveChanges();
        return Result.Ok();
    }

    public void RemoveDevices(string[] deviceIDs)
    {
        using var dbContext = _appDbFactory.GetContext();

        var devices = dbContext.Devices
            .Include(x => x.ScriptResults)
            .Include(x => x.ScriptRuns)
            .Include(x => x.ScriptSchedules)
            .Include(x => x.DeviceGroup)
            .Include(x => x.Alerts)
            .Where(x => deviceIDs.Contains(x.ID));

        dbContext.Devices.RemoveRange(devices);
        dbContext.SaveChanges();
    }

    public async Task<bool> RemoveUserFromDeviceGroup(string orgID, string groupID, string userID)
    {
        using var dbContext = _appDbFactory.GetContext();

        var deviceGroup = await dbContext.DeviceGroups
            .Include(x => x.Users)
            .ThenInclude(x => x.DeviceGroups)
            .FirstOrDefaultAsync(x =>
                x.ID == groupID &&
                x.OrganizationID == orgID);

        if (deviceGroup?.Users?.Any(x => x.Id == userID) != true)
        {
            return false;
        }

        var user = deviceGroup.Users.FirstOrDefault(x => x.Id == userID);

        if (user is null)
        {
            return false;
        }

        user.DeviceGroups.Remove(deviceGroup);
        deviceGroup.Users.Remove(user);

        await dbContext.SaveChangesAsync();
        return true;
    }

    public async Task<Result> RenameApiToken(string userName, string tokenId, string tokenName)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName);
        if (user is null)
        {
            return Result.Fail("User not found.");
        }

        var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x =>
            x.OrganizationID == user.OrganizationID &&
            x.ID == tokenId);

        if (token is null)
        {
            return Result.Fail("API token not found.");
        }

        token.Name = tokenName;
        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task ResetBranding(string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var organization = await dbContext.Organizations
           .Include(x => x.BrandingInfo)
           .FirstOrDefaultAsync(x => x.ID == organizationId);

        if (organization?.BrandingInfo is null)
        {
            return;
        }

        var entry = dbContext.Entry(organization.BrandingInfo);
        entry.CurrentValues.SetValues(BrandingInfo.Default);
        
        await dbContext.SaveChangesAsync();
    }

    public async Task SaveSettings(SettingsModel settings)
    {
        await _settingsLock.WaitAsync();
        try
        {
            using var dbContext = _appDbFactory.GetContext();
            var record = await dbContext.KeyValueRecords.FindAsync(SettingsModel.DbKey);
            if (record is null)
            {
                record = new()
                {
                    Key = SettingsModel.DbKey,
                };
                await dbContext.KeyValueRecords.AddAsync(record);
                await dbContext.SaveChangesAsync();
            }
            record.Value = JsonSerializer.Serialize(settings);
            await dbContext.SaveChangesAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error while saving settings to database.");
        }
        finally
        {
            _settingsLock.Release();
        }
    }

    public async Task SetAllDevicesNotOnline()
    {
        using var dbContext = _appDbFactory.GetContext();

        await dbContext.Devices.ForEachAsync(x =>
        {
            x.IsOnline = false;
        });
        await dbContext.SaveChangesAsync();
    }

    public async Task SetDisplayName(RemotelyUser user, string displayName)
    {
        using var dbContext = _appDbFactory.GetContext();

        dbContext.Attach(user);
        user.UserOptions ??= new();
        user.UserOptions.DisplayName = displayName;
        await dbContext.SaveChangesAsync();
    }

    public async Task SetIsDefaultOrganization(string orgID, bool isDefault)
    {
        using var dbContext = _appDbFactory.GetContext();

        var organization = await dbContext.Organizations.FindAsync(orgID);
        if (organization is null)
        {
            return;
        }

        if (isDefault)
        {
            await dbContext.Organizations.ForEachAsync(x => x.IsDefaultOrganization = false);
        }

        organization.IsDefaultOrganization = isDefault;
        await dbContext.SaveChangesAsync();
    }

    public async Task SetIsServerAdmin(string targetUserId, bool isServerAdmin, string callerUserId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var caller = await dbContext.Users.FindAsync(callerUserId);
        if (caller?.IsServerAdmin != true)
        {
            return;
        }

        var targetUser = await dbContext.Users.FindAsync(targetUserId);

        if (targetUser is null)
        {
            return;
        }

        if (caller.Id == targetUser.Id)
        {
            // A server admin can't change themselves.
            return;
        }

        targetUser.IsServerAdmin = isServerAdmin;
        await dbContext.SaveChangesAsync();
    }

    public void SetServerVerificationToken(string deviceID, string verificationToken)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = dbContext.Devices.Find(deviceID);
        if (device != null)
        {
            device.ServerVerificationToken = verificationToken;
            dbContext.SaveChanges();
        }
    }

    public async Task<bool> TempPasswordSignIn(string email, string password)
    {
        if (string.IsNullOrWhiteSpace(password))
        {
            return false;
        }

        var userResult = await GetUserByName(email);

        if (!userResult.IsSuccess)
        {
            return false;
        }

        var user = userResult.Value;

        using var dbContext = _appDbFactory.GetContext();

        if (user.TempPassword != password)
        {
            return false;
        }

        user.TempPassword = string.Empty;
        await dbContext.SaveChangesAsync();
        return true;
    }

    public async Task UpdateBrandingInfo(
        string organizationId,
        string productName,
        byte[] iconBytes)
    {
        using var dbContext = _appDbFactory.GetContext();

        var organization = await dbContext.Organizations
            .Include(x => x.BrandingInfo)
            .FirstOrDefaultAsync(x => x.ID == organizationId);

        if (organization is null)
        {
            return;
        }

        organization.BrandingInfo ??= new BrandingInfo();

        organization.BrandingInfo.Product = productName;

        if (iconBytes?.Any() == true)
        {
            organization.BrandingInfo.Icon = iconBytes;
        }

        await dbContext.SaveChangesAsync();
    }

    public async Task UpdateDevice(string deviceId, string? tag, string? alias, string? deviceGroupId, string? notes)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices
            .Include(x => x.DeviceGroup)
            .FirstOrDefaultAsync(x => x.ID == deviceId);

        if (device is null)
        {
            return;
        }

        if (string.IsNullOrWhiteSpace(deviceGroupId))
        {
            device.DeviceGroup?.Devices?.RemoveAll(x => x.ID == deviceId);
            device.DeviceGroup = null;
            device.DeviceGroupID = null;
        }
        else
        {
            device.DeviceGroupID = deviceGroupId;
        }

        device.Tags = tag;
        device.Alias = alias;
        device.Notes = notes;
        await dbContext.SaveChangesAsync();
    }

    public async Task<Result<Device>> UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices.FindAsync(deviceOptions.DeviceID);
        if (device == null || device.OrganizationID != organizationId)
        {
            return Result.Fail<Device>("Device not found.");
        }

        var group = await dbContext.DeviceGroups.FirstOrDefaultAsync(x =>
          x.Name.ToLower() == $"{deviceOptions.DeviceGroupName}".ToLower() &&
          x.OrganizationID == device.OrganizationID);
        device.DeviceGroup = group;

        device.Alias = deviceOptions.DeviceAlias;
        await dbContext.SaveChangesAsync();
        return Result.Ok(device);
    }

    public async Task<Result> UpdateOrganizationName(string orgId, string newName)
    {
        using var dbContext = _appDbFactory.GetContext();

        var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.ID == orgId);

        if (org is null)
        {
            return Result.Fail("Organization not found.");
        }
        
        org.OrganizationName = newName;
        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task UpdateTags(string deviceID, string tags)
    {
        using var dbContext = _appDbFactory.GetContext();

        var device = await dbContext.Devices.FindAsync(deviceID);
        if (device == null)
        {
            return;
        }

        device.Tags = tags;
        await dbContext.SaveChangesAsync();
    }

    public async Task<Result> UpdateUserOptions(string userName, RemotelyUserOptions options)
    {
        using var dbContext = _appDbFactory.GetContext();

        var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName);

        if (user is null)
        {
            return Result.Fail("User not found.");
        }
        user.UserOptions = options;
        await dbContext.SaveChangesAsync();
        return Result.Ok();
    }

    public async Task<bool> ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP)
    {
        using var dbContext = _appDbFactory.GetContext();

        var hasher = new PasswordHasher<string>();
        var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x => x.ID == keyId);

        var isValid = 
            !string.IsNullOrWhiteSpace(token?.Secret) &&
            hasher.VerifyHashedPassword(string.Empty, token.Secret, apiSecret) == PasswordVerificationResult.Success;

        if (token is not null)
        {
            token.LastUsed = DateTimeOffset.Now;
            await dbContext.SaveChangesAsync();
        }

        _logger.LogInformation(
            "API token used.  Token: {keyId}.  Path: {requestPath}.  Validated: {isValid}.  Remote IP: {remoteIP}", 
            keyId,
            requestPath,
            isValid,
            remoteIP);

        return isValid;
    }

    private async Task<string> AddSharedFileImpl(
        string fileName,
        byte[] fileContents,
        string contentType,
        string organizationId)
    {
        var settings = await GetSettings();
        using var dbContext = _appDbFactory.GetContext();

        var expirationDate = DateTimeOffset.Now.AddDays(-settings.DataRetentionInDays);
        var expiredFiles = dbContext.SharedFiles.Where(x => x.Timestamp < expirationDate);
        dbContext.RemoveRange(expiredFiles);

        var sharedFile = new SharedFile()
        {
            FileContents = fileContents,
            FileName = fileName,
            ContentType = contentType,
            OrganizationID = organizationId
        };

        dbContext.SharedFiles.Add(sharedFile);

        await dbContext.SaveChangesAsync();
        return sharedFile.ID;
    }

    private string[] FilterUsersByDevicePermissionInternal(AppDb dbContext, IEnumerable<string> userIDs, string deviceID)
    {
        var device = dbContext.Devices
             .Include(x => x.DeviceGroup)
             .ThenInclude(x => x!.Users)
             .FirstOrDefault(x => x.ID == deviceID);

        if (device is null)
        {
            return Array.Empty<string>();
        }

        var orgUsers = dbContext.Users
            .Where(user =>
                user.OrganizationID == device.OrganizationID &&
                userIDs.Contains(user.Id));

        if (string.IsNullOrWhiteSpace(device.DeviceGroupID))
        {
            return orgUsers
                .Select(x => x.Id)
                .ToArray();
        }

        var allowedUsers = device?.DeviceGroup?.Users?.Select(x => x.Id) ?? Array.Empty<string>();

        return orgUsers
            .Where(user =>
                user.IsAdministrator ||
                allowedUsers.Contains(user.Id)
            )
            .Select(x => x.Id)
            .ToArray();
    }
}