using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OTSSignsOrchestrator.Core.Configuration;

namespace OTSSignsOrchestrator.Core.Services;

/// <summary>
/// Runs once after a Xibo CMS stack is deployed to complete post-install setup:
/// <list type="number">
///   <item>Waits for the Xibo web service to become available.</item>
///   <item>Authenticates using the OAuth2 application credentials supplied by the user.</item>
///   <item>Creates the OTS admin user with a random password.</item>
///   <item>Registers a dedicated client_credentials OAuth2 application for OTS.</item>
///   <item>Activates the <c>otssigns</c> theme.</item>
///   <item>Stores all generated credentials in Bitwarden Secrets Manager.</item>
///   <item>Deletes the default <c>xibo_admin</c> account.</item>
/// </list>
///
/// The user must first create an OAuth2 application (client_credentials) in the
/// Xibo web UI using the default <c>xibo_admin / password</c> account that ships
/// with every new Xibo CMS instance.
///
/// Invoked from the Create Instance UI after the user supplies the OAuth credentials.
/// Progress is tracked in the operation log; failures are logged but do not roll back the deployment.
/// </summary>
/// </summary>
public class PostInstanceInitService
{
    private readonly IServiceProvider _services;
    private readonly ILogger<PostInstanceInitService> _logger;

    // How long to wait for Xibo to become healthy before aborting post-init.
    private static readonly TimeSpan XiboReadinessTimeout = TimeSpan.FromMinutes(15);

    public PostInstanceInitService(
        IServiceProvider services,
        ILogger<PostInstanceInitService> logger)
    {
        _services = services;
        _logger   = logger;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Entry point
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Executes the post-instance initialisation sequence.
    /// Called from the UI after the user supplies OAuth2 client credentials.
    /// </summary>
    public async Task RunAsync(
        string abbrev,
        string instanceUrl,
        string clientId,
        string clientSecret,
        CancellationToken ct = default)
    {
        _logger.LogInformation("[PostInit] Starting post-instance init for {Abbrev} ({Url})", abbrev, instanceUrl);

        try
        {
            using var scope      = _services.CreateScope();
            var xibo             = scope.ServiceProvider.GetRequiredService<XiboApiService>();
            var bws              = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
            var settings         = scope.ServiceProvider.GetRequiredService<SettingsService>();

            // ── 1. Wait for Xibo readiness ────────────────────────────────────
            _logger.LogInformation("[PostInit] Waiting for Xibo to become ready at {Url}...", instanceUrl);
            var ready = await xibo.WaitForReadyAsync(instanceUrl, XiboReadinessTimeout, ct);
            if (!ready)
                throw new TimeoutException(
                    $"Xibo instance at {instanceUrl} did not become ready within {XiboReadinessTimeout.TotalMinutes} minutes.");

            // ── 2. Authenticate with user-supplied OAuth2 credentials ─────────
            _logger.LogInformation("[PostInit] Obtaining access token via client_credentials");
            var accessToken = await xibo.LoginAsync(instanceUrl, clientId, clientSecret);

            // ── 3. Generate credentials ───────────────────────────────────────
            var adminUsername = $"ots-admin-{abbrev}";
            var adminPassword = GeneratePassword(24);
            var adminEmail    = $"ots-admin-{abbrev}@ots-signs.com";

            // ── 4. Create OTS admin group ─────────────────────────────────────
            var adminGroupName = $"ots-admins-{abbrev}";
            _logger.LogInformation("[PostInit] Creating OTS admin group '{GroupName}'", adminGroupName);
            var adminGroupId = await xibo.CreateUserGroupAsync(instanceUrl, accessToken, adminGroupName);

            // ── 5. Create OTS admin user ──────────────────────────────────────
            _logger.LogInformation("[PostInit] Creating OTS admin user '{Username}'", adminUsername);
            int userId = await xibo.CreateAdminUserAsync(
                instanceUrl, accessToken,
                adminUsername, adminPassword, adminEmail, adminGroupId);

            // ── 5a. Assign admin user to OTS admin group ──────────────────────
            _logger.LogInformation("[PostInit] Assigning '{Username}' to group '{GroupName}'", adminUsername, adminGroupName);
            await xibo.AssignUserToGroupAsync(instanceUrl, accessToken, adminGroupId, userId);

            // ── 6. Register dedicated OAuth2 application for OTS ──────────────
            _logger.LogInformation("[PostInit] Registering OTS OAuth2 application");
            var (otsClientId, otsClientSecret) = await xibo.RegisterOAuthClientAsync(
                instanceUrl, accessToken,
                $"OTS Signs — {abbrev.ToUpperInvariant()}");

            // ── 6. Set theme ──────────────────────────────────────────────────
            _logger.LogInformation("[PostInit] Setting Xibo theme to 'otssigns'");
            await xibo.SetThemeAsync(instanceUrl, accessToken, "otssigns");

            // ── 7. Store credentials in Bitwarden ─────────────────────────────
            _logger.LogInformation("[PostInit] Storing credentials in Bitwarden");

            var adminSecretId = await bws.CreateInstanceSecretAsync(
                key:   $"{abbrev}/xibo-admin-password",
                value: adminPassword,
                note:  $"Xibo CMS admin password for instance {abbrev}. Username: {adminUsername}");

            var oauthSecretId = await bws.CreateInstanceSecretAsync(
                key:   $"{abbrev}/xibo-oauth-secret",
                value: otsClientSecret,
                note:  $"Xibo CMS OAuth2 client secret for OTS application. ClientId: {otsClientId}");

            // Persist Bitwarden secret IDs + OAuth client ID as config settings
            await settings.SetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev), adminSecretId,
                SettingsService.CatInstance, isSensitive: false);
            await settings.SetAsync(SettingsService.InstanceOAuthSecretId(abbrev), oauthSecretId,
                SettingsService.CatInstance, isSensitive: false);
            await settings.SetAsync(SettingsService.InstanceOAuthClientId(abbrev), otsClientId,
                SettingsService.CatInstance, isSensitive: false);

            // ── 8. Remove the default xibo_admin account ──────────────────────
            _logger.LogInformation("[PostInit] Removing default xibo_admin user");
            var xiboAdminId = await xibo.GetUserIdByNameAsync(instanceUrl, accessToken, "xibo_admin");
            await xibo.DeleteUserAsync(instanceUrl, accessToken, xiboAdminId);
            _logger.LogInformation("[PostInit] xibo_admin user removed (userId={UserId})", xiboAdminId);

            _logger.LogInformation(
                "[PostInit] Post-instance init complete for {Abbrev}. Admin user: {Username}, OAuth ClientId: {ClientId}",
                abbrev, adminUsername, otsClientId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[PostInit] Post-instance init failed for {Abbrev}: {Message}", abbrev, ex.Message);
            throw;   // Propagate to calling UI so the user sees the error
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Initialise using caller-supplied OAuth credentials (no new app registration)
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Initialises a Xibo CMS instance using OAuth2 credentials supplied by the user.
    /// Unlike <see cref="RunAsync"/>, this method does NOT register a new OAuth application;
    /// instead it stores the caller-supplied credentials for future API operations.
    /// Steps: wait → authenticate → create OTS admin → set theme → remove xibo_admin → store credentials.
    /// </summary>
    public async Task InitializeWithOAuthAsync(
        string abbrev,
        string instanceUrl,
        string clientId,
        string clientSecret,
        CancellationToken ct = default)
    {
        _logger.LogInformation("[PostInit] Starting initialisation for {Abbrev} ({Url})", abbrev, instanceUrl);

        try
        {
            using var scope  = _services.CreateScope();
            var xibo         = scope.ServiceProvider.GetRequiredService<XiboApiService>();
            var bws          = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
            var settings     = scope.ServiceProvider.GetRequiredService<SettingsService>();

            // ── 1. Wait for Xibo readiness ────────────────────────────────────
            _logger.LogInformation("[PostInit] Waiting for Xibo at {Url}...", instanceUrl);
            var ready = await xibo.WaitForReadyAsync(instanceUrl, XiboReadinessTimeout, ct);
            if (!ready)
                throw new TimeoutException(
                    $"Xibo at {instanceUrl} did not become ready within {XiboReadinessTimeout.TotalMinutes} minutes.");

            // ── 2. Authenticate with caller-supplied OAuth2 credentials ───────
            _logger.LogInformation("[PostInit] Obtaining access token");
            var accessToken = await xibo.LoginAsync(instanceUrl, clientId, clientSecret);

            // ── 3. Generate OTS admin credentials ─────────────────────────────
            var adminUsername = $"ots-admin-{abbrev}";
            var adminPassword = GeneratePassword(24);
            var adminEmail    = $"ots-admin-{abbrev}@ots-signs.com";

            // ── 4. Rename built-in xibo_admin to OTS admin ───────────────────
            _logger.LogInformation("[PostInit] Looking up xibo_admin user");
            var xiboAdminId = await xibo.GetUserIdByNameAsync(instanceUrl, accessToken, "xibo_admin");
            _logger.LogInformation("[PostInit] Updating xibo_admin (id={Id}) → '{Username}'", xiboAdminId, adminUsername);
            await xibo.UpdateUserAsync(instanceUrl, accessToken, xiboAdminId, adminUsername, adminPassword, adminEmail);

            // ── 5. Set theme ──────────────────────────────────────────────────
            _logger.LogInformation("[PostInit] Setting theme to 'otssigns'");
            await xibo.SetThemeAsync(instanceUrl, accessToken, "otssigns");

            // ── 6. Store admin password in Bitwarden ──────────────────────────
            _logger.LogInformation("[PostInit] Storing credentials in Bitwarden");
            var adminSecretId = await bws.CreateInstanceSecretAsync(
                key:   $"{abbrev}/xibo-admin-password",
                value: adminPassword,
                note:  $"Xibo CMS admin password for instance {abbrev}. Username: {adminUsername}");

            await settings.SetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev), adminSecretId,
                SettingsService.CatInstance, isSensitive: false);

            // ── 7. Store caller-supplied OAuth credentials in Bitwarden ───────
            var oauthSecretId = await bws.CreateInstanceSecretAsync(
                key:   $"{abbrev}/xibo-oauth-secret",
                value: clientSecret,
                note:  $"Xibo CMS OAuth2 client secret for instance {abbrev}. ClientId: {clientId}");

            await settings.SetAsync(SettingsService.InstanceOAuthSecretId(abbrev), oauthSecretId,
                SettingsService.CatInstance, isSensitive: false);
            await settings.SetAsync(SettingsService.InstanceOAuthClientId(abbrev), clientId,
                SettingsService.CatInstance, isSensitive: false);
            _logger.LogInformation("[PostInit] xibo_admin removed");

            _logger.LogInformation("[PostInit] Initialisation complete for {Abbrev}", abbrev);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[PostInit] Initialisation failed for {Abbrev}: {Message}", abbrev, ex.Message);
            throw;
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Credential rotation (called from the UI)
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Generates a new OTS admin password, updates it in Xibo, and rotates the Bitwarden secret.
    /// </summary>
    public async Task<string> RotateAdminPasswordAsync(string abbrev, string instanceUrl)
    {
        _logger.LogInformation("[PostInit] Rotating admin password for {Abbrev}", abbrev);

        using var scope   = _services.CreateScope();
        var xibo          = scope.ServiceProvider.GetRequiredService<XiboApiService>();
        var bws           = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
        var settings      = scope.ServiceProvider.GetRequiredService<SettingsService>();

        var adminUsername  = $"ots-admin-{abbrev}";
        var newPassword    = GeneratePassword(24);

        // Log in using the stored OTS OAuth2 client credentials
        var oauthClientId = await settings.GetAsync(SettingsService.InstanceOAuthClientId(abbrev));
        var oauthSecretId = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
        if (string.IsNullOrWhiteSpace(oauthClientId) || string.IsNullOrWhiteSpace(oauthSecretId))
            throw new InvalidOperationException(
                $"No OAuth credentials found for instance '{abbrev}'. Was post-init completed?");

        var oauthSecret   = await bws.GetSecretAsync(oauthSecretId);
        var accessToken   = await xibo.LoginAsync(instanceUrl, oauthClientId, oauthSecret.Value);

        // Look up the OTS admin user ID
        var userId = await xibo.GetUserIdByNameAsync(instanceUrl, accessToken, adminUsername);

        await xibo.RotateUserPasswordAsync(instanceUrl, accessToken, userId, newPassword);

        // Update Bitwarden secret
        var secretId = await settings.GetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev));
        if (!string.IsNullOrWhiteSpace(secretId))
        {
            await bws.UpdateInstanceSecretAsync(secretId,
                key:   $"{abbrev}/xibo-admin-password",
                value: newPassword,
                note:  $"Rotated on {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC. Username: {adminUsername}");
        }
        else
        {
            // Secret doesn't exist yet in Bitwarden — create it now
            var newSecretId = await bws.CreateInstanceSecretAsync(
                $"{abbrev}/xibo-admin-password", newPassword,
                $"Rotated on {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC. Username: {adminUsername}");
            await settings.SetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev), newSecretId,
                SettingsService.CatInstance, isSensitive: false);
        }

        _logger.LogInformation("[PostInit] Admin password rotated for {Abbrev}", abbrev);
        return newPassword;
    }

    /// <summary>
    /// Returns the stored admin password for an instance from Bitwarden.
    /// </summary>
    public async Task<InstanceCredentials> GetCredentialsAsync(string abbrev)
    {
        using var scope  = _services.CreateScope();
        var bws          = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
        var settings     = scope.ServiceProvider.GetRequiredService<SettingsService>();

        var adminUsername  = $"ots-admin-{abbrev}";
        var oauthClientId  = await settings.GetAsync(SettingsService.InstanceOAuthClientId(abbrev));
        string? adminPassword = null;
        string? oauthClientSecret = null;

        var adminSecretId = await settings.GetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev));
        if (!string.IsNullOrWhiteSpace(adminSecretId))
        {
            try
            {
                var secret = await bws.GetSecretAsync(adminSecretId);
                adminPassword = secret.Value;
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not retrieve admin password from Bitwarden for {Abbrev}", abbrev);
            }
        }

        var oauthSecretId = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
        if (!string.IsNullOrWhiteSpace(oauthSecretId))
        {
            try
            {
                var secret = await bws.GetSecretAsync(oauthSecretId);
                oauthClientSecret = secret.Value;
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not retrieve OAuth secret from Bitwarden for {Abbrev}", abbrev);
            }
        }

        return new InstanceCredentials
        {
            AdminUsername     = adminUsername,
            AdminPassword     = adminPassword,
            OAuthClientId     = oauthClientId,
            OAuthClientSecret = oauthClientSecret,
        };
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SAML configuration deployment
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Provisions a SAML application in Authentik, renders the settings-custom.php template,
    /// and writes the rendered file to the instance's NFS-backed cms-custom volume.
    /// The template is resolved from (a) the git repo cache, or (b) the local bundled
    /// <c>templates/</c> directory shipped with the application.
    /// Errors are logged but do not fail the overall deployment.
    /// </summary>
    public async Task DeploySamlConfigurationAsync(
        string abbrev,
        string instanceUrl,
        SettingsService settings,
        CancellationToken ct)
    {
        try
        {
            _logger.LogInformation("[PostInit] Deploying SAML settings-custom.php for {Abbrev}", abbrev);

            using var scope = _services.CreateScope();
            var authentik   = scope.ServiceProvider.GetRequiredService<IAuthentikService>();
            var git         = scope.ServiceProvider.GetRequiredService<GitTemplateService>();
            var docker      = scope.ServiceProvider.GetRequiredService<IDockerCliService>();

            // ── 1. Locate settings-custom.php.template ────────────────────────
            string? templateContent = null;

            // 1a. Try git repo cache first
            var repoUrl = await settings.GetAsync(SettingsService.GitRepoUrl);
            var repoPat = await settings.GetAsync(SettingsService.GitRepoPat);
            if (!string.IsNullOrWhiteSpace(repoUrl))
            {
                try
                {
                    var templateConfig = await git.FetchAsync(repoUrl, repoPat);
                    var gitPath = Path.Combine(templateConfig.CacheDir, "settings-custom.php.template");
                    if (File.Exists(gitPath))
                    {
                        templateContent = await File.ReadAllTextAsync(gitPath, ct);
                        _logger.LogInformation("[PostInit] Using template from git repo cache: {Path}", gitPath);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "[PostInit] Could not fetch template from git — trying local fallback");
                }
            }

            // 1b. Fall back to local templates/ directory (bundled with app)
            if (templateContent == null)
            {
                var candidates = new[]
                {
                    Path.Combine(AppContext.BaseDirectory, "templates", "settings-custom.php.template"),
                    Path.Combine(Directory.GetCurrentDirectory(), "templates", "settings-custom.php.template"),
                };

                foreach (var candidate in candidates)
                {
                    if (File.Exists(candidate))
                    {
                        templateContent = await File.ReadAllTextAsync(candidate, ct);
                        _logger.LogInformation("[PostInit] Using local template: {Path}", candidate);
                        break;
                    }
                }
            }

            if (templateContent == null)
            {
                _logger.LogWarning(
                    "[PostInit] settings-custom.php.template not found in git repo or local templates/ — skipping SAML deployment");
                return;
            }

            // ── 2. Provision Authentik SAML application ───────────────────────
            var samlBaseUrl = instanceUrl.TrimEnd('/') + "/saml";
            Models.DTOs.AuthentikSamlConfig? samlConfig = null;
            try
            {
                samlConfig = await authentik.ProvisionSamlAsync(abbrev, instanceUrl, ct);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex,
                    "[PostInit] Authentik provisioning failed for {Abbrev} — skipping SAML config deployment to avoid broken Xibo instance",
                    abbrev);
                return; // Do NOT write a settings-custom.php with empty IdP values — it will crash Xibo
            }

            // ── 3. Render template ────────────────────────────────────────────
            var rendered = templateContent
                .Replace("{{SAML_BASE_URL}}", samlBaseUrl)
                .Replace("{{SAML_SP_ENTITY_ID}}", $"{samlBaseUrl}/metadata")
                .Replace("{{AUTHENTIK_IDP_ENTITY_ID}}", samlConfig?.IdpEntityId ?? "")
                .Replace("{{AUTHENTIK_SSO_URL}}", samlConfig?.SsoUrlRedirect ?? "")
                .Replace("{{AUTHENTIK_SLO_URL}}", samlConfig?.SloUrlRedirect ?? "")
                .Replace("{{AUTHENTIK_IDP_X509_CERT}}", samlConfig?.IdpX509Cert ?? "");

            // ── 4. Write rendered file to NFS volume ──────────────────────────
            var nfsServer       = await settings.GetAsync(SettingsService.NfsServer);
            var nfsExport       = await settings.GetAsync(SettingsService.NfsExport);
            var nfsExportFolder = await settings.GetAsync(SettingsService.NfsExportFolder);

            if (string.IsNullOrWhiteSpace(nfsServer) || string.IsNullOrWhiteSpace(nfsExport))
                throw new InvalidOperationException("NFS settings are not configured — cannot write SAML config to volume.");

            // Path within the NFS export: {abbrev}/cms-custom/settings-custom.php
            var nfsRelativePath = $"{abbrev}/cms-custom/settings-custom.php";

            var (success, error) = await docker.WriteFileToNfsAsync(
                nfsServer, nfsExport, nfsRelativePath, rendered, nfsExportFolder);

            if (!success)
                throw new InvalidOperationException($"Failed to write settings-custom.php to NFS: {error}");

            _logger.LogInformation(
                "[PostInit] SAML configuration deployed for {Abbrev}{ProviderInfo}",
                abbrev,
                samlConfig != null
                    ? $" (Authentik provider={samlConfig.ProviderId})"
                    : " (without Authentik — needs manual IdP config)");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[PostInit] SAML deployment failed for {Abbrev}: {Message}. " +
                "Instance will continue without SAML — configure manually if needed.", abbrev, ex.Message);
            // Don't rethrow — SAML failure should not block the rest of post-init
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Helpers
    // ─────────────────────────────────────────────────────────────────────────

    private static string GeneratePassword(int length)
    {
        const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
        return RandomNumberGenerator.GetString(chars, length);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Import existing instance secrets on startup
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Scans all Bitwarden secrets for existing instance-level credentials
    /// (matching the <c>{abbrev}/xibo-admin-password</c> and <c>{abbrev}/xibo-oauth-secret</c>
    /// naming convention) and imports their mappings into the config settings so
    /// the app knows about them without a manual re-provisioning step.
    /// Safe to call on every startup — existing mappings are never overwritten.
    /// </summary>
    public async Task ImportExistingInstanceSecretsAsync()
    {
        try
        {
            using var scope  = _services.CreateScope();
            var bws          = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
            var settings     = scope.ServiceProvider.GetRequiredService<SettingsService>();

            if (!await bws.IsConfiguredAsync())
            {
                _logger.LogDebug("[Import] Bitwarden not configured — skipping instance secret import");
                return;
            }

            var allSecrets = await bws.ListSecretsAsync();
            var imported   = 0;

            foreach (var summary in allSecrets)
            {
                // ── Admin password pattern: {abbrev}/xibo-admin-password ──
                var adminMatch = Regex.Match(summary.Key, @"^([a-zA-Z0-9_-]+)/xibo-admin-password$");
                if (adminMatch.Success)
                {
                    var abbrev   = adminMatch.Groups[1].Value;
                    var existing = await settings.GetAsync(SettingsService.InstanceAdminPasswordSecretId(abbrev));
                    if (string.IsNullOrWhiteSpace(existing))
                    {
                        await settings.SetAsync(
                            SettingsService.InstanceAdminPasswordSecretId(abbrev),
                            summary.Id, SettingsService.CatInstance);
                        imported++;
                        _logger.LogInformation(
                            "[Import] Imported admin password secret for instance {Abbrev} (id={Id})",
                            abbrev, summary.Id);
                    }
                    continue;
                }

                // ── OAuth secret pattern: {abbrev}/xibo-oauth-secret ──
                var oauthMatch = Regex.Match(summary.Key, @"^([a-zA-Z0-9_-]+)/xibo-oauth-secret$");
                if (oauthMatch.Success)
                {
                    var abbrev   = oauthMatch.Groups[1].Value;
                    var existing = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
                    if (string.IsNullOrWhiteSpace(existing))
                    {
                        await settings.SetAsync(
                            SettingsService.InstanceOAuthSecretId(abbrev),
                            summary.Id, SettingsService.CatInstance);
                        imported++;
                        _logger.LogInformation(
                            "[Import] Imported OAuth secret for instance {Abbrev} (id={Id})",
                            abbrev, summary.Id);

                        // Try to extract the OAuth client_id from the secret's Note field
                        try
                        {
                            var full = await bws.GetSecretAsync(summary.Id);
                            var cidMatch = Regex.Match(full.Note ?? "", @"ClientId:\s*(\S+)");
                            if (cidMatch.Success)
                            {
                                var existingCid = await settings.GetAsync(
                                    SettingsService.InstanceOAuthClientId(abbrev));
                                if (string.IsNullOrWhiteSpace(existingCid))
                                {
                                    await settings.SetAsync(
                                        SettingsService.InstanceOAuthClientId(abbrev),
                                        cidMatch.Groups[1].Value, SettingsService.CatInstance);
                                    _logger.LogInformation(
                                        "[Import] Imported OAuth client ID for instance {Abbrev}", abbrev);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            _logger.LogWarning(ex,
                                "[Import] Could not fetch full OAuth secret for {Abbrev} to extract client ID",
                                abbrev);
                        }
                    }
                }
            }

            if (imported > 0)
                _logger.LogInformation("[Import] Imported {Count} instance secret mapping(s) from Bitwarden", imported);
            else
                _logger.LogDebug("[Import] No new instance secrets to import");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "[Import] Failed to import existing instance secrets from Bitwarden");
        }
    }
}

/// <summary>Snapshot of provisioned credentials for a CMS instance.</summary>
public class InstanceCredentials
{
    public string  AdminUsername     { get; set; } = string.Empty;
    public string? AdminPassword     { get; set; }
    public string? OAuthClientId     { get; set; }
    public string? OAuthClientSecret { get; set; }

    public bool HasAdminPassword     => !string.IsNullOrWhiteSpace(AdminPassword);
    public bool HasOAuthCredentials  => !string.IsNullOrWhiteSpace(OAuthClientId) &&
                                        !string.IsNullOrWhiteSpace(OAuthClientSecret);
}
