using System.Collections.ObjectModel; using System.IO; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using Microsoft.Win32; using SharepointToolbox.Core.Models; using SharepointToolbox.Services; using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory; namespace SharepointToolbox.ViewModels; public partial class ProfileManagementViewModel : ObservableObject { private readonly ProfileService _profileService; private readonly IBrandingService _brandingService; private readonly AppGraphClientFactory _graphClientFactory; private readonly ILogger _logger; [ObservableProperty] private TenantProfile? _selectedProfile; [ObservableProperty] private string _newName = string.Empty; [ObservableProperty] private string _newTenantUrl = string.Empty; [ObservableProperty] private string _newClientId = string.Empty; [ObservableProperty] private string _validationMessage = string.Empty; private string? _clientLogoPreview; public string? ClientLogoPreview { get => _clientLogoPreview; private set { _clientLogoPreview = value; OnPropertyChanged(); } } public ObservableCollection Profiles { get; } = new(); public IAsyncRelayCommand AddCommand { get; } public IAsyncRelayCommand RenameCommand { get; } public IAsyncRelayCommand DeleteCommand { get; } public IAsyncRelayCommand BrowseClientLogoCommand { get; } public IAsyncRelayCommand ClearClientLogoCommand { get; } public IAsyncRelayCommand AutoPullClientLogoCommand { get; } public ProfileManagementViewModel( ProfileService profileService, IBrandingService brandingService, AppGraphClientFactory graphClientFactory, ILogger logger) { _profileService = profileService; _brandingService = brandingService; _graphClientFactory = graphClientFactory; _logger = logger; AddCommand = new AsyncRelayCommand(AddAsync, CanAdd); RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedProfile != null && !string.IsNullOrWhiteSpace(NewName)); DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedProfile != null); BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null); ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null); AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null); } public async Task LoadAsync() { try { var profiles = await _profileService.GetProfilesAsync(); Profiles.Clear(); foreach (var p in profiles) Profiles.Add(p); } catch (Exception ex) { _logger.LogError(ex, "Failed to load profiles."); } } partial void OnNewNameChanged(string value) => NotifyCommandsCanExecuteChanged(); partial void OnNewTenantUrlChanged(string value) => NotifyCommandsCanExecuteChanged(); partial void OnNewClientIdChanged(string value) => NotifyCommandsCanExecuteChanged(); partial void OnSelectedProfileChanged(TenantProfile? value) { ClientLogoPreview = FormatLogoPreview(value?.ClientLogo); BrowseClientLogoCommand.NotifyCanExecuteChanged(); ClearClientLogoCommand.NotifyCanExecuteChanged(); AutoPullClientLogoCommand.NotifyCanExecuteChanged(); RenameCommand.NotifyCanExecuteChanged(); DeleteCommand.NotifyCanExecuteChanged(); } private static string? FormatLogoPreview(LogoData? logo) => logo is not null ? $"data:{logo.MimeType};base64,{logo.Base64}" : null; private void NotifyCommandsCanExecuteChanged() { AddCommand.NotifyCanExecuteChanged(); RenameCommand.NotifyCanExecuteChanged(); } private bool CanAdd() { if (string.IsNullOrWhiteSpace(NewName)) return false; if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false; if (string.IsNullOrWhiteSpace(NewClientId)) return false; return true; } private async Task AddAsync() { if (!CanAdd()) return; try { var profile = new TenantProfile { Name = NewName.Trim(), TenantUrl = NewTenantUrl.Trim(), ClientId = NewClientId.Trim() }; await _profileService.AddProfileAsync(profile); Profiles.Add(profile); NewName = string.Empty; NewTenantUrl = string.Empty; NewClientId = string.Empty; ValidationMessage = string.Empty; } catch (Exception ex) { ValidationMessage = ex.Message; _logger.LogError(ex, "Failed to add profile."); } } private async Task RenameAsync() { if (SelectedProfile == null || string.IsNullOrWhiteSpace(NewName)) return; try { await _profileService.RenameProfileAsync(SelectedProfile.Name, NewName.Trim()); SelectedProfile.Name = NewName.Trim(); NewName = string.Empty; } catch (Exception ex) { ValidationMessage = ex.Message; _logger.LogError(ex, "Failed to rename profile."); } } private async Task DeleteAsync() { if (SelectedProfile == null) return; try { await _profileService.DeleteProfileAsync(SelectedProfile.Name); Profiles.Remove(SelectedProfile); SelectedProfile = null; } catch (Exception ex) { ValidationMessage = ex.Message; _logger.LogError(ex, "Failed to delete profile."); } } private async Task BrowseClientLogoAsync() { if (SelectedProfile == null) return; var dialog = new OpenFileDialog { Title = "Select client logo", Filter = "Image files (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg", }; if (dialog.ShowDialog() != true) return; try { var logo = await _brandingService.ImportLogoAsync(dialog.FileName); SelectedProfile.ClientLogo = logo; ClientLogoPreview = FormatLogoPreview(logo); await _profileService.UpdateProfileAsync(SelectedProfile); ValidationMessage = string.Empty; } catch (Exception ex) { ValidationMessage = ex.Message; _logger.LogError(ex, "Failed to import client logo."); } } private async Task ClearClientLogoAsync() { if (SelectedProfile == null) return; try { SelectedProfile.ClientLogo = null; ClientLogoPreview = null; await _profileService.UpdateProfileAsync(SelectedProfile); ValidationMessage = string.Empty; } catch (Exception ex) { ValidationMessage = ex.Message; _logger.LogError(ex, "Failed to clear client logo."); } } private async Task AutoPullClientLogoAsync() { if (SelectedProfile == null) return; try { var graphClient = await _graphClientFactory.CreateClientAsync( SelectedProfile.ClientId, CancellationToken.None); var orgs = await graphClient.Organization.GetAsync(); var orgId = orgs?.Value?.FirstOrDefault()?.Id; if (orgId is null) { ValidationMessage = "Could not determine organization ID."; return; } var stream = await graphClient.Organization[orgId] .Branding.Localizations["default"].SquareLogo.GetAsync(); if (stream is null || stream.Length == 0) { ValidationMessage = "No branding logo found for this tenant."; return; } using var ms = new MemoryStream(); await stream.CopyToAsync(ms); var bytes = ms.ToArray(); var logo = await _brandingService.ImportLogoFromBytesAsync(bytes); SelectedProfile.ClientLogo = logo; ClientLogoPreview = FormatLogoPreview(logo); await _profileService.UpdateProfileAsync(SelectedProfile); ValidationMessage = "Client logo pulled from Entra branding."; } catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == 404) { ValidationMessage = "No Entra branding configured for this tenant."; } catch (Exception ex) { ValidationMessage = $"Failed to pull logo: {ex.Message}"; _logger.LogWarning(ex, "Auto-pull client logo failed."); } } }