Functions/GenXdev.Helpers/PSGenXdevCmdlet.KeyValueStore.cs

// ################################################################################
// Part of PowerShell module : GenXdev.Helpers
// Original cmdlet filename : PSGenXdevCmdlet.KeyValueStore.cs
// Original author : René Vaessen / GenXdev
// Version : 3.28.2026
// ################################################################################
// Copyright (c) 2026 René Vaessen / GenXdev
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ################################################################################



using System.Collections;
using System.IO;
using System.Management.Automation;

public abstract partial class PSGenXdevCmdlet : PSCmdlet
{
    /// <summary>
    /// <para type="synopsis">
    /// Provides local key-value store functionality for PowerShell cmdlets in the
    /// GenXdev framework.
    /// </para>
    ///
    /// <para type="description">
    /// This partial class extends the base PSGenXdevCmdlet to include methods for
    /// managing persistent local key-value stores using JSON files. Stores are
    /// organized by store names with automatic directory initialization.
    /// </para>
    ///
    /// <para type="description">
    /// Key features include:
    /// - Persistent storage of key-value pairs with metadata (last modified, user,
    /// deletion status)
    /// - Automatic initialization of store directories
    /// - Atomic writes with retry logic
    /// - Store enumeration and key queries
    /// </para>
    /// </summary>

    /// <summary>
    /// Retrieves an array of unique store names from the local key-value store
    /// directory.
    /// </summary>
    /// <param name="DatabasePath">
    /// Optional custom path for the key-value store database. If not provided,
    /// uses the default GenXdev application data path.
    /// </param>
    /// <returns>An array of unique store names.</returns>
    protected string[] GetKeyValueStoreNames(string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose($"Using KeyValueStore directory: {basePath}");

        // Ensure store directory exists
        if (!System.IO.Directory.Exists(basePath))
        {
            WriteVerbose("Store directory not found, creating...");
            ExpandPath(basePath, CreateDirectory: true);
        }

        WriteVerbose("Scanning for stores");

        // Get all JSON files in the store directory
        var jsonFiles = new System.Collections.Generic.List<string>();
        try
        {
            var files = System.IO.Directory.GetFiles(basePath, "*.json");
            foreach (var file in files)
            {
                jsonFiles.Add(System.IO.Path.GetFileName(file));
            }
        }
        catch (System.IO.DirectoryNotFoundException)
        {
            // Directory doesn't exist, return empty list
        }

        // Create dictionary to collect unique store names
        var storeNames = new System.Collections.Generic.Dictionary<string, bool>();

        // Parse filenames to extract store names
        foreach (var fileName in jsonFiles)
        {
            // Filename format: Local_StoreName.json
            var match = System.Text.RegularExpressions.Regex.Match(fileName,
                @"^(.+?)_(.+?)\.json$");
            if (match.Success)
            {
                // Extract the store name from the filename
                var fileStoreName = match.Groups[2].Value;

                // Add to unique store names collection
                if (!storeNames.ContainsKey(fileStoreName))
                {
                    storeNames[fileStoreName] = true;
                }
            }
        }

        // Return sorted unique store names
        return storeNames.Keys.OrderBy(name => name).ToArray();
    }

    /// <summary>
    /// Constructs the full file path for a key-value store based on synchronization key and store name.
    /// </summary>
    /// <param name="SynchronizationKey">
    /// The synchronization key for the store (e.g., "Local", "Global").
    /// </param>
    /// <param name="StoreName">
    /// The name of the key-value store.
    /// </param>
    /// <param name="BasePath">
    /// Optional base path for the store. If not provided, uses the default GenXdev app data path.
    /// </param>
    /// <returns>The full file path to the JSON store file.</returns>
    protected string GetKeyValueStorePath(string SynchronizationKey, string StoreName,
        string BasePath = null)
    {
        // Use default path if not provided
        if (string.IsNullOrWhiteSpace(BasePath))
        {
            BasePath = GetGenXdevAppDataPath("KeyValueStore");
        }

        WriteVerbose($"Constructing store file path for store '{StoreName}' with sync key " +
            $"'{SynchronizationKey}'");

        // Sanitize the sync key to remove invalid filename characters
        string safeSyncKey = System.Text.RegularExpressions.Regex.Replace(SynchronizationKey,
            @"[\\/:*?""<>|]", "_");

        // Sanitize the store name to remove invalid filename characters
        string safeStoreName = System.Text.RegularExpressions.Regex.Replace(StoreName,
            @"[\\/:*?""<>|]", "_");

        // Construct the filename by combining safe sync key and store name
        string filename = $"{safeSyncKey}_{safeStoreName}.json";

        // Return the full path by combining base path with filename
        return System.IO.Path.Combine(BasePath, filename);
    }

    /// <summary>
    /// Retrieves all active (non-deleted) keys from a local key-value store.
    /// </summary>
    /// <param name="StoreName">
    /// The name of the key-value store to query.
    /// </param>
    /// <param name="DatabasePath">
    /// Optional custom database path. Uses default if not specified.
    /// </param>
    /// <returns>An array of active key names in the store.</returns>
    protected string[] GetStoreKeys(string StoreName, string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose($"Using KeyValueStore directory: {basePath}");

        // Ensure store directory exists
        if (!System.IO.Directory.Exists(basePath))
        {
            WriteVerbose("Store directory not found, creating...");
            ExpandPath(basePath, CreateDirectory: true);
        }

        // Get the local store file path
        string storeFilePath = GetKeyValueStorePath("Local", StoreName, basePath);

        WriteVerbose($"Querying keys from store file: {storeFilePath}");

        // Read the JSON store data with retry logic
        var storeData = (Hashtable)ReadJsonWithRetry(storeFilePath, asHashtable: true);

        var keys = new System.Collections.Generic.List<string>();

        // Return active (non-deleted) key names
        foreach (string keyName in storeData.Keys)
        {
            var entry = storeData[keyName];

            // Check if entry has metadata structure
            if (entry is Hashtable hashtable && hashtable.ContainsKey("deletedDate"))
            {
                // Entry has metadata, check if not deleted
                if (hashtable["deletedDate"] == null)
                {
                    keys.Add(keyName);
                }
            }
            else
            {
                // Legacy format without metadata, return key name
                keys.Add(keyName);
            }
        }

        return keys.ToArray();
    }

    /// <summary>
    /// Retrieves the value associated with a specific key from a local
    /// key-value store.
    /// </summary>
    /// <param name="StoreName">
    /// The name of the key-value store.
    /// </param>
    /// <param name="KeyName">
    /// The key whose value to retrieve.
    /// </param>
    /// <param name="DefaultValue">
    /// The default value to return if the key is not found or is deleted.
    /// </param>
    /// <param name="DatabasePath">
    /// Optional custom database path.
    /// </param>
    /// <returns>The value associated with the key, or the default value if not found.</returns>
    protected object GetValueByKeyFromStore(string StoreName, string KeyName,
        string DefaultValue = null, string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose($"Using KeyValueStore directory: {basePath}");

        // Ensure store directory exists
        if (!System.IO.Directory.Exists(basePath))
        {
            WriteVerbose("Store directory not found, creating...");
            ExpandPath(basePath, CreateDirectory: true);
        }

        // Get JSON file path for this store
        string storeFilePath = GetKeyValueStorePath("Local", StoreName, basePath);

        // Log the query operation details
        WriteVerbose($"Querying store '{StoreName}' for key '{KeyName}' at: {storeFilePath}");

        // Read the JSON store data with retry logic
        var storeData = (Hashtable)ReadJsonWithRetry(storeFilePath, asHashtable: true);

        // Check if key exists and is not deleted
        if (storeData.ContainsKey(KeyName))
        {
            var entry = storeData[KeyName];

            // Check if entry has metadata structure
            if (entry is Hashtable hashtable && hashtable.ContainsKey("deletedDate"))
            {
                // Entry has metadata, check if deleted
                if (hashtable["deletedDate"] == null || hashtable["deletedDate"].ToString() == "")
                {
                    // Log successful value retrieval
                    WriteVerbose("Value found");

                    // Return the value from the entry
                    return hashtable["value"];
                }
            }
            else if (entry is Hashtable hashtable2)
            {
                // Return the value from the entry
                return hashtable2["value"];
            }
            else
            {
                // Legacy format without metadata, return directly
                WriteVerbose("Value found (legacy format)");
                return entry;
            }
        }

        // Log fallback to default value
        WriteVerbose("No value found, returning default");

        // Return the specified default value
        return DefaultValue;
    }

    /// <summary>
    /// Removes a key from a local key-value store by marking it as deleted.
    /// </summary>
    /// <param name="StoreName">
    /// The name of the key-value store.
    /// </param>
    /// <param name="KeyName">
    /// The key to remove.
    /// </param>
    /// <param name="DatabasePath">
    /// Optional custom database path.
    /// </param>
    protected void RemoveKeyFromStore(string StoreName, string KeyName,
        string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose($"Using KeyValueStore directory: {basePath}");

        // Ensure store directory exists
        if (!System.IO.Directory.Exists(basePath))
        {
            WriteVerbose("Store directory not found, creating...");
            ExpandPath(basePath, CreateDirectory: true);
        }

        // Get current user info for audit trail
        string computerName = Environment.GetEnvironmentVariable("COMPUTERNAME");
        string userName = Environment.GetEnvironmentVariable("USERNAME");
        string lastModifiedBy = $"{computerName}\\{userName}";

        WriteVerbose($"Preparing to remove key '{KeyName}' from store '{StoreName}'");

        // Get JSON file path for this store
        string storeFilePath = GetKeyValueStorePath("Local", StoreName, basePath);

        // Read existing store data with retry logic
        var storeData = (Hashtable)ReadJsonWithRetry(storeFilePath, asHashtable: true);

        // Check if key exists
        if (storeData.ContainsKey(KeyName))
        {
            // Mark as deleted
            WriteVerbose("Marking key as deleted");

            var keyValue = storeData[KeyName];
            if (keyValue is Hashtable keyHashtable)
            {
                keyHashtable["deletedDate"] = DateTime.UtcNow.ToString("o");
                keyHashtable["lastModified"] = DateTime.UtcNow.ToString("o");
                keyHashtable["lastModifiedBy"] = lastModifiedBy;
            }
            else
            {
                // Legacy format, convert to new format with deletion
                var newValue = new Hashtable
                {
                    ["value"] = keyValue,
                    ["lastModified"] = DateTime.UtcNow.ToString("o"),
                    ["lastModifiedBy"] = lastModifiedBy,
                    ["deletedDate"] = DateTime.UtcNow.ToString("o")
                };
                storeData[KeyName] = newValue;
            }

            // Write updated store data atomically with retry logic
            WriteJsonAtomic(storeFilePath, storeData);
        }
        else
        {
            WriteVerbose($"Key '{KeyName}' not found in store '{StoreName}'");
        }
    }

    /// <summary>
    /// Removes an entire local key-value store by physically deleting its
    /// JSON file.
    /// </summary>
    /// <param name="StoreName">
    /// The name of the store to remove.
    /// </param>
    /// <param name="DatabasePath">
    /// Optional custom database path.
    /// </param>
    protected void RemoveKeyValueStore(string StoreName, string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose($"Using KeyValueStore directory: {basePath}");

        // Get JSON file path for this store
        string storeFilePath = GetKeyValueStorePath("Local", StoreName, basePath);

        // Physically remove the file
        if (System.IO.File.Exists(storeFilePath))
        {
            WriteVerbose($"Permanently deleting store file: {storeFilePath}");
            System.IO.File.Delete(storeFilePath);
        }
        else
        {
            WriteVerbose($"Store file not found: {storeFilePath}");
        }
    }

    /// <summary>
    /// Sets or updates a value for a specific key in a local key-value store.
    /// </summary>
    /// <param name="StoreName">
    /// The name of the key-value store.
    /// </param>
    /// <param name="KeyName">
    /// The key to set or update.
    /// </param>
    /// <param name="Value">
    /// The value to store.
    /// </param>
    /// <param name="DatabasePath">
    /// Optional custom database path.
    /// </param>
    protected void SetValueByKeyInStore(string StoreName, string KeyName, string Value,
        string DatabasePath = null)
    {
        // Determine base path
        string basePath = string.IsNullOrWhiteSpace(DatabasePath) ?
            GetGenXdevAppDataPath("KeyValueStore") : DatabasePath;

        WriteVerbose("Using KeyValueStore directory: " + basePath);

        // Ensure store directory exists
        if (!System.IO.Directory.Exists(basePath))
        {
            WriteVerbose("Store directory not found. Creating...");
            ExpandPath(basePath, CreateDirectory: true);
        }

        // Get current user identity for audit trail purposes
        string lastModifiedBy = Environment.MachineName + "\\" + Environment.UserName;

        WriteVerbose("Setting value as user: " + lastModifiedBy);

        WriteVerbose("Executing upsert for key '" + KeyName + "' in store '" + StoreName + "'");

        // Get JSON file path for this store
        string storeFilePath = GetKeyValueStorePath("Local", StoreName, basePath);

        // Read existing store data with retry logic
        var storeData = (Hashtable)ReadJsonWithRetry(storeFilePath, asHashtable: true);

        // Create or update the entry with metadata
        var entry = new Hashtable
        {
            ["value"] = Value,
            ["lastModified"] = DateTime.UtcNow.ToString("o"),
            ["lastModifiedBy"] = lastModifiedBy,
            ["deletedDate"] = null
        };

        storeData[KeyName] = entry;

        // Write updated store data atomically with retry logic
        WriteJsonAtomic(storeFilePath, storeData);
    }

}