modules/Core/ConfigurationManager.psm1

# Configuration Manager Module
# Provides centralized configuration management for Spotify CLI live features

# Configuration schema definition
class ConfigurationSchema {
    [hashtable] $Schema = @{
        liveDisplay = @{
            refreshInterval = @{ Type = "int"; Min = 500; Max = 5000; Default = 1000 }
            displayMode = @{ Type = "string"; ValidValues = @("detailed", "compact", "minimal"); Default = "detailed" }
            sidecarPosition = @{ Type = "string"; ValidValues = @("left", "right", "top", "bottom"); Default = "right" }
            sidecarWidth = @{ Type = "int"; Min = 20; Max = 100; Default = 40 }
            showAlbumArt = @{ Type = "bool"; Default = $true }
            colorScheme = @{ Type = "string"; ValidValues = @("auto", "light", "dark"); Default = "auto" }
            useAnsiEscapes = @{ Type = "bool"; Default = $true }
        }
        lyrics = @{
            preferredProvider = @{ Type = "string"; ValidValues = @("genius", "musixmatch", "mock"); Default = "mock" }
            cacheEnabled = @{ Type = "bool"; Default = $true }
            syncHighlighting = @{ Type = "bool"; Default = $true }
            scrollSpeed = @{ Type = "int"; Min = 1; Max = 10; Default = 3 }
            cacheDirectory = @{ Type = "string"; Default = "" }
            maxCacheAgeDays = @{ Type = "int"; Min = 1; Max = 365; Default = 30 }
            geniusApiKey = @{ Type = "string"; Default = "" }
        }
        statistics = @{
            trackingEnabled = @{ Type = "bool"; Default = $true }
            retentionDays = @{ Type = "int"; Min = 30; Max = 1095; Default = 365 }
            exportFormat = @{ Type = "string"; ValidValues = @("json", "csv"); Default = "json" }
            defaultPeriod = @{ Type = "string"; ValidValues = @("day", "week", "month", "year"); Default = "month" }
            dataDirectory = @{ Type = "string"; Default = "" }
            autoCleanup = @{ Type = "bool"; Default = $true }
        }
        apiClient = @{
            maxRequestsPerMinute = @{ Type = "int"; Min = 10; Max = 100; Default = 60 }
            timeoutMs = @{ Type = "int"; Min = 5000; Max = 30000; Default = 10000 }
            retryAttempts = @{ Type = "int"; Min = 1; Max = 5; Default = 3 }
            cacheEnabled = @{ Type = "bool"; Default = $true }
            cacheDurationMs = @{ Type = "int"; Min = 1000; Max = 300000; Default = 60000 }
        }
    }
    
    [bool] ValidateValue([string]$section, [string]$key, $value) {
        if (-not $this.Schema.ContainsKey($section)) {
            return $false
        }
        
        $sectionSchema = $this.Schema[$section]
        if (-not $sectionSchema.ContainsKey($key)) {
            return $false
        }
        
        $keySchema = $sectionSchema[$key]
        
        switch ($keySchema.Type) {
            "int" {
                if ($value -isnot [int]) {
                    return $false
                }
                if ($keySchema.ContainsKey('Min') -and $value -lt $keySchema.Min) {
                    return $false
                }
                if ($keySchema.ContainsKey('Max') -and $value -gt $keySchema.Max) {
                    return $false
                }
                return $true
            }
            "bool" {
                return $value -is [bool]
            }
            "string" {
                if ($value -isnot [string]) {
                    return $false
                }
                if ($keySchema.ContainsKey('ValidValues') -and $value -notin $keySchema.ValidValues) {
                    return $false
                }
                return $true
            }
            default {
                return $true
            }
        }
    }
    
    [hashtable] GetDefaultConfiguration() {
        $config = @{}
        
        foreach ($section in $this.Schema.Keys) {
            $config[$section] = @{}
            foreach ($key in $this.Schema[$section].Keys) {
                $config[$section][$key] = $this.Schema[$section][$key].Default
            }
        }
        
        return $config
    }
    
    [string[]] GetValidationErrors([hashtable]$config) {
        $errors = @()
        
        foreach ($section in $config.Keys) {
            if (-not $this.Schema.ContainsKey($section)) {
                $errors += "Unknown configuration section: $section"
                continue
            }
            
            foreach ($key in $config[$section].Keys) {
                if (-not $this.Schema[$section].ContainsKey($key)) {
                    $errors += "Unknown configuration key: $section.$key"
                    continue
                }
                
                if (-not $this.ValidateValue($section, $key, $config[$section][$key])) {
                    $keySchema = $this.Schema[$section][$key]
                    $errors += "Invalid value for $section.$key : Expected $($keySchema.Type)"
                    
                    if ($keySchema.ContainsKey('ValidValues')) {
                        $errors += " Valid values: $($keySchema.ValidValues -join ', ')"
                    }
                    if ($keySchema.ContainsKey('Min') -and $keySchema.ContainsKey('Max')) {
                        $errors += " Valid range: $($keySchema.Min) - $($keySchema.Max)"
                    }
                }
            }
        }
        
        return $errors
    }
}

# Configuration manager class
class ConfigurationManager {
    [string] $ConfigurationFile
    [string] $ConfigurationDirectory
    [ConfigurationSchema] $Schema
    [hashtable] $CurrentConfiguration
    [hashtable] $DefaultConfiguration
    [bool] $IsLoaded = $false
    
    ConfigurationManager([string]$configDir) {
        $this.ConfigurationDirectory = $configDir
        $this.ConfigurationFile = Join-Path $configDir "live_features_config.json"
        $this.Schema = [ConfigurationSchema]::new()
        $this.DefaultConfiguration = $this.Schema.GetDefaultConfiguration()
        $this.CurrentConfiguration = $this.DefaultConfiguration.Clone()
        
        $this.EnsureConfigurationDirectory()
    }
    
    [void] EnsureConfigurationDirectory() {
        if (-not (Test-Path $this.ConfigurationDirectory)) {
            New-Item -ItemType Directory -Path $this.ConfigurationDirectory -Force | Out-Null
        }
    }
    
    [void] LoadConfiguration() {
        try {
            if (Test-Path $this.ConfigurationFile) {
                $json = Get-Content -Path $this.ConfigurationFile -Raw -Encoding UTF8
                $loadedConfig = $json | ConvertFrom-Json
                
                # Convert PSCustomObject to hashtable
                $config = $this.ConvertToHashtable($loadedConfig)
                
                # Validate configuration
                $errors = $this.Schema.GetValidationErrors($config)
                if ($errors.Count -gt 0) {
                    Write-Warning "Configuration validation errors found:"
                    foreach ($error in $errors) {
                        Write-Warning " $error"
                    }
                    Write-Warning "Using default configuration for invalid settings"
                }
                
                # Merge with defaults (defaults take precedence for invalid values)
                $this.CurrentConfiguration = $this.MergeConfigurations($this.DefaultConfiguration, $config)
            } else {
                # Create default configuration file
                $this.SaveConfiguration()
            }
            
            $this.IsLoaded = $true
        } catch {
            Write-Warning "Failed to load configuration: $($_.Exception.Message)"
            Write-Warning "Using default configuration"
            $this.CurrentConfiguration = $this.DefaultConfiguration.Clone()
            $this.IsLoaded = $true
        }
    }
    
    [hashtable] ConvertToHashtable([PSCustomObject]$obj) {
        $hashtable = @{}
        
        foreach ($property in $obj.PSObject.Properties) {
            if ($property.Value -is [PSCustomObject]) {
                $hashtable[$property.Name] = $this.ConvertToHashtable($property.Value)
            } else {
                $hashtable[$property.Name] = $property.Value
            }
        }
        
        return $hashtable
    }
    
    [hashtable] MergeConfigurations([hashtable]$default, [hashtable]$override) {
        $merged = $default.Clone()
        
        foreach ($section in $override.Keys) {
            if ($merged.ContainsKey($section)) {
                foreach ($key in $override[$section].Keys) {
                    if ($merged[$section].ContainsKey($key)) {
                        # Validate the override value
                        if ($this.Schema.ValidateValue($section, $key, $override[$section][$key])) {
                            $merged[$section][$key] = $override[$section][$key]
                        }
                    }
                }
            }
        }
        
        return $merged
    }
    
    [void] SaveConfiguration() {
        try {
            $json = $this.CurrentConfiguration | ConvertTo-Json -Depth 10
            Set-Content -Path $this.ConfigurationFile -Value $json -Encoding UTF8
        } catch {
            Write-Warning "Failed to save configuration: $($_.Exception.Message)"
        }
    }
    
    [hashtable] GetConfiguration() {
        if (-not $this.IsLoaded) {
            $this.LoadConfiguration()
        }
        return $this.CurrentConfiguration.Clone()
    }
    
    [hashtable] GetSection([string]$section) {
        $config = $this.GetConfiguration()
        if ($config.ContainsKey($section)) {
            return $config[$section].Clone()
        }
        return @{}
    }
    
    [$value] GetValue([string]$section, [string]$key) {
        $sectionConfig = $this.GetSection($section)
        if ($sectionConfig.ContainsKey($key)) {
            return $sectionConfig[$key]
        }
        
        # Return default value if not found
        if ($this.DefaultConfiguration.ContainsKey($section) -and 
            $this.DefaultConfiguration[$section].ContainsKey($key)) {
            return $this.DefaultConfiguration[$section][$key]
        }
        
        return $null
    }
    
    [void] SetValue([string]$section, [string]$key, $value) {
        if (-not $this.IsLoaded) {
            $this.LoadConfiguration()
        }
        
        # Validate the value
        if (-not $this.Schema.ValidateValue($section, $key, $value)) {
            throw [System.ArgumentException]::new("Invalid value for $section.$key")
        }
        
        # Ensure section exists
        if (-not $this.CurrentConfiguration.ContainsKey($section)) {
            $this.CurrentConfiguration[$section] = @{}
        }
        
        $this.CurrentConfiguration[$section][$key] = $value
        $this.SaveConfiguration()
    }
    
    [void] SetSection([string]$section, [hashtable]$sectionConfig) {
        if (-not $this.IsLoaded) {
            $this.LoadConfiguration()
        }
        
        # Validate all values in the section
        foreach ($key in $sectionConfig.Keys) {
            if (-not $this.Schema.ValidateValue($section, $key, $sectionConfig[$key])) {
                throw [System.ArgumentException]::new("Invalid value for $section.$key")
            }
        }
        
        $this.CurrentConfiguration[$section] = $sectionConfig.Clone()
        $this.SaveConfiguration()
    }
    
    [void] ResetToDefaults() {
        $this.CurrentConfiguration = $this.DefaultConfiguration.Clone()
        $this.SaveConfiguration()
    }
    
    [void] ResetSection([string]$section) {
        if ($this.DefaultConfiguration.ContainsKey($section)) {
            $this.CurrentConfiguration[$section] = $this.DefaultConfiguration[$section].Clone()
            $this.SaveConfiguration()
        }
    }
    
    [hashtable] GetConfigurationInfo() {
        return @{
            ConfigurationFile = $this.ConfigurationFile
            ConfigurationDirectory = $this.ConfigurationDirectory
            IsLoaded = $this.IsLoaded
            HasCustomizations = $this.HasCustomizations()
            LastModified = if (Test-Path $this.ConfigurationFile) { 
                (Get-Item $this.ConfigurationFile).LastWriteTime 
            } else { 
                $null 
            }
        }
    }
    
    [bool] HasCustomizations() {
        if (-not $this.IsLoaded) {
            return $false
        }
        
        # Compare current config with defaults
        return -not $this.ConfigurationsEqual($this.CurrentConfiguration, $this.DefaultConfiguration)
    }
    
    [bool] ConfigurationsEqual([hashtable]$config1, [hashtable]$config2) {
        if ($config1.Keys.Count -ne $config2.Keys.Count) {
            return $false
        }
        
        foreach ($section in $config1.Keys) {
            if (-not $config2.ContainsKey($section)) {
                return $false
            }
            
            $section1 = $config1[$section]
            $section2 = $config2[$section]
            
            if ($section1.Keys.Count -ne $section2.Keys.Count) {
                return $false
            }
            
            foreach ($key in $section1.Keys) {
                if (-not $section2.ContainsKey($key) -or $section1[$key] -ne $section2[$key]) {
                    return $false
                }
            }
        }
        
        return $true
    }
    
    [string] ExportConfiguration([string]$format) {
        $config = $this.GetConfiguration()
        
        switch ($format.ToLower()) {
            "json" {
                return $config | ConvertTo-Json -Depth 10
            }
            "yaml" {
                # Simple YAML export (basic implementation)
                $yaml = @()
                foreach ($section in $config.Keys) {
                    $yaml += "$section :"
                    foreach ($key in $config[$section].Keys) {
                        $value = $config[$section][$key]
                        if ($value -is [string]) {
                            $yaml += " $key : `"$value`""
                        } else {
                            $yaml += " $key : $value"
                        }
                    }
                    $yaml += ""
                }
                return $yaml -join "`n"
            }
            default {
                throw [System.ArgumentException]::new("Unsupported export format: $format")
            }
        }
    }
    
    [void] ImportConfiguration([string]$configData, [string]$format) {
        try {
            switch ($format.ToLower()) {
                "json" {
                    $importedConfig = $configData | ConvertFrom-Json
                    $config = $this.ConvertToHashtable($importedConfig)
                    
                    # Validate imported configuration
                    $errors = $this.Schema.GetValidationErrors($config)
                    if ($errors.Count -gt 0) {
                        throw [System.ArgumentException]::new("Invalid configuration: $($errors -join '; ')")
                    }
                    
                    $this.CurrentConfiguration = $this.MergeConfigurations($this.DefaultConfiguration, $config)
                    $this.SaveConfiguration()
                }
                default {
                    throw [System.ArgumentException]::new("Unsupported import format: $format")
                }
            }
        } catch {
            throw [System.InvalidOperationException]::new("Failed to import configuration: $($_.Exception.Message)")
        }
    }
    
    [void] MigrateConfiguration([string]$fromVersion, [string]$toVersion) {
        <#
        .SYNOPSIS
        Migrate configuration from one version to another
        .DESCRIPTION
        Handles configuration migration between different versions of the live features
        #>

        try {
            Write-Verbose "Migrating configuration from version $fromVersion to $toVersion"
            
            # Create backup before migration
            $backupFile = Join-Path $this.ConfigurationDirectory "config_backup_pre_migration_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
            $currentJson = $this.CurrentConfiguration | ConvertTo-Json -Depth 10
            Set-Content -Path $backupFile -Value $currentJson -Encoding UTF8
            Write-Verbose "Configuration backed up to: $backupFile"
            
            # Version-specific migration logic
            switch ("$fromVersion->$toVersion") {
                "1.0->1.1" {
                    # Example migration: Add new default values
                    if (-not $this.CurrentConfiguration.ContainsKey('apiClient')) {
                        $this.CurrentConfiguration['apiClient'] = $this.DefaultConfiguration['apiClient'].Clone()
                    }
                }
                "1.1->1.2" {
                    # Example migration: Rename or restructure settings
                    if ($this.CurrentConfiguration['liveDisplay'].ContainsKey('oldSetting')) {
                        $this.CurrentConfiguration['liveDisplay']['newSetting'] = $this.CurrentConfiguration['liveDisplay']['oldSetting']
                        $this.CurrentConfiguration['liveDisplay'].Remove('oldSetting')
                    }
                }
                default {
                    # Generic migration: merge with new defaults
                    $this.CurrentConfiguration = $this.MergeConfigurations($this.DefaultConfiguration, $this.CurrentConfiguration)
                }
            }
            
            # Add version metadata
            $this.CurrentConfiguration['_metadata'] = @{
                version = $toVersion
                migratedAt = [DateTimeOffset]::UtcNow.ToString('o')
                migratedFrom = $fromVersion
                backupFile = $backupFile
            }
            
            $this.SaveConfiguration()
            Write-Verbose "Configuration migration completed successfully"
        }
        catch {
            Write-Warning "Configuration migration failed: $($_.Exception.Message)"
            throw
        }
    }
    
    [string] GetConfigurationVersion() {
        <#
        .SYNOPSIS
        Get the current configuration version
        #>

        if ($this.CurrentConfiguration.ContainsKey('_metadata') -and 
            $this.CurrentConfiguration['_metadata'].ContainsKey('version')) {
            return $this.CurrentConfiguration['_metadata']['version']
        }
        return "1.0"  # Default version for configurations without metadata
    }
    
    [bool] RequiresMigration([string]$currentVersion) {
        <#
        .SYNOPSIS
        Check if configuration requires migration
        #>

        $configVersion = $this.GetConfigurationVersion()
        return $configVersion -ne $currentVersion
    }
}

# Configuration factory for creating managers
class ConfigurationFactory {
    static [ConfigurationManager] CreateManager([string]$configDir) {
        return [ConfigurationManager]::new($configDir)
    }
    
    static [ConfigurationManager] CreateDefaultManager() {
        $defaultDir = Join-Path $env:APPDATA "SpotifyCLI\LiveFeatures"
        return [ConfigurationManager]::new($defaultDir)
    }
}

# Export classes and functions
Export-ModuleMember -Function @() -Variable @()