"""Graphical user interface for llmserver."""

from __future__ import annotations

import logging
import os
import pathlib
import sys
from enum import Enum
from pathlib import Path
from typing import Callable, ClassVar

from PySide2.QtCore import QProcess, Qt, QTextStream, QUrl
from PySide2.QtGui import (
    QBrush,
    QCloseEvent,
    QColor,
    QDesktopServices,
    QMoveEvent,
    QResizeEvent,
    QTextCharFormat,
    QTextCursor,
)
from PySide2.QtWidgets import (
    QApplication,
    QComboBox,
    QFileDialog,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QPushButton,
    QSpinBox,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)
from pytola_core.config import ConfigMixin
from pytola_qtstyles import apply_theme

CONFIG_FILE = Path.home() / ".pytola" / "llmserver.json"
logging.basicConfig(level="INFO", format="%(message)s")


class Language(Enum):
    """Supported languages."""

    CHINESE = "zh"
    ENGLISH = "en"


class TranslationManager:
    """Manage UI translations for LLM server."""

    def __init__(self) -> None:
        """Initialize translation manager with default Chinese."""
        self.current_language = Language.CHINESE
        self.translations = self._load_translations()

    def _load_translations(self) -> dict[str, dict[str, str]]:
        """Load translation dictionaries."""
        return {
            Language.CHINESE.value: {
                # Window title
                "window_title": "Llama本地模型服务器 v1.0",
                # Language selector
                "language": "语言",
                # Group box titles
                "server_config": "服务器配置",
                "server_control": "服务器控制",
                "server_output": "服务器输出",
                # Labels
                "model_path": "模型路径",
                "port": "端口",
                "threads": "线程数",
                # Buttons
                "browse": "浏览",
                "start_server": "启动服务器",
                "stop_server": "停止服务器",
                "open_browser": "打开浏览器",
                # Messages
                "choose_model_file": "选择模型文件...",
                "server_started": "服务器已启动",
                "server_stopped": "服务器已停止",
                "server_output_placeholder": "服务器输出将显示在这里...",
                "warning_model_not_found": "警告: 之前选择的模型文件未找到: {path}",
                "select_gguf_model": "选择GGUF模型文件",
                "gguf_models": "GGUF模型 (*.gguf)",
                "all_files": "所有文件 (*.*)",
                "model_selected": "模型已选择: {path}",
                "shutting_down_server": "正在关闭服务器...",
                "server_shutdown_complete": "服务器关闭完成",
                "non_zero_exit": ("注意: 非零退出代码可能表示错误. 请检查上方输出了解详情. "),
            },
            Language.ENGLISH.value: {
                # Window title
                "window_title": "Llama Local Model Server v1.0",
                # Language selector
                "language": "Language",
                # Group box titles
                "server_config": "Server Configuration",
                "server_control": "Server Control",
                "server_output": "Server Output",
                # Labels
                "model_path": "Model Path",
                "port": "Port",
                "threads": "Threads",
                # Buttons
                "browse": "Browse",
                "start_server": "Start Server",
                "stop_server": "Stop Server",
                "open_browser": "Open Browser",
                # Messages
                "choose_model_file": "Choose model file...",
                "server_started": "Server started",
                "server_stopped": "Server stopped",
                "server_output_placeholder": "Server output will appear here...",
                "warning_model_not_found": ("Warning: Previously selected model file not found: {path}"),
                "select_gguf_model": "Select GGUF Model File",
                "gguf_models": "GGUF Models (*.gguf)",
                "all_files": "All Files (*)",
                "model_selected": "Model selected: {path}",
                "shutting_down_server": "Shutting down server...",
                "server_shutdown_complete": "Server shutdown complete",
                "non_zero_exit": ("Note: Non-zero exit code may indicate an error. Check output above for details."),
            },
        }

    def set_language(self, language: Language) -> None:
        """Set current language."""
        self.current_language = language

    def tr(self, key: str, **kwargs: object) -> str:
        """Translate key to current language."""
        translation = self.translations[self.current_language.value].get(key, key)
        if kwargs:
            translation = translation.format(**kwargs)
        return translation

    def get_available_languages(self) -> list[tuple[str, str]]:
        """Get available languages for UI display."""
        return [(Language.CHINESE.value, "中文"), (Language.ENGLISH.value, "English")]


# Global translation manager
_translation_manager = TranslationManager()

logger = logging.getLogger(__name__)


class LLMServerConfig(ConfigMixin):
    """Llama local model server configuration with persistent settings."""

    # Application metadata
    TITLE: ClassVar[str] = "Llama Local Model Server"

    # Window configuration - instance attributes for persistent settings
    WIN_SIZE: list[int] = [600, 500]  # noqa: RUF012
    WIN_POS: list[int] = [200, 200]  # noqa: RUF012

    # Server configuration
    MODEL_PATH: str = ""
    URL: str = "http://127.0.0.1"
    LISTEN_PORT: int = 8080
    LISTEN_PORT_RNG: ClassVar[list[int]] = [1024, 65535]
    THREAD_COUNT: int = 4
    THREAD_COUNT_RNG: ClassVar[list[int]] = [1, 24]


conf = LLMServerConfig()


class LlamaServerGUI(QMainWindow):
    """Llama local model server GUI."""

    # Color constants
    COLOR_ERROR = QColor(255, 0, 0)
    COLOR_WARNING = QColor(255, 165, 0)
    COLOR_INFO = QColor(0, 0, 0)

    # Process constants
    PROCESS_TERMINATE_TIMEOUT_MS = 2000

    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("LLM Local Model Server")

        # Apply window geometry from configuration
        self.setGeometry(conf.WIN_POS[0], conf.WIN_POS[1], conf.WIN_SIZE[0], conf.WIN_SIZE[1])

        self.process: QProcess

        apply_theme(self, "light")

        # Now initialize UI - widget styles will override theme fonts
        self.init_ui()
        self.setup_process()

        # Apply loaded configuration to UI controls
        self.apply_config_to_ui()

        # Update UI text to current language
        self._update_ui_text()

    def init_ui(self) -> None:
        """Initialize user interface with comfortable spacing and multi-language support."""
        # Main layout with comfortable spacing
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        # Language selector at the top
        language_layout = QVBoxLayout()

        self.language_label = QLabel(_translation_manager.tr("language"))
        language_layout.addWidget(self.language_label)

        self.language_combo = QComboBox()

        # Populate language options
        for lang_code, lang_name in _translation_manager.get_available_languages():
            self.language_combo.addItem(lang_name, lang_code)

        # Set current language
        current_lang_index = self.language_combo.findData(_translation_manager.current_language.value)
        if current_lang_index >= 0:
            self.language_combo.setCurrentIndex(current_lang_index)

        self.language_combo.currentIndexChanged.connect(self._change_language)
        language_layout.addWidget(self.language_combo)
        language_layout.addStretch()

        main_layout.addLayout(language_layout)

        # Configuration panel with ultra-compact design
        self.config_group = QGroupBox(_translation_manager.tr("server_config"))
        self.config_group.setObjectName("configGroup")
        config_layout = QVBoxLayout()
        self.config_group.setLayout(config_layout)

        # Model path selection section with ultra-compact spacing
        model_section = QWidget()
        model_layout = QVBoxLayout()
        model_section.setLayout(model_layout)

        # Add compact label for model path
        self.model_label = QLabel(_translation_manager.tr("model_path"))
        model_layout.addWidget(self.model_label)

        self.model_path_input = QLineEdit()
        self.model_path_input.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.model_path_input.setPlaceholderText(_translation_manager.tr("choose_model_file"))
        # Add tooltip to show full path when hovered
        self.model_path_input.setToolTip("")
        # Connect text change signal to update tooltip
        self.model_path_input.textChanged.connect(self._update_model_path_tooltip)
        self.load_model_btn = QPushButton(_translation_manager.tr("browse"))
        self.load_model_btn.clicked.connect(self.on_load_model)

        model_layout.addWidget(self.model_path_input, 1)
        model_layout.addWidget(self.load_model_btn)

        # Server parameters section with compact grid layout
        params_section = QWidget()
        params_layout = QHBoxLayout()
        params_section.setLayout(params_layout)

        # Port configuration - compact row
        self.port_label = QLabel(_translation_manager.tr("port"))
        self.port_spin = QSpinBox()
        self.port_spin.setRange(*conf.LISTEN_PORT_RNG)
        self.port_spin.setValue(conf.LISTEN_PORT)
        self.port_spin.valueChanged.connect(self.on_config_changed)

        params_layout.addWidget(self.port_label)
        params_layout.addWidget(self.port_spin)

        # Threads configuration - compact row
        self.threads_label = QLabel(_translation_manager.tr("threads"))
        self.threads_spin = QSpinBox()
        self.threads_spin.setRange(*conf.THREAD_COUNT_RNG)
        self.threads_spin.setValue(conf.THREAD_COUNT)
        self.threads_spin.valueChanged.connect(self.on_config_changed)

        params_layout.addWidget(self.threads_label)
        params_layout.addWidget(self.threads_spin)
        params_layout.addStretch()

        # Add sections to config layout
        config_layout.addWidget(model_section)
        config_layout.addWidget(params_section)

        # Control buttons with comfortable styling
        self.control_group = QGroupBox(_translation_manager.tr("server_control"))
        self.control_group.setObjectName("controlGroup")
        control_layout = QHBoxLayout()
        self.control_group.setLayout(control_layout)

        self.start_btn = QPushButton(_translation_manager.tr("start_server"))
        self.start_btn.clicked.connect(self.toggle_server)

        self.browser_btn = QPushButton(_translation_manager.tr("open_browser"))
        self.browser_btn.setEnabled(False)
        self.browser_btn.clicked.connect(self.on_start_browser)

        control_layout.addWidget(self.start_btn)
        control_layout.addWidget(self.browser_btn)
        control_layout.addStretch()

        # Output display - standalone area without GroupBox
        self.output_label = QLabel(_translation_manager.tr("server_output"))

        self.output_area = QTextEdit("")
        self.output_area.setObjectName("output_area")
        self.output_area.setReadOnly(True)
        self.output_area.setLineWrapMode(QTextEdit.NoWrap)
        self.output_area.setPlaceholderText(_translation_manager.tr("server_output_placeholder"))

        # Set colors for different message types
        self.error_format = self.create_text_format(self.COLOR_ERROR)
        self.warning_format = self.create_text_format(self.COLOR_WARNING)
        self.info_format = self.create_text_format(self.COLOR_INFO)

        # Assemble main layout
        main_layout.addWidget(self.config_group)
        main_layout.addWidget(self.control_group)
        main_layout.addWidget(self.output_label)
        main_layout.addWidget(self.output_area, 1)

        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)

    def _change_language(self, index: int) -> None:
        """Change UI language and update all text elements."""
        # Get selected language
        lang_code = self.language_combo.itemData(index)
        if lang_code == Language.CHINESE.value:
            _translation_manager.set_language(Language.CHINESE)
        else:
            _translation_manager.set_language(Language.ENGLISH)

        # Update all UI elements
        self._update_ui_text()

    def _update_ui_text(self) -> None:
        """Update all UI text elements to current language with reduced complexity."""
        # Cache translation function for better performance
        tr = _translation_manager.tr

        # Update basic UI elements
        self._update_window_title(tr)
        self._update_language_label(tr)

        # Update group elements
        self._update_group_box_titles(tr)
        self._update_labels(tr)
        self._update_buttons(tr)
        self._update_placeholders(tr)

    def _update_window_title(self, tr: Callable[[str], str]) -> None:
        """Update window title."""
        self.setWindowTitle(tr("window_title"))

    def _update_language_label(self, tr: Callable[[str], str]) -> None:
        """Update language selector label."""
        self.language_label.setText(tr("language"))

    def _update_group_box_titles(self, tr: Callable[[str], str]) -> None:
        """Update group box titles."""
        if hasattr(self, "config_group"):
            self.config_group.setTitle(tr("server_config"))
        if hasattr(self, "control_group"):
            self.control_group.setTitle(tr("server_control"))
        if hasattr(self, "output_label"):
            self.output_label.setText(tr("server_output"))

    def _update_labels(self, tr: Callable[[str], str]) -> None:
        """Update various labels in the UI."""
        if hasattr(self, "model_label"):
            self.model_label.setText(tr("model_path"))
        if hasattr(self, "port_label"):
            self.port_label.setText(tr("port"))
        if hasattr(self, "threads_label"):
            self.threads_label.setText(tr("threads"))

    def _update_buttons(self, tr: Callable[[str], str]) -> None:
        """Update button texts with appropriate logic."""
        if hasattr(self, "load_model_btn"):
            self.load_model_btn.setText(tr("browse"))

        if hasattr(self, "start_btn"):
            self._update_start_button_text(tr)
        if hasattr(self, "browser_btn"):
            self.browser_btn.setText(tr("open_browser"))

    def _update_start_button_text(self, tr: Callable[[str], str]) -> None:
        """Update start button text based on server state."""
        if self.process and self.process.state() == QProcess.Running:
            self.start_btn.setText(tr("stop_server"))
        else:
            self.start_btn.setText(tr("start_server"))

    def _update_placeholders(self, tr: Callable[[str], str]) -> None:
        """Update placeholder texts for input fields."""
        if hasattr(self, "model_path_input"):
            self.model_path_input.setPlaceholderText(tr("choose_model_file"))
        if hasattr(self, "output_area"):
            self.output_area.setPlaceholderText(tr("server_output_placeholder"))

    @staticmethod
    def create_text_format(color: QColor) -> QTextCharFormat:
        """Create text format.

        Args:
            color: Text color.

        Returns
        -------
            Text format.
        """
        text_format = QTextCharFormat()
        text_format.setForeground(QBrush(color))
        return text_format

    def setup_process(self) -> None:
        """Initialize process."""
        self.process = QProcess(self)
        self.process.readyReadStandardOutput.connect(self.handle_stdout)
        self.process.readyReadStandardError.connect(self.handle_stderr)
        self.process.finished.connect(self.on_process_finished)

    def apply_config_to_ui(self) -> None:
        """Apply loaded configuration to UI controls."""
        # Set model path
        model_path = conf.MODEL_PATH
        if model_path and Path(model_path).exists():
            self.model_path_input.setText(str(model_path))
        else:
            self.model_path_input.setPlaceholderText(_translation_manager.tr("choose_model_file"))
            if model_path:
                self.append_output(
                    _translation_manager.tr("warning_model_not_found", path=model_path) + "\n", self.warning_format
                )

        # Set port and thread values
        self.port_spin.setValue(conf.LISTEN_PORT)
        self.threads_spin.setValue(conf.THREAD_COUNT)

    def _update_model_path_tooltip(self, text: str) -> None:
        """Update model path input tooltip to show full path."""
        self.model_path_input.setToolTip(text)

    def on_config_changed(self) -> None:
        """Handle configuration changes and update internal state."""
        conf.MODEL_PATH = self.model_path_input.text().strip()
        conf.LISTEN_PORT = self.port_spin.value()
        conf.THREAD_COUNT = self.threads_spin.value()
        # Configuration will be saved on exit via atexit.register

    def on_load_model(self) -> None:
        """Select model file with improved file dialog."""
        initial_dir = (
            str(Path(conf.MODEL_PATH).parent)
            if conf.MODEL_PATH and Path(conf.MODEL_PATH).exists()
            else str(Path.home())
        )

        path, _ = QFileDialog.getOpenFileName(
            self,
            _translation_manager.tr("select_gguf_model"),
            initial_dir,
            f"{_translation_manager.tr('gguf_models')};;{_translation_manager.tr('all_files')}",
        )

        if path:
            conf.MODEL_PATH = path
            normalized_path = os.path.normpath(path)
            self.model_path_input.setText(normalized_path)
            # Update tooltip to show full path
            self.model_path_input.setToolTip(normalized_path)
            self.append_output(_translation_manager.tr("model_selected", path=path) + "\n", self.info_format)

    def toggle_server(self) -> None:
        """Start or stop server."""
        if self.process.state() == QProcess.Running:
            self.stop_server()
        else:
            self.start_server()

    def start_server(self) -> None:
        """Start server with enhanced validation and error handling."""
        model_path = pathlib.Path(self.model_path_input.text().strip())

        # Enhanced validation
        if not model_path.exists():
            self.append_output(f"Error: Model file not found: {model_path}\n", self.error_format)
            return

        if not model_path.is_file():
            self.append_output(f"Error: Path is not a file: {model_path}\n", self.error_format)
            return

        if model_path.suffix.lower() != ".gguf":
            self.append_output(
                f"Warning: Model file does not have .gguf extension: {model_path}\n", self.warning_format
            )

        # Check if llama-server is available
        import shutil

        llama_server_path = shutil.which("llama-server")
        if not llama_server_path:
            self.append_output("Error: llama-server not found in PATH. Please install llama.cpp.\n", self.error_format)
            return

        self.append_output(f"Found llama-server at: {llama_server_path}\n", self.info_format)

        try:
            os.chdir(str(model_path.parent))
        except OSError as e:
            self.append_output(f"Error: Cannot access model directory: {e}\n", self.error_format)
            return

        cmd = [
            "llama-server",
            "--model",
            model_path.name,
            "--port",
            str(self.port_spin.value()),
            "--threads",
            str(self.threads_spin.value()),
        ]

        self.append_output(f"Starting server: {' '.join(cmd)}\n", self.info_format)
        self.append_output(f"Working directory: {model_path.parent}\n", self.info_format)

        try:
            self.process.start(cmd[0], cmd[1:])
            if self.process.waitForStarted(5000):  # Wait up to 5 seconds for startup
                self.update_ui_state(running=True)
                self.append_output(_translation_manager.tr("server_started") + "\n", self.info_format)
            else:
                error_msg = f"Error: Failed to start server process. Error: {self.process.errorString()}\n"
                self.append_output(error_msg, self.error_format)
        except (OSError, RuntimeError) as e:
            self.append_output(f"Start failed: {e!s}\n", self.error_format)

    def stop_server(self) -> None:
        """Stop server."""
        if self.process.state() == QProcess.Running:
            self.append_output(_translation_manager.tr("shutting_down_server") + "\n", self.info_format)
            self.process.terminate()
            if not self.process.waitForFinished(self.PROCESS_TERMINATE_TIMEOUT_MS):
                self.process.kill()

    @staticmethod
    def on_start_browser() -> None:
        """Start browser."""
        QDesktopServices.openUrl(QUrl(f"{conf.URL}:{conf.LISTEN_PORT}"))

    def on_process_finished(self, exit_code: int, exit_status: int) -> None:
        """Process finished with enhanced status reporting."""
        status_msg = f"Server stopped. Exit code: {exit_code}, Status: {exit_status}"
        self.append_output(f"\n{status_msg}\n", self.info_format)

        # Provide detailed error information
        if exit_code != 0 or exit_status == QProcess.CrashExit:
            self.append_output(_translation_manager.tr("non_zero_exit") + "\n", self.warning_format)

            # Show process error if available
            error = self.process.error()
            if error != QProcess.UnknownError:
                error_strings = {
                    QProcess.FailedToStart: "Failed to start",
                    QProcess.Crashed: "Crashed",
                    QProcess.Timedout: "Timed out",
                    QProcess.WriteError: "Write error",
                    QProcess.ReadError: "Read error",
                }
                error_name = error_strings.get(error, f"Unknown error ({error})")
                self.append_output(f"Process error: {error_name}\n", self.error_format)

            error_string = self.process.errorString()
            if error_string:
                self.append_output(f"Error details: {error_string}\n", self.error_format)

        self.update_ui_state(running=False)

    def handle_stdout(self) -> None:
        """Handle standard output."""
        data = self.process.readAllStandardOutput()
        text = QTextStream(data).readAll()
        self.append_output(text, self.info_format)

    def handle_stderr(self) -> None:
        """Handle standard error."""
        data = self.process.readAllStandardError()
        text = QTextStream(data).readAll()
        self.append_output(text, self.error_format)

    def append_output(self, text: str, text_format: QTextCharFormat | None = None) -> None:
        """Append output."""
        cursor: QTextCursor = self.output_area.textCursor()
        cursor.movePosition(QTextCursor.MoveOperation.End)

        if text_format:
            cursor.setCharFormat(text_format)

        cursor.insertText(text)

        self.output_area.setTextCursor(cursor)
        self.output_area.ensureCursorVisible()

    def update_ui_state(self, *, running: bool) -> None:
        """Update UI state."""
        self.model_path_input.setEnabled(not running)
        self.load_model_btn.setEnabled(not running)
        self.port_spin.setEnabled(not running)
        self.threads_spin.setEnabled(not running)
        self.browser_btn.setEnabled(running)

        if running:
            self.start_btn.setText("Stop Server")
        else:
            self.start_btn.setText("Start Server")

    def moveEvent(self, event: QMoveEvent) -> None:
        """Handle window move event."""
        top_left = self.geometry().topLeft()
        conf.WIN_POS = [top_left.x(), top_left.y()]
        return super().moveEvent(event)

    def resizeEvent(self, event: QResizeEvent) -> None:
        """Handle window resize event."""
        geometry = self.geometry()
        conf.WIN_SIZE = [geometry.width(), geometry.height()]
        return super().resizeEvent(event)

    def closeEvent(self, event: QCloseEvent) -> None:
        """Handle window close event with enhanced cleanup."""
        try:
            # Save current configuration
            logger.info("Configuration saved successfully on exit")
        except (RuntimeError, OSError, TypeError, ValueError):
            logger.exception("Failed to save configuration on exit")

        try:
            # Stop server if running
            if hasattr(self, "process") and self.process.state() == QProcess.Running:
                self.append_output(_translation_manager.tr("shutting_down_server") + "\n", self.info_format)
                self.process.terminate()
                if not self.process.waitForFinished(3000):  # Wait up to 3 seconds
                    self.process.kill()
                    self.process.waitForFinished(1000)
                self.append_output(_translation_manager.tr("server_shutdown_complete") + "\n", self.info_format)
        except (OSError, RuntimeError):
            logger.exception("Error during shutdown")
        finally:
            event.accept()


def main() -> None:
    """Run entry point for the GUI application.

    Graphical interface for llmserver
    """
    app = QApplication(sys.argv)
    # Font already configured by setup_pyside_env() in env.py
    # No need to set font here - it will use the cross-platform font stack
    window = LlamaServerGUI()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
