using Microsoft.Extensions.Logging;

namespace OTSSignsOrchestrator.Core.Services;

/// <summary>
/// Reads and writes application settings from Bitwarden Secrets Manager.
/// Each setting is stored as a Bitwarden secret with key prefix "ots-config/".
/// The secret's Note field stores metadata (category|isSensitive).
/// </summary>
public class SettingsService
{
    private readonly IBitwardenSecretService _bws;
    private readonly ILogger<SettingsService> _logger;

    /// <summary>Prefix applied to all config secret keys in Bitwarden.</summary>
    private const string KeyPrefix = "ots-config/";

    // ── Category constants ─────────────────────────────────────────────────
    public const string CatGit = "Git";
    public const string CatMySql = "MySql";
    public const string CatSmtp = "Smtp";
    public const string CatPangolin = "Pangolin";
    public const string CatNfs = "Nfs";
    public const string CatDefaults = "Defaults";
    public const string CatAuthentik = "Authentik";

    // ── Key constants ──────────────────────────────────────────────────────
    // Git
    public const string GitRepoUrl = "Git.RepoUrl";
    public const string GitRepoPat = "Git.RepoPat";

    // MySQL Admin
    public const string MySqlHost = "MySql.Host";
    public const string MySqlPort = "MySql.Port";
    public const string MySqlAdminUser = "MySql.AdminUser";
    public const string MySqlAdminPassword = "MySql.AdminPassword";

    // SMTP
    public const string SmtpServer = "Smtp.Server";
    public const string SmtpPort = "Smtp.Port";
    public const string SmtpUsername = "Smtp.Username";
    public const string SmtpPassword = "Smtp.Password";
    public const string SmtpUseTls = "Smtp.UseTls";
    public const string SmtpUseStartTls = "Smtp.UseStartTls";
    public const string SmtpRewriteDomain = "Smtp.RewriteDomain";
    public const string SmtpHostname = "Smtp.Hostname";
    public const string SmtpFromLineOverride = "Smtp.FromLineOverride";

    // Pangolin
    public const string PangolinEndpoint = "Pangolin.Endpoint";

    // NFS
    public const string NfsServer = "Nfs.Server";
    public const string NfsExport = "Nfs.Export";
    public const string NfsExportFolder = "Nfs.ExportFolder";
    public const string NfsOptions = "Nfs.Options";

    // Instance Defaults
    public const string DefaultCmsImage = "Defaults.CmsImage";
    public const string DefaultNewtImage = "Defaults.NewtImage";
    public const string DefaultMemcachedImage = "Defaults.MemcachedImage";
    public const string DefaultQuickChartImage = "Defaults.QuickChartImage";
    public const string DefaultCmsServerNameTemplate = "Defaults.CmsServerNameTemplate";
    public const string DefaultThemeHostPath = "Defaults.ThemeHostPath";
    public const string DefaultMySqlDbTemplate = "Defaults.MySqlDbTemplate";
    public const string DefaultMySqlUserTemplate = "Defaults.MySqlUserTemplate";
    public const string DefaultPhpPostMaxSize = "Defaults.PhpPostMaxSize";
    public const string DefaultPhpUploadMaxFilesize = "Defaults.PhpUploadMaxFilesize";
    public const string DefaultPhpMaxExecutionTime = "Defaults.PhpMaxExecutionTime";

    // Xibo bootstrap OAuth2 credentials (one-time global setup for orchestrator API access)
    public const string CatXibo = "Xibo";
    public const string XiboBootstrapClientId     = "Xibo.BootstrapClientId";
    public const string XiboBootstrapClientSecret = "Xibo.BootstrapClientSecret";

    // Authentik (SAML IdP provisioning)
    public const string AuthentikUrl                   = "Authentik.Url";
    public const string AuthentikApiKey                = "Authentik.ApiKey";
    public const string AuthentikAuthorizationFlowSlug = "Authentik.AuthorizationFlowSlug";
    public const string AuthentikInvalidationFlowSlug  = "Authentik.InvalidationFlowSlug";
    public const string AuthentikSigningKeypairId      = "Authentik.SigningKeypairId";

    // Instance-specific (keyed by abbreviation)
    /// <summary>
    /// Builds a per-instance settings key for the MySQL password.
    /// </summary>
    public static string InstanceMySqlPassword(string abbrev)         => $"Instance.{abbrev}.MySqlPassword";
    /// <summary>Bitwarden secret ID for the instance's OTS admin password.</summary>
    public static string InstanceAdminPasswordSecretId(string abbrev) => $"Instance.{abbrev}.AdminPasswordBwsId";
    /// <summary>Bitwarden secret ID for the instance's Xibo OAuth2 client secret.</summary>
    public static string InstanceOAuthSecretId(string abbrev)         => $"Instance.{abbrev}.OAuthSecretBwsId";
    /// <summary>Xibo OAuth2 client_id generated for this instance's OTS application.</summary>
    public static string InstanceOAuthClientId(string abbrev)         => $"Instance.{abbrev}.OAuthClientId";
    public const string CatInstance = "Instance";

    // ── In-memory cache of secrets (loaded on first access) ────────────────
    // Maps Bitwarden secret key (with prefix) → (id, value)
    // Static so the cache is shared across all transient SettingsService instances.
    private static Dictionary<string, (string Id, string Value)>? s_cache;

    public SettingsService(
        IBitwardenSecretService bws,
        ILogger<SettingsService> logger)
    {
        _bws = bws;
        _logger = logger;
    }

    /// <summary>Get a single setting value from Bitwarden.</summary>
    public async Task<string?> GetAsync(string key)
    {
        var cache = await EnsureCacheAsync();
        var bwKey = KeyPrefix + key;
        if (!cache.TryGetValue(bwKey, out var entry))
            return null;
        // Treat single-space sentinel as empty (used to work around SDK marshalling limitation)
        return string.IsNullOrWhiteSpace(entry.Value) ? null : entry.Value;
    }

    /// <summary>Get a setting with a fallback default.</summary>
    public async Task<string> GetAsync(string key, string defaultValue)
        => await GetAsync(key) ?? defaultValue;

    /// <summary>Set a single setting in Bitwarden (creates or updates).</summary>
    public async Task SetAsync(string key, string? value, string category, bool isSensitive = false)
    {
        var cache = await EnsureCacheAsync();
        var bwKey = KeyPrefix + key;
        var note  = $"{category}|{(isSensitive ? "sensitive" : "plain")}";
        // Use a single space for empty/null values — the Bitwarden SDK native FFI
        // cannot marshal empty strings reliably.
        var safeValue = string.IsNullOrEmpty(value) ? " " : value;

        if (cache.TryGetValue(bwKey, out var existing))
        {
            // Update existing secret
            await _bws.UpdateSecretAsync(existing.Id, bwKey, safeValue, note);
            cache[bwKey] = (existing.Id, safeValue);
            s_cache = cache;
        }
        else if (!string.IsNullOrWhiteSpace(value))
        {
            // Only create new secrets when there is an actual value to store
            var newId = await _bws.CreateSecretAsync(bwKey, safeValue, note);
            cache[bwKey] = (newId, safeValue);
            s_cache = cache;
        }
    }

    /// <summary>Save multiple settings in a batch.</summary>
    public async Task SaveManyAsync(IEnumerable<(string Key, string? Value, string Category, bool IsSensitive)> settings)
    {
        var count = 0;
        var errors = new List<string>();
        foreach (var (key, value, category, isSensitive) in settings)
        {
            try
            {
                await SetAsync(key, value, category, isSensitive);
                count++;
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to save setting {Key}", key);
                errors.Add(key);
            }
        }

        _logger.LogInformation("Saved {Count} setting(s) to Bitwarden", count);

        if (errors.Count > 0)
            throw new AggregateException(
                $"Failed to save {errors.Count} setting(s): {string.Join(", ", errors)}");
    }

    /// <summary>Get all settings in a category (by examining cached keys).</summary>
    public async Task<Dictionary<string, string?>> GetCategoryAsync(string category)
    {
        var cache = await EnsureCacheAsync();
        var prefix = KeyPrefix + category + ".";
        var result = new Dictionary<string, string?>();

        foreach (var (bwKey, entry) in cache)
        {
            if (bwKey.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                // Strip the "ots-config/" prefix to return the original key
                var originalKey = bwKey[KeyPrefix.Length..];
                result[originalKey] = entry.Value;
            }
        }

        return result;
    }

    /// <summary>
    /// Invalidates the in-memory cache so next access re-fetches from Bitwarden.
    /// </summary>
    public void InvalidateCache() => s_cache = null;

    /// <summary>
    /// Pre-loads the settings cache from Bitwarden.
    /// Call once at startup so settings are available immediately.
    /// </summary>
    public async Task PreloadCacheAsync()
    {
        InvalidateCache();
        await EnsureCacheAsync();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Cache management
    // ─────────────────────────────────────────────────────────────────────────

    private async Task<Dictionary<string, (string Id, string Value)>> EnsureCacheAsync()
    {
        if (s_cache is not null)
            return s_cache;

        var cache = new Dictionary<string, (string, string)>(StringComparer.OrdinalIgnoreCase);

        // Skip loading if Bitwarden is not yet configured (normal on first run)
        if (!await _bws.IsConfiguredAsync())
        {
            _logger.LogInformation("Bitwarden is not configured yet — settings will be available after setup");
            s_cache = cache;
            return s_cache;
        }

        try
        {
            // List all secrets, then fetch full value for those matching our prefix
            var summaries = await _bws.ListSecretsAsync();
            var configSecrets = summaries
                .Where(s => s.Key.StartsWith(KeyPrefix, StringComparison.OrdinalIgnoreCase))
                .ToList();

            _logger.LogInformation("Loading {Count} config secrets from Bitwarden", configSecrets.Count);

            foreach (var summary in configSecrets)
            {
                try
                {
                    var full = await _bws.GetSecretAsync(summary.Id);
                    cache[full.Key] = (full.Id, full.Value);
                }
                catch (Exception ex)
                {
                    _logger.LogWarning(ex, "Failed to load secret {Key} ({Id})", summary.Key, summary.Id);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load settings from Bitwarden — settings will be empty until Bitwarden is configured");
        }

        s_cache = cache;
        return s_cache;
    }
}
