using System.Collections.ObjectModel; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using Serilog; using SharepointToolbox.Core.Models; using SharepointToolbox.Infrastructure.Persistence; using SharepointToolbox.Localization; using SharepointToolbox.Services; namespace SharepointToolbox.ViewModels.Tabs; public partial class TemplatesViewModel : FeatureViewModelBase { private readonly ITemplateService _templateService; private readonly TemplateRepository _templateRepo; private readonly ISessionManager _sessionManager; private readonly ILogger _logger; private TenantProfile? _currentProfile; // Template list private ObservableCollection _templates = new(); public ObservableCollection Templates { get => _templates; private set { _templates = value; OnPropertyChanged(); } } [ObservableProperty] private SiteTemplate? _selectedTemplate; // Capture options [ObservableProperty] private string _templateName = string.Empty; [ObservableProperty] private bool _captureLibraries = true; [ObservableProperty] private bool _captureFolders = true; [ObservableProperty] private bool _capturePermissions = true; [ObservableProperty] private bool _captureLogo = true; [ObservableProperty] private bool _captureSettings = true; // Apply options [ObservableProperty] private string _newSiteTitle = string.Empty; [ObservableProperty] private string _newSiteAlias = string.Empty; private bool _aliasManuallyEdited; public IAsyncRelayCommand CaptureCommand { get; } public IAsyncRelayCommand ApplyCommand { get; } public IAsyncRelayCommand RenameCommand { get; } public IAsyncRelayCommand DeleteCommand { get; } public IAsyncRelayCommand RefreshCommand { get; } public TenantProfile? CurrentProfile => _currentProfile; // Factory for rename dialog — set by View code-behind public Func? RenameDialogFactory { get; set; } public TemplatesViewModel( ITemplateService templateService, TemplateRepository templateRepo, ISessionManager sessionManager, ILogger logger) : base(logger) { _templateService = templateService; _templateRepo = templateRepo; _sessionManager = sessionManager; _logger = logger; CaptureCommand = new AsyncRelayCommand(CaptureAsync, () => !IsRunning); ApplyCommand = new AsyncRelayCommand(ApplyAsync, () => !IsRunning && SelectedTemplate != null); RenameCommand = new AsyncRelayCommand(RenameAsync, () => SelectedTemplate != null); DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedTemplate != null); RefreshCommand = new AsyncRelayCommand(RefreshListAsync); } protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) { // Not used directly — Capture and Apply have their own async commands await Task.CompletedTask; } private async Task CaptureAsync() { var T = TranslationSource.Instance; if (_currentProfile == null) throw new InvalidOperationException(T["err.no_tenant"]); if (string.IsNullOrWhiteSpace(TemplateName)) throw new InvalidOperationException(T["err.template_name_required"]); var captureSiteUrl = GlobalSites.Select(s => s.Url).FirstOrDefault(u => !string.IsNullOrWhiteSpace(u)); if (string.IsNullOrWhiteSpace(captureSiteUrl)) throw new InvalidOperationException(T["err.no_sites_selected"]); try { IsRunning = true; StatusMessage = T["templates.status.capturing"]; var profile = new TenantProfile { Name = _currentProfile.Name, TenantUrl = captureSiteUrl, ClientId = _currentProfile.ClientId, }; var ctx = await _sessionManager.GetOrCreateContextAsync(profile, CancellationToken.None); var options = new SiteTemplateOptions { CaptureLibraries = CaptureLibraries, CaptureFolders = CaptureFolders, CapturePermissionGroups = CapturePermissions, CaptureLogo = CaptureLogo, CaptureSettings = CaptureSettings, }; var progress = new Progress(p => StatusMessage = p.Message); var template = await _templateService.CaptureTemplateAsync(ctx, options, progress, CancellationToken.None); template.Name = TemplateName; await _templateRepo.SaveAsync(template); Log.Information("Template captured: {Name} from {Url}", template.Name, captureSiteUrl); await RefreshListAsync(); StatusMessage = T["templates.status.success"]; } catch (Exception ex) { StatusMessage = string.Format(T["templates.status.capture_failed"], ex.Message); Log.Error(ex, "Template capture failed"); } finally { IsRunning = false; } } private async Task ApplyAsync() { if (_currentProfile == null || SelectedTemplate == null) return; var T = TranslationSource.Instance; if (string.IsNullOrWhiteSpace(NewSiteTitle)) throw new InvalidOperationException(T["err.site_title_required"]); // Auto-fill alias from title if user left it blank if (string.IsNullOrWhiteSpace(NewSiteAlias)) { var generated = GenerateAliasFromTitle(NewSiteTitle); if (string.IsNullOrWhiteSpace(generated)) throw new InvalidOperationException(T["err.site_alias_required"]); NewSiteAlias = generated; } try { IsRunning = true; StatusMessage = T["templates.status.applying"]; var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, CancellationToken.None); var progress = new Progress(p => StatusMessage = p.Message); var siteUrl = await _templateService.ApplyTemplateAsync( ctx, SelectedTemplate, NewSiteTitle, NewSiteAlias, progress, CancellationToken.None); StatusMessage = string.Format(T["templates.status.applied"], siteUrl); Log.Information("Template applied. New site: {Url}", siteUrl); } catch (Exception ex) { StatusMessage = string.Format(T["templates.status.apply_failed"], ex.Message); Log.Error(ex, "Template apply failed"); } finally { IsRunning = false; } } private async Task RenameAsync() { if (SelectedTemplate == null) return; if (RenameDialogFactory != null) { var newName = RenameDialogFactory(SelectedTemplate.Name); if (!string.IsNullOrWhiteSpace(newName)) { await _templateRepo.RenameAsync(SelectedTemplate.Id, newName); await RefreshListAsync(); Log.Information("Template renamed."); } } } private async Task DeleteAsync() { if (SelectedTemplate == null) return; await _templateRepo.DeleteAsync(SelectedTemplate.Id); await RefreshListAsync(); Log.Information("Template deleted."); } private async Task RefreshListAsync() { var templates = await _templateRepo.GetAllAsync(); await Application.Current.Dispatcher.InvokeAsync(() => { Templates = new ObservableCollection(templates); }); } protected override void OnTenantSwitched(TenantProfile profile) { _currentProfile = profile; TemplateName = string.Empty; NewSiteTitle = string.Empty; NewSiteAlias = string.Empty; _aliasManuallyEdited = false; StatusMessage = string.Empty; _ = RefreshListAsync(); } partial void OnSelectedTemplateChanged(SiteTemplate? value) { ApplyCommand.NotifyCanExecuteChanged(); RenameCommand.NotifyCanExecuteChanged(); DeleteCommand.NotifyCanExecuteChanged(); } partial void OnNewSiteTitleChanged(string value) { if (_aliasManuallyEdited) return; var generated = GenerateAliasFromTitle(value); if (NewSiteAlias != generated) { // Bypass user-edit flag while we sync alias to title _suppressAliasEditedFlag = true; NewSiteAlias = generated; _suppressAliasEditedFlag = false; } } private bool _suppressAliasEditedFlag; partial void OnNewSiteAliasChanged(string value) { if (_suppressAliasEditedFlag) return; _aliasManuallyEdited = !string.IsNullOrWhiteSpace(value) && value != GenerateAliasFromTitle(NewSiteTitle); if (string.IsNullOrWhiteSpace(value)) _aliasManuallyEdited = false; } internal static string GenerateAliasFromTitle(string title) { if (string.IsNullOrWhiteSpace(title)) return string.Empty; var normalized = title.Normalize(System.Text.NormalizationForm.FormD); var sb = new System.Text.StringBuilder(normalized.Length); foreach (var c in normalized) { var cat = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c); if (cat == System.Globalization.UnicodeCategory.NonSpacingMark) continue; if (char.IsLetterOrDigit(c)) sb.Append(c); else if (c == ' ' || c == '-' || c == '_') sb.Append('-'); } var collapsed = System.Text.RegularExpressions.Regex.Replace(sb.ToString(), "-+", "-"); return collapsed.Trim('-'); } }