# -*- coding: utf-8 -*-
"""
/***************************************************************************
 AGGRADockWidget
                                 A QGIS plugin
 An autonomous agent framework to select geospatial data and then fetch data by generating and executing programs with self-debugging.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-08-01
        git sha              : $Format:%H$
        copyright            : (C) 2024 by Geoinformation and Big Data Research Laboratory (GIBD)
        email                : tea5209@psu.edu
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                      *
 ***************************************************************************/
"""
import base64
import configparser
import os
import platform
import re
import shutil
import subprocess
import urllib

import requests
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
import os
import sys
from qgis.PyQt.QtCore import QSettings
import time
import traceback
from io import StringIO
from qgis.PyQt.QtCore import Qt, QCoreApplication
from qgis.PyQt import uic
from qgis.PyQt import QtWidgets
from qgis._core import QgsProject, QgsVectorLayer, QgsCoordinateReferenceSystem, \
    QgsCoordinateTransform, QgsFeature, Qgis
from qgis.utils import iface

QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)

from PyQt5.QtCore import QUrl, QThread, pyqtSignal, pyqtSlot, QSettings, QObject
from PyQt5.QtGui import QTextCursor, QSyntaxHighlighter, QTextCharFormat, QColor, QDesktopServices

from PyQt5.QtWidgets import QGridLayout, QHBoxLayout, QWidget, QPushButton, QFileDialog, QMenu, QAction, QCompleter, \
    QVBoxLayout, QLineEdit, QTableWidgetItem, QDialog, QLabel, QMessageBox, QInputDialog, QComboBox,QTextEdit, QProgressDialog

from qgis.gui import QgsPasswordLineEdit
from qgis.PyQt.QtWebKitWidgets import QWebView

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'AGGRA_dockwidget_base.ui'))

current_script_dir = os.path.dirname(os.path.abspath(__file__))
keys_dir = os.path.join(current_script_dir, 'LLM_Find', 'Keys')
handbooks_dir = os.path.join(current_script_dir, 'LLM_Find', 'Handbooks')
from .install_packages.check_packages import check_missing_libraries, \
    read_libraries_from_file, check_and_install_with_versions, parse_requirements_with_versions, check_version_mismatches



def python_env():
    # Get the os type
    system = platform.system()
    python_exe = getattr(sys, "_base_executable", None)
    # Grab the “real” interpreter path for each OS
    if system == "Windows":
        # usually C:\…\apps\PythonXX\python.exe
        # python_exe = getattr(sys, "_base_executable", None)
        if not python_exe or "qgis" in os.path.basename(python_exe).lower():
            python_exe = os.path.join(sys.prefix, "python.exe")


    elif system == "Linux":
        # usually /usr/bin/python3 or under sys.prefix/bin/
        # python_exe = getattr(sys, "_base_executable", None)
        if not python_exe or "qgis" in os.path.basename(python_exe).lower():
            candidate = os.path.join(sys.prefix, "bin", "python3")
            python_exe = candidate if os.path.isfile(candidate) else "python3"

    elif system == "Darwin":
        # macOS QGIS bundles are similar to Linux
        # python_exe = getattr(sys, "_base_executable", None)
        if not python_exe or "qgis" in os.path.basename(python_exe).lower():
            candidate = os.path.join(sys.prefix, "bin", "python3")
            python_exe = candidate if os.path.isfile(candidate) else "python3"

    else:
        raise RuntimeError(f"Unsupported OS: {system!r}")

    if not python_exe:
        raise RuntimeError("Could not determine Python executable.")

    return python_exe


# ****************************************************************************************************************
def check_pip_installed():
    """
    Check if pip is available in the current Python environment.
    Returns True if pip is available, False otherwise.
    """
    try:
        pip_cmd = [python_env(), "-m", "pip", "--version"]
        subprocess.check_output(pip_cmd)
        return True
    except subprocess.CalledProcessError as e:
        print(f"Pip check failed: {e}")
        return False
    except FileNotFoundError:
        return False


def get_requirements_file():
    """
    Get the path to the requirements file.

    Returns:
        str: Path to requirements.txt
    """
    current_script_dir = os.path.dirname(os.path.abspath(__file__))
    requirements_file = os.path.join(current_script_dir, 'install_packages', 'requirements.txt')
    return requirements_file


class LibraryCheckThread(QThread):
    finished_checking = pyqtSignal(list, dict, bool)  # missing packages list, version mismatches dict, force_reinstall flag

    def __init__(self, filename):
        QThread.__init__(self)
        self.filename = filename

    def run(self):
        try:
            import sys
            import os

            # Add the plugin directory to the path to ensure imports work correctly
            plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            if plugin_dir not in sys.path:
                sys.path.insert(0, plugin_dir)


            # Perform the library check in this thread
            missing_packages = check_missing_libraries(read_libraries_from_file(self.filename))

            # Also check for version mismatches in already-installed packages
            version_mismatches = check_version_mismatches(self.filename)

            self.finished_checking.emit(missing_packages, version_mismatches, False)
        except Exception as e:
            error_str = str(e)
            print(f"Error during library check: {e}")
            # Check if this is a binary incompatibility error (numpy dtype size changed)
            force_reinstall = "numpy.dtype size changed" in error_str or "binary incompatibility" in error_str.lower()

            if force_reinstall:
                print("[WARNING] Binary incompatibility detected. Will use --force-reinstall flag.")
                # Report that force reinstall is needed
                self.finished_checking.emit([], {}, True)
            else:
                # If there's another error in version checking, just report missing packages
                try:
                    missing_packages = check_missing_libraries(read_libraries_from_file(self.filename))
                    self.finished_checking.emit(missing_packages, {}, False)
                except:
                    self.finished_checking.emit([], {}, False)




class InstallLibrariesThread(QThread):
    install_finished = pyqtSignal(bool, str)  # success flag, message

    def __init__(self, requirements_file, force_reinstall=False):
        super().__init__()
        self.requirements_file = requirements_file
        self.force_reinstall = force_reinstall

    def run(self):
        try:
            import sys
            import os

            # Add the plugin directory to the path to ensure imports work correctly
            plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            if plugin_dir not in sys.path:
                sys.path.insert(0, plugin_dir)

            # from install_packages.check_packages import parse_requirements_with_versions

            # Parse requirements with versions
            requirements = parse_requirements_with_versions(self.requirements_file)

            if not requirements:
                self.install_finished.emit(False, "No packages found in requirements file.")
                return

            # Build list of packages with version specs
            packages_to_install = []
            for package_name, version_spec in requirements.items():
                if version_spec:
                    packages_to_install.append(package_name + version_spec)
                else:
                    packages_to_install.append(package_name)

            # Build pip command with optional force reinstall flags
            lib_cmd = [python_env(), "-m", "pip", "install", "--user"]

            if self.force_reinstall:
                print("[INFO] Using --force-reinstall --upgrade to fix binary incompatibility...")
                lib_cmd.extend(["--force-reinstall", "--upgrade"])

            lib_cmd.extend(packages_to_install)

            # Use subprocess.run to capture stderr for better error diagnostics
            result = subprocess.run(lib_cmd, capture_output=True, text=True)

            if result.returncode != 0:
                # Pip failed - include stderr in the error message
                error_message = "Installation failed:\n"
                if result.stderr:
                    error_message += f"\nPip Error Output:\n{result.stderr}"
                else:
                    error_message += f"\nExit Status: {result.returncode}"

                if result.stdout:
                    error_message += f"\n\nPip Output:\n{result.stdout[-1000:]}"  # Last 1000 chars of stdout

                self.install_finished.emit(False, error_message)
            else:
                self.install_finished.emit(True, "All dependencies were successfully installed. It is highly recommended to restart QGIS after the installation of all dependencies.")
        except subprocess.CalledProcessError as e:
            self.install_finished.emit(False, f"Installation failed:\n{str(e)}. Click here for help")
        except Exception as e:
            self.install_finished.emit(False, f"Error: {str(e)}")


# **********************************************************************************************************************
class InstallPipThread(QThread):
    pip_installed = pyqtSignal(bool, str)  # success, message

    def run(self):
        import urllib.request, subprocess, os
        try:
            url = "https://bootstrap.pypa.io/get-pip.py"
            dest = os.path.join(os.path.expanduser("~"), "get-pip.py")
            urllib.request.urlretrieve(url, dest)

            subprocess.check_call([python_env(), dest])
            self.pip_installed.emit(True, "pip was successfully installed.")
        except Exception as e:
            print(f"pip installation failed: {e}")
            self.pip_installed.emit(False, f"Failed to install pip:\n{str(e)}")



class AGGRADockWidget(QtWidgets.QDockWidget, FORM_CLASS):
    closingPlugin = pyqtSignal()

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

        self.setupUi(self)

        required_packages = get_requirements_file()

        self.library_check_thread = LibraryCheckThread(required_packages)
        self.library_check_thread.finished_checking.connect(self.handle_missing_libraries)
        self.library_check_thread.start()  # Start the background thread



        self.load_OpenAI_key()

        # Store request tracking information for feedback
        self.current_request_id = None
        self.current_task = None
        self.error_message = None
        self.error_traceback = None
        self.generated_code = None
        self.current_feedback = ""  # Track the currently selected feedback (good/bad/empty)
        self.current_feedback_message = ""  # Track the feedback message

        self.initUI()

        # Initialize conversation history
        self.conversation_history = []

        self.api_keys = {}  # Dictionary to store API keys

        self.task_history = []
        self.saved_fname_history = []

        self.stopFlag = False
        # Initialize QCompleter for task_LineEdit
        self.task_completer = QCompleter(self.task_history, self)
        self.task_completer.setCaseSensitivity(Qt.CaseInsensitive)
        # self.task_LineEdit.setCompleter(self.task_completer)

        # Initialize QCompleter for data_pathLineEdit
        self.saved_fname_completer = QCompleter(self.saved_fname_history, self)
        self.saved_fname_completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.saved_fnameLineEdit.setCompleter(self.saved_fname_completer)

        # self.ChatMode_checkbox.toggled.connect(self.toggle_saved_fnameLineEdit)

        # Connect model selection to reasoning effort visibility
        self.modelNameComboBox.currentTextChanged.connect(self.on_model_changed)

        # Initially hide reasoning effort controls
        self.toggle_reasoning_effort_visibility()

        # Initialize OpenAI key field state based on current model
        current_model = self.modelNameComboBox.currentText()
        self.toggle_openai_key_field(current_model)

        self.tabWidget = self.findChild(QtWidgets.QTabWidget, 'tabWidget')
        self.tab_3_index = self.tabWidget.indexOf(self.tab_3)

        # Set the initial tab to the first tab (change this to the desired tab)
        self.tabWidget.setCurrentIndex(0)

        # Apply the syntax highlighter
        self.highlighter = PythonHighlighter(self.output_text_edit.document())
        self.code_highlighter = PythonHighlighter(self.CodeEditor.document(), always_highlight=True)

    def initUI(self):
        self.run_button = self.findChild(QPushButton, 'run_button')

        # self.run_button.clicked.connect(self.run_script)
        self.run_button.clicked.connect(self.send_button_clicked)

        # self.run_button.clicked.connect(lambda: self.append_message(self.task_LineEdit.toPlainText()))
        self.interrupt_button.clicked.connect(self.interrupt)

        # self.chatgpt_ans.setReadOnly(True)  # Make the text edit read-only (if desired)
        # self.chatgpt_ans.setOpenExternalLinks(False)  # Use custom link handler
        self.chatgpt_ans_textBrowser.setOpenExternalLinks(False)
        self.chatgpt_ans_textBrowser.setOpenLinks(False)
        self.chatgpt_ans_textBrowser.anchorClicked.connect(self.open_link)

        # self.chatgpt_ans.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard | Qt.LinksAccessibleByMouse)
        # self.chatgpt_ans.anchorClicked.connect(self.open_link)
        self.SelectDataPath_ToolBtn.clicked.connect(self.select_output_directory)
        self.clear_chatgpt_ansBtn.clicked.connect(self.clear_textboxes)
        self.interrupt_button.clicked.connect(self.stop_script)

        self.thumbs_up_button.clicked.connect(self.send_request_feedback)
        self.thumbs_down_button.clicked.connect(self.send_request_feedback)
        self.feedback_message_button.clicked.connect(self.send_request_feedback)
        # Disable feedback buttons by default (enabled only after a request is made)
        # self.thumbs_up_button.setEnabled(False)
        # self.thumbs_down_button.setEnabled(False)
        # self.feedback_message_button.setEnabled(False)


        self.save_code_button.clicked.connect(self.save_code_to_file)
        self.open_code_button.clicked.connect(self.load_code_from_file)
        self.clear_code_editorBtn.clicked.connect(self.clear_output_window)
        self.Run_Generated_code.clicked.connect(self.run_generated_code)
        self.SelectDataPath_ToolBtn.clicked.connect(self.save_settings)
        self.task_LineEdit.textChanged.connect(self.save_settings)
        self.saved_fnameLineEdit.textChanged.connect(self.save_settings)

        # self.OpenAI_key_LineEdit.textChanged.connect(self.save_settings)
        self.modelNameComboBox.currentIndexChanged.connect(self.save_settings)
        self.SelectDataPath_ToolBtn.clicked.connect(self.save_settings)

        # Connect the button click to the method that adds a new row
        self.addrowButton.clicked.connect(self.add_row)
        self.removerowButton.clicked.connect(self.remove_row)
        self.add_document_button.clicked.connect(self.add_documentation_file)
        # self.add_document_github_button.clicked.connect(self.open_upload_dialog)
        # self.add_document_github_button.clicked.connect(self.show_contribution_dialog) ## For adding data source to GitHub
        self.Add_new_key_btn.clicked.connect(self.show_add_key_dialog)
        self.remove_keyfile_btn.clicked.connect(self.show_remove_key_dialog)

        # Let the table expand both horizontally and vertically
        self.tableWidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        # Set the second column to stretch, first column to resize-to-contents
        header = self.tableWidget.horizontalHeader()
        # First, set the default width of column 0
        self.tableWidget.setColumnWidth(0, 200)
        header.setSectionResizeMode(0, QtWidgets.QHeaderView.Interactive)
        header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)

        self.tableWidget.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Interactive)

        # Initialize the row label counter
        self.row_label_counter = 0
        self.textBrowser.setOpenExternalLinks(True)

        # self.load_api_keys()
        self.setup_initial_rows()  # Set up initial rows in the table
        # self.read_updated_config()

    def show_contribution_dialog(self):
        """Open the ContributionDialog for user interaction."""

        self.contribution_dialog = ContributionDialog(self)

        self.contribution_dialog.exec_()

    def handle_missing_libraries(self, missing_packages, version_mismatches, force_reinstall=False):
        has_issues = False
        message = ""

        # Check if binary incompatibility was detected
        if force_reinstall:
            has_issues = True
            message += "BINARY INCOMPATIBILITY DETECTED:\n\n"
            message += "NumPy or another core library appears to be incompatible.\n"
            message += "This will be fixed by upgrading all packages to correct versions.\n\n"

        # Check for missing packages
        if missing_packages:
            has_issues = True
            message += "The following Python packages are MISSING:\n\n"
            message += "\n".join(missing_packages)
            message += "\n\n"

        # Check for version mismatches
        if version_mismatches:
            has_issues = True
            message += "The following packages have VERSION MISMATCHES:\n\n"
            for package_name, (required_spec, installed_version) in version_mismatches.items():
                if installed_version is None:
                    message += f"• {package_name}: NOT INSTALLED (required: {required_spec})\n"
                else:
                    message += f"• {package_name}: installed {installed_version}, required {required_spec}\n"
            message += "\n"

        if has_issues:
            message += "Would you like to install/fix these packages now?\n"

            reply = QMessageBox.question(self, 'Missing or Mismatched Dependencies',
                                         message,
                                         QMessageBox.Yes | QMessageBox.No)
            if reply == QMessageBox.Yes:
                if not check_pip_installed():
                    reply = QMessageBox.question(self, "Pip Not Found",
                                                 "The 'pip' tool is not available in this Python environment.\n"
                                                 "Please ensure pip is installed before continuing.\n\n"
                                                 "Would you like to try installing it automatically?",
                                                 QMessageBox.Yes | QMessageBox.No
                                                 )
                    if reply == QMessageBox.Yes:
                        success = self.install_pip_with_progress()
                        return

                # Pass requirements file path and force_reinstall flag
                current_script_dir = os.path.dirname(os.path.abspath(__file__))
                required_packages = os.path.join(current_script_dir, 'install_packages', 'requirements.txt')
                self.install_libraries_with_progress(required_packages, force_reinstall)

                # Optional: remember that user responded yes, to avoid checking again
                settings = QSettings()
                required_libraries = read_libraries_from_file(required_packages)
                settings.setValue("cached_libraries", required_libraries)

    def import_libraries(self):
        """Dynamically import the third-party libraries after ensuring they're installed."""

        """Dynamically import the third-party libraries after ensuring they're installed."""

        pass


    def read_updated_config(self):
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')
        # config_path = os.path.join(os.path.dirname(self.script_path), 'openai_key_config.ini')
        config = configparser.ConfigParser()
        config.read(config_path)
        OpenAI_key = config['API_Key']['OpenAI_key']
        self.OpenAI_key_LineEdit.setText(OpenAI_key)

    def update_openai_config_file(self):
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')
        # Ensure the directory exists, if not, create it
        config_dir = os.path.dirname(config_path)
        if not os.path.exists(config_dir):
            os.makedirs(config_dir)

        config = configparser.ConfigParser()

        # Check if the config file exists
        if os.path.exists(config_path):
            # If the config file exists, read the existing content
            config.read(config_path)

        if 'API_Key' not in config:
            config['API_Key'] = {}
            # Retrieve the API key from the line edit
        OpenAI_key = self.OpenAI_key_LineEdit.text().strip()
        config['API_Key']['OpenAI_key'] = OpenAI_key

        with open(config_path, 'w') as configfile:
            config.write(configfile)

        # # Update the QSettings (optional, if you want to store it there too)
        settings = QSettings('YourOrganization', 'YourApplication')
        settings.setValue('API_Key/OpenAI_key', OpenAI_key)

    def load_OpenAI_key(self):
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')
        config = configparser.ConfigParser()
        if os.path.exists(config_path):
            config.read(config_path)
            if 'API_Key' in config and 'OpenAI_key' in config['API_Key']:
                api_key = config['API_Key']['OpenAI_key']
                self.OpenAI_key_LineEdit.setText(api_key)
        else:
            self.update_openai_config_file()

    def setup_initial_rows(self, keys_directory=keys_dir):
        # Get all .toml files from the 'Handbooks' directory to show ALL available datasources
        handbooks_files = [f for f in os.listdir(handbooks_dir) if f.endswith('.toml') and f != 'template.toml']
        # Strip the .toml extension for display purposes
        all_datasources = [os.path.splitext(f)[0] for f in handbooks_files]

        # Add a row for each datasource (both with and without API keys)
        for datasource in sorted(all_datasources):
            self.add_row(key_name=datasource, keys_directory=keys_directory)

    def add_row(self, key_name=None, keys_directory=keys_dir):
        # Get the current number of rows
        row_count = self.tableWidget.rowCount()
        # Insert a new row at the end
        self.tableWidget.insertRow(row_count)

        # Create a QWidget container for the password field
        container_widget = QtWidgets.QWidget()
        # Ensure the container widget can expand
        container_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        # Create the password field
        password_edit = QgsPasswordLineEdit(container_widget)
        # Set the size policy for the password edit
        password_edit.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        # Set placeholder text for when API key is required
        password_edit.setPlaceholderText("Input your data source API key")

        # Set the layout for the container widget
        layout = QtWidgets.QHBoxLayout(container_widget)
        layout.addWidget(password_edit)
        layout.setContentsMargins(0, 0, 0, 0)  # Remove margins

        # Store reference to password field in the container for easy access
        container_widget.password_edit = password_edit

        # Create a QComboBox for the first column
        combo_box = QtWidgets.QComboBox()
        # Set size policy for the combo box
        combo_box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

        # Add a blank option first
        combo_box.addItem("")  # Blank option at the top

        # Get ALL datasource names from the Handbooks directory (both with and without API keys)
        handbooks_files = [f for f in os.listdir(handbooks_dir) if f.endswith('.toml') and f != 'template.toml']
        all_datasources = [os.path.splitext(f)[0] for f in handbooks_files]

        # Add all datasource name options (both those that require API keys and those that don't)
        combo_box.addItems(sorted(all_datasources))

            # Connect the combo box change signal to a function that sets the corresponding API key
        combo_box.currentIndexChanged.connect(lambda: self.set_api_key(combo_box, container_widget, keys_directory))

        # Connect the password field change signal to update the .keys file when edited
        password_edit.textChanged.connect(
            lambda: self.update_datasources_api_key_file(combo_box, container_widget, keys_directory))

        if key_name:
            combo_box.setCurrentText(key_name)
        self.tableWidget.setCellWidget(row_count, 0, combo_box)

        # Set the API key if provided
        # if key_name:
        #     settings = QSettings('YourOrganization', 'YourApplication')
        #     api_key = settings.value(f'API_Key/{key_name}', '')
        #     password_edit.setText(api_key)

        # Load the API key from the corresponding file
        if key_name and keys_directory:
            self.set_api_key(combo_box, container_widget, keys_directory)
            # key_file_path = os.path.join(keys_directory, f"{key_name}.keys")
            # try:
            #     with open(key_file_path, 'r') as key_file:
            #         # Assuming the key file contains a line like: OpenAI_key = <actual_api_key>
            #         for line in key_file:
            #             if "=" in line:
            #                 key_value = line.split('=')[1].strip()
            #                 password_edit.setText(key_value)  # Set the API key in the password field
            #                 break  # We only expect one line containing the key
            # except FileNotFoundError:
            #     QMessageBox.warning(self, "File Error", f"File not found: {key_file_path}")

        # Create a QLineEdit for the second column
        # password_edit = QgsPasswordLineEdit
        self.tableWidget.setCellWidget(row_count, 1, container_widget)
        # self.tableWidget.setColumnWidth(1, 160)  # Adjust the width as needed
        # Let the table adjust the row height to fit the content
        self.tableWidget.resizeRowToContents(row_count)

        # Increment the row label counter
        self.row_label_counter += 1
        # # Adjust the table height
        # self.adjust_table_height()

    def set_api_key(self, combo_box, container_widget, keys_directory):
        # Get the selected key name from the combo box
        selected_key_name = combo_box.currentText()

        # Get reference to password field from the container
        password_edit = container_widget.password_edit

        if selected_key_name:
            # Construct the path to the corresponding .keys file
            key_file_path = os.path.join(keys_directory, f"{selected_key_name}.keys")

            try:
                # Open the file and read the API key
                with open(key_file_path, 'r') as key_file:
                    for line in key_file:
                        if "=" in line:
                            key_value = line.split('=')[1].strip()

                            # Check if the key value indicates no API key is required
                            if "do not require" in key_value.lower() or "don not require" in key_value.lower():
                                # Make read-only and show "no key required" message
                                password_edit.setReadOnly(True)
                                password_edit.setEchoMode(QLineEdit.Normal)  # Show text normally, not masked
                                password_edit.setText("Data source do not require API Key")
                                password_edit.setStyleSheet("QLineEdit { color: gray; font-style: italic; }")
                            elif key_value == "":
                                # Empty key value means API key is required but not yet filled
                                password_edit.setReadOnly(False)
                                password_edit.setEchoMode(QLineEdit.Password)  # Mask the API key
                                password_edit.setStyleSheet("")  # Reset style
                                password_edit.setPlaceholderText("Input API key for this data source")
                                # password_edit.setPlaceholderText("Input API key for this data source")
                                password_edit.clear()  # Leave empty for user to fill
                            else:
                                # Make editable and show the API key
                                password_edit.setReadOnly(False)
                                password_edit.setEchoMode(QLineEdit.Password)  # Mask the API key
                                password_edit.setStyleSheet("")  # Reset style
                                password_edit.setText(key_value)  # Set the API key in the password field
                            break  # Only expecting one key per file
            except FileNotFoundError:
                # If no .keys file exists, this datasource doesn't need an API key
                # Make read-only and show "no key required" message
                password_edit.setReadOnly(True)
                password_edit.setEchoMode(QLineEdit.Normal)  # Show text normally, not masked
                password_edit.setText("Data source do not require API Key")
                password_edit.setStyleSheet("QLineEdit { color: gray; font-style: italic; }")
        else:
            # Clear the field if no valid key is selected
            password_edit.setReadOnly(False)
            password_edit.setEchoMode(QLineEdit.Password)  # Reset to password mode
            password_edit.setStyleSheet("")  # Reset style
            password_edit.clear()

    def update_datasources_api_key_file(self, combo_box, container_widget, keys_directory):
        # Get the selected key name from the combo box
        selected_key_name = combo_box.currentText()

        # Get reference to password field from the container
        password_edit = container_widget.password_edit

        # Don't update if the field is read-only (data source doesn't require API key)
        if password_edit.isReadOnly():
            return

        # Ensure a key name is selected
        if selected_key_name:
            # Construct the path to the corresponding .keys file
            key_file_path = os.path.join(keys_directory, f"{selected_key_name}.keys")
            API_keyname = f"{selected_key_name}_key"

            # Get the new API key from the password edit field
            new_api_key = password_edit.text()

            try:
                # Check if the .keys file exists
                if os.path.exists(key_file_path):
                    # File exists - update the existing key
                    with open(key_file_path, 'r') as key_file:
                        lines = key_file.readlines()

                    # Find the line with the API key and update only that line
                    with open(key_file_path, 'w') as key_file:
                        for line in lines:
                            if line.startswith(f"{API_keyname} ="):
                                # Replace the old key with the new key (empty or not)
                                key_file.write(f"{API_keyname} = {new_api_key}\n")
                            else:
                                # Write the line back as it is if it's not the key line
                                key_file.write(line)
                else:
                    # File doesn't exist - only create a new .keys file if API key is not empty
                    if new_api_key.strip():
                        with open(key_file_path, 'w') as key_file:
                            key_file.write(f"[API_Key]\n{API_keyname} = {new_api_key}\n")

                        # Update all combo boxes to reflect that this datasource now has a key
                        # (This ensures consistency across all rows in the table)
                        self.add_new_keyname_to_combo_boxes(selected_key_name)

            except Exception as e:
                QMessageBox.warning(self, "File Error",
                                    f"Failed to update/create the key file: {key_file_path}\nError: {str(e)}")

    def show_add_key_dialog(self):
        # Create and display the dialog
        keys_directory = keys_dir  # Adjust this path as needed
        dialog = AddKeyDialog(keys_directory, self)

        if dialog.exec_():  # If the dialog is successfully accepted (after pressing save)
            # Get the new key name (the user input from the dialog)
            new_key_name = dialog.name_input.text().strip()

            # Add the new key name to the existing combo boxes
            self.add_new_keyname_to_combo_boxes(new_key_name)
            self.add_row(key_name=new_key_name, keys_directory=keys_directory)

    def add_new_keyname_to_combo_boxes(self, new_key_name):
        # Iterate over the rows in your table and update the QComboBox for each row
        for row in range(self.tableWidget.rowCount()):
            combo_box = self.tableWidget.cellWidget(row, 0)  # Get the combo box in the first column of each row
            if isinstance(combo_box, QtWidgets.QComboBox):
                # Check if the new key is already in the combo box
                if new_key_name not in [combo_box.itemText(i) for i in range(combo_box.count())]:
                    # Add the new key to the combo box
                    combo_box.addItem(new_key_name)

        # After the dialog closes, refresh your table or UI to show the new key
        # self.refresh_key_table()  # Assuming you have a method to refresh your table

    # Function to show the remove key dialog and handle UI updates
    def show_remove_key_dialog(self):
        keys_directory = keys_dir  # Adjust the path as needed
        dialog = RemoveKeyDialog(keys_directory, self)

        if dialog.exec_():  # If the dialog is accepted (key successfully removed)
            # Get the removed key name from the combo box
            removed_key_name = dialog.combo_box.currentText()

            # Remove the key from the combo boxes
            self.remove_key_from_combo_boxes(removed_key_name)

    def remove_key_from_combo_boxes(self, removed_key_name):
        # First, iterate over the rows and remove the row where the key is selected
        for row in reversed(range(self.tableWidget.rowCount())):  # Reverse to avoid index issues when removing rows
            combo_box = self.tableWidget.cellWidget(row, 0)  # Get the combo box in the first column of each row
            if isinstance(combo_box, QtWidgets.QComboBox):
                # Check if the key is currently selected in this row
                if combo_box.currentText() == removed_key_name:
                    # Remove the entire row
                    self.tableWidget.removeRow(row)

        # Iterate over the rows in your table and update the QComboBox for each row
        for row in range(self.tableWidget.rowCount()):
            combo_box = self.tableWidget.cellWidget(row, 0)  # Get the combo box in the first column of each row
            if isinstance(combo_box, QtWidgets.QComboBox):
                # Find the index of the removed key in the combo box
                index = combo_box.findText(removed_key_name)
                if index != -1:
                    # Remove the key from the combo box if it exists
                    combo_box.removeItem(index)

    def on_model_changed(self, model_name):
        """Handle model selection change"""
        model_name = self.modelNameComboBox.currentText()
        self.toggle_reasoning_effort_visibility(model_name)
        self.toggle_openai_key_field(model_name)

    def toggle_reasoning_effort_visibility(self, model_name=None):
        """Show or hide reasoning effort controls and update options based on model selection"""
        if model_name is None:
            # Check current model selection
            model_name = self.modelNameComboBox.currentText()

        # Check if this model supports reasoning effort
        show = model_name in ['gpt-5', 'gpt-5.1', 'gpt-5.2']

        # Show/hide the reasoning effort controls
        self.reasoningEffortLabel.setVisible(show)
        self.reasoningEffortComboBox.setVisible(show)

        if show:
            # Update combo box items based on model
            if model_name == 'gpt-5.1':
                # GPT-5.1 supports: none, low, high
                effort_options = ['none', 'low', 'high']
                default_effort = 'low'
            elif model_name == 'gpt-5':
                # GPT-5 supports: minimal, low, medium, high
                effort_options = ['minimal', 'low', 'medium', 'high']
                default_effort = 'minimal'
            elif model_name == 'gpt-5.2':
                # GPT-5.2 supports: minimal, low, medium, high
                effort_options = ['none', 'low','medium','high','xhigh']
                default_effort = 'low'

            # Update the combo box items
            # current_text = self.reasoningEffortComboBox.currentText()
            self.reasoningEffortComboBox.clear()
            self.reasoningEffortComboBox.addItems(effort_options)
            # Always set to default when model is selected
            self.reasoningEffortComboBox.setCurrentText(default_effort)
    # def toggle_reasoning_effort_visibility(self, model_name=None):
    #     """Show or hide reasoning effort controls based on model selection"""
    #     if model_name is None:
    #         # Check current model selection
    #         model_name = self.modelNameComboBox.currentText()
    #
    #     # Check if this model supports reasoning effort
    #     show = model_name in ['gpt-5', 'gpt-5.1', 'gpt-5.2']
    #
    #     # Show/hide the reasoning effort controls
    #     self.reasoningEffortLabel.setVisible(show)
    #     self.reasoningEffortComboBox.setVisible(show)
    #
    #     # Set default reasoning effort if GPT-5/GPT-5.1 is selected
    #     if show:
    #         # Update combo box items based on model
    #         if model_name == 'gpt-5.1':
    #             # GPT-5.1 supports: none, low, high
    #             effort_options = ['none', 'low', 'high']
    #             default_effort = 'none'
    #         else:
    #             # GPT-5 supports: minimal, low, medium, high
    #             effort_options = ['minimal', 'low', 'medium', 'high']
    #             default_effort = 'minimal'
    #
    #         # Update the combo box items
    #         current_text = self.reasoningEffortComboBox.currentText()
    #         self.reasoningEffortComboBox.clear()
    #         self.reasoningEffortComboBox.addItems(effort_options)
    #
    #         # Set appropriate default if current selection is invalid for this model
    #         if current_text in effort_options:
    #             self.reasoningEffortComboBox.setCurrentText(current_text)
    #         else:
    #             self.reasoningEffortComboBox.setCurrentText(default_effort)

    # def update_reasoning_effort_options(self, model_name):
    #     """Update reasoning effort options based on selected model"""
    #     # Block signals to avoid triggering changed events during update
    #     self.reasoningEffortComboBox.blockSignals(True)
    #
    #     # Clear existing items
    #     self.reasoningEffortComboBox.clear()
    #
    #     if model_name == 'gpt-5.1':
    #         # GPT-5.1 only supports: none, low, high
    #         self.reasoningEffortComboBox.addItems(['none', 'low', 'high'])
    #         # Set default to low
    #         self.reasoningEffortComboBox.setCurrentText('low')
    #     elif model_name == 'gpt-5':
    #         # GPT-5 supports: minimal, low, medium, high
    #         self.reasoningEffortComboBox.addItems(['minimal', 'low', 'medium', 'high'])
    #         # Set default to medium
    #         self.reasoningEffortComboBox.setCurrentText('medium')
    #
    #     # Unblock signals
    #     self.reasoningEffortComboBox.blockSignals(False)

    def toggle_openai_key_field(self, model_name):
        """Enable or disable OpenAI key field based on model selection"""
        try:
            current_script_dir = os.path.dirname(os.path.abspath(__file__))
            sys.path.insert(0, os.path.join(current_script_dir, 'SpatialAnalysisAgent'))
            from SpatialAnalysisAgent_ModelProvider import ModelProviderFactory

            provider_name = ModelProviderFactory._model_providers.get(model_name, 'openai')

            if provider_name == 'ollama':
                # Local model doesn't need OpenAI key - make field read-only and show placeholder
                self.OpenAI_key_LineEdit.setReadOnly(True)
                self.OpenAI_key_LineEdit.setPlaceholderText("API key not required for local models")
                self.OpenAI_key_LineEdit.setStyleSheet("QLineEdit { background-color: #f0f0f0; color: #666666; }")
            else:
                # OpenAI model needs key - make field editable
                self.OpenAI_key_LineEdit.setReadOnly(False)
                self.OpenAI_key_LineEdit.setPlaceholderText("Enter your OpenAI API key or GIBD API key")
                self.OpenAI_key_LineEdit.setStyleSheet("QLineEdit { background-color: white; color: black; }")

        except ImportError:
            # Fallback: keep field editable for all models
            self.OpenAI_key_LineEdit.setReadOnly(False)
            self.OpenAI_key_LineEdit.setPlaceholderText("Enter your OpenAI API key or GIBD API key")
            self.OpenAI_key_LineEdit.setStyleSheet("QLineEdit { background-color: white; color: black; }")

        # # Get the selected key name from the combo box
        # selected_key_name = combo_box.currentText()
        #
        # if selected_key_name:
        #     # Construct the path to the corresponding .keys file
        #     key_file_path = os.path.join(keys_directory, f"{selected_key_name}.keys")
        #
        #     # Get the new API key from the password edit field
        #     new_api_key = password_edit.text()
        #
        #     # Update the .keys file with the new API key
        #     try:
        #         with open(key_file_path, 'w') as key_file:
        #             # Write the new API key in the format: key_name = new_api_key
        #             key_file.write(f"{selected_key_name} = {new_api_key}")
        #     except Exception as e:
        #         QMessageBox.warning(self, "File Error",
        #                             f"Failed to update the key file: {key_file_path}\nError: {str(e)}")

        #
        # self.api_keys = {}
        # settings = QSettings('YourOrganization', 'YourApplication')
        # for row in range(self.tableWidget.rowCount()):
        #     key_name = self.tableWidget.cellWidget(row, 0).currentText()
        #     api_key = self.tableWidget.cellWidget(row, 1).findChild(QgsPasswordLineEdit).text()
        #     self.api_keys[key_name] = api_key
        #     settings.setValue(f'API_Key/{key_name}', api_key)

    # def load_api_keys(self):
    #     settings = QSettings('YourOrganization', 'YourApplication')
    #     for row in range(self.tableWidget.rowCount()):
    #         key_name = self.tableWidget.cellWidget(row, 0).currentText()
    #         api_key = settings.value(f'API_Key/{key_name}', '')
    #         self.tableWidget.cellWidget(row, 1).findChild(QgsPasswordLineEdit).setText(api_key)

    def remove_row(self):
        # Get the selected row
        selected_row = self.tableWidget.currentRow()
        if selected_row >= 0:  # Ensure a row is selected
            self.tableWidget.removeRow(selected_row)
            # # Adjust the table height
            # self.adjust_table_height()

    # def adjust_table_height(self):
    #     total_height = self.tableWidget.horizontalHeader().height()
    #     for row in range(self.tableWidget.rowCount()):
    #         total_height += self.tableWidget.rowHeight(row)
    #     self.tableWidget.setFixedHeight(total_height)

    # def set_initial_size(self, width, height):
    #     """Set the initial size of the plugin window."""
    #     self.resize(width, height)

    def send_button_clicked(self):
        """Slot to handle the send button click."""
        user_message = self.task_LineEdit.toPlainText().strip()
        self.CodeEditor.clear()
        self.execution_output_text_edit.clear()

        # Check if API key is empty
        api_key = self.OpenAI_key_LineEdit.text().strip()
        if not api_key:
            self.update_chatgpt_ans_textBrowser(f"API key is empty. Enter your OpenAI API key or GIBD API key", is_user=False)
            return

        if not user_message:
            # self.update_chatgpt_ans(f"AI: Please enter a data request in the request field.", is_user=False)
            self.update_chatgpt_ans_textBrowser(f"AI: Please enter a data request in the request field.", is_user=False)
            return  # Stop further execution if the task is empty

        self.update_chatgpt_ans_textBrowser(
            f"--------------------------------------------------------------------------------------------",
            is_user=None)

        self.append_message(user_message)

        # Call update_config_file to save the latest API key
        self.update_openai_config_file()

        # Now read the updated config file to refresh the API key
        self.read_updated_config()

        if not self.saved_fnameLineEdit.isEnabled() and not self.saved_fnameLineEdit.text().strip():
            # self.update_chatgpt_ans(f"AI: Please specify the output directory.", is_user=False)
            self.update_chatgpt_ans_textBrowser(f"AI: Please specify the output directory.", is_user=False)
            return  # Stop further execution if data path is required but empty

        # if not self.ChatMode_checkbox.isChecked() and self.saved_fnameLineEdit.isEnabled() and not self.saved_fnameLineEdit.text().strip():
        #     # self.update_chatgpt_ans(f"AI: Please specify the output directory.", is_user=False)
        #     self.update_chatgpt_ans(f"AI: Please specify the output directory.", is_user=False)
        #     return  # Stop further execution if data path is required but empty

        # if self.ChatMode_checkbox.isChecked():  # Assuming SwitchControl behaves like a checkbox
        #     self.chatgpt_direct_answer(user_message)

        else:
            self.run_script()

    def chatgpt_direct_answer(self, user_message):
        """Method to interact with GPT-4 and display the result in output_text_edit_2."""
        # Retrieve the API key from the config
        self.OpenAI_key = self.get_openai_key()  # This retrieves the latest key from the config
        self.model_name = self.modelNameComboBox.currentText()

        self.gpt_thread = GPTRequestThread(user_message, self.OpenAI_key, self.model_name,
                                           self.conversation_history)  # your-api-key-here
        # self.gpt_thread = GPTRequestThread(user_message, "AAzz", self.conversation_history)#your-api-key-here
        self.gpt_thread.output_line.connect(self.update_output)
        self.gpt_thread.finished_signal.connect(lambda: self.update_chatgpt_ans_textBrowser("AI: Done", is_user=False))

        self.gpt_thread.start()

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

    # def helpPage (self):
    #     """
    #             change the page of the manual according to the plot type selected and
    #             the language (looks for translations)
    #             """
    #
    #     # locale = QSettings().value('locale/userLocale', 'en_US')[0:2]
    #
    #     self.help_view.load(QUrl(''))
    #     self.layouth.addWidget(self.help_view)
    #     help_url = QUrl(
    #         'https://github.com/gladcolor/LLM-Find/blob/master/README.md')
    #     self.help_view.load(help_url)

    def save_settings(self):
        settings = QSettings('YourOrganization', 'YourApplication')

        settings.setValue('task', self.task_LineEdit.toPlainText())
        settings.setValue('saved_fname', self.saved_fnameLineEdit.text())
        # settings.setValue('OpenAI_key', self.OpenAI_key_LineEdit.text())
        settings.setValue('model_name', self.modelNameComboBox.currentText())

    def load_settings(self):
        settings = QSettings('YourOrganization', 'YourApplication')

        self.task_LineEdit.setPlainText(settings.value('task', ''))
        self.saved_fnameLineEdit.setText(settings.value('saved_fname', ''))
        # self.OpenAI_key_LineEdit.setText(settings.value('OpenAI_key', ''))
        self.modelNameComboBox.setCurrentText(settings.value('model_name', ''))

    def select_output_directory(self):
        file_dialog = QFileDialog(self, "Select Output Directory and Filename")
        file_dialog.setAcceptMode(QFileDialog.AcceptSave)
        file_dialog.setFileMode(QFileDialog.AnyFile)
        file_dialog.setDefaultSuffix("shp")

        # Set default directory to Downloads folder
        downloads_path = os.path.join(os.path.expanduser("~"), "Downloads")
        file_dialog.setDirectory(downloads_path)

        file_dialog.setNameFilters([
            "Shapefile (*.shp)",
            "GeoPackage (*.gpkg *.GPKG)",
            "CSV files (*.csv)",
            "TIFF files (*.tif *.tiff *.TIF *.TIFF)",
            "PNG files (*.png *.PNG)",
            "All Files (*)"
        ])
        if file_dialog.exec_() == QFileDialog.Accepted:
            selected_file = file_dialog.selectedFiles()[0]
            self.saved_fnameLineEdit.setText(selected_file)
            self.saved_fname = selected_file

    def reproject_layer_to_wgs84(self, layer):
        wgs84_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        transform_context = QgsProject.instance().transformContext()
        transform = QgsCoordinateTransform(layer.crs(), wgs84_crs, transform_context)

        # Create a new layer with WGS84 CRS
        reprojected_layer = QgsVectorLayer(
            layer.dataProvider().dataSourceUri(),
            layer.name() + "_wgs84",
            "memory"
        )
        reprojected_layer.setCrs(wgs84_crs)
        reprojected_layer_data_provider = reprojected_layer.dataProvider()

        # Copy fields from the original layer
        reprojected_layer_data_provider.addAttributes(layer.fields())
        reprojected_layer.updateFields()

        # Reproject features and add to the new layer
        features = []
        for feature in layer.getFeatures():
            reprojected_feature = QgsFeature()
            reprojected_feature.setGeometry(feature.geometry().transform(transform))
            reprojected_feature.setAttributes(feature.attributes())
            features.append(reprojected_feature)

        reprojected_layer_data_provider.addFeatures(features)
        reprojected_layer.updateExtents()

        return reprojected_layer

    def openFileDialog(self):
        # Define the file filter
        file_filter = "Data files (*.csv *.shp *.geojson)"
        saved_fname, _ = QFileDialog.getOpenFileName(None, "Select Data Path", "", file_filter)
        if saved_fname:
            # Set the chosen path in the line edit widget
            self.saved_fnameLineEdit.setText(f"{saved_fname}")

    def save_code_to_file(self):
        code = self.CodeEditor.toPlainText()
        if not code.strip():
            QMessageBox.warning(self, "No Code", "There is no code to save.")
            return
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "Save Code As",
            "",
            "Python(*.py);;Text file (*.txt);;All Files (*),",
            options=options
        )
        if file_name:
            try:
                with open(file_name, "w", encoding='utf-8') as file:
                    file.write(code)
                QMessageBox.information(self, "Success", f"Code saved to:\n{file_name}")
            except Exception as e:
                QMessageBox.warning(self, "Error", f"Failed to save code:\n{str(e)}")

    def load_code_from_file(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "Open Code File",
            "",
            "Python(*.py);;Text file (*.txt);;All Files (*),",
            options=options
        )
        if file_name:
            try:
                with open(file_name, "r", encoding='utf-8') as file:
                    code = file.read()
                self.CodeEditor.setPlainText(code)
            except Exception as e:
                QMessageBox.information(self, "Error", f"Failed to load code:\n{str(e)}")

    def clear_output_window(self):
        self.execution_output_text_edit.clear()

    def run_generated_code(self):
        self.append_execution_output("Running code ...")
        code_to_run = self.CodeEditor.toPlainText()

        if not code_to_run.strip():
            QMessageBox.warning(self, "No Code", "There is no code to run.")
            return

        import __main__

        if 'processing' not in __main__.__dict__:
            import processing
            __main__.processing = processing

        exec_globals = __main__.__dict__
        exec_locals = {}

        self.generated_code_thread = RunGeneratedCodeThread(code_to_run, exec_globals)
        self.generated_code_thread.CodeEditor_output_line.connect(self.append_execution_output)
        self.generated_code_thread.execution_error.connect(self.append_execution_output)
        self.generated_code_thread.finished.connect(self.generated_code_execution_finished)
        self.generated_code_thread.start()

    def append_execution_output(self, line):

        if not line.strip():
            return
        lines = line.strip().split("\n")
        for line in lines:
            formatted_line = f">>> {line}"

            if "Traceback" in line or "Error" in line:
                color = QColor("red")
            elif "Warning" in line:
                color = QColor("orange")
            elif "Execution completed" in line:
                color = QColor("green")
            else:
                color = QColor("black")

            self.append_colored_text(self.execution_output_text_edit, formatted_line, color)

        self.execution_output_text_edit.moveCursor(QTextCursor.End)
        self.execution_output_text_edit.verticalScrollBar().setValue(
            self.execution_output_text_edit.verticalScrollBar().maximum()
        )

    def append_colored_text(self, text_edit, text, color):
        cursor = text_edit.textCursor()
        cursor.movePosition(QTextCursor.End)
        text_edit.setTextCursor(cursor)

        format = QTextCharFormat()
        format.setForeground(color)

        cursor.insertText(text + '\n', format)

    def generated_code_execution_finished(self):
        # QMessageBox.information(self, "Execution Complete", "The generated code has finished executing.")
        if self.generated_code_thread.success:
            self.append_execution_output("Execution completed")
        else:
            self.append_execution_output("The script finished with errors.")

    def run_script(self):
        # self.update_api_keys()
        # self.tabWidget.setCurrentIndex(self.tab_3_index)

        # Clear the output_text_edit before starting
        self.output_text_edit.clear()

        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        script_path = os.path.join(current_script_dir, 'LLM_Find', 'LLM_FIND.py')
        # OpenAI_key = helper.load_OpenAI_key()
        self.OpenAI_key = self.get_openai_key()  # Retrieve the API key from the line edit
        # self.OpenAI_key = self.api_keys.get("OpenAI_key")

        self.model_name = self.modelNameComboBox.currentText()

        # self.task = self.task_LineEdit.text()
        self.task = self.task_LineEdit.toPlainText()
        self.current_task=self.task
        self.saved_fname = self.saved_fnameLineEdit.text()
        filename_only = os.path.basename(self.saved_fname)  # .split('.')[0]

        # # Enable feedback buttons only if using gibd-services API key
        # if 'gibd-services' in (self.OpenAI_key or ''):
        #     self.thumbs_up_button.setEnabled(True)
        #     self.thumbs_down_button.setEnabled(True)
        #     self.feedback_message_button.setEnabled(True)

        # Add task to history and update completer
        if self.task not in self.task_history:
            self.task_history.append(self.task)
            self.task_completer.model().setStringList(self.task_history)

        # Add data path to history and update completer
        if self.saved_fname not in self.saved_fname_history:
            self.saved_fname_history.append(self.saved_fname)
            self.saved_fname_completer.model().setStringList(self.saved_fname_history)

        # Get reasoning effort if GPT-5 or GPT-5.1 is selected
        self.reasoning_effort_value = 'medium'  # default
        if self.model_name in ['gpt-5', 'gpt-5.1', 'gpt-5.2']:
            self.reasoning_effort_value = self.reasoningEffortComboBox.currentText()

        self.thread = ScriptThread(script_path, self.task, self.saved_fname, self.api_keys, self.model_name, self.reasoning_effort_value)

        # self.task = self.task_LineEdit.text()
        # self.saved_fname = self.saved_fnameLineEdit.text()
        # filename_only = os.path.basename(self.saved_fname)  # .split('.')[0]

        # Add task to history and update completer
        if self.task not in self.task_history:
            self.task_history.append(self.task)
            self.task_completer.model().setStringList(self.task_history)

        # Add data path to history and update completer
        if self.saved_fname not in self.saved_fname_history:
            self.saved_fname_history.append(self.saved_fname)
            self.saved_fname_completer.model().setStringList(self.saved_fname_history)

        self.thread.output_line.connect(self.update_output)
        self.thread.chatgpt_update.connect(self.update_chatgpt_ans_textBrowser)
        self.thread.generated_code_ready.connect(self.update_code_editor)
        self.thread.finished.connect(self.thread_finished)
        self.thread.start()

        # Disable the send_button
        self.run_button.setEnabled(False)
        self.clear_chatgpt_ansBtn.setEnabled(False)
        self.task_LineEdit.setEnabled(False)
        self.saved_fnameLineEdit.setEnabled(False)
        self.SelectDataPath_ToolBtn.setEnabled(False)

    def update_code_editor(self, code):
        """Update the code_editor widget with the last extracted code block."""
        self.CodeEditor.setPlainText(code)

    def stop_script(self):
        if self.thread:
            self.thread.terminate()
            self.update_chatgpt_ans_textBrowser(f"AI: Script terminated")

            # print("Script terminated")
        self.run_button.setEnabled(True)
        self.clear_chatgpt_ansBtn.setEnabled(True)
        self.task_LineEdit.setEnabled(True)
        self.saved_fnameLineEdit.setEnabled(True)
        self.SelectDataPath_ToolBtn.setEnabled(True)

    def update_chatgpt_ans_textBrowser(self, message, is_user=False):
        # Append new message to conversation history
        self.conversation_history.append((message, is_user))
        self.chatgpt_ans_textBrowser.clear()
        for msg, user in self.conversation_history:
            self.append_text_with_format(msg, user)
            # self.chatgpt_ans.append(msg)
        self.chatgpt_ans_textBrowser.repaint()
        self.chatgpt_ans_textBrowser.verticalScrollBar().setValue(self.chatgpt_ans_textBrowser.verticalScrollBar().maximum())

    # def append_text_with_format(self, text, is_user=True):
    #     cursor = self.chatgpt_ans.textCursor()
    #     cursor.movePosition(QTextCursor.End)
    #
    #     if is_user:
    #         html = f'<div style="text-align: left; padding: 10px; margin: 5px; border: 2px solid blue; border-radius: 10px;">{text}</div>'
    #     else:
    #         html = f'<div style="text-align: right; padding: 10px; margin: 5px; border: 2px solid green; border-radius: 10px;">{text}</div>'
    #
    #     cursor.insertHtml(html)
    #     cursor.insertHtml('<br>')  # Add a line break between messages
    #     # self.task_LineEdit.clear()
    #
    #     self.chatgpt_ans_textBrowser.setTextCursor(cursor)

    def append_text_with_format(self, text, is_user=True):
        # Check if the text already contains HTML links (tool documentation, AI thoughts, data overview links, or any anchor tags)
        # Also skip URL processing for trial messages to avoid making numbers clickable
        # if ('<a href="tool-doc:' in text or '<a href="ai-thoughts:' in text or '<a href="data-attributes:' in text or
        if ('<a href=' in text or
                "Executing the code" in text or "Trial" in text):
            # Don't process URLs if it already contains formatted links or is a trial message
            pass
        else:
            # Only apply URL processing for non-tool-link messages
            url_pattern = re.compile(
                r'((?:https?://|file:///)[^\s]+)'  # URLs starting with http://, https://, or file:///
                r'|'
                r'((?:[A-Za-z]:)?[\\/][^\n]+)'  # Windows or Unix file paths, allowing spaces
            )

            def replace_urls(match):
                url = match.group(0)
                if url.startswith(('http://', 'https://', 'file:///')):
                    # URL is already in correct format
                    return f'<a href="{url}">{url}</a>'
                else:
                    # It's a local file path; convert it to a file URL
                    # Normalize the path separators
                    file_path = os.path.normpath(url).replace('\\', '/')
                    # Handle spaces and special characters
                    file_url = 'file:///' + urllib.parse.quote(file_path)
                    display_path = url  # Keep the original path for display
                    return f'<a href="{file_url}">{display_path}</a>'

            # Process the text to replace URLs and file paths with HTML links
            text = url_pattern.sub(replace_urls, text)

        cursor = self.chatgpt_ans_textBrowser.textCursor()
        cursor.movePosition(QTextCursor.End)

        cursor = self.chatgpt_ans_textBrowser.textCursor()
        cursor.movePosition(QTextCursor.End)

        if is_user:
            prefix = "User: "
            color_prefix = "green"
            color_message = "black"
            message = text
        elif is_user is False:
            prefix = "AI: "
            color_prefix = 'blue'
            color_message = 'black'

        else:
            prefix = ""  # No prefix
            color_prefix = ""  # No color
            color_message = 'black'

        message = text

        is_processing_status = message.endswith("...") or "Executing the code" in message

        # Apply special styling for processing status messages and regular AI messages
        if is_processing_status and is_user is False:
            message_style = f"color: orange; font-style: italic; font-size: 90%;"
        elif is_user is False:
            # Regular AI informational messages in green
            message_style = f"color: green;"
        else:
            message_style = f"color: {color_message};"

        html = f'''
                    <div style= "text-align: left; padding: 10px; margin: 5px; border: 2px solid gray; border-radius: 10px; ">
                        <span style="color: {color_prefix};">{prefix}</span><span style="{message_style}">{message}</span>

                    </div>
                    '''

        cursor.insertHtml(html)
        cursor.insertHtml('<br>')  # Add a line break between messages
        # self.task_LineEdit.clear()

        self.chatgpt_ans_textBrowser.setTextCursor(cursor)

    def open_link(self, url):
        if url.scheme() == 'datasource-doc':
            datasource_id = url.path()  # Get the tool ID from the URL path
            self.show_datasource_documentation(datasource_id)

    def show_datasource_documentation(self, datasource_id):
        """Open tool documentation file with the default system editor"""
        try:
            # Find the documentation file for the tool
            current_script_dir = os.path.dirname(os.path.abspath(__file__))
            docs_dir = os.path.join(current_script_dir, 'LLM_Find', 'Handbooks')

            # Convert tool_id to filename format (replace : with _)
            # datasource_filename = datasource_id.replace(':', '_')
            datasource_filename = datasource_id

            # Look for the tool documentation file in all subdirectories
            doc_file = None
            for root, dirs, files in os.walk(docs_dir):
                for file in files:
                    if file.endswith('.toml'):
                        # Check if the tool filename is in the file name
                        if datasource_filename in file or datasource_id in file:
                            doc_file = os.path.join(root, file)
                            break
                        # Also check for exact matches like "native_buffer.toml"
                        file_without_ext = os.path.splitext(file)[0]
                        if file_without_ext == datasource_filename:
                            doc_file = os.path.join(root, file)
                            break
                if doc_file:
                    break

            if doc_file:
                # Open the file with the default system editor
                QDesktopServices.openUrl(QUrl.fromLocalFile(doc_file))
            else:
                QMessageBox.information(self, "Documentation Not Found",
                                        f"No documentation found for tool: {datasource_id}\nSearched for: {datasource_filename}")

        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to open tool documentation: {str(e)}")

    # @pyqtSlot(str)
    # def append_message(self, message):
    #     message = self.task_LineEdit.toPlainText()
    #     if message.strip():  # Check if message is not empty
    #         # self.conversation_history.append(f"User: {message}")
    #         self.update_chatgpt_ans(f"User: {message}", is_user=True)
    #         self.update_chatgpt_ans(f"AI:Loading ...", is_user=False)
    #         # Clear the input field after sending the message
    #         # self.task_LineEdit.clear()

    @pyqtSlot(str)
    def append_message(self, message):
        # message = self.task_LineEdit.toPlainText()
        if message.strip():  # Check if message is not empty
            self.update_chatgpt_ans_textBrowser(f"{message}", is_user=True)
            self.update_output(
                "\n*************************************************************************")  # Separator in the output window
            self.update_output(f"{message}")

    def strip_ansi_sequences(self, text):
        ansi_escape = re.compile(r'\x1b\[[0-9;]*[A-Za-z]')
        return ansi_escape.sub('', text)

    # def update_output(self, line):
    #     # self.output_text_edit.append(line)
    #
    #     self.output_text_edit.insertPlainText(line)
    #     self.output_text_edit.insertPlainText('\n')  # Add a newline after each line
    #     self.output_text_edit.moveCursor(QTextCursor.End)  # Ensure cursor is at the end
    #     self.output_text_edit.repaint()

    def update_output(self, line):
        clean_line = self.strip_ansi_sequences(line)

        # Capture REQUEST_ID from output

        if "RequestID:" in line:
            # request_id = line.split("REQUEST_ID:")[1].strip()
            request_id = line.split("RequestID:")[1].strip()
            self.current_request_id = request_id
            # Don't display this line to the user
            return

            # Handle CODE_READY_URLENCODED BEFORE adding to output

        elif "CODE_READY_URLENCODED:" in line:
            try:
                import urllib.parse
                encoded = line.split("CODE_READY_URLENCODED:", 1)[1].strip()
                decoded_code = urllib.parse.unquote(encoded)
                # cache + show
                self.latest_generated_code = decoded_code
                self.CodeEditor.setPlainText(self.latest_generated_code)
                self.CodeEditor.moveCursor(QTextCursor.Start)
                # Also give a friendly nudge in the chat panel (optional)
                self.update_chatgpt_ans_textBrowser("Code generation completed (see Generated Code tab).",
                                                    is_user=False)
                self.update_chatgpt_ans_textBrowser("Executing generated code...",
                                                    is_user=False)

            except Exception as e:
                self.update_chatgpt_ans_textBrowser(f"Failed to decode generated code: {e}", is_user=False)
            return  # Don't add code pattern to output_text_edit


        # Code for handling the output text edit
        self.output_text_edit.insertPlainText(clean_line)
        if not clean_line.endswith('\n'):
            self.output_text_edit.insertPlainText('\n')
        self.output_text_edit.moveCursor(QTextCursor.End)
        self.output_text_edit.repaint()
        self.output_text_edit.verticalScrollBar().setValue(self.output_text_edit.verticalScrollBar().maximum())

        # Code for handling the chatgpt_ans_textBrowser
        if "AI IS SELECTING DATA SOURCE ..." in line:
            current_model = self.modelNameComboBox.currentText()
            if current_model == 'gpt-5':
                self.update_chatgpt_ans_textBrowser(
                    "Selecting data source (may take some time while GPT-5 is reasoning)...",
                    is_user=False)
            else:
                self.update_chatgpt_ans_textBrowser("Selecting data source...", is_user=False)







        elif "data_source_ID:" in line:
            data_source_IDs = line.split("data_source_ID:")[1].strip()
            if data_source_IDs:
                # self.update_chatgpt_ans_textBrowser_textBrowser("Selecting tools...", is_user=False)
                # Format tool IDs as clickable links
                linked_data_sources = self.format_datasource_ids_as_links(data_source_IDs)
                self.update_chatgpt_ans_textBrowser(f"Selected data_source(s): {linked_data_sources}")

        elif "AI IS GENERATING THE DATA FETCHING PROGRAM ..." in line:
            current_model = self.modelNameComboBox.currentText()
            if current_model == 'gpt-5':
                self.update_chatgpt_ans_textBrowser(
                    "Generating code for data retriever (may take some time while GPT-5 is reasoning)...", is_user=False)
            else:
                self.update_chatgpt_ans_textBrowser("Generating code for data retriever...", is_user=False)




        elif "AI IS GENERATING THE DATA FETCHING PROGRAM ..." in line:
            current_model = self.modelNameComboBox.currentText()
            if current_model == 'gpt-5':
                self.update_chatgpt_ans_textBrowser(
                    "Generating code for data retriever (may take some time while GPT-5 is reasoning)...", is_user=False)
            else:
                self.update_chatgpt_ans_textBrowser("Generating code for data retriever...", is_user=False)

        elif "AI IS DEBUGGING THE CODE..." in line:
            current_model = self.modelNameComboBox.currentText()
            if current_model == 'gpt-5':
                self.update_chatgpt_ans_textBrowser(
                    "An error occurred, debugging code (may take some time while GPT-5 is reasoning)...", is_user=False)
            else:
                self.update_chatgpt_ans_textBrowser("An error occurred, debugging code...", is_user=False)

        # elif "Successfully executed code:" in line:
        #     self.update_chatgpt_ans_textBrowser("Code execution completed", is_user=False)



    def format_datasource_ids_as_links(self, datasource_ids_text):
        """Convert tool IDs to clickable HTML links"""
        import re
        import ast

        try:
            # Parse the tool IDs list (remove quotes and brackets)
            datasource_ids_text = datasource_ids_text.strip()
            if datasource_ids_text.startswith('[') and datasource_ids_text.endswith(']'):
                datasource_ids = ast.literal_eval(datasource_ids_text)
            else:
                # Handle cases where it's not a proper list format
                datasource_ids = [datasource_ids_text.strip("[]'\"")]

            # Create HTML links for each tool ID
            links = []
            for datasource_id in datasource_ids:
                datasource_id = datasource_id.strip("'\"")  # Remove quotes
                link = f'<a href="datasource-doc:{datasource_id}" style="color: blue; text-decoration: underline;">{datasource_id}</a>'
                links.append(link)

            return ', '.join(links)

        except Exception as e:
            # If parsing fails, return the original text
            return datasource_ids_text







    def thread_finished(self, success):
        if success:
            # self.output_text_edit.append("The script ran successfully.")
            self.output_text_edit.insertPlainText("The script ran successfully.")
            self.update_chatgpt_ans_textBrowser(f"Done")
        else:
            # self.output_text_edit.append("The script finished with errors.")
            self.output_text_edit.insertPlainText("The script finished with errors.")
            self.update_chatgpt_ans_textBrowser(f"AI: The script finished with errors.")

        # Re-enable the send_button    #Not working
        self.run_button.setEnabled(True)
        self.clear_chatgpt_ansBtn.setEnabled(True)
        self.task_LineEdit.setEnabled(True)
        self.saved_fnameLineEdit.setEnabled(True)
        self.SelectDataPath_ToolBtn.setEnabled(True)

    def clear_textboxes(self):
        self.output_text_edit.clear()
        self.task_LineEdit.clear()
        self.chatgpt_ans_textBrowser.clear()
        # self.output_text_edit_2.clear()
        # Clear conversation history to ensure no previous responses are carried forward
        self.conversation_history = []

    def toggle_saved_fnameLineEdit(self, checked):
        """Enable or disable data_pathLineEdit based on the switch_control state."""
        self.saved_fnameLineEdit.setEnabled(not checked)
        self.SelectDataPath_ToolBtn.setEnabled(not checked)

    def interrupt(self):
        if self.thread:
            self.thread.stop()  # Call the stop method to set the flag

    def get_openai_key(self):
        api_key = self.OpenAI_key_LineEdit.text()
        if not api_key:
            raise ValueError("API key is empty. Enter your OpenAI API key or GIBD API key")
        return api_key

    def add_documentation_file(self):
        try:
            destination_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LLM_Find", "Handbooks")

            # Ensure the destination directory exists; if not, create it
            if not os.path.exists(destination_dir):
                os.makedirs(destination_dir)
            # Open file dialog to select .toml files
            files, _ = QFileDialog.getOpenFileNames(
                None, 'Select Documentation Files', '', 'TOML Files (*.toml)'
            )

            # If files are selected, process them
            if files:
                for file_path in files:
                    # Determine the new path for the file in the destination directory
                    new_file_path = os.path.join(destination_dir, os.path.basename(file_path))
                    # Copy the file to the new directory
                    shutil.copy(file_path, new_file_path)
                    # print(f"File {file_path} copied to {new_file_path}")  # or update your UI to reflect the change
                    # Display success message
                QMessageBox.information(None, 'Success',
                                        f'Documentation files have been successfully uploaded to {destination_dir}')
                # else:
                #     # If no files were selected, show an info message
                #     QMessageBox.information(None, 'No Files Selected', 'No documentation files were selected.')

        except Exception as e:
            # Display failure message in case of any errors
            QMessageBox.critical(None, 'Error', f'Failed to upload documentation files: {str(e)}')


    # ******************************************************************************************************
    # NEW FUNCTIONS
    # ********************************************************************************************************
    def user_feedback(self):
        """Handle user feedback from thumbs up, thumbs down, and feedback message buttons."""
        # Detect which button was clicked
        sender = self.sender()

        # Initialize feedback variables
        # feedback = ""
        # feedback_message = ""

        # Determine feedback based on which button was clicked
        if sender == self.thumbs_up_button:
            feedback = "good"
            # feedback_message = ""
            self.update_chatgpt_ans_textBrowser("Thank you for your positive feedback!", is_user=False)
        elif sender == self.thumbs_down_button:
            feedback = "bad"
            # feedback_message = ""
            self.update_chatgpt_ans_textBrowser("Thank you for your feedback. We'll work to improve!", is_user=False)
        elif sender == self.feedback_message_button:
            # feedback = ""
            feedback_message = "Feedback is parsed"
            self.update_chatgpt_ans_textBrowser("Your feedback message has been recorded. Thank you!", is_user=False)
        else:
            # Unknown sender
            self.update_chatgpt_ans_textBrowser("Feedback received.", is_user=False)
            return

        # Get API key and current task info (same pattern as run_script)
        try:
            api_key = self.get_openai_key()
            user_query = self.current_task if hasattr(self,
                                                      'current_task') and self.current_task else "No task available"
            request_id = self.current_request_id if hasattr(self,
                                                            'current_request_id') and self.current_request_id else None

            # Send feedback to server (same HTTP call as helper.send_error but in dockwidget)
            url = f"https://www.gibd.online/api/feedback/{api_key}"
            data = {
                "service_name": "Spatial Data Retrieval Agent",
                "question_id": request_id,
                "question": user_query,
                "feedback": feedback,
                "feedback_message": feedback_message,
                # "error": "",
                # "error_msg": "",
                # "error_traceback": "",
                # "generated_code": ""
            }

            response = requests.post(
                url,
                headers={"Content-Type": "application/json"},
                json=data
            )

            if response.status_code == 201:
                print("Feedback sent successfully!")
            else:
                print(f"Error sending feedback: {response.status_code}: {response.text}")

        except Exception as e:
            print(f"Error sending feedback: {e}")
    def send_request_feedback(self):
        """Handle user feedback specifically for requests sent via run_button."""
        # Detect which button was clicked
        sender = self.sender()
        # self.current_feedback=""
        # self.current_feedback_message=""

        # Check if using gibd-services API key
        api_key = self.get_openai_key()
        if 'gibd-services' not in (api_key or ''):
            self.update_chatgpt_ans_textBrowser(
                "Feedback functionality is only available with GIBD API key. Obtain a GIBD API Key <a href='https://www.gibd.online/'>here</a>",
                is_user=False)
            return

        # Check if there's a valid request to provide feedback on
        # Only check for current_task since it's set immediately when run_button is pressed
        if not hasattr(self, 'current_task') or not self.current_task:
            self.update_chatgpt_ans_textBrowser("No request available to provide feedback on. Please run a task first.",
                                                is_user=False)
            return

        # Determine feedback based on which button was clicked
        if sender == self.thumbs_up_button:
            # User clicked thumbs up
            self.current_feedback = "good"
            # self.current_feedback_message = ""
            self.update_chatgpt_ans_textBrowser("Thank you for your positive feedback on this request!", is_user=False)
            self.send_feedback_to_api()

        elif sender == self.thumbs_down_button:
            # User clicked thumbs down
            self.current_feedback = "bad"
            # self.current_feedback_message = ""
            self.update_chatgpt_ans_textBrowser("Thank you for your feedback. We'll work to improve the results!",
                                                is_user=False)
            self.send_feedback_to_api()

        elif sender == self.feedback_message_button:
            # Show custom dialog to collect user feedback with multi-line text area
            dialog = QDialog(self)
            dialog.setWindowTitle('Provide Feedback')
            dialog.resize(500, 350)

            layout = QVBoxLayout(dialog)

            # Add instruction label
            label = QLabel('Please enter your feedback message:')
            layout.addWidget(label)

            # Add text edit with word wrap
            text_edit = QTextEdit()
            text_edit.setLineWrapMode(QTextEdit.WidgetWidth)  # Enable word wrap at widget width
            text_edit.setAcceptRichText(False)  # Plain text only
            layout.addWidget(text_edit)

            # Add OK and Cancel buttons
            button_layout = QHBoxLayout()
            ok_button = QPushButton('OK')
            cancel_button = QPushButton('Cancel')
            button_layout.addStretch()
            button_layout.addWidget(ok_button)
            button_layout.addWidget(cancel_button)
            layout.addLayout(button_layout)

            # Connect buttons
            ok_button.clicked.connect(dialog.accept)
            cancel_button.clicked.connect(dialog.reject)

            # Show dialog and get result
            ok = dialog.exec_() == QDialog.Accepted
            feedback_message = text_edit.toPlainText()

            # If user cancels or provides no input, return without sending feedback
            if not ok or not feedback_message.strip():
                self.update_chatgpt_ans_textBrowser("Feedback cancelled.", is_user=False)
                return

            # Store the message while preserving the existing feedback rating (good/bad/none)
            self.current_feedback_message = feedback_message
            self.update_chatgpt_ans_textBrowser("Your comment has been recorded. Thank you!", is_user=False)
            self.send_feedback_to_api()
        else:
            # Unknown sender
            self.update_chatgpt_ans_textBrowser("Feedback received.", is_user=False)
            return


    def send_feedback_to_api(self):
        """Send the currently stored feedback to the API."""
        try:
            api_key = self.get_openai_key()
            user_query = self.current_task
            request_id = self.current_request_id

            # Send feedback to server for this specific request
            url = f"https://www.gibd.online/api/feedback/{api_key}"
            data = {
                "service_name": "Spatial Data Retrieval Agent",
                "question_id": request_id,
                "question": user_query,
                "feedback": self.current_feedback,
                "feedback_message": self.current_feedback_message,
            }

            response = requests.post(
                url,
                headers={"Content-Type": "application/json"},
                json=data
            )

            if response.status_code == 201:
                print(f"Feedback sent successfully for request: {request_id}")
            else:
                print(f"Error sending feedback: {response.status_code}: {response.text}")

        except Exception as e:
            print(f"Error sending feedback: {e}")
            self.update_chatgpt_ans_textBrowser(f"Failed to send feedback: {e}", is_user=False)

    # ******************************************************************************************************
    # NEW FUNCTIONS END
    # ********************************************************************************************************

    def install_libraries_with_progress(self, libraries, force_reinstall=False):
        self.progress_dialog = QProgressDialog("Installing required libraries...", None, 0, 0, self)
        self.progress_dialog.setWindowTitle("Installing Dependencies")
        self.progress_dialog.setWindowModality(Qt.WindowModal)
        self.progress_dialog.setCancelButton(None)
        self.progress_dialog.setMinimumDuration(0)
        self.progress_dialog.show()

        self.install_thread = InstallLibrariesThread(libraries, force_reinstall)
        self.install_thread.install_finished.connect(self.on_install_finished)
        self.install_thread.start()

    def on_install_finished(self, success, message):
        self.progress_dialog.cancel()
        if success:
            QMessageBox.information(self, "Success", message)
        else:
            QMessageBox.critical(self, "Error", message)

    def install_pip_with_progress(self):
        self.pip_progress = QProgressDialog("Installing pip...", None, 0, 0, self)
        self.pip_progress.setWindowTitle("Installing pip")
        self.pip_progress.setWindowModality(Qt.WindowModal)
        self.pip_progress.setCancelButton(None)
        self.pip_progress.setMinimumDuration(0)
        self.pip_progress.show()

        self.pip_thread = InstallPipThread()
        self.pip_thread.pip_installed.connect(self.on_pip_install_finished)
        self.pip_thread.start()

    def on_pip_install_finished(self, success, message):
        self.pip_progress.cancel()
        if success and check_pip_installed():
            QMessageBox.information(self, "Pip Installed", message + "\n\nPlease restart QGIS before continuing.")
        else:
            QMessageBox.critical(self, "Installation Failed", message)

# The classFactory function must be placed at the end of this file
def classFactory(iface):
    """Load SpatialAnalysisAgentPlugin class."""
    return AGGRADockWidget(iface)

class ScriptThread(QThread):
    output_line = pyqtSignal(str)
    chatgpt_update = pyqtSignal(str)
    # graph_ready = pyqtSignal(str)
    # GraphReady = pyqtSignal(str)
    generated_code_ready = pyqtSignal(str)
    finished = pyqtSignal(bool)

    def __init__(self, script_path, task, saved_fname, OpenAI_key, model_name, reasoning_effort_value):
        super().__init__()
        self.script_path = script_path
        self.task = task
        self.saved_fname = saved_fname
        self.OpenAI_key = OpenAI_key
        self.model_name = model_name
        self.reasoning_effort_value = reasoning_effort_value


    def run(self):
        original_stdout = sys.stdout
        original_stderr = sys.stderr

        try:
            # Update the config file with the API keys
            # self.update_config_file()

            # #Re-read the config to ensure the keys are updated
            # self.read_updated_config()

            # Ensure that the updated configuration is read by reloading the config
            config_path = os.path.join(os.path.dirname(self.script_path), 'LLM_Find', 'openai_key_config.ini')
            config = configparser.ConfigParser()
            config.read(config_path)

            # Read the script content
            with open(self.script_path, "r") as script_file:
                script_content = script_file.read()

            local_vars = {
                'task': self.task,
                'saved_fname': self.saved_fname,
                # 'OpenAI_key': self.OpenAI_key,  # Add OpenAI_key to local variables
                'model_name': self.model_name,
                'reasoning_effort_value': self.reasoning_effort_value
            }
            # # Add each API key to local_vars
            # for key_name, api_key in self.api_keys.items():
            #     local_vars[key_name] = api_key

            ## Redirect stdout and stderr to capture the output
            # original_stdout = sys.stdout
            # original_stderr = sys.stderr
            # sys_stdout_capture = StringIO()
            # sys_stderr_capture = StringIO()
            # sys.stdout = sys_stdout_capture
            # sys.stderr = sys_stderr_capture
            # Redirect stdout and stderr
            stream_redirector = StreamRedirector()
            stream_redirector.output_written.connect(self.output_line.emit)
            # stream_redirector.output_written.connect(capture_output_code)

            sys.stdout = stream_redirector
            sys.stderr = stream_redirector


            # def emit_output(self, sys_stdout_capture, sys_stderr_capture):
            #     sys_stdout_capture.flush()
            #     sys_stderr_capture.flush()
            #     captured_stdout = sys_stdout_capture.getvalue()
            #     captured_stderr = sys_stderr_capture.getvalue()
            #     sys_stdout_capture.truncate(0)
            #     sys_stderr_capture.truncate(0)
            #     sys_stdout_capture.seek(0)
            #     sys_stderr_capture.seek(0)
            #
            #     if captured_stdout:
            #         for line in captured_stdout.splitlines(keepends=True):
            #             if line.endswith('\n'):
            #                 self.output_line.emit(line.rstrip())
            #             else:
            #                 # handle the case where the line doesn't end with a newline
            #                 self.output_line.emit(line)
            #
            #             # if "data_source_ID:" in line:
            #             #     datasource_ids = line.split("data_source_ID:")[1].strip()
            #             #     if datasource_ids:
            #             #         # self.tool_filename_ready.emit(tool_filename)  # Emit the tool filename
            #             #         # self.chatgpt_update.emit(f"AI: Selected tool(s): {tool_filename}")
            #             #         self.chatgpt_update.emit(f"AI: Selected data source: {datasource_ids}")
            #
            #             if "data_source_ID:" in line:
            #                 datasource_ids = line.split("data_source_ID:")[1].strip()
            #                 if datasource_ids:
            #                     # Format tool IDs as clickable links (access through parent)
            #                     try:
            #                         # Get the main widget instance to access the formatting function
            #                         main_widget = self.parent()
            #                         while main_widget and not hasattr(main_widget, 'format_datasource_ids_as_links'):
            #                             main_widget = main_widget.parent()
            #
            #                         if main_widget and hasattr(main_widget, 'format_datasource_ids_as_links'):
            #                             linked_tools = main_widget.format_datasource_ids_as_links(datasource_ids)
            #                             self.chatgpt_update.emit(f"Selected data source(s): {linked_tools}")
            #                         else:
            #                             self.chatgpt_update.emit(f"Selected data source(s): {datasource_ids}")
            #                     except:
            #                         self.chatgpt_update.emit(f"Selected data source(s): {datasource_ids}")
            #
            #
            #
            #     if captured_stderr:
            #         for line in captured_stderr.splitlines(keepends=True):
            #             if line.endswith('\n'):
            #                 self.output_line.emit(f"Error: {line.rstrip()}")
            #             else:
            #                 # handle the case where the line doesn't end with a newline
            #                 self.output_line.emit(f"Error: {line}")

            # Execute the script using exec
            exec_globals = globals()
            exec_locals = local_vars

            # # This will allow for real-time capturing and emitting of output
            # import threading
            # stop_thread = threading.Event()
            #
            # def monitor_output():
            #     while not stop_thread.is_set():
            #         emit_output()
            #         time.sleep(0.1)  # Adjust sleep time as needed
            #
            # monitor_thread = threading.Thread(target=monitor_output)
            # monitor_thread.start()

            # try:
            exec(script_content, exec_globals, exec_locals)
            # finally:
            #     stop_thread.set()
            #     monitor_thread.join()
            #
            # # Emit any remaining output
            # emit_output()
            #
            # # Restore original stdout and stderr
            # sys.stdout = original_stdout
            # sys.stderr = original_stderr

            if 'generated_code' in exec_locals:
                self.generated_code_ready.emit(exec_locals['generated_code'])

            else:
                self.output_line.emit("Error: 'generated_code' not found after script execution.")

            # Emit success signal
            self.finished.emit(True)

        except Exception as e:
            # Print traceback error to the text_edit
            traceback_str = traceback.format_exc()
            self.output_line.emit(f"Error: {e}\n{traceback_str}")  # Emit any exceptions to the UI
            # self.chatgpt_update.emit(f"Error: {e}\n{traceback_str}")  # Emit any exceptions)
            self.chatgpt_update.emit(f"Error: {e}")  # Emit any exceptions)
            self.finished.emit(False)  # Signal failure

        finally:
            sys.stdout = original_stdout
            sys.stderr = original_stderr

    # def update_config_file(self):
    #     current_script_dir = os.path.dirname(os.path.abspath(__file__))
    #     config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')
    #     # config_path = os.path.join(os.path.dirname(self.script_path), 'openai_key_config.ini')
    #     config = configparser.ConfigParser()
    #     config.read(config_path)
    #
    #     if 'API_Key' not in config:
    #         config['API_Key'] = {}
    #
    #     for key_name, api_key in self.api_keys.items():
    #         config['API_Key'][key_name] = api_key
    #
    #     with open(config_path, 'w') as configfile:
    #         config.write(configfile)

    # def read_updated_config(self):
    #     current_script_dir = os.path.dirname(os.path.abspath(__file__))
    #     config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')
    #     # config_path = os.path.join(os.path.dirname(self.script_path), 'openai_key_config.ini')
    #     config = configparser.ConfigParser()
    #     config.read(config_path)
    #     if 'API_Key' in config:
    #         for row in range(self.tableWidget.rowCount()):
    #             key_name = self.tableWidget.cellWidget(row, 0).currentText()
    #             if key_name in config['API_Key']:
    #                 api_key = config['API_Key'][key_name]
    #                 self.tableWidget.cellWidget(row, 1).findChild(QgsPasswordLineEdit).setText(api_key)

    def stop(self):
        self._is_running = False

    def isRunning(self):
        return self._is_running

    # def emit_output(self, sys_stdout_capture, sys_stderr_capture):
    #     sys_stdout_capture.flush()
    #     sys_stderr_capture.flush()
    #     captured_stdout = sys_stdout_capture.getvalue()
    #     captured_stderr = sys_stderr_capture.getvalue()
    #     sys_stdout_capture.truncate(0)
    #     sys_stderr_capture.truncate(0)
    #     sys_stdout_capture.seek(0)
    #     sys_stderr_capture.seek(0)
    #
    #     if captured_stdout:
    #         for line in captured_stdout.splitlines(keepends=True):
    #             if line.endswith('\n'):
    #                 self.output_line.emit(line.rstrip())
    #             else:
    #                 # handle the case where the line doesn't end with a newline
    #                 self.output_line.emit(line)
    #
    #             # if "data_source_ID:" in line:
    #             #     datasource_ids = line.split("data_source_ID:")[1].strip()
    #             #     if datasource_ids:
    #             #         # self.tool_filename_ready.emit(tool_filename)  # Emit the tool filename
    #             #         # self.chatgpt_update.emit(f"AI: Selected tool(s): {tool_filename}")
    #             #         self.chatgpt_update.emit(f"AI: Selected data source: {datasource_ids}")
    #
    #             if "data_source_ID:" in line:
    #                 datasource_ids = line.split("data_source_ID:")[1].strip()
    #                 if datasource_ids:
    #                     # Format tool IDs as clickable links (access through parent)
    #                     try:
    #                         # Get the main widget instance to access the formatting function
    #                         main_widget = self.parent()
    #                         while main_widget and not hasattr(main_widget, 'format_datasource_ids_as_links'):
    #                             main_widget = main_widget.parent()
    #
    #                         if main_widget and hasattr(main_widget, 'format_datasource_ids_as_links'):
    #                             linked_tools = main_widget.format_datasource_ids_as_links(datasource_ids)
    #                             self.chatgpt_update.emit(f"Selected data source(s): {linked_tools}")
    #                         else:
    #                             self.chatgpt_update.emit(f"Selected data source(s): {datasource_ids}")
    #                     except:
    #                         self.chatgpt_update.emit(f"Selected data source(s): {datasource_ids}")



class GPTRequestThread(QThread):
    output_line = pyqtSignal(str)
    finished_signal = pyqtSignal()

    def __init__(self, prompt, OpenAI_key, model_name, conversation_history):
        super().__init__()
        self.prompt = prompt
        self.OpenAI_key = OpenAI_key
        self.model_name = model_name

    def load_api_key_from_config(self):
        """Load OpenAI API key from openai_key_config.ini."""
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'LLM_Find', 'openai_key_config.ini')

        config = configparser.ConfigParser()
        config.read(config_path)

        if 'API_Key' in config and 'OpenAI_key' in config['API_Key']:
            return config['API_Key']['OpenAI_key']
        else:
            raise ValueError("API Key not found in config file.")

    def run(self):
        try:
            # self.update_config_file()
            from openai import OpenAI
            client = OpenAI(api_key=self.OpenAI_key)
            response = client.chat.completions.create(
                model=self.model_name,  # "gpt-4",
                messages=[
                    {"role": "user", "content": self.prompt},
                ]
            )
            reply = response.choices[0].message.content.strip()
            self.output_line.emit(f"AI: {reply}")
        except Exception as e:
            self.output_line.emit(f"Error: {str(e)}")
        finally:
            self.finished_signal.emit()


class PythonHighlighter(QSyntaxHighlighter):
    def __init__(self, document, always_highlight=False):
        super(PythonHighlighter, self).__init__(document)
        self.always_highlight = always_highlight
        self.python_block = False

        self.highlighting_rules = []

        keyword_format = QTextCharFormat()
        keyword_format.setForeground(QColor("blue"))
        keywords = [
            "def", "class", "if", "else", "elif", "while", "for", "return", "import", "from", "as", "with", "try",
            "except", "finally", "raise", "yield", "lambda", "pass", "break", "continue", "global", "nonlocal",
            "assert", "del", "and", "as", "assert", "break", "class", "continue", "del", "elif", "else", "except",
            "False", "finally", "for", "in", "is", "None", "not", "or", "pass", "raise", "return", "True", "print"
        ]

        for keyword in keywords:
            pattern = re.compile(r'\b' + keyword + r'\b')
            self.highlighting_rules.append((pattern, keyword_format))

        # Strings
        string_format = QTextCharFormat()
        string_format.setForeground(QColor("green"))
        self.highlighting_rules.append((re.compile(r'"[^"\\]*(\\.[^"\\]*)*"'), string_format))
        self.highlighting_rules.append((re.compile(r"'[^'\\]*(\\.[^'\\]*)*'"), string_format))

        # Comments
        comment_format = QTextCharFormat()
        comment_format.setForeground(QColor("gray"))
        self.highlighting_rules.append((re.compile(r'#.*'), comment_format))

    def highlightBlock(self, text):
        if not self.always_highlight:
            if text.strip() == "```python":
                self.python_block = True
                return  # Don't highlight the marker line
            elif text.strip() == "```":
                self.python_block = False
                return  # Don't highlight the marker line

        # Apply syntax highlighting only if we're inside a Python block
        # if self.python_block:
        if self.always_highlight or self.python_block:
            for pattern, format in self.highlighting_rules:
                for match in pattern.finditer(text):
                    start, end = match.span()
                    self.setFormat(start, end - start, format)


class ContributionDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.plugin = parent  # Reference to the main plugin class

        self.setWindowTitle("Contribute to AutonomousGIS_GeodataRetrieverAgent")
        self.setMinimumWidth(400)

        self.setWindowTitle("Contribute to Spatial Analysis Agent")
        layout = QVBoxLayout(self)

        # Add a label for instructions
        instructions = QLabel("Instructions for contribution:")
        layout.addWidget(instructions)

        # Instructional label
        instructions = QLabel("""
        <h3>How to Contribute</h3>
        <ol>
            <li><b>Fork this repository</b> on GitHub: <a href='https://github.com/Teakinboyewa/AutonomousGIS_GeodataRetrieverAgent'>Click Here</a>.</li>
            <li><b>Clone your fork</b> to your local machine.</li>
            <li>Upload a TOML file using this dialog (it will go to your forked repository).</li>
            <li>After uploading, go to GitHub and <b>open a pull request</b> from your fork to the main repository.</li>
        </ol>
        """)
        instructions.setOpenExternalLinks(True)
        layout.addWidget(instructions)

        # File upload button
        self.upload_button = QPushButton("Upload TOML File to Fork")
        self.upload_button.clicked.connect(self.upload_toml_file)
        layout.addWidget(self.upload_button)

    def get_github_token(self):
        """Retrieve the GitHub token from the config file or prompt the user to enter one."""

        # Path to the configuration file
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        githubtokenConfig_path = os.path.join(current_script_dir, "config_files", "GitHubTokenConfig.ini")

        config = configparser.ConfigParser()

        try:
            # Check if the config file exists
            if os.path.exists(githubtokenConfig_path):
                # If the config file exists, read the token from it
                config.read(githubtokenConfig_path)
                token = config.get("GitHub", "token", fallback=None)

                # If no token found, prompt for token
                if not token:
                    token = self.prompt_for_token(githubtokenConfig_path)
                return token

            else:
                # If the config file doesn't exist, create it and prompt for token
                os.makedirs(os.path.dirname(githubtokenConfig_path), exist_ok=True)
                return self.prompt_for_token(githubtokenConfig_path)

        except (configparser.Error, IOError) as e:
            QMessageBox.warning(self, "Error", f"Failed to read or write the token configuration: {e}")
            return None

    def prompt_for_token(self, config_file_path):
        """Prompt the user for a GitHub token and store it in the config file."""
        token, ok = QInputDialog.getText(self, 'GitHub Token', 'Please enter your GitHub token:')
        if ok and token:
            self.save_github_token(config_file_path, token)
            return token
        else:
            return None

    def save_github_token(self, config_file_path, token):
        """Save the GitHub token to the configuration file."""
        config = configparser.ConfigParser()
        config.read(config_file_path)
        config["GitHub"] = {"token": token}

        with open(config_file_path, "w") as config_file:
            config.write(config_file)

    def check_if_fork_exists(self, token, username):
        repo = "Teakinboyewa/AutonomousGIS_GeodataRetrieverAgent"
        url = f"https://api.github.com/repos/{username}/AutonomousGIS_GeodataRetrieverAgent"

        headers = {
            "Authorization": f"token {token}",
            "Accept": "application/vnd.github.v3+json"
        }

        response = requests.get(url, headers=headers)

        if response.status_code == 200:
            return True  # The fork exists
        else:
            return False

    def upload_to_user_fork(self, token, file_path, username):
        repo = f"{username}/AutonomousGIS_GeodataRetrieverAgent"  # Target the user's fork
        FOLDER_IN_REPO = "LLM_Find/Handbook"  # Folder inside the repository
        file_name = os.path.basename(file_path)
        path_in_repo = f"{FOLDER_IN_REPO}/{file_name}"
        url = f"https://api.github.com/repos/{repo}/contents/{path_in_repo}"

        headers = {
            "Authorization": f"token {token}",

            "Accept": "application/vnd.github.v3+json"
        }

        # Check if the file already exists to get its S
        response = requests.get(url, headers=headers)

        if response.status_code == 200:
            file_data = response.json()
            sha = file_data["sha"]  # Get the SHA of the existing file
            file_exists = True
        elif response.status_code == 404:
            file_exists = False
            sha = None  # File doesn't exist, no SHA needed
        else:
            print(f"Error checking file existence: {response.json()}")
            raise Exception(f"Error checking file existence: {response.json()}")

        # Read the file content to upload
        with open(file_path, 'rb') as file:
            content = file.read()

        encoded_content = base64.b64encode(content).decode("utf-8")

        data = {
            "message": "Adding a new TOML file via QGIS plugin",
            "content": encoded_content
        }

        # If the file exists, include the SHA to update it
        if file_exists:
            data["sha"] = sha

        response = requests.put(url, json=data, headers=headers)

        if response.status_code in [200, 201]:
            print("File successfully uploaded/updated in the forked GitHub repository.")
        else:
            print(f"Failed to upload/update file: {response.json()}")
            raise Exception(f"GitHub upload/update failed: {response.json()}")

        # if response.status_code == 201:
        #     print("File successfully uploaded to the forked GitHub repository.")
        # else:
        #     print(f"Failed to upload file: {response.json()}")
        #     raise Exception(f"GitHub upload failed: {response.json()}")

    def prompt_pull_request(self, username):
        pr_url = f"https://github.com/{username}/AutonomousGIS_GeodataRetrieverAgent/compare"
        msg = QMessageBox()
        msg.setIcon(QMessageBox.Information)
        msg.setText(
            f"File uploaded successfully to your fork.\nPlease open a pull request to merge it into the main repository.")
        msg.setInformativeText(f"<a href='{pr_url}'>Click here to open a pull request</a>")
        msg.setStandardButtons(QMessageBox.Ok)
        msg.exec_()

    def upload_toml_file(self):
        """Handle the file upload to the user's fork."""
        token = self.get_github_token()  # Get GitHub token from main plugin

        if not token:
            QMessageBox.warning(self, "Error", "GitHub token is required.")
            return

        # Prompt user to select a file
        file_dialog = QFileDialog(self)
        toml_files, _ = file_dialog.getOpenFileNames(self, "Select a TOML file", "", "TOML Files (*.toml)")

        if toml_files:
            # Ask for GitHub username (you can automate this with the token if preferred)
            username, ok = QInputDialog.getText(self, 'GitHub Username', 'Enter your GitHub username:')

            if ok and username:
                for toml_file in toml_files:
                    # Upload the file to the user's fork
                    self.upload_to_user_fork(token, toml_file, username)

                # Prompt the user to open a pull request
                self.prompt_pull_request(username)
            else:
                QMessageBox.warning(self, "Error", "GitHub username is required.")


class AddKeyDialog(QDialog):
    def __init__(self, keys_directory, parent=None):
        super().__init__(parent)
        self.keys_directory = keys_directory
        self.setWindowTitle("Add New Key")

        # Dialog layout
        layout = QVBoxLayout()

        # Key Name input
        self.name_label = QLabel("Enter the Key Name:")
        self.name_input = QLineEdit()
        layout.addWidget(self.name_label)
        layout.addWidget(self.name_input)

        # Key Value input
        self.key_label = QLabel("Enter the Key Value:")
        self.key_input = QLineEdit()
        layout.addWidget(self.key_label)
        layout.addWidget(self.key_input)

        # Save button
        self.save_button = QPushButton("Save")
        self.save_button.clicked.connect(self.save_key)
        layout.addWidget(self.save_button)

        self.setLayout(layout)

    def save_key(self):
        key_name = self.name_input.text().strip()
        key_value = self.key_input.text().strip()

        if not key_name or not key_value:
            QMessageBox.warning(self, "Input Error", "Both key name and key value are required.")
            return

        # Create the filename
        file_name = f"{key_name}.keys"
        file_path = os.path.join(self.keys_directory, file_name)

        # Construct the content of the file
        api_key_name = f"{key_name}_key"
        content = f"[API_Key]\n{api_key_name} = {key_value}\n"

        try:
            # Write the content to the new .keys file
            with open(file_path, 'w') as key_file:
                key_file.write(content)

            # Show success message
            # QMessageBox.information(self, "Success", f"New key file '{file_name}' created successfully.")

            # Close the dialog
            self.accept()

        except Exception as e:
            QMessageBox.warning(self, "File Error", f"Failed to create the key file: {file_name}\nError: {str(e)}")


class RemoveKeyDialog(QDialog):
    def __init__(self, keys_directory, parent=None):
        super().__init__(parent)
        self.keys_directory = keys_directory
        self.setWindowTitle("Remove Key")

        # Dialog layout
        layout = QVBoxLayout()

        # Combo box to list all the key files
        self.combo_box = QComboBox()
        self.load_key_names()
        layout.addWidget(QLabel("Select a key to remove:"))
        layout.addWidget(self.combo_box)

        # Remove button
        self.remove_button = QPushButton("Remove")
        self.remove_button.clicked.connect(self.remove_key)
        layout.addWidget(self.remove_button)

        self.setLayout(layout)

    def load_key_names(self):
        # Get all .keys files from the 'Keys' directory (only removable keys, not public datasources)
        keys_files = [f for f in os.listdir(self.keys_directory) if f.endswith('.keys') and f != 'template.keys']
        all_key_names = [os.path.splitext(f)[0] for f in keys_files]
        self.combo_box.addItems(all_key_names)

    def remove_key(self):
        selected_key_name = self.combo_box.currentText()

        if not selected_key_name:
            QMessageBox.warning(self, "Selection Error", "Please select a key to remove.")
            return

        # Confirm deletion
        confirm = QMessageBox.question(self, "Confirm Delete",
                                       f"Are you sure you want to remove the key '{selected_key_name}'?",
                                       QMessageBox.Yes | QMessageBox.No)

        if confirm == QMessageBox.Yes:
            file_path = os.path.join(self.keys_directory, f"{selected_key_name}.keys")
            try:
                # Remove the key file
                os.remove(file_path)

                # Show success message
                QMessageBox.information(self, "Success", f"Key file '{selected_key_name}' removed successfully.")

                # Close the dialog and return success
                self.accept()

            except Exception as e:
                QMessageBox.warning(self, "File Error", f"Failed to remove the key file: {file_path}\nError: {str(e)}")


class StreamRedirector(QObject):
    output_written = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.buffer = ''

    def write(self, text):
        if text:
            self.buffer += text
            while '\n' in self.buffer:
                line, self.buffer = self.buffer.split('\n', 1)
                self.output_written.emit(line)

    def flush(self):
        if self.buffer:
            self.output_written.emit(self.buffer)
            self.buffer = ''


class RunGeneratedCodeThread(QThread):
    CodeEditor_output_line = pyqtSignal(str)
    execution_error = pyqtSignal(str)
    report_ready = pyqtSignal(str)

    def __init__(self, code_to_run, exec_globals):
        super().__init__()
        self.code_to_run = code_to_run
        self.exec_globals = exec_globals  # Store exec_globals

    def run(self):
        self.success = True
        # Redirect stdout and stderr
        original_stdout = sys.stdout
        original_stderr = sys.stderr
        sys.stdout = StreamRedirector()
        sys.stderr = StreamRedirector()

        sys.stdout.output_written.connect(self.handle_output_line)
        # sys.stdout.output_written.connect(self.CodeEditor_output_line.emit)
        sys.stderr.output_written.connect(self.CodeEditor_output_line.emit)

        try:
            # exec_locals = {}
            exec(self.code_to_run, self.exec_globals)
        except Exception as e:
            self.success = False
            traceback_str = traceback.format_exc()
            self.execution_error.emit(f"Error executing code:\n{traceback_str}")
        finally:
            sys.stdout = original_stdout
            sys.stderr = original_stderr

    def handle_output_line(self, line):
        # Emit the line to the execution output
        self.CodeEditor_output_line.emit(line)
        path_pattern = re.compile(r'([A-Za-z]:\\[^\\/:*?"<>|\r\n]+(?:\\[^\\/:*?"<>|\r\n]+)*\.\w+|/[^/ ]+/[^ ]+)')
        match = path_pattern.search(line)
        if match:
            generated_output = match.group(0)
            if generated_output:
                self.report_ready.emit(generated_output)