#!/usr/bin/env python3

import os
import sys
import configparser
import dialog
import subprocess
import time
import select
import tempfile
from typing import Dict, List, Optional, Tuple

class FenrirConfigTool:
    def __init__(self):
        os.environ['DIALOGOPTS'] = '--no-lines --visit-items'
        self.tui = dialog.Dialog(dialog="dialog")
        self.settingsFile = '/etc/fenrirscreenreader/settings/settings.conf'
        
        # Check if we need to re-run with elevated privileges
        if not self.check_root():
            self.escalate_privileges()
            sys.exit(0)
            
        self.instructions = {
            'menu': "\nNavigation: Use Up/Down arrows to move, Enter to select, Escape to go back",
            'radiolist': "\nNavigation: Use Up/Down arrows to move, Space to select option, Enter to confirm, Escape to cancel",
            'inputbox': "\nEnter your value and press Enter to confirm, or Escape to cancel"
        }
        
        # Configuration presets and help text from original FenrirConfig
        self.presetOptions = {
            'sound.driver': ['genericDriver', 'gstreamerDriver'],
            'speech.driver': ['speechdDriver', 'genericDriver'],
            'screen.driver': ['vcsaDriver', 'dummyDriver', 'ptyDriver', 'debugDriver'],
            'keyboard.driver': ['evdevDriver', 'dummyDriver'],
            'remote.driver': ['unixDriver', 'tcpDriver'],
            'keyboard.charEchoMode': ['0', '1', '2'],
            'general.punctuationLevel': ['none', 'some', 'most', 'all'],
            'general.debugMode': ['File', 'Print']
        }
        
        self.helpText = {
            'sound.volume': 'Volume level from 0 (quietest) to 1.0 (loudest)',
            'speech.rate': 'Speech rate from 0 (slowest) to 1.0 (fastest)',
            'speech.pitch': 'Voice pitch from 0 (lowest) to 1.0 (highest)',
            'keyboard.charEchoMode': '0 = None, 1 = always, 2 = only while capslock'
        }

    def check_root(self) -> bool:
        return os.geteuid() == 0

    def find_privilege_escalation_tool(self) -> Optional[str]:
        for tool in ['sudo', 'doas']:
            if subprocess.run(['which', tool], stdout=subprocess.PIPE).returncode == 0:
                return tool
        return None

    def escalate_privileges(self):
        tool = self.find_privilege_escalation_tool()
        if not tool:
            self.tui.msgbox("Error: Neither sudo nor doas found. Please run this script as root.")
            sys.exit(1)
        
        try:
            scriptPath = os.path.abspath(sys.argv[0])
            command = [tool, sys.executable, scriptPath] + sys.argv[1:]
            os.execvp(tool, command)
        except Exception as e:
            self.tui.msgbox(f"Error escalating privileges: {str(e)}")
            sys.exit(1)

    def is_boolean_option(self, value: str) -> bool:
        """Check if the current value is likely a boolean option"""
        return value.lower() in ['true', 'false']

    def validate_input(self, section: str, option: str, value: str) -> tuple[bool, str]:
        """Validate user input based on the option type and constraints"""
        try:
            if option.endswith('volume') or option.endswith('rate') or option.endswith('pitch'):
                floatVal = float(value)
                if not 0 <= floatVal <= 1.0:
                    return False, "Value must be between 0 and 1.0"
            return True, value
        except ValueError:
            return False, "Invalid number format"

    def get_value_with_presets(self, section: str, option: str, currentValue: str) -> Optional[str]:
        """Get value using appropriate input method based on option type"""
        key = f"{section}.{option}"
        
        # Handle boolean options
        if self.is_boolean_option(currentValue):
            choices = [
                ('True', '', currentValue.lower() == 'true'),
                ('False', '', currentValue.lower() == 'false')
            ]
            code, tag = self.tui.radiolist(
                f"Select value for '{option}'" + self.instructions['radiolist'],
                choices=choices
            )
            return tag if code == self.tui.OK else None
        
        # Handle other preset options
        elif key in self.presetOptions:
            choices = [(opt, "", opt == currentValue) for opt in self.presetOptions[key]]
            code, tag = self.tui.radiolist(
                f"Select value for '{option}'" +
                (f"\n{self.helpText[key]}" if key in self.helpText else "") +
                self.instructions['radiolist'],
                choices=choices
            )
            return tag if code == self.tui.OK else None
        
        # Handle free-form input
        else:
            helpText = self.helpText.get(key, "")
            code, value = self.tui.inputbox(
                f"Enter value for '{option}'" +
                (f"\n{helpText}" if helpText else "") +
                self.instructions['inputbox'],
                init=currentValue
            )
            if code == self.tui.OK:
                isValid, message = self.validate_input(section, option, value)
                if not isValid:
                    self.tui.msgbox(f"Invalid input: {message}")
                    return None
                return value
            return None

    def run_command(self, cmd: List[str], needsRoot: bool = False) -> Optional[str]:
        try:
            if needsRoot and not self.check_root():
                tool = self.find_privilege_escalation_tool()
                if tool:
                    cmd = [tool] + cmd
            result = subprocess.run(cmd, capture_output=True, text=True)
            return result.stdout.strip() if result.returncode == 0 else None
        except Exception as e:
            self.tui.msgbox(f"Error running command {' '.join(cmd)}: {e}")
            return None

    def get_speechd_modules(self) -> List[str]:
        output = self.run_command(['spd-say', '-O'], True)
        if output:
            lines = output.split('\n')
            return [line.strip() for line in lines[1:] if line.strip()]
        return []

    def process_espeak_voice(self, voiceLine: str) -> Optional[str]:
        parts = [p for p in voiceLine.split() if p]
        if len(parts) < 2:
            return None
        langCode = parts[-2].lower()
        variant = parts[-1].lower()
        return f"{langCode}+{variant}" if variant and variant != 'none' else langCode

    def get_module_voices(self, moduleName: str) -> List[str]:
        output = self.run_command(['spd-say', '-o', moduleName, '-L'], True)
        if output:
            lines = output.split('\n')
            voices = []
            for line in lines[1:]:
                if not line.strip():
                    continue
                if moduleName.lower() == 'espeak-ng':
                    voice = self.process_espeak_voice(line)
                    if voice:
                        voices.append(voice)
                else:
                    voices.append(line.strip())
            return voices
        return []

    def configure_speech(self) -> None:
        moduleList = self.get_speechd_modules()
        if not moduleList:
            self.tui.msgbox("No speech-dispatcher modules found!")
            return

        code, moduleChoice = self.tui.menu(
            "Select speech module:" + self.instructions['menu'],
            choices=[(module, "") for module in moduleList]
        )
        
        if code != self.tui.OK:
            return

        voiceList = self.get_module_voices(moduleChoice)
        if not voiceList:
            self.tui.msgbox(f"No voices found for module {moduleChoice}")
            return

        code, voice = self.tui.menu(
            f"Select voice for {moduleChoice}:" + self.instructions['menu'],
            choices=[(v, "") for v in voiceList]
        )
        
        if code != self.tui.OK:
            return

        # Test voice configuration
        if self.test_voice(moduleChoice, voice):
            config = configparser.ConfigParser(interpolation=None)
            config.read(self.settingsFile)
            
            if 'speech' not in config:
                config['speech'] = {}
                
            config['speech'].update({
                'driver': 'speechdDriver',
                'module': moduleChoice,
                'voice': voice,
                'enabled': 'True',
                'rate': '0.25',
                'pitch': '0.5',
                'volume': '1.0'
            })
            
            with open(self.settingsFile, 'w') as configfile:
                config.write(configfile)
            
            self.tui.msgbox("Speech configuration updated successfully!\nPlease restart Fenrir for changes to take effect.")

    def test_voice(self, moduleName: str, voiceName: str) -> bool:
        testMessage = "If you hear this message, press Enter within 30 seconds to confirm."
        try:
            process = subprocess.Popen(
                ['spd-say', '-o', moduleName, '-y', voiceName, testMessage]
            )
            
            code = self.tui.pause(
                "Waiting for voice test...\n"
                "Press Enter if you hear the message, or wait for timeout.",
                30
            )
            
            process.terminate()
            return code == self.tui.OK
            
        except Exception as e:
            self.tui.msgbox(f"Error testing voice: {e}")
            return False

    def edit_general_config(self) -> None:
        while True:
            config = configparser.ConfigParser(interpolation=None)
            config.read(self.settingsFile)
            sections = config.sections()

            code, section = self.tui.menu(
                "Select a section to configure:" + self.instructions['menu'],
                choices=[(s, "") for s in sections] + [("Go Back", "")]
            )

            if code != self.tui.OK or section == "Go Back":
                break

            while True:
                options = config.options(section)
                choices = [(o, f"Current: {config.get(section, o)}") for o in options]
                choices.append(("Go Back", ""))

                code, option = self.tui.menu(
                    f"Select option to edit in '{section}':" + self.instructions['menu'],
                    choices=choices
                )

                if code != self.tui.OK or option == "Go Back":
                    break

                currentValue = config.get(section, option)
                newValue = self.get_value_with_presets(section, option, currentValue)

                if newValue is not None and newValue != currentValue:
                    config.set(section, option, newValue)
                    with open(self.settingsFile, 'w') as configfile:
                        config.write(configfile)
                    self.tui.msgbox("Setting updated successfully.")

    def run(self):
        while True:
            code, choice = self.tui.menu(
                "Fenrir Configuration Tool" + self.instructions['menu'],
                choices=[
                    ("speech-dispatcher", "Configure module and voice"),
                    ("Advanced", "Edit Fenrir settings"),
                    ("Exit", "")
                ]
            )

            if code != self.tui.OK or choice == "Exit":
                break

            if choice == "speech-dispatcher":
                self.configure_speech()
            elif choice == "Advanced":
                self.edit_general_config()

if __name__ == "__main__":
    configTool = FenrirConfigTool()
    try:
        configTool.run()
    except Exception as e:
        print(f"Unexpected error occurred: {str(e)}", file=sys.stderr)
        sys.exit(1)
