"""
Centralized configuration schema for lsfg-vk.

This module defines the complete configuration structure for lsfg-vk, managing TOML-based config files, including:
- Field definitions with types, defaults, and metadata
- TOML generation logic
- Validation rules
- Type definitions
"""

import logging
import re
import sys
from typing import TypedDict, Dict, Any, Union, cast, List
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

# Import shared configuration constants
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from shared_config import CONFIG_SCHEMA_DEF, ConfigFieldType, get_field_names, get_defaults, get_field_types

# Import auto-generated configuration components
from .config_schema_generated import ConfigurationData, get_script_parsing_logic, get_script_generation_logic


@dataclass
class ConfigField:
    """Configuration field definition"""
    name: str
    field_type: ConfigFieldType
    default: Union[bool, int, float, str]
    description: str
    
    def get_toml_value(self, value: Union[bool, int, float, str]) -> Union[bool, int, float, str]:
        """Get the value for TOML output"""
        return value


# Use shared configuration schema as source of truth
CONFIG_SCHEMA: Dict[str, ConfigField] = {
    field_name: ConfigField(
        name=field_def["name"],
        field_type=ConfigFieldType(field_def["fieldType"]),
        default=field_def["default"],
        description=field_def["description"]
    )
    for field_name, field_def in CONFIG_SCHEMA_DEF.items()
}

# Override DLL default to empty (will be populated dynamically)
CONFIG_SCHEMA["dll"] = ConfigField(
    name="dll",
    field_type=ConfigFieldType.STRING,
    default="",  # Will be populated dynamically based on detection
    description="specify where Lossless.dll is stored"
)

# Get script-only fields dynamically from shared config
SCRIPT_ONLY_FIELDS = {
    field_name: ConfigField(
        name=field_def["name"],
        field_type=ConfigFieldType(field_def["fieldType"]),
        default=field_def["default"],
        description=field_def["description"]
    )
    for field_name, field_def in CONFIG_SCHEMA_DEF.items()
    if field_def.get("location") == "script"
}

# Complete configuration schema (TOML + script-only fields)
COMPLETE_CONFIG_SCHEMA = {**CONFIG_SCHEMA, **SCRIPT_ONLY_FIELDS}


# Import auto-generated configuration components
from .config_schema_generated import ConfigurationData, get_script_parsing_logic, get_script_generation_logic

# Constants for profile management
DEFAULT_PROFILE_NAME = "decky-lsfg-vk"
GLOBAL_SECTION_FIELDS = {"dll", "no_fp16"}

# Note: ConfigurationData is now imported from generated file
# No need to manually maintain the TypedDict anymore!


class ProfileData(TypedDict):
    """Profile data with current profile tracking"""
    current_profile: str
    profiles: Dict[str, ConfigurationData]  # profile_name -> config
    global_config: Dict[str, Any]  # Global settings (dll, no_fp16)


class ConfigurationManager:
    """Centralized configuration management"""
    
    @staticmethod
    def get_defaults() -> ConfigurationData:
        """Get default configuration values"""
        # Use shared defaults and add script-only fields
        shared_defaults = get_defaults()
        
        # Add script-only fields that aren't in the shared schema
        script_defaults = {
            field.name: field.default 
            for field in SCRIPT_ONLY_FIELDS.values()
        }
        
        return cast(ConfigurationData, {**shared_defaults, **script_defaults})
    
    @staticmethod
    def get_defaults_with_dll_detection(dll_detection_service=None) -> ConfigurationData:
        """Get default configuration values with DLL path detection
        
        Args:
            dll_detection_service: Optional DLL detection service instance
            
        Returns:
            ConfigurationData with detected DLL path if available
        """
        defaults = ConfigurationManager.get_defaults()
        
        # Try to detect DLL path if service provided
        if dll_detection_service:
            try:
                dll_result = dll_detection_service.check_lossless_scaling_dll()
                if dll_result.get("detected") and dll_result.get("path"):
                    defaults["dll"] = dll_result["path"]
            except (OSError, IOError, KeyError, TypeError) as e:
                # If detection fails, keep empty default
                logging.getLogger(__name__).debug(f"DLL detection failed: {e}")
        
        # If DLL path is still empty, use a reasonable fallback
        if not defaults["dll"]:
            defaults["dll"] = "/home/deck/.local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll"
        
        return defaults
    
    @staticmethod
    def get_field_names() -> list[str]:
        """Get ordered list of configuration field names"""
        # Use shared field names and add script-only fields
        shared_names = get_field_names()
        script_names = list(SCRIPT_ONLY_FIELDS.keys())
        return shared_names + script_names
    
    @staticmethod
    def get_field_types() -> Dict[str, ConfigFieldType]:
        """Get field type mapping"""
        # Use shared field types and add script-only field types
        shared_types = {name: ConfigFieldType(type_str) for name, type_str in get_field_types().items()}
        script_types = {field.name: field.field_type for field in SCRIPT_ONLY_FIELDS.values()}
        return {**shared_types, **script_types}
    
    @staticmethod
    def validate_config(config: Dict[str, Any]) -> ConfigurationData:
        """Validate and convert configuration data"""
        validated = {}
        
        for field_name, field_def in COMPLETE_CONFIG_SCHEMA.items():
            value = config.get(field_name, field_def.default)
            
            # Type validation and conversion
            if field_def.field_type == ConfigFieldType.BOOLEAN:
                validated[field_name] = bool(value)
            elif field_def.field_type == ConfigFieldType.INTEGER:
                validated[field_name] = int(value)
            elif field_def.field_type == ConfigFieldType.FLOAT:
                validated[field_name] = float(value)
            elif field_def.field_type == ConfigFieldType.STRING:
                validated[field_name] = str(value)
            else:
                validated[field_name] = value
        
        return cast(ConfigurationData, validated)
    
    @staticmethod
    def generate_toml_content(config: ConfigurationData) -> str:
        """Generate TOML configuration file content for single profile (backward compatibility)"""
        # For backward compatibility, create a single profile structure
        profile_data: ProfileData = {
            "current_profile": DEFAULT_PROFILE_NAME,
            "profiles": {DEFAULT_PROFILE_NAME: config},
            "global_config": {
                "dll": config.get("dll", ""),
                "no_fp16": False  # Always enabled even if previously set
            }
        }
        return ConfigurationManager.generate_toml_content_multi_profile(profile_data)
    
    @staticmethod
    def generate_toml_content_multi_profile(profile_data: ProfileData) -> str:
        """Generate TOML configuration file content with multiple profiles"""
        lines = ["version = 1"]
        lines.append("")
        
        # Add global section with global fields
        lines.append("[global]")
        
        # Add current_profile field
        lines.append(f"# Currently selected profile")
        lines.append(f'current_profile = "{profile_data["current_profile"]}"')
        lines.append("")
        
        # Add dll field if specified
        dll_path = profile_data["global_config"].get("dll", "")
        if dll_path:
            lines.append(f"# specify where Lossless.dll is stored")
            lines.append(f'dll = "{dll_path}"')
            lines.append("")
            
        lines.append(f"# FP16 acceleration")
        lines.append(f"no_fp16 = false")
        lines.append("")
        
        # Add game sections for each profile
        # Sort profiles to ensure consistent order (default profile first)
        sorted_profiles = sorted(profile_data["profiles"].items(), 
                               key=lambda x: (x[0] != DEFAULT_PROFILE_NAME, x[0]))
        
        for profile_name, config in sorted_profiles:
            lines.append("[[game]]")
            if profile_name == DEFAULT_PROFILE_NAME:
                lines.append("# Plugin-managed game entry (default profile)")
            else:
                lines.append(f"# Profile: {profile_name}")
            lines.append(f'exe = "{profile_name}"')
            lines.append("")
            
            # Add all configuration fields to the game section (excluding global fields)
            for field_name, field_def in CONFIG_SCHEMA.items():
                # Skip global fields - they go in global section
                if field_name in GLOBAL_SECTION_FIELDS:
                    continue
                    
                value = config.get(field_name, field_def.default)
                
                # Add field description comment
                lines.append(f"# {field_def.description}")
                
                # Format value based on type
                if isinstance(value, bool):
                    lines.append(f"{field_name} = {str(value).lower()}")
                elif isinstance(value, str) and value:  # Only add non-empty strings
                    lines.append(f'{field_name} = "{value}"')
                elif isinstance(value, (int, float)):  # Always include numbers, even if 0 or 1
                    lines.append(f"{field_name} = {value}")
                
                lines.append("")  # Empty line for readability
        
        return "\n".join(lines)
    
    @staticmethod
    def parse_toml_content(content: str) -> ConfigurationData:
        """Parse TOML content into configuration data for the currently selected profile (backward compatibility)"""
        profile_data = ConfigurationManager.parse_toml_content_multi_profile(content)
        current_profile = profile_data["current_profile"]
        
        # Merge global config with current profile config
        current_config = profile_data["profiles"].get(current_profile, ConfigurationManager.get_defaults())
        
        # Add global fields to the config
        for field_name in GLOBAL_SECTION_FIELDS:
            if field_name in profile_data["global_config"]:
                current_config[field_name] = profile_data["global_config"][field_name]
        
        return current_config
    
    @staticmethod
    def parse_toml_content_multi_profile(content: str) -> ProfileData:
        """Parse TOML content into profile data structure"""
        profiles: Dict[str, ConfigurationData] = {}
        global_config: Dict[str, Any] = {}
        current_profile = DEFAULT_PROFILE_NAME
        
        try:
            # Look for both [global] and [[game]] sections
            lines = content.split('\n')
            in_global_section = False
            in_game_section = False
            current_game_exe = None
            current_game_config: Dict[str, Any] = {}
            
            for line in lines:
                line = line.strip()
                
                # Skip comments and empty lines
                if not line or line.startswith('#'):
                    continue
                
                # Check for section headers
                if line.startswith('[') and line.endswith(']'):
                    # Save previous game section if we were in one
                    if in_game_section and current_game_exe:
                        # Validate and store the profile config
                        validated_config = ConfigurationManager.get_defaults()
                        for key, value in current_game_config.items():
                            if key in CONFIG_SCHEMA:
                                field_def = CONFIG_SCHEMA[key]
                                try:
                                    if field_def.field_type == ConfigFieldType.BOOLEAN:
                                        validated_config[key] = value
                                    elif field_def.field_type == ConfigFieldType.INTEGER:
                                        validated_config[key] = int(value) if not isinstance(value, int) else value
                                    elif field_def.field_type == ConfigFieldType.FLOAT:
                                        validated_config[key] = float(value) if not isinstance(value, float) else value
                                    elif field_def.field_type == ConfigFieldType.STRING:
                                        validated_config[key] = str(value)
                                except (ValueError, TypeError):
                                    # If conversion fails, keep default value
                                    pass
                        profiles[current_game_exe] = validated_config
                        current_game_config = {}
                    
                    # Set new section state
                    if line == '[global]':
                        in_global_section = True
                        in_game_section = False
                    elif line == '[[game]]':
                        in_global_section = False
                        in_game_section = True
                        current_game_exe = None
                    else:
                        in_global_section = False
                        in_game_section = False
                    continue
                
                # Parse key = value lines
                if '=' in line:
                    key, value = line.split('=', 1)
                    key = key.strip()
                    value = value.strip()
                    
                    # Remove quotes from string values
                    if value.startswith('"') and value.endswith('"'):
                        value = value[1:-1]
                    elif value.startswith("'") and value.endswith("'"):
                        value = value[1:-1]
                    
                    # Handle global section
                    if in_global_section:
                        if key == "current_profile":
                            current_profile = value
                        elif key == "dll":
                            global_config["dll"] = value
                        elif key == "no_fp16":
                            # Always enforce FP16 to be enabled (no_fp16 = false)
                            global_config["no_fp16"] = False
                    
                    # Handle game section
                    elif in_game_section:
                        # Track the exe for this game section
                        if key == "exe":
                            current_game_exe = value
                        # Store config fields for current game
                        elif key in CONFIG_SCHEMA:
                            field_def = CONFIG_SCHEMA[key]
                            try:
                                if field_def.field_type == ConfigFieldType.BOOLEAN:
                                    current_game_config[key] = value.lower() in ('true', '1', 'yes', 'on')
                                elif field_def.field_type == ConfigFieldType.INTEGER:
                                    current_game_config[key] = int(value)
                                elif field_def.field_type == ConfigFieldType.FLOAT:
                                    current_game_config[key] = float(value)
                                elif field_def.field_type == ConfigFieldType.STRING:
                                    current_game_config[key] = value
                            except (ValueError, TypeError):
                                # If conversion fails, keep default value
                                pass
            
            # Handle final game section if we were in one
            if in_game_section and current_game_exe:
                validated_config = ConfigurationManager.get_defaults()
                for key, value in current_game_config.items():
                    if key in CONFIG_SCHEMA:
                        field_def = CONFIG_SCHEMA[key]
                        try:
                            if field_def.field_type == ConfigFieldType.BOOLEAN:
                                validated_config[key] = value
                            elif field_def.field_type == ConfigFieldType.INTEGER:
                                validated_config[key] = int(value) if not isinstance(value, int) else value
                            elif field_def.field_type == ConfigFieldType.FLOAT:
                                validated_config[key] = float(value) if not isinstance(value, float) else value
                            elif field_def.field_type == ConfigFieldType.STRING:
                                validated_config[key] = str(value)
                        except (ValueError, TypeError):
                            # If conversion fails, keep default value
                            pass
                profiles[current_game_exe] = validated_config
            
            # Ensure we have at least the default profile
            if not profiles:
                profiles[DEFAULT_PROFILE_NAME] = ConfigurationManager.get_defaults()
            
            # Ensure current_profile exists in profiles
            if current_profile not in profiles:
                current_profile = DEFAULT_PROFILE_NAME
                if DEFAULT_PROFILE_NAME not in profiles:
                    profiles[DEFAULT_PROFILE_NAME] = ConfigurationManager.get_defaults()
            
            return ProfileData(
                current_profile=current_profile,
                profiles=profiles,
                global_config=global_config
            )
            
        except (ValueError, KeyError, TypeError, AttributeError) as e:
            # If parsing fails completely, return default profile structure
            logging.getLogger(__name__).warning(f"Failed to parse TOML profiles, using defaults: {e}")
            return ProfileData(
                current_profile=DEFAULT_PROFILE_NAME,
                profiles={DEFAULT_PROFILE_NAME: ConfigurationManager.get_defaults()},
                global_config={}
            )
    
    @staticmethod
    def parse_script_content(script_content: str) -> Dict[str, Union[bool, int, str]]:
        """Parse launch script content to extract environment variable values
        
        Args:
            script_content: Content of the launch script file
            
        Returns:
            Dict containing parsed script-only field values
        """
        # Use auto-generated parsing logic
        parse_script_values = get_script_parsing_logic()
        return parse_script_values(script_content.split('\n'))
    
    @staticmethod
    def merge_config_with_script(toml_config: ConfigurationData, script_values: Dict[str, Union[bool, int, str]]) -> ConfigurationData:
        """Merge TOML configuration with script environment variable values
        
        Args:
            toml_config: Configuration loaded from TOML file
            script_values: Environment variable values parsed from script
            
        Returns:
            Complete configuration with script values overlaid on TOML config
        """
        merged_config = dict(toml_config)
        
        # Update script-only fields with values from script
        for field_name in SCRIPT_ONLY_FIELDS.keys():
            if field_name in script_values:
                merged_config[field_name] = script_values[field_name]
        
        return cast(ConfigurationData, merged_config)

    @staticmethod
    @staticmethod
    def create_config_from_args(**kwargs) -> ConfigurationData:
        """Create configuration from keyword arguments - USES GENERATED CODE"""
        from .config_schema_generated import create_config_dict
        return create_config_dict(**kwargs)
    
    @staticmethod
    def normalize_profile_name(profile_name: str) -> str:
        """Normalize profile name by converting spaces to dashes and trimming
        
        This allows users to enter names with spaces, which are then safely
        converted to dashes for storage and shell script compatibility.
        
        Args:
            profile_name: The raw profile name from user input
            
        Returns:
            Normalized profile name with spaces converted to dashes
        """
        if not profile_name:
            return profile_name
        
        # Trim whitespace and convert spaces to dashes
        normalized = profile_name.strip().replace(' ', '-')
        
        # Collapse multiple consecutive dashes into one
        while '--' in normalized:
            normalized = normalized.replace('--', '-')
        
        # Remove leading/trailing dashes
        normalized = normalized.strip('-')
        
        return normalized
    
    @staticmethod
    def validate_profile_name(profile_name: str) -> bool:
        """Validate profile name for safety (after normalization)"""
        if not profile_name:
            return False
        
        # Normalize first - this converts spaces to dashes
        normalized = ConfigurationManager.normalize_profile_name(profile_name)
        
        if not normalized:
            return False
        
        # Check for invalid characters that could cause issues in shell scripts or TOML
        # Note: spaces are now allowed as input (they get converted to dashes)
        invalid_chars = set('\t\n\r\'"\\/$|&;()<>{}[]`*?')
        if any(char in invalid_chars for char in normalized):
            return False
        
        # Check for reserved names
        reserved_names = {'global', 'game', 'current_profile'}
        if normalized.lower() in reserved_names:
            return False
        
        return True
    
    @staticmethod
    def create_profile(profile_data: ProfileData, profile_name: str, source_profile: str = None) -> ProfileData:
        """Create a new profile by copying from source profile or defaults"""
        if not ConfigurationManager.validate_profile_name(profile_name):
            raise ValueError(f"Invalid profile name: {profile_name}")
        
        # Normalize the profile name (converts spaces to dashes)
        profile_name = ConfigurationManager.normalize_profile_name(profile_name)
        
        if profile_name in profile_data["profiles"]:
            raise ValueError(f"Profile '{profile_name}' already exists")
        
        # Copy from source profile or use defaults
        if source_profile and source_profile in profile_data["profiles"]:
            new_config = dict(profile_data["profiles"][source_profile])
        else:
            new_config = ConfigurationManager.get_defaults()
        
        # Create new profile data structure
        new_profile_data = ProfileData(
            current_profile=profile_data["current_profile"],
            profiles=dict(profile_data["profiles"]),
            global_config=dict(profile_data["global_config"])
        )
        new_profile_data["profiles"][profile_name] = new_config
        
        return new_profile_data
    
    @staticmethod
    def delete_profile(profile_data: ProfileData, profile_name: str) -> ProfileData:
        """Delete a profile (cannot delete default profile)"""
        if profile_name == DEFAULT_PROFILE_NAME:
            raise ValueError(f"Cannot delete default profile '{DEFAULT_PROFILE_NAME}'")
        
        if profile_name not in profile_data["profiles"]:
            raise ValueError(f"Profile '{profile_name}' does not exist")
        
        # Create new profile data structure
        new_profile_data = ProfileData(
            current_profile=profile_data["current_profile"],
            profiles=dict(profile_data["profiles"]),
            global_config=dict(profile_data["global_config"])
        )
        
        # Remove the profile
        del new_profile_data["profiles"][profile_name]
        
        # If we deleted the current profile, switch to default
        if new_profile_data["current_profile"] == profile_name:
            new_profile_data["current_profile"] = DEFAULT_PROFILE_NAME
            # Ensure default profile exists
            if DEFAULT_PROFILE_NAME not in new_profile_data["profiles"]:
                new_profile_data["profiles"][DEFAULT_PROFILE_NAME] = ConfigurationManager.get_defaults()
        
        return new_profile_data
    
    @staticmethod
    def rename_profile(profile_data: ProfileData, old_name: str, new_name: str) -> ProfileData:
        """Rename a profile"""
        if old_name == DEFAULT_PROFILE_NAME:
            raise ValueError(f"Cannot rename default profile '{DEFAULT_PROFILE_NAME}'")
        
        if not ConfigurationManager.validate_profile_name(new_name):
            raise ValueError(f"Invalid profile name: {new_name}")
        
        # Normalize the new name (converts spaces to dashes)
        new_name = ConfigurationManager.normalize_profile_name(new_name)
        
        if old_name not in profile_data["profiles"]:
            raise ValueError(f"Profile '{old_name}' does not exist")
        
        if new_name in profile_data["profiles"]:
            raise ValueError(f"Profile '{new_name}' already exists")
        
        # Create new profile data structure
        new_profile_data = ProfileData(
            current_profile=profile_data["current_profile"],
            profiles={},
            global_config=dict(profile_data["global_config"])
        )
        
        # Copy profiles with new name
        for profile_name, config in profile_data["profiles"].items():
            if profile_name == old_name:
                new_profile_data["profiles"][new_name] = dict(config)
            else:
                new_profile_data["profiles"][profile_name] = dict(config)
        
        # Update current_profile if necessary
        if new_profile_data["current_profile"] == old_name:
            new_profile_data["current_profile"] = new_name
        
        return new_profile_data
    
    @staticmethod
    def set_current_profile(profile_data: ProfileData, profile_name: str) -> ProfileData:
        """Set the current active profile"""
        if profile_name not in profile_data["profiles"]:
            raise ValueError(f"Profile '{profile_name}' does not exist")
        
        # Create new profile data structure
        new_profile_data = ProfileData(
            current_profile=profile_name,
            profiles=dict(profile_data["profiles"]),
            global_config=dict(profile_data["global_config"])
        )
        
        return new_profile_data
