import sys
import os
import json
import logging
from pathlib import Path
from functools import partial

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QPushButton, QLabel, QTabWidget, QComboBox,
    QLineEdit, QTextEdit, QSpinBox, QTableWidget, QGroupBox, QFormLayout,
    QHeaderView, QAbstractItemView, QListWidgetItem, QTableWidgetItem, QMessageBox,
    QFileDialog, QInputDialog, QSizePolicy
)
from PySide6.QtCore import Qt, Signal, QObject, Slot
from PySide6.QtGui import QAction # Keep QAction if needed for context menus within editor later

# --- Constants ---
# Assuming project root is parent of the directory containing this file
script_dir = Path(__file__).parent
project_root = script_dir.parent
PRESETS_DIR = project_root / "Presets"
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
APP_SETTINGS_PATH_LOCAL = project_root / "config" / "app_settings.json"


log = logging.getLogger(__name__)

# --- Preset Editor Widget ---

class PresetEditorWidget(QWidget):
    """
    Widget dedicated to managing and editing presets.
    Contains the preset list, editor tabs, and save/load functionality.
    """
    # Signal emitted when presets list changes (saved, deleted, new)
    presets_changed_signal = Signal()
    # Signal emitted when the selected preset (or LLM/Placeholder) changes
    # Emits: mode ("preset", "llm", "placeholder"), preset_name (str or None)
    preset_selection_changed_signal = Signal(str, str)

    def __init__(self, parent=None):
        super().__init__(parent)

        # --- Internal State ---
        self._last_valid_preset_name = None # Store the name of the last valid preset loaded
        self.current_editing_preset_path = None
        self.editor_unsaved_changes = False
        self._is_loading_editor = False # Flag to prevent signals during load

        # --- UI Setup ---
        self._init_ui()

        # --- Initial State ---
        self._ftd_keys = self._get_file_type_definition_keys()
        self._clear_editor()
        self._set_editor_enabled(False)
        self.populate_presets()

        # --- Connect Editor Signals ---
        self._connect_editor_change_signals()

    def _get_file_type_definition_keys(self) -> list[str]:
        """Loads FILE_TYPE_DEFINITIONS keys from app_settings.json."""
        keys = []
        try:
            if APP_SETTINGS_PATH_LOCAL.is_file():
                with open(APP_SETTINGS_PATH_LOCAL, 'r', encoding='utf-8') as f:
                    settings = json.load(f)
                ftd = settings.get("FILE_TYPE_DEFINITIONS", {})
                keys = list(ftd.keys())
                log.debug(f"Successfully loaded {len(keys)} FILE_TYPE_DEFINITIONS keys.")
            else:
                log.error(f"app_settings.json not found at {APP_SETTINGS_PATH_LOCAL} for PresetEditorWidget.")
        except json.JSONDecodeError as e:
            log.error(f"Failed to parse app_settings.json in PresetEditorWidget: {e}")
        except Exception as e:
            log.error(f"Error loading FILE_TYPE_DEFINITIONS keys in PresetEditorWidget: {e}")
        return keys

    def _init_ui(self):
        """Initializes the UI elements for the preset editor."""
        main_layout = QVBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0) # Let containers manage margins
        main_layout.setSpacing(0) # No space between selector and editor containers

        # Preset List and Controls
        self.selector_container = QWidget()
        selector_layout = QVBoxLayout(self.selector_container)
        selector_layout.setContentsMargins(5, 5, 5, 5) # Margins for selector area

        selector_layout.addWidget(QLabel("Presets:"))
        self.editor_preset_list = QListWidget()
        self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing)
        selector_layout.addWidget(self.editor_preset_list)

        list_button_layout = QHBoxLayout()
        self.editor_new_button = QPushButton("New")
        self.editor_delete_button = QPushButton("Delete")
        self.editor_new_button.clicked.connect(self._new_preset)
        self.editor_delete_button.clicked.connect(self._delete_selected_preset)
        list_button_layout.addWidget(self.editor_new_button)
        list_button_layout.addWidget(self.editor_delete_button)
        selector_layout.addLayout(list_button_layout)
        main_layout.addWidget(self.selector_container)

        # Editor Tabs
        self.json_editor_container = QWidget()
        editor_layout = QVBoxLayout(self.json_editor_container)
        editor_layout.setContentsMargins(5, 0, 5, 5) # Margins for editor area (no top margin)

        self.editor_tab_widget = QTabWidget()
        self.editor_tab_general_naming = QWidget()
        self.editor_tab_mapping_rules = QWidget()
        self.editor_tab_widget.addTab(self.editor_tab_general_naming, "General & Naming")
        self.editor_tab_widget.addTab(self.editor_tab_mapping_rules, "Mapping & Rules")
        self._create_editor_general_tab()
        self._create_editor_mapping_tab()
        editor_layout.addWidget(self.editor_tab_widget, 1) # Allow tabs to stretch

        # Save Buttons
        save_button_layout = QHBoxLayout()
        self.editor_save_button = QPushButton("Save")
        self.editor_save_as_button = QPushButton("Save As...")
        self.editor_save_button.setEnabled(False)
        self.editor_save_button.clicked.connect(self._save_current_preset)
        self.editor_save_as_button.clicked.connect(self._save_preset_as)
        save_button_layout.addStretch()
        save_button_layout.addWidget(self.editor_save_button)
        save_button_layout.addWidget(self.editor_save_as_button)
        editor_layout.addLayout(save_button_layout)

        main_layout.addWidget(self.json_editor_container)

    def _create_editor_general_tab(self):
        """Creates the widgets and layout for the 'General & Naming' editor tab."""
        layout = QVBoxLayout(self.editor_tab_general_naming)
        form_layout = QFormLayout()
        form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)

        # Basic Info
        self.editor_preset_name = QLineEdit()
        self.editor_supplier_name = QLineEdit()
        self.editor_notes = QTextEdit()
        self.editor_notes.setAcceptRichText(False)
        self.editor_notes.setFixedHeight(60)
        form_layout.addRow("Preset Name:", self.editor_preset_name)
        form_layout.addRow("Supplier Name:", self.editor_supplier_name)
        form_layout.addRow("Notes:", self.editor_notes)

        layout.addLayout(form_layout)

        # Source Naming Group
        naming_group = QGroupBox("Source File Naming Rules")
        naming_layout_outer = QVBoxLayout(naming_group)
        naming_layout_form = QFormLayout()
        self.editor_separator = QLineEdit()
        self.editor_separator.setMaxLength(1)
        self.editor_spin_base_name_idx = QSpinBox()
        self.editor_spin_base_name_idx.setMinimum(-1)
        self.editor_spin_map_type_idx = QSpinBox()
        self.editor_spin_map_type_idx.setMinimum(-1)
        naming_layout_form.addRow("Separator:", self.editor_separator)
        naming_layout_form.addRow("Base Name Index:", self.editor_spin_base_name_idx)
        naming_layout_form.addRow("Map Type Index:", self.editor_spin_map_type_idx)
        naming_layout_outer.addLayout(naming_layout_form)
        # Gloss Keywords List
        self._setup_list_widget_with_controls(naming_layout_outer, "Glossiness Keywords", "editor_list_gloss_keywords")
        # Bit Depth Variants Table
        self._setup_table_widget_with_controls(naming_layout_outer, "16-bit Variant Patterns", "editor_table_bit_depth_variants", ["Map Type", "Pattern"])
        self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
        self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        layout.addWidget(naming_group)

        # Extra Files Group
        self._setup_list_widget_with_controls(layout, "Move to 'Extra' Folder Patterns", "editor_list_extra_patterns")

        layout.addStretch(1)

    def _create_editor_mapping_tab(self):
        """Creates the widgets and layout for the 'Mapping & Rules' editor tab."""
        layout = QVBoxLayout(self.editor_tab_mapping_rules)

        # Map Type Mapping Group
        self._setup_table_widget_with_controls(layout, "Map Type Mapping (Standard Type <- Input Keywords)", "editor_table_map_type_mapping", ["Standard Type", "Input Keywords (comma-sep)"])
        self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
        self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)

        # Category Rules Group
        category_group = QGroupBox("Asset Category Rules")
        category_layout = QVBoxLayout(category_group)
        self._setup_list_widget_with_controls(category_layout, "Model File Patterns", "editor_list_model_patterns")
        self._setup_list_widget_with_controls(category_layout, "Decal Keywords", "editor_list_decal_keywords")
        layout.addWidget(category_group)

        # Archetype Rules Group
        self._setup_table_widget_with_controls(layout, "Archetype Rules", "editor_table_archetype_rules", ["Archetype Name", "Match Any (comma-sep)", "Match All (comma-sep)"])
        self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
        self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)

        layout.addStretch(1)

    # --- Helper Functions for UI Setup (Moved into class) ---
    def _setup_list_widget_with_controls(self, parent_layout, label_text, attribute_name):
        """Adds a QListWidget with Add/Remove buttons to a layout."""
        list_widget = QListWidget()
        list_widget.setAlternatingRowColors(True)
        list_widget.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
        setattr(self, attribute_name, list_widget)

        add_button = QPushButton("+")
        remove_button = QPushButton("-")
        add_button.setFixedWidth(30)
        remove_button.setFixedWidth(30)

        button_layout = QVBoxLayout()
        button_layout.addWidget(add_button)
        button_layout.addWidget(remove_button)
        button_layout.addStretch()

        list_layout = QHBoxLayout()
        list_layout.addWidget(list_widget)
        list_layout.addLayout(button_layout)

        group_box = QGroupBox(label_text)
        group_box_layout = QVBoxLayout(group_box)
        group_box_layout.addLayout(list_layout)

        parent_layout.addWidget(group_box)

        # Connections
        add_button.clicked.connect(partial(self._editor_add_list_item, list_widget))
        remove_button.clicked.connect(partial(self._editor_remove_list_item, list_widget))
        list_widget.itemChanged.connect(self._mark_editor_unsaved)

    def _setup_table_widget_with_controls(self, parent_layout, label_text, attribute_name, columns):
        """Adds a QTableWidget with Add/Remove buttons to a layout."""
        table_widget = QTableWidget()
        table_widget.setColumnCount(len(columns))
        table_widget.setHorizontalHeaderLabels(columns)
        table_widget.setAlternatingRowColors(True)
        setattr(self, attribute_name, table_widget)

        add_button = QPushButton("+ Row")
        remove_button = QPushButton("- Row")

        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(add_button)
        button_layout.addWidget(remove_button)

        group_box = QGroupBox(label_text)
        group_box_layout = QVBoxLayout(group_box)
        group_box_layout.addWidget(table_widget)
        group_box_layout.addLayout(button_layout)

        parent_layout.addWidget(group_box)

        # Connections
        add_button.clicked.connect(partial(self._editor_add_table_row, table_widget))
        remove_button.clicked.connect(partial(self._editor_remove_table_row, table_widget))
        table_widget.itemChanged.connect(self._mark_editor_unsaved)

    # --- Preset Population and Handling ---
    def populate_presets(self):
        """Scans presets dir and populates the editor list."""
        log.debug("Populating preset list in PresetEditorWidget...")
        current_list_item = self.editor_preset_list.currentItem()
        current_list_selection_text = current_list_item.text() if current_list_item else None

        self.editor_preset_list.clear()
        log.debug("Preset list cleared.")

        placeholder_item = QListWidgetItem("--- Select a Preset ---")
        placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable)
        placeholder_item.setData(Qt.ItemDataRole.UserRole, "__PLACEHOLDER__")
        self.editor_preset_list.addItem(placeholder_item)
        log.debug("Added '--- Select a Preset ---' placeholder item.")

        llm_item = QListWidgetItem("- LLM Interpretation -")
        llm_item.setData(Qt.ItemDataRole.UserRole, "__LLM__") # Special identifier
        self.editor_preset_list.addItem(llm_item)
        log.debug("Added '- LLM Interpretation -' item.")

        if not PRESETS_DIR.is_dir():
            msg = f"Error: Presets directory not found at {PRESETS_DIR}"
            log.error(msg)
            return

        presets = sorted([f for f in PRESETS_DIR.glob("*.json") if f.is_file() and not f.name.startswith('_')])

        if not presets:
            msg = "Warning: No presets found in presets directory."
            log.warning(msg)
        else:
            for preset_path in presets:
                item = QListWidgetItem(preset_path.stem)
                item.setData(Qt.ItemDataRole.UserRole, preset_path)
                self.editor_preset_list.addItem(item)
            log.info(f"Loaded {len(presets)} presets into editor list.")

        # Select the "Select a Preset" item by default
        log.debug("Preset list populated. Selecting '--- Select a Preset ---' item.")
        self.editor_preset_list.setCurrentItem(placeholder_item)

    # --- Preset Editor Methods ---

    def _editor_add_list_item(self, list_widget: QListWidget):
        """Adds an editable item to the specified list widget in the editor."""
        text, ok = QInputDialog.getText(self, f"Add Item", "Enter value:")
        if ok and text:
            item = QListWidgetItem(text)
            list_widget.addItem(item)
            self._mark_editor_unsaved()

    def _editor_remove_list_item(self, list_widget: QListWidget):
        """Removes the selected item from the specified list widget in the editor."""
        selected_items = list_widget.selectedItems()
        if not selected_items: return
        for item in selected_items: list_widget.takeItem(list_widget.row(item))
        self._mark_editor_unsaved()

    def _editor_add_table_row(self, table_widget: QTableWidget):
        """Adds an empty row to the specified table widget in the editor."""
        row_count = table_widget.rowCount()
        table_widget.insertRow(row_count)

        if table_widget == self.editor_table_map_type_mapping:
            # Column 0: Standard Type (QComboBox)
            combo_box = QComboBox()
            if self._ftd_keys:
                combo_box.addItems(self._ftd_keys)
            else:
                log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping.")
            combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
            table_widget.setCellWidget(row_count, 0, combo_box)
            # Column 1: Input Keywords (QTableWidgetItem)
            table_widget.setItem(row_count, 1, QTableWidgetItem(""))
        else: # For other tables
            for col in range(table_widget.columnCount()):
                table_widget.setItem(row_count, col, QTableWidgetItem(""))
        self._mark_editor_unsaved()

    def _editor_remove_table_row(self, table_widget: QTableWidget):
        """Removes the selected row(s) from the specified table widget in the editor."""
        selected_rows = sorted(list(set(index.row() for index in table_widget.selectedIndexes())), reverse=True)
        if not selected_rows:
            if table_widget.rowCount() > 0: selected_rows = [table_widget.rowCount() - 1]
            else: return
        for row in selected_rows: table_widget.removeRow(row)
        self._mark_editor_unsaved()

    def _mark_editor_unsaved(self):
        """Marks changes in the editor panel as unsaved."""
        if self._is_loading_editor: return
        self.editor_unsaved_changes = True
        self.editor_save_button.setEnabled(True)

    def _connect_editor_change_signals(self):
        """Connect signals from all editor widgets to mark_editor_unsaved."""
        self.editor_preset_name.textChanged.connect(self._mark_editor_unsaved)
        self.editor_supplier_name.textChanged.connect(self._mark_editor_unsaved)
        self.editor_notes.textChanged.connect(self._mark_editor_unsaved)
        self.editor_separator.textChanged.connect(self._mark_editor_unsaved)
        self.editor_spin_base_name_idx.valueChanged.connect(self._mark_editor_unsaved)
        self.editor_spin_map_type_idx.valueChanged.connect(self._mark_editor_unsaved)
        # List/Table widgets are connected via helper functions
    def check_unsaved_changes(self) -> bool:
        """
        Checks for unsaved changes in the editor and prompts the user.
        Returns True if the calling action should be cancelled.
        (Called by MainWindow's closeEvent or before loading a new preset).
        """
        if not self.editor_unsaved_changes: return False # No unsaved changes, proceed
        reply = QMessageBox.question(self, "Unsaved Preset Changes", # Use self as parent
                                     "You have unsaved changes in the preset editor. Discard them?",
                                     QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
                                     QMessageBox.StandardButton.Cancel)
        if reply == QMessageBox.StandardButton.Save:
            save_successful = self._save_current_preset()
            return not save_successful # Return True (cancel) if save fails
        elif reply == QMessageBox.StandardButton.Discard:
            return False # Discarded, proceed
        else: # Cancelled
            return True # Cancel the original action

    def _set_editor_enabled(self, enabled: bool):
        """Enables or disables all editor widgets."""
        # Target the container holding the tabs and save buttons
        self.json_editor_container.setEnabled(enabled)
        # Save button state still depends on unsaved changes, but only if container is enabled
        self.editor_save_button.setEnabled(enabled and self.editor_unsaved_changes)

    def _clear_editor(self):
        """Clears the editor fields and resets state."""
        self._is_loading_editor = True
        try:
            self.editor_preset_name.clear()
            self.editor_supplier_name.clear()
            self.editor_notes.clear()
            self.editor_separator.clear()
            self.editor_spin_base_name_idx.setValue(0)
            self.editor_spin_map_type_idx.setValue(1)
            self.editor_list_gloss_keywords.clear()
            self.editor_table_bit_depth_variants.setRowCount(0)
            self.editor_list_extra_patterns.clear()
            self.editor_table_map_type_mapping.setRowCount(0)
            self.editor_list_model_patterns.clear()
            self.editor_list_decal_keywords.clear()
            self.editor_table_archetype_rules.setRowCount(0)
            self.current_editing_preset_path = None
            self.editor_unsaved_changes = False
            self.editor_save_button.setEnabled(False)
            self._set_editor_enabled(False)
        finally:
            self._is_loading_editor = False

    def _populate_editor_from_data(self, preset_data: dict):
        """Helper method to populate editor UI widgets from a preset data dictionary."""
        self._is_loading_editor = True
        try:
            self.editor_preset_name.setText(preset_data.get("preset_name", ""))
            self.editor_supplier_name.setText(preset_data.get("supplier_name", ""))
            self.editor_notes.setText(preset_data.get("notes", ""))
            naming_data = preset_data.get("source_naming", {})
            self.editor_separator.setText(naming_data.get("separator", "_"))
            indices = naming_data.get("part_indices", {})
            self.editor_spin_base_name_idx.setValue(indices.get("base_name", 0))
            self.editor_spin_map_type_idx.setValue(indices.get("map_type", 1))
            self.editor_list_gloss_keywords.clear()
            self.editor_list_gloss_keywords.addItems(naming_data.get("glossiness_keywords", []))
            self.editor_table_bit_depth_variants.setRowCount(0)
            bit_depth_vars = naming_data.get("bit_depth_variants", {})
            for i, (map_type, pattern) in enumerate(bit_depth_vars.items()):
                self.editor_table_bit_depth_variants.insertRow(i)
                self.editor_table_bit_depth_variants.setItem(i, 0, QTableWidgetItem(map_type))
                self.editor_table_bit_depth_variants.setItem(i, 1, QTableWidgetItem(pattern))
            self.editor_list_extra_patterns.clear()
            self.editor_list_extra_patterns.addItems(preset_data.get("move_to_extra_patterns", []))

            self.editor_table_map_type_mapping.setRowCount(0) # Clear before populating
            map_mappings = preset_data.get("map_type_mapping", [])
            for i, mapping_dict in enumerate(map_mappings):
                if isinstance(mapping_dict, dict) and "target_type" in mapping_dict and "keywords" in mapping_dict:
                    std_type = mapping_dict["target_type"]
                    keywords = mapping_dict["keywords"]
                    self.editor_table_map_type_mapping.insertRow(i)

                    # Column 0: Standard Type (QComboBox)
                    combo_box = QComboBox()
                    if self._ftd_keys:
                        combo_box.addItems(self._ftd_keys)
                        if std_type in self._ftd_keys:
                            combo_box.setCurrentText(std_type)
                        else:
                            log.warning(f"Preset '{preset_data.get('preset_name', 'Unknown')}': target_type '{std_type}' not found in FILE_TYPE_DEFINITIONS. Selecting first available.")
                            if self._ftd_keys: combo_box.setCurrentIndex(0)
                    else:
                        log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping during population.")
                    
                    combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
                    self.editor_table_map_type_mapping.setCellWidget(i, 0, combo_box)

                    # Column 1: Input Keywords (QTableWidgetItem)
                    keywords_str = [str(k) for k in keywords if isinstance(k, str)]
                    self.editor_table_map_type_mapping.setItem(i, 1, QTableWidgetItem(", ".join(keywords_str)))
                else:
                    log.warning(f"Skipping invalid map_type_mapping item during editor population: {mapping_dict}")
            
            category_rules = preset_data.get("asset_category_rules", {})
            self.editor_list_model_patterns.clear()
            self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", []))
            self.editor_list_decal_keywords.clear()
            self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", []))
            # Archetype rules population (assuming table exists)
            self.editor_table_archetype_rules.setRowCount(0)
            arch_rules_data = preset_data.get("archetype_rules", [])
            for i, rule_entry in enumerate(arch_rules_data):
                 # Handle both list and dict format for backward compatibility? Assuming list for now.
                 if isinstance(rule_entry, (list, tuple)) and len(rule_entry) == 2:
                     name, conditions = rule_entry
                     if isinstance(conditions, dict):
                         match_any = conditions.get("match_any", [])
                         match_all = conditions.get("match_all", [])
                         self.editor_table_archetype_rules.insertRow(i)
                         self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(str(name)))
                         self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(", ".join(map(str, match_any))))
                         self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(", ".join(map(str, match_all))))
                     else:
                         log.warning(f"Skipping invalid archetype rule condition format: {conditions}")
                 else:
                     log.warning(f"Skipping invalid archetype rule format: {rule_entry}")

        finally:
            self._is_loading_editor = False

    def _load_preset_for_editing(self, file_path: Path):
        """Loads the content of the selected preset file into the editor widgets."""
        if not file_path or not file_path.is_file():
            self._clear_editor()
            return
        log.info(f"Loading preset into editor: {file_path.name}")
        try:
            with open(file_path, 'r', encoding='utf-8') as f: preset_data = json.load(f)
            self._populate_editor_from_data(preset_data)
            self._set_editor_enabled(True)
            self.current_editing_preset_path = file_path
            self.editor_unsaved_changes = False
            self.editor_save_button.setEnabled(False)
            log.info(f"Preset '{file_path.name}' loaded into editor.")
        except json.JSONDecodeError as json_err:
             log.error(f"Invalid JSON in {file_path.name}: {json_err}")
             QMessageBox.warning(self, "Load Error", f"Failed to load preset '{file_path.name}'.\nInvalid JSON structure:\n{json_err}")
             self._clear_editor()
        except Exception as e:
            log.exception(f"Error loading preset file {file_path}: {e}")
            QMessageBox.critical(self, "Error", f"Could not load preset file:\n{file_path}\n\nError: {e}")
            self._clear_editor()

    @Slot(QListWidgetItem, QListWidgetItem)
    def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem):
        """Loads the preset currently selected in the editor list and emits selection change signal."""
        log.debug(f"PresetEditor: currentItemChanged signal triggered. current: {current_item.text() if current_item else 'None'}")

        mode = "placeholder"
        preset_name = None

        # Check for unsaved changes before proceeding
        if self.check_unsaved_changes():
            # If user cancels, revert selection
            if previous_item:
                log.debug("Unsaved changes check cancelled. Reverting selection.")
                self.editor_preset_list.blockSignals(True)
                self.editor_preset_list.setCurrentItem(previous_item)
                self.editor_preset_list.blockSignals(False)
            return # Stop processing

        # Determine mode and preset name based on selection
        if current_item:
            item_data = current_item.data(Qt.ItemDataRole.UserRole)
            if item_data == "__PLACEHOLDER__":
                log.debug("Placeholder item selected.")
                self._clear_editor()
                self._set_editor_enabled(False)
                mode = "placeholder"
                self._last_valid_preset_name = None # Clear last valid name
            elif item_data == "__LLM__":
                log.debug("LLM Interpretation item selected.")
                self._clear_editor()
                self._set_editor_enabled(False)
                mode = "llm"
                # Keep _last_valid_preset_name as it was
            elif isinstance(item_data, Path):
                log.debug(f"Loading preset for editing: {current_item.text()}")
                preset_path = item_data
                self._load_preset_for_editing(preset_path)
                self._last_valid_preset_name = preset_path.stem
                mode = "preset"
                preset_name = self._last_valid_preset_name
            else:
                log.error(f"Invalid data type for preset path: {type(item_data)}. Clearing editor.")
                self._clear_editor()
                self._set_editor_enabled(False)
                mode = "placeholder" # Treat as placeholder on error
                self._last_valid_preset_name = None
        else:
             log.debug("No preset selected. Clearing editor.")
             self._clear_editor()
             self._set_editor_enabled(False)
             mode = "placeholder"
             self._last_valid_preset_name = None

        # Emit the signal regardless of what was selected
        log.debug(f"Emitting preset_selection_changed_signal: mode='{mode}', preset_name='{preset_name}'")
        self.preset_selection_changed_signal.emit(mode, preset_name)

    def _gather_editor_data(self) -> dict:
        """Gathers data from all editor UI widgets and returns a dictionary."""
        preset_data = {}
        preset_data["preset_name"] = self.editor_preset_name.text().strip()
        preset_data["supplier_name"] = self.editor_supplier_name.text().strip()
        preset_data["notes"] = self.editor_notes.toPlainText().strip()
        naming_data = {}
        naming_data["separator"] = self.editor_separator.text()
        naming_data["part_indices"] = { "base_name": self.editor_spin_base_name_idx.value(), "map_type": self.editor_spin_map_type_idx.value() }
        naming_data["glossiness_keywords"] = [self.editor_list_gloss_keywords.item(i).text() for i in range(self.editor_list_gloss_keywords.count())]
        naming_data["bit_depth_variants"] = {self.editor_table_bit_depth_variants.item(r, 0).text(): self.editor_table_bit_depth_variants.item(r, 1).text()
                                             for r in range(self.editor_table_bit_depth_variants.rowCount()) if self.editor_table_bit_depth_variants.item(r, 0) and self.editor_table_bit_depth_variants.item(r, 1)}
        preset_data["source_naming"] = naming_data
        preset_data["move_to_extra_patterns"] = [self.editor_list_extra_patterns.item(i).text() for i in range(self.editor_list_extra_patterns.count())]
        
        map_mappings = []
        for r in range(self.editor_table_map_type_mapping.rowCount()):
            target_type_widget = self.editor_table_map_type_mapping.cellWidget(r, 0)
            keywords_item = self.editor_table_map_type_mapping.item(r, 1)
            
            target_type = ""
            if isinstance(target_type_widget, QComboBox):
                target_type = target_type_widget.currentText()
            elif self.editor_table_map_type_mapping.item(r, 0): # Fallback if item is not a widget
                target_type_item = self.editor_table_map_type_mapping.item(r, 0)
                if target_type_item:
                    target_type = target_type_item.text().strip()

            if target_type and keywords_item and keywords_item.text():
                 keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()]
                 if keywords: # Ensure keywords list is not empty after stripping
                      map_mappings.append({"target_type": target_type, "keywords": keywords})
                 else:
                      log.warning(f"Skipping row {r} in map type mapping table due to empty keywords after processing for target_type '{target_type}'.")
            else:
                 # Log if target_type is empty or keywords_item is problematic
                 if not target_type:
                     log.warning(f"Skipping row {r} in map type mapping table due to empty target_type.")
                 if not (keywords_item and keywords_item.text()):
                     log.warning(f"Skipping row {r} in map type mapping table for target_type '{target_type}' due to missing or empty keywords item.")
        preset_data["map_type_mapping"] = map_mappings
        
        category_rules = {}
        category_rules["model_patterns"] = [self.editor_list_model_patterns.item(i).text() for i in range(self.editor_list_model_patterns.count())]
        category_rules["decal_keywords"] = [self.editor_list_decal_keywords.item(i).text() for i in range(self.editor_list_decal_keywords.count())]
        preset_data["asset_category_rules"] = category_rules
        arch_rules = []
        for r in range(self.editor_table_archetype_rules.rowCount()):
            name_item = self.editor_table_archetype_rules.item(r, 0)
            any_item = self.editor_table_archetype_rules.item(r, 1)
            all_item = self.editor_table_archetype_rules.item(r, 2)
            if name_item and name_item.text() and any_item and all_item: # Check name has text
                 match_any = [k.strip() for k in any_item.text().split(',') if k.strip()]
                 match_all = [k.strip() for k in all_item.text().split(',') if k.strip()]
                 # Only add if name is present and at least one condition list is non-empty? Or allow empty conditions?
                 # Let's allow empty conditions for now.
                 arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}])
            else:
                 log.warning(f"Skipping row {r} in archetype rules table due to missing items or empty name.")
        preset_data["archetype_rules"] = arch_rules
        return preset_data

    def _save_current_preset(self) -> bool:
        """Saves the current editor content to the currently loaded file path."""
        if not self.current_editing_preset_path: return self._save_preset_as()
        log.info(f"Saving preset: {self.current_editing_preset_path.name}")
        try:
            preset_data = self._gather_editor_data()
            if not preset_data.get("preset_name"): QMessageBox.warning(self, "Save Error", "Preset Name cannot be empty."); return False
            if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save Error", "Supplier Name cannot be empty."); return False
            content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)
            with open(self.current_editing_preset_path, 'w', encoding='utf-8') as f: f.write(content_to_save)
            self.editor_unsaved_changes = False
            self.editor_save_button.setEnabled(False)
            self.presets_changed_signal.emit()
            log.info("Preset saved successfully.")
            self.populate_presets()
            # Reselect the saved item
            items = self.editor_preset_list.findItems(self.current_editing_preset_path.stem, Qt.MatchFlag.MatchExactly)
            if items: self.editor_preset_list.setCurrentItem(items[0])
            return True
        except Exception as e:
            log.exception(f"Error saving preset file {self.current_editing_preset_path}: {e}")
            QMessageBox.critical(self, "Save Error", f"Could not save preset file:\n{self.current_editing_preset_path}\n\nError: {e}")
            return False

    def _save_preset_as(self) -> bool:
        """Saves the current editor content to a new file chosen by the user."""
        log.debug("Save As action triggered.")
        try:
            preset_data = self._gather_editor_data()
            new_preset_name = preset_data.get("preset_name")
            if not new_preset_name: QMessageBox.warning(self, "Save As Error", "Preset Name cannot be empty."); return False
            if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save As Error", "Supplier Name cannot be empty."); return False
            content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)
            suggested_name = f"{new_preset_name}.json"
            default_path = PRESETS_DIR / suggested_name
            file_path_str, _ = QFileDialog.getSaveFileName(self, "Save Preset As", str(default_path), "JSON Files (*.json);;All Files (*)")
            if not file_path_str: log.debug("Save As cancelled by user."); return False
            save_path = Path(file_path_str)
            if save_path.suffix.lower() != ".json": save_path = save_path.with_suffix(".json")
            if save_path.exists() and save_path != self.current_editing_preset_path:
                 reply = QMessageBox.warning(self, "Confirm Overwrite", f"Preset '{save_path.name}' already exists. Overwrite?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
                 if reply == QMessageBox.StandardButton.No: log.debug("Save As overwrite cancelled."); return False
            log.info(f"Saving preset as: {save_path.name}")
            with open(save_path, 'w', encoding='utf-8') as f: f.write(content_to_save)
            self.current_editing_preset_path = save_path
            self.editor_unsaved_changes = False
            self.editor_save_button.setEnabled(False)
            self.presets_changed_signal.emit()
            log.info("Preset saved successfully (Save As).")
            # Refresh list and select the new item
            self.populate_presets()
            items = self.editor_preset_list.findItems(save_path.stem, Qt.MatchFlag.MatchExactly)
            if items: self.editor_preset_list.setCurrentItem(items[0])
            return True
        except Exception as e:
            log.exception(f"Error saving preset file (Save As): {e}")
            QMessageBox.critical(self, "Save Error", f"Could not save preset file.\n\nError: {e}")
            return False

    def _new_preset(self):
        """Clears the editor and loads data from _template.json."""
        log.debug("New Preset action triggered.")
        if self.check_unsaved_changes(): return # Check unsaved changes first
        self._clear_editor()
        if TEMPLATE_PATH.is_file():
            log.info("Loading new preset from _template.json")
            try:
                with open(TEMPLATE_PATH, 'r', encoding='utf-8') as f: template_data = json.load(f)
                self._populate_editor_from_data(template_data)
                # Override specific fields for a new preset
                self.editor_preset_name.setText("NewPreset")
            except Exception as e:
                log.exception(f"Error loading template preset file {TEMPLATE_PATH}: {e}")
                QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}")
                self._clear_editor()
            self.editor_supplier_name.setText("MySupplier")
        else:
            log.warning("Presets/_template.json not found. Creating empty preset.")
            self.editor_preset_name.setText("NewPreset")
            self.editor_supplier_name.setText("MySupplier")
        self._set_editor_enabled(True)
        self.editor_unsaved_changes = True
        self.editor_save_button.setEnabled(True)
        # Select the placeholder item to avoid auto-loading the "NewPreset"
        placeholder_item = self.editor_preset_list.findItems("--- Select a Preset ---", Qt.MatchFlag.MatchExactly)
        if placeholder_item:
            self.editor_preset_list.setCurrentItem(placeholder_item[0])
        # Emit selection change for the new state (effectively placeholder)
        self.preset_selection_changed_signal.emit("placeholder", None)


    def _delete_selected_preset(self):
        """Deletes the currently selected preset file from the editor list after confirmation."""
        current_item = self.editor_preset_list.currentItem()
        if not current_item: QMessageBox.information(self, "Delete Preset", "Please select a preset from the list to delete."); return

        item_data = current_item.data(Qt.ItemDataRole.UserRole)
        # Ensure it's a real preset path before attempting delete
        if not isinstance(item_data, Path):
            QMessageBox.information(self, "Delete Preset", "Cannot delete placeholder or LLM option.")
            return

        preset_path = item_data
        preset_name = preset_path.stem
        reply = QMessageBox.warning(self, "Confirm Delete", f"Are you sure you want to permanently delete the preset '{preset_name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.Yes:
            log.info(f"Deleting preset: {preset_path.name}")
            try:
                preset_path.unlink()
                log.info("Preset deleted successfully.")
                if self.current_editing_preset_path == preset_path: self._clear_editor()
                self.presets_changed_signal.emit()
                self.populate_presets()
            except Exception as e:
                log.exception(f"Error deleting preset file {preset_path}: {e}")
                QMessageBox.critical(self, "Delete Error", f"Could not delete preset file:\n{preset_path}\n\nError: {e}")

    # --- Public Access Methods for MainWindow ---

    def get_selected_preset_mode(self) -> tuple[str, str | None]:
        """
        Returns the current selection mode and preset name (if applicable).
        Returns: tuple(mode_string, preset_name_string_or_None)
                 mode_string can be "preset", "llm", "placeholder"
        """
        current_item = self.editor_preset_list.currentItem()
        if current_item:
            item_data = current_item.data(Qt.ItemDataRole.UserRole)
            if item_data == "__PLACEHOLDER__":
                return "placeholder", None
            elif item_data == "__LLM__":
                return "llm", None
            elif isinstance(item_data, Path):
                return "preset", item_data.stem
        return "placeholder", None # Default or if no item selected

    def get_last_valid_preset_name(self) -> str | None:
        """
        Returns the name (stem) of the last valid preset that was loaded.
        Used by delegates to populate dropdowns based on the original context.
        """
        return self._last_valid_preset_name

    # --- Slots for MainWindow Interaction ---
