using System.Collections.ObjectModel;
using System.Security.Cryptography;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OTSSignsOrchestrator.Core.Data;
using OTSSignsOrchestrator.Core.Models.DTOs;
using OTSSignsOrchestrator.Core.Models.Entities;
using OTSSignsOrchestrator.Core.Services;
using OTSSignsOrchestrator.Desktop.Services;
namespace OTSSignsOrchestrator.Desktop.ViewModels;
///
/// ViewModel for the Create Instance form.
/// Simplified: Customer Name, Abbreviation, SSH Host, and optional Newt credentials.
/// All other config comes from the Settings page.
///
public partial class CreateInstanceViewModel : ObservableObject
{
private readonly IServiceProvider _services;
[ObservableProperty] private string _statusMessage = string.Empty;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string _deployOutput = string.Empty;
[ObservableProperty] private double _progressPercent;
[ObservableProperty] private string _progressStep = string.Empty;
// Core form fields — only these two are required from the user
[ObservableProperty] private string _customerName = string.Empty;
[ObservableProperty] private string _customerAbbrev = string.Empty;
// Optional Pangolin/Newt credentials (per-instance)
[ObservableProperty] private string _newtId = string.Empty;
[ObservableProperty] private string _newtSecret = string.Empty;
// CIFS / SMB credentials (per-instance, defaults loaded from global settings)
[ObservableProperty] private string _cifsServer = string.Empty;
[ObservableProperty] private string _cifsShareBasePath = string.Empty;
[ObservableProperty] private string _cifsUsername = string.Empty;
[ObservableProperty] private string _cifsPassword = string.Empty;
[ObservableProperty] private string _cifsExtraOptions = string.Empty;
// SSH host selection
[ObservableProperty] private ObservableCollection _availableHosts = new();
[ObservableProperty] private SshHost? _selectedSshHost;
// ── Derived preview properties ───────────────────────────────────────────
public string PreviewStackName => Valid ? $"{Abbrev}-cms-stack" : "—";
public string PreviewServiceWeb => Valid ? $"{Abbrev}-web" : "—";
public string PreviewServiceCache => Valid ? $"{Abbrev}-memcached" : "—";
public string PreviewServiceChart => Valid ? $"{Abbrev}-quickchart" : "—";
public string PreviewServiceNewt => Valid ? $"{Abbrev}-newt" : "—";
public string PreviewNetwork => Valid ? $"{Abbrev}-net" : "—";
public string PreviewVolCustom => Valid ? $"{Abbrev}-cms-custom" : "—";
public string PreviewVolBackup => Valid ? $"{Abbrev}-cms-backup" : "—";
public string PreviewVolLibrary => Valid ? $"{Abbrev}-cms-library" : "—";
public string PreviewVolUserscripts => Valid ? $"{Abbrev}-cms-userscripts": "—";
public string PreviewVolCaCerts => Valid ? $"{Abbrev}-cms-ca-certs" : "—";
public string PreviewSecret => Valid ? $"{Abbrev}-cms-db-password": "—";
public string PreviewMySqlDb => Valid ? $"{Abbrev}_cms_db" : "—";
public string PreviewMySqlUser => Valid ? $"{Abbrev}_cms" : "—";
public string PreviewCmsUrl => Valid ? $"https://{Abbrev}.ots-signs.com" : "—";
private string Abbrev => CustomerAbbrev.Trim().ToLowerInvariant();
private bool Valid => Abbrev.Length == 3 && System.Text.RegularExpressions.Regex.IsMatch(Abbrev, "^[a-z]{3}$");
// ─────────────────────────────────────────────────────────────────────────
public CreateInstanceViewModel(IServiceProvider services)
{
_services = services;
_ = LoadHostsAsync();
_ = LoadCifsDefaultsAsync();
}
partial void OnCustomerAbbrevChanged(string value) => RefreshPreview();
private void RefreshPreview()
{
OnPropertyChanged(nameof(PreviewStackName));
OnPropertyChanged(nameof(PreviewServiceWeb));
OnPropertyChanged(nameof(PreviewServiceCache));
OnPropertyChanged(nameof(PreviewServiceChart));
OnPropertyChanged(nameof(PreviewServiceNewt));
OnPropertyChanged(nameof(PreviewNetwork));
OnPropertyChanged(nameof(PreviewVolCustom));
OnPropertyChanged(nameof(PreviewVolBackup));
OnPropertyChanged(nameof(PreviewVolLibrary));
OnPropertyChanged(nameof(PreviewVolUserscripts));
OnPropertyChanged(nameof(PreviewVolCaCerts));
OnPropertyChanged(nameof(PreviewSecret));
OnPropertyChanged(nameof(PreviewMySqlDb));
OnPropertyChanged(nameof(PreviewMySqlUser));
OnPropertyChanged(nameof(PreviewCmsUrl));
}
private async Task LoadHostsAsync()
{
using var scope = _services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var hosts = await db.SshHosts.OrderBy(h => h.Label).ToListAsync();
AvailableHosts = new ObservableCollection(hosts);
}
private async Task LoadCifsDefaultsAsync()
{
using var scope = _services.CreateScope();
var settings = scope.ServiceProvider.GetRequiredService();
CifsServer = await settings.GetAsync(SettingsService.CifsServer) ?? string.Empty;
CifsShareBasePath = await settings.GetAsync(SettingsService.CifsShareBasePath) ?? string.Empty;
CifsUsername = await settings.GetAsync(SettingsService.CifsUsername) ?? string.Empty;
CifsPassword = await settings.GetAsync(SettingsService.CifsPassword) ?? string.Empty;
CifsExtraOptions = await settings.GetAsync(SettingsService.CifsOptions, "file_mode=0777,dir_mode=0777");
}
[RelayCommand]
private async Task DeployAsync()
{
// ── Validation ───────────────────────────────────────────────────
if (SelectedSshHost == null)
{
StatusMessage = "Select an SSH host first.";
return;
}
if (string.IsNullOrWhiteSpace(CustomerName))
{
StatusMessage = "Customer Name is required.";
return;
}
if (!Valid)
{
StatusMessage = "Abbreviation must be exactly 3 lowercase letters (a-z).";
return;
}
IsBusy = true;
StatusMessage = "Starting deployment...";
DeployOutput = string.Empty;
ProgressPercent = 0;
try
{
// Wire SSH host into docker services
var dockerCli = _services.GetRequiredService();
dockerCli.SetHost(SelectedSshHost);
var dockerSecrets = _services.GetRequiredService();
dockerSecrets.SetHost(SelectedSshHost);
var ssh = _services.GetRequiredService();
using var scope = _services.CreateScope();
var instanceSvc = scope.ServiceProvider.GetRequiredService();
// ── Step 1: Clone template repo ────────────────────────────────
SetProgress(10, "Cloning template repository...");
// Handled inside InstanceService.CreateInstanceAsync
// ── Step 2: Generate MySQL password → Docker secret ────────────
SetProgress(20, "Generating secrets...");
var mysqlPassword = GenerateRandomPassword(32);
// ── Step 3: Create MySQL database + user via SSH ───────────────
SetProgress(35, "Creating MySQL database and user...");
var (mysqlOk, mysqlMsg) = await instanceSvc.CreateMySqlDatabaseAsync(
Abbrev,
mysqlPassword,
cmd => ssh.RunCommandAsync(SelectedSshHost, cmd));
AppendOutput($"[MySQL] {mysqlMsg}");
if (!mysqlOk)
{
StatusMessage = mysqlMsg;
return;
}
// ── Step 4: Create Docker Swarm secret ────────────────────────
SetProgress(50, "Creating Docker Swarm secrets...");
var secretName = $"{Abbrev}-cms-db-password";
var (_, secretId) = await dockerSecrets.EnsureSecretAsync(secretName, mysqlPassword);
AppendOutput($"[Secret] {secretName} → {secretId}");
// Password is now ONLY on the Swarm — clear from memory
mysqlPassword = string.Empty;
// ── Step 5: Deploy stack ──────────────────────────────────────
SetProgress(70, "Rendering compose & deploying stack...");
var dto = new CreateInstanceDto
{
CustomerName = CustomerName.Trim(),
CustomerAbbrev = Abbrev,
SshHostId = SelectedSshHost.Id,
NewtId = string.IsNullOrWhiteSpace(NewtId) ? null : NewtId.Trim(),
NewtSecret = string.IsNullOrWhiteSpace(NewtSecret) ? null : NewtSecret.Trim(),
CifsServer = string.IsNullOrWhiteSpace(CifsServer) ? null : CifsServer.Trim(),
CifsShareBasePath = string.IsNullOrWhiteSpace(CifsShareBasePath) ? null : CifsShareBasePath.Trim(),
CifsUsername = string.IsNullOrWhiteSpace(CifsUsername) ? null : CifsUsername.Trim(),
CifsPassword = string.IsNullOrWhiteSpace(CifsPassword) ? null : CifsPassword.Trim(),
CifsExtraOptions = string.IsNullOrWhiteSpace(CifsExtraOptions) ? null : CifsExtraOptions.Trim(),
};
var result = await instanceSvc.CreateInstanceAsync(dto);
AppendOutput(result.Output ?? string.Empty);
SetProgress(100, result.Success ? "Deployment complete!" : "Deployment failed.");
StatusMessage = result.Success
? $"Instance '{Abbrev}-cms-stack' deployed in {result.DurationMs}ms!"
: $"Deploy failed: {result.ErrorMessage}";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
AppendOutput(ex.ToString());
SetProgress(0, "Failed.");
}
finally
{
IsBusy = false;
}
}
private void SetProgress(double pct, string step)
{
ProgressPercent = pct;
ProgressStep = step;
AppendOutput($"[{pct:0}%] {step}");
}
private void AppendOutput(string text)
{
if (!string.IsNullOrWhiteSpace(text))
DeployOutput += (DeployOutput.Length > 0 ? "\n" : "") + text;
}
private static string GenerateRandomPassword(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
return RandomNumberGenerator.GetString(chars, length);
}
}