## -*- coding: utf-8 -*-
"""
/***************************************************************************
 Spatial_Analysis_AgentDockWidget
                                 A QGIS plugin
 A plugin integration between QGIS and Large Language Model (LLM) for Spatial Analysis
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2024-08-12
        git sha              : $Format:%H$
        copyright            : (C) 2024 by GIBD
        email                : teakinboyewa@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 os
import platform
import shutil
import sys
import urllib
import subprocess
import traceback
import qgis
import requests
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtCore import QSettings
import re
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
import configparser
from PyQt5.QtWebKitWidgets import QWebView
from qgis.PyQt.QtCore import Qt, QCoreApplication
from qgis._core import QgsVectorLayer, QgsRasterLayer, QgsProcessing

QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
from PyQt5.QtWidgets import QDialog, QFileDialog, QTextEdit, QApplication, QWidget, QSizeGrip, QMessageBox, \
    QInputDialog, QTextBrowser, QProgressDialog
from PyQt5.QtCore import QThread, pyqtSignal, QUrl, QObject, pyqtSlot, QPropertyAnimation, QPoint, QRect, QSettings
from PyQt5 import QtWidgets, uic
from PyQt5.QtWidgets import QDialog, QHBoxLayout
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QGridLayout, QWidget, QTextEdit, QPushButton, \
    QLabel, QLineEdit, QMenu, QAction, QCompleter
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtGui import QTextCursor, QTextCharFormat, QFont, QColor, QPainter, QBrush, QSyntaxHighlighter, \
    QDesktopServices, QTextOption
from qgis.gui import QgsMapCanvas, QgsLayerTreeView, QgsLayerTreeMapCanvasBridge, QgsAttributeDialog
from qgis.core import QgsProject, QgsLayerTreeModel, QgsLayerTreeNode, QgsRectangle
from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject, QgsVectorLayer, \
    QgsCoordinateTransformContext, QgsGeometry, QgsFeature, QgsVectorFileWriter

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

current_script_dir = os.path.dirname(os.path.abspath(__file__))

from .install_packages.check_packages import read_libraries_from_file, check_missing_libraries, \
    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:
            # 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, shapely, or other core library issues)
            force_reinstall = (
                "numpy.dtype size changed" in error_str or
                "binary incompatibility" in error_str.lower() or
                "has no attribute" in error_str.lower() or  # Shapely 2.0 incompatibility
                "module" in error_str.lower()  # Module import incompatibility
            )

            if force_reinstall:
                print("[WARNING] Package incompatibility detected. Will use --force-reinstall flag.")
                print(f"[DEBUG] Error detected: {error_str}")
                # 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 SpatialAnalysisAgentDockWidget(QtWidgets.QDockWidget, FORM_CLASS):
    closingPlugin = pyqtSignal()

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

        self.setupUi(self)
        self.resize(400, 2)

        required_packages = get_requirements_file()

        self.is_task_breakdown = False
        self.task_breakdown_lines = []
        self.is_data_attributes = False
        self.data_attributes_lines = []

        # 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

        # from .install_packages.check_packages import check_and_install_libraries
        # Run the check before the class definition
        # current_script_dir = os.path.dirname(os.path.abspath(__file__))
        # required_packages = os.path.join(current_script_dir, 'install_packages', 'requirements.txt')

        # check_and_install_libraries(required_packages)

        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

        # # Start the OpenAI version check thread
        # self.version_check_thread = VersionCheckThread()
        # self.version_check_thread.version_check_completed.connect(self.handle_version_check)
        # self.version_check_thread.start()

        self.chatgpt_ans_textBrowser.setOpenExternalLinks(False)
        self.chatgpt_ans_textBrowser.setOpenLinks(False)
        self.chatgpt_ans_textBrowser.anchorClicked.connect(self.open_link)

        self.import_libraries()

        self.load_OpenAI_key()

        self.initUI()
        # Connect to layer added and removed signals
        QgsProject.instance().layerWasAdded.connect(self.on_layer_added)
        QgsProject.instance().layerWillBeRemoved.connect(self.on_layer_removed)

        # self.thread = None  # Initialize thread variable
        self.interrupt_button.clicked.connect(self.interrupt)
        # Set the window size

        # Ensure the window has minimize and maximize buttons
        self.setWindowFlags(self.windowFlags() |
                            Qt.WindowMinimizeButtonHint |
                            Qt.WindowMaximizeButtonHint)

        # Initialize conversation history
        self.conversation_history = []
        self.task_history = []
        self.data_path_history = []

        self.stopFlag = False
        self.interrupt_button.clicked.connect(self.interrupt)

        # 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.data_path_completer = QCompleter(self.data_path_history, self)
        self.data_path_completer.setCaseSensitivity(Qt.CaseInsensitive)
        # self.data_pathLineEdit.setCompleter(self.data_path_completer)

        # Connect the ChatMode_checkbox to the slot function
        # self.ChatMode_checkbox.toggled.connect(self.toggle_data_path_line_edit)
        
        # 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)

        # Add a map view to display the solution graph
        self.web_view_layout = QVBoxLayout()
        self.web_view_layout.setContentsMargins(0, 0, 0, 0)
        self.graph_widget.setLayout(self.web_view_layout)
        self.web_view = QWebView()
        self.web_view_layout.addWidget(self.web_view)
        # self.graphview()

        # Add a map view to display reports
        self.report_web_view_layout = QVBoxLayout()
        self.report_web_view_layout.setContentsMargins(0, 0, 0, 0)
        self.report_widget.setLayout(self.report_web_view_layout)
        self.report_web_view = QWebView()
        self.report_web_view_layout.addWidget(self.report_web_view)
        # self.graphview()

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

        # Set default workspace directory to plugin directory
        # current_script_dir = os.path.dirname(os.path.abspath(__file__))
        workspace_dir = os.path.join(current_script_dir, 'Default_workspace')
        self.create_default_workspace(workspace_dir)
        # self.workspace_directoryLineEdit.setPlainText(workspace_dir)
        self.workspace_directoryLineEdit2.setText(workspace_dir)

        # Connect button to open directory dialog
        self.select_workspace_Btn.clicked.connect(self.open_directory_dialog)
        self.Run_Generated_code.clicked.connect(self.run_generated_code)

        # Connect the visibility changed signal for all layers
        root = QgsProject.instance().layerTreeRoot()
        root.visibilityChanged.connect(self.on_layer_visibility_changed)

        # Show model info dialog on first load
        # Use QTimer to delay the dialog so it appears after the plugin is fully loaded
        from PyQt5.QtCore import QTimer
        QTimer.singleShot(1000, self.show_model_info_dialog)

    def initUI(self):

        # Disable the data_pathLineEdit permanently
        # self.data_pathLineEdit.setEnabled(False)
        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.ESGpushButton.clicked.connect(self.run_slnGraph_script)sss
        self.interrupt_button.clicked.connect(self.interrupt)
        self.interrupt_button.clicked.connect(self.stop_script)
        # Connect feedback buttons to send_request_feedback (for run_button requests)
        # self.thumbs_up_button.clicked.connect(self.user_feedback)
        # self.thumbs_down_button.clicked.connect(self.user_feedback)
        # self.feedback_message_button.clicked.connect(self.user_feedback)
        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)
        # Connect buttons to methods
        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_code_editor)
        # self.pushButton = self.findChild(QPushButton, 'pushButton')
        # self.SelectDataPath_ToolBtn.clicked.connect(self.openFileDialog)
        self.clear_textboxesBtn.clicked.connect(self.clear_textboxes)
        self.loadData.clicked.connect(self.load_data)
        # self.refresh_slnGraph_Btn.clicked.connect(self.refresh_slnGraph)
        # self.refresh_report_Btn.clicked.connect(self.refresh_report)
        self.run_button.clicked.connect(self.clear_report)
        self.Run_Generated_code.clicked.connect(self.clear_report)
        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.tabWidget.setCurrentIndex(0)

        # self.read_updated_config()
        # Populate data_pathLineEdit with currently loaded layers
        # self.populate_data_path_line_edit()
        self.on_layer_visibility_changed()

    def append_execution_output(self, line):
        # Check if text is empty

        if not line.strip():
            return
        # Split the text into individual lines
        lines = line.strip().split('\n')
        for line in lines:
            # Prepend '>>>' to each line
            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")
            # Append the formatted line to the text edit
            # self.execution_output_text_edit.appendPlainText(formatted_line)
            # Append the formatted line with color
            self.append_colored_text(self.execution_output_text_edit, formatted_line, color)
        # Ensure the cursor moves to the end
        self.execution_output_text_edit.moveCursor(QTextCursor.End)
        # Scroll to the bottom
        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 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.critical(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 or Text Files (*.py *.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)
                # If needed, rehighlight
                # self.code_highlighter.rehighlight()
                # QMessageBox.information(self, "Success", f"Code loaded from:\n{file_name}")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to load code:\n{str(e)}")

    def run_generated_code(self):
        self.report_web_view.setHtml('')
        self.append_execution_output("Running code ...")
        # Get the code from the CodeEditor
        code_to_run = self.CodeEditor.toPlainText()

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

        # Prepare the execution environment without limitations
        import __main__
        # Import 'processing' into __main__ if not already imported
        if 'processing' not in __main__.__dict__:
            import processing
            __main__.processing = processing  # Add 'processing' to __main__

        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.report_ready.connect(self.update_report)
        self.generated_code_thread.finished.connect(self.generated_code_execution_finished)
        self.generated_code_thread.start()

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

        self.contribution_dialog = ContributionDialog(self)

        self.contribution_dialog.exec_()

    def open_link(self, url):
        # Check if it's a tool documentation link
        if url.scheme() == 'tool-doc':
            tool_id = url.path()  # Get the tool ID from the URL path
            self.show_tool_documentation(tool_id)
        elif url.scheme() == 'ai-thoughts':
            # Get the full URL string and extract content after the scheme
            url_string = url.toString()
            thoughts_content = url_string.split('ai-thoughts:', 1)[1] if 'ai-thoughts:' in url_string else ''
            self.show_ai_thoughts(thoughts_content)
        elif url.scheme() == 'data-attributes':
            # Get the full URL string and extract content after the scheme
            url_string = url.toString()
            data_content = url_string.split('data-attributes:', 1)[1] if 'data-attributes:' in url_string else ''
            self.show_data_attributes(data_content)
        elif url.scheme() == 'file':
            file_path = url.toLocalFile()
            # Prompt the user for confirmation
            reply = QMessageBox.question(
                self, 'Open File',
                f'Do you want to open the file:\n{file_path}?',
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                QDesktopServices.openUrl(url)
        else:
            # For other URLs, open directly
            QDesktopServices.openUrl(url)
        # # url is a QUrl object
        # QDesktopServices.openUrl(url)

    def create_default_workspace(self, workspace_dir):
        """Create the Default_workspace directory if it doesn't exist."""
        if not os.path.exists(workspace_dir):
            try:
                os.makedirs(workspace_dir)

            except Exception as e:
                print(f"Error creating default workspace: {e}")

    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 handle_version_check(self, needs_update):
        if needs_update:
            message = (
                "A new version of the 'openai' package is available.\n"
                "Would you like to update it now? This may require administrator privileges."
            )
            reply = QMessageBox.question(
                self, 'Update Available', message,
                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
            )
            if reply == QMessageBox.Yes:
                self.update_openai_package()

    def update_openai_package(self):
        try:
            import subprocess
            import sys

            # Run the pip install command to update the package
            subprocess.check_call(['python3', "-m", "pip", "install", "--upgrade", "openai"])

            QMessageBox.information(
                self, 'Update Successful',
                "The 'openai' package has been updated. Please restart the application."
            )
        except Exception as e:
            QMessageBox.critical(
                self, 'Update Failed',
                f"Failed to update 'openai' package:\n{e}"
            )

    def on_layer_visibility_changed(self):
        """Update data_pathLineEdit based on visible layers."""
        root = QgsProject.instance().layerTreeRoot()
        visible_layers = []

        # Traverse all layers and check their visibility
        for layer_node in root.findLayers():
            layer = layer_node.layer()  # Get the actual layer from the layer tree node

            if layer and layer_node.isVisible():  # Ensure the layer is valid and visible
                try:
                    layer_path = layer.dataProvider().dataSourceUri().split("|")[0]
                    visible_layers.append(layer_path)
                except AttributeError:
                    # Handle cases where dataSourceUri might not be available
                    continue

        # Update data_pathLineEdit with the paths of visible layers, each on a new line
        all_paths = "\n".join(visible_layers)

        self.data_pathLineEdit.setPlainText(all_paths)

    def on_layer_added(self, layer):
        # Get the current text in the LineEdit
        existing_paths = self.data_pathLineEdit.toPlainText()
        # Get the layer's data source and name it correctly
        # Ensure the data provider is not None before accessing dataSourceUri
        if layer.dataProvider() is not None:
            # Get the layer's data source path
            layer_path = layer.dataProvider().dataSourceUri().split("|")[0]

            # Append the new layer's path if it doesn't already exist
            if layer_path not in existing_paths:
                if existing_paths:
                    all_paths = existing_paths + "\n" + layer_path
                else:
                    all_paths = layer_path

                self.data_pathLineEdit.setPlainText(all_paths)

        # Connect the visibilityChanged signal for the newly added layer
        root = QgsProject.instance().layerTreeRoot()
        node = root.findLayer(layer.id())
        if node:
            node.visibilityChanged.connect(self.on_layer_visibility_changed)

    def on_layer_removed(self, layer_id):
        # Get the current text in the LineEdit
        existing_paths = self.data_pathLineEdit.toPlainText()

        # Get the layer's data source and name it correctly
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return

        # Initialize layer_path as None
        layer_path = None

        layer_path = layer.dataProvider().dataSourceUri().split("|")[0]

        if layer_path in existing_paths:
            updated_paths = existing_paths.replace(layer_path, "").replace("\n\n", "\n")
            # Clean up leading/trailing newlines
            updated_paths = updated_paths.strip("\n")

            self.data_pathLineEdit.setPlainText(updated_paths)
        # Update data_pathLineEdit based on the remaining visible layers
        self.on_layer_visibility_changed()

    def toggle_data_path_line_edit(self, checked):
        """Enable or disable data_pathLineEdit based on the ChatMode_Checkbox state."""
        self.data_pathLineEdit.setEnabled(not checked)
        self.loadData.setEnabled(not checked)
    
    def on_model_changed(self):
        """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']

        # 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 = '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 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; }")

    def show_tool_documentation(self, tool_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, 'SpatialAnalysisAgent', 'Tools_Documentation')
            
            # Convert tool_id to filename format (replace : with _)
            tool_filename = tool_id.replace(':', '_')
            
            # 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 tool_filename in file or tool_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 == tool_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: {tool_id}\nSearched for: {tool_filename}")
                
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to open tool documentation: {str(e)}")

    def show_ai_thoughts(self, thoughts_content):
        """Display AI thoughts content in a popup window"""
        try:
            # Decode the URL-encoded content
            import urllib.parse
            decoded_content = urllib.parse.unquote(thoughts_content)

            # Create a popup window to display the AI thoughts
            thoughts_dialog = QDialog(self)
            thoughts_dialog.setWindowTitle("Task breakdown")
            thoughts_dialog.setMinimumSize(600, 400)
            thoughts_dialog.resize(800, 600)

            layout = QVBoxLayout(thoughts_dialog)

            # Create a text browser to display the content
            text_browser = QTextBrowser()

            # Set larger font size
            font = text_browser.font()
            font.setPointSize(12)  # Increase font size to 12pt (default is usually 8-10pt)
            text_browser.setFont(font)

            text_browser.setPlainText(decoded_content)
            text_browser.setWordWrapMode(QTextOption.WordWrap)
            layout.addWidget(text_browser)

            # Add a close button
            close_button = QPushButton("Close")
            close_button.clicked.connect(thoughts_dialog.close)
            layout.addWidget(close_button)

            thoughts_dialog.show()

        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to display AI thoughts: {str(e)}")

    def show_data_attributes(self, data_content):
        """Display data overview content in a popup window"""
        try:
            # Decode the URL-encoded content
            import urllib.parse
            decoded_content = urllib.parse.unquote(data_content)

            # Clean up the content if it looks like JSON data or has special formatting
            import json
            import re

            # Try to parse and format the data in a more readable way
            try:
                # Check if the content looks like JSON-like data (contains quotes and braces/brackets)
                if ('Columns:' in decoded_content) or ('{' in decoded_content and '"' in decoded_content) or (decoded_content.startswith('[') and decoded_content.endswith(']')):
                    # Try to format JSON-like structures and new-style data
                    cleaned_content = self.format_data_attributes_content(decoded_content)
                else:
                    cleaned_content = decoded_content
            except:
                cleaned_content = decoded_content

            # Create a popup window to display the data overview
            data_dialog = QDialog(self)
            data_dialog.setWindowTitle("Data overview")
            data_dialog.setMinimumSize(600, 400)
            data_dialog.resize(800, 600)

            layout = QVBoxLayout(data_dialog)

            # Create a text browser to display the content
            text_browser = QTextBrowser()

            # Set larger font size
            font = text_browser.font()
            font.setPointSize(12)  # Increase font size to 12pt (default is usually 8-10pt)
            text_browser.setFont(font)

            text_browser.setPlainText(cleaned_content)
            text_browser.setWordWrapMode(QTextOption.WordWrap)
            layout.addWidget(text_browser)

            # Add a close button
            close_button = QPushButton("Close")
            close_button.clicked.connect(data_dialog.close)
            layout.addWidget(close_button)

            data_dialog.show()

        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to display data overview: {str(e)}")

    def format_data_attributes_content(self, content):
        """Format data overview content for better readability"""
        try:
            import re

            # Handle case where content might contain HTML-like markup
            if content.startswith('[') and 'href=' in content:
                # This looks like it might contain HTML markup that got passed incorrectly
                # Extract just the content part after any HTML tags
                content = re.sub(r'<[^>]*>', '', content)
                content = re.sub(r'\[.*?href=.*?\]', '', content)
                content = content.strip('[]"')

            # Replace escaped quotes and format the content
            formatted_content = content.replace('\\"', '"').replace("\\'", "'")

            # Check if the content is already formatted with "Columns:" (new format)
            # If so, just clean it up and return without further processing
            if 'Columns:' in formatted_content:
                # Process list format: ['item1', 'item2'] or just a string
                if formatted_content.startswith('[') and formatted_content.endswith(']'):
                    # This is a list representation - extract the items
                    # Remove outer brackets
                    inner_content = formatted_content[1:-1].strip()

                    # Try to use ast.literal_eval to properly parse the list
                    import ast
                    import codecs
                    try:
                        # Parse the list - this will handle quotes and escape sequences properly
                        parsed_list = ast.literal_eval('[' + inner_content + ']')
                        items = []
                        for item in parsed_list:
                            # The item is already a string, just ensure newlines are rendered
                            # No need to decode because ast.literal_eval already did that
                            items.append(str(item))
                        result = '\n\n'.join(items)
                        return result
                    except:
                        # If ast.literal_eval fails, try manual extraction
                        # Use regex to find quoted strings in the list
                        import re
                        # Match single or double quoted strings
                        pattern = r'''(?:'([^']*)'|"([^"]*)")'''
                        matches = re.findall(pattern, inner_content)

                        items = []
                        if matches:
                            # Extract the matched groups (either single or double quoted)
                            for match in matches:
                                item = match[0] if match[0] else match[1]
                                # Decode Python string escapes (like \n, \t, etc.)
                                try:
                                    decoded_item = codecs.decode(item, 'unicode_escape')
                                    items.append(decoded_item)
                                except:
                                    # If decoding fails, use as-is
                                    items.append(item)
                        else:
                            # Last resort fallback: just use the content and decode manually
                            content = inner_content.strip('"\'')
                            # Manually replace common escape sequences
                            content = content.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
                            items = [content]

                        result = '\n\n'.join(items)
                        return result
                else:
                    # Not a list format, just decode escape sequences and return
                    import codecs
                    try:
                        return formatted_content.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
                    except:
                        return formatted_content

            # Original formatting logic for old-style data
            # Split into lines and format file paths and data overview sections
            lines = []
            current_line = ""

            # Split by common delimiters and clean up
            parts = formatted_content.split('", "')

            for i, part in enumerate(parts):
                part = part.strip('"[]')

                if part.endswith('.geojson') or part.endswith('.tif') or part.endswith('.shp'):
                    if current_line:
                        lines.append(current_line)
                    current_line = f"File: {part}"
                elif 'Data overview:' in part:
                    if current_line:
                        current_line += "\n" + part
                    else:
                        current_line = part
                    lines.append(current_line)
                    current_line = ""
                else:
                    if current_line:
                        current_line += " " + part
                    else:
                        current_line = part

            if current_line:
                lines.append(current_line)

            return "\n\n".join(lines)

        except Exception:
            # If formatting fails, return original content cleaned up
            return content.replace('\\"', '"').replace("\\'", "'")

    def format_ai_thoughts_link(self, thoughts_content):
        """Convert AI thoughts content to a clickable 'thoughts' link"""
        try:
            # URL encode the content to handle special characters
            import urllib.parse
            encoded_content = urllib.parse.quote(thoughts_content)

            # Create a clickable link
            link = f'<a href="ai-thoughts:{encoded_content}" style="color: blue; text-decoration: underline;">task breakdown</a>'
            return link

        except Exception as e:
            # If encoding fails, return the original text
            return thoughts_content

    def format_data_attributes_link(self, data_content):
        """Convert data overview content to a clickable 'data overview' link"""
        try:
            # URL encode the content to handle special characters
            import urllib.parse
            data_encoded_content = urllib.parse.quote(data_content)

            # Create a clickable link
            data_link = f'<a href="data-attributes:{data_encoded_content}" style="color: blue; text-decoration: underline;">data overview</a>'
            return data_link

        except Exception as e:
            # If encoding fails, return the original text
            return data_content

    def format_tool_ids_as_links(self, tool_ids_text):
        """Convert tool IDs to clickable HTML links"""
        import re
        import ast
        
        try:
            # Parse the tool IDs list (remove quotes and brackets)
            tool_ids_text = tool_ids_text.strip()
            if tool_ids_text.startswith('[') and tool_ids_text.endswith(']'):
                tool_ids = ast.literal_eval(tool_ids_text)
            else:
                # Handle cases where it's not a proper list format
                tool_ids = [tool_ids_text.strip("[]'\"")]
            
            # Create HTML links for each tool ID
            links = []
            for tool_id in tool_ids:
                tool_id = tool_id.strip("'\"")  # Remove quotes
                link = f'<a href="tool-doc:{tool_id}" style="color: blue; text-decoration: underline;">{tool_id}</a>'
                links.append(link)
            
            return ', '.join(links)
            
        except Exception as e:
            # If parsing fails, return the original text
            return tool_ids_text

    def show_model_info_dialog(self):
        """Show informational dialog about GPT-5 default model and model selection guidance"""
        try:
            # Check if user has already seen this dialog
            settings = QSettings()
            shown_before = settings.value("SpatialAnalysisAgent/model_info_dialog_shown", False, type=bool)

            # Show dialog if not shown before or if user wants to see it again
            if not shown_before:
                msg_box = QMessageBox(self)
                msg_box.setWindowTitle("Model Selection Information")
                msg_box.setIcon(QMessageBox.Information)

                # Main message text
                main_text = ("GPT-5 is set as the default model for deeper reasoning capabilities.\n\n"
                           "GPT-5 offers deeper reasoning for complex spatial analysis tasks but may require "
                           "more waiting time. Use GPT-4o for faster responses on simpler tasks.")

                msg_box.setText(main_text)

                # Additional information text with clickable link
                info_text = (
                    'You can change the model selection in Settings at any time.'
                    '<a href="https://openai.com/api/pricing/">Learn more about OpenAI models</a><br><br>'
                           )

                msg_box.setInformativeText(info_text)

                # Make links clickable
                msg_box.setTextFormat(Qt.RichText)
                msg_box.setTextInteractionFlags(Qt.TextBrowserInteraction)

                # Add custom buttons
                msg_box.addButton("OK", QMessageBox.AcceptRole)
                dont_show_btn = msg_box.addButton("Don't show again", QMessageBox.RejectRole)

                # Links in the informative text will handle URL opening automatically
                # No need to connect finished signal to open URLs

                result = msg_box.exec_()

                # Handle "Don't show again" option
                if msg_box.clickedButton() == dont_show_btn:
                    settings.setValue("SpatialAnalysisAgent/model_info_dialog_shown", True)

        except Exception as e:
            # Silently handle any errors to avoid disrupting plugin initialization
            print(f"Error showing model info dialog: {e}")

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

    # def clear_report(self):
    #     if not self.ChatMode_checkbox.isChecked() and self.data_pathLineEdit.toPlainText().strip() and self.task_LineEdit.toPlainText().strip():
    #         self.report_web_view.setHtml('')
    #         # self.refresh_report_Btn.clicked.connect(self.refresh_report)
    #     else:
    #         return

    def clear_report(self):
        if not self.data_pathLineEdit.toPlainText().strip() and self.task_LineEdit.toPlainText().strip():
            self.report_web_view.setHtml('')
            # self.refresh_report_Btn.clicked.connect(self.refresh_report)
        else:
            return

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

    def populate_data_path_line_edit(self):
        # Retrieve all layers currently in the project
        layers = QgsProject.instance().mapLayers().values()
        paths = []

        for layer in layers:
            layer_path = None
            # Check if the layer has a valid data provider
            data_provider = layer.dataProvider()
            if data_provider is not None:
                layer_path = data_provider.dataSourceUri().split("|")[0]
            # layer_path = layer.dataProvider().dataSourceUri().split("|")[0]

            # Handle vector layers
            if isinstance(layer, QgsVectorLayer):
                if layer.isValid() and layer.isTemporary():
                    paths.append(f"Temporary Layer: {layer.name()}")
                else:
                    paths.append(layer_path)

            # Handle raster layers
            elif isinstance(layer, QgsRasterLayer):
                if layer.isValid() and layer.isTemporary():
                    paths.append(f"Temporary Layer: {layer.name()}")
                else:
                    paths.append(layer_path)

            # Handle other types of layers if needed
            # Filter out any None values just in case
        paths = [path for path in paths if path is not None]
        # Join paths with a semicolon and update data_pathLineEdit
        all_paths = "; ".join(paths)
        self.data_pathLineEdit.setPlainText(all_paths)

    def read_updated_config(self):
        # self.update_config_file()
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'SpatialAnalysisAgent', 'config.ini')
        # config_path = os.path.join(os.path.dirname(self.script_path), '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_config_file(self):
        # Retrieve the API key from the line edit
        # OpenAI_key = self.OpenAI_key_LineEdit.text()
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'SpatialAnalysisAgent', 'config.ini')
        # config_path = os.path.join(os.path.dirname(self.script_path), '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 update_OpenAI_key(self):
        self.OpenAI_key = {}
        settings = QSettings('YourOrganization', 'YourApplication')
        api_key = self.OpenAI_key_LineEdit.text()
        # Store the key in the dictionary and save it in the QSettings
        self.OpenAI_key['OpenAI_key'] = api_key
        settings.setValue('API_Key/OpenAI_key', api_key)

    def load_OpenAI_key(self):
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'SpatialAnalysisAgent', '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_config_file()

    # # Set the loaded key in the OpenAI_key_LineEdit widget
    # self.OpenAI_key_LineEdit.setText(api_key)

    def update_graph(self, html_path):
        self.web_view.load(QUrl.fromLocalFile(html_path))

    def update_report(self, generated_report_path):
        # Check the file extension to determine if it's an image or HTML
        file_extension = os.path.splitext(generated_report_path)[1].lower()

        if file_extension in ['.png', '.jpg', '.jpeg', '.gif']:  # Handle image files
            # Create a simple HTML file that embeds the image
            image_html_path = os.path.join(os.path.dirname(generated_report_path), 'image_report.html')
            normalized_path = os.path.normpath(generated_report_path).replace('\\', '/')
            with open(image_html_path, 'w') as f:
                f.write(f'<html><body><img src="file:///{normalized_path}" alt="Report Image" /></body></html>')

            # Load the generated HTML file that contains the image
            self.report_web_view.load(QUrl.fromLocalFile(image_html_path))

        elif file_extension == '.html':  # Handle HTML files
            # Directly load the HTML file
            self.report_web_view.load(QUrl.fromLocalFile(generated_report_path))

        else:
            print("Unsupported file type.")

        # self.report_web_view.load(QUrl.fromLocalFile(generated_report_path))

    def refresh_slnGraph(self):
        # Clear the web view content
        self.web_view.setHtml("<html><body><h1>Solution Graph Cleared</h1></body></html>")

    def set_initial_extent(self):
        project = QgsProject.instance()
        layers = list(project.mapLayers().values())
        if layers:
            extent = layers[0].extent()
            for layer in layers[1:]:
                extent.combineExtentWith(layer.extent())
            self.mapCanvas.setExtent(extent)

    def removeLayer(self):
        selectedIndex = self.layerTreeView.currentIndex()
        node = self.layerTreeModel.index2node(selectedIndex)
        if isinstance(node, QgsLayerTreeNode):
            QgsProject.instance().layerTreeRoot().removeChildNode(node)

    def showAttributeTable(self):
        selectedIndex = self.layerTreeView.currentIndex()
        node = self.layerTreeModel.index2node(selectedIndex)
        if node and isinstance(node, QgsLayerTreeNode):
            layer = node.layer()
            if layer:
                dlg = QgsAttributeDialog(layer)
                dlg.exec_()

    def zoomToExtent(self):
        selectedIndex = self.layerTreeView.currentIndex()
        node = self.layerTreeModel.index2node(selectedIndex)
        if node and isinstance(node, QgsLayerTreeNode):
            layer = node.layer()
            if layer:
                extent = layer.extent()
                self.mapCanvas.setExtent(extent)
                self.mapCanvas.refresh()

    def load_data(self):

        file_filter = "Data Files(*.shp *.csv *.gpkg *.tif *.jpg)"
        data_paths, _ = QFileDialog.getOpenFileNames(self, "Open File", "", file_filter)
        if data_paths:
            # Get the current content of the data_pathLineEdit
            existing_paths = self.data_pathLineEdit.toPlainText()

            # Concatenate the new paths with the existing ones
            new_paths = f"\n ".join(data_paths)
            if existing_paths:
                # If there are already existing paths, add a semicolon before appending the new paths
                all_paths = f"{existing_paths}\n{new_paths}"
            else:
                # If there are no existing paths, just use the new paths
                all_paths = new_paths

            for data_path in data_paths:
                file_extension = os.path.splitext(data_path)[1].lower()

                if file_extension in ['.shp', '.csv', '.xlsx']:
                    # Extract the file name without the extension
                    layer_name = os.path.splitext(os.path.basename(data_path))[0]
                    # Load the vector data
                    layer = QgsVectorLayer(data_path, os.path.basename(layer_name), "ogr")
                    # Add the layer to the project
                    QgsProject.instance().addMapLayer(layer)
                    # self.data_pathLineEdit.appendPlainText(f"{data_path}")

                elif file_extension in ['.gpkg']:
                    # GPKG can contain multiple layers, so iterate over them
                    layers = QgsVectorLayer(data_path, '', 'ogr')
                    for layer_name in layers.dataProvider().subLayers():
                        layer_name = layer_name.split('!!::!!')[1]  # Extract the actual layer name
                        # Load each layer from the GPKG
                        layer = QgsVectorLayer(f"{data_path}|layername={layer_name}", layer_name, "ogr")
                        if layer.isValid():
                            # Add the layer to the project
                            QgsProject.instance().addMapLayer(layer)
                        else:
                            print(f"Failed to load GPKG layer: {layer_name} from {data_path}")

                elif file_extension in ['.tif', '.jpg']:
                    # Extract the file name without the extension
                    layer_name = os.path.splitext(os.path.basename(data_path))[0]
                    # Load the raster data
                    layer = QgsRasterLayer(data_path, os.path.basename(layer_name), "gdal")

                    # Add the raster layer to the project
                    QgsProject.instance().addMapLayer(layer)
                    # self.data_pathLineEdit.appendPlainText(f"{data_path}")

                else:
                    print("Unsupported file format!")

            # Set all paths in data_pathLineEdit separated by a semicolon
            self.data_pathLineEdit.setPlainText(all_paths)


    def send_button_clicked(self):
        """Slot to handle the send button click."""

        # self.chatgpt_ans_textBrowser.setAlignment(Qt.AlignLeft)
        user_message = self.task_LineEdit.toPlainText().strip()
        self.CodeEditor.clear()
        self.execution_output_text_edit.clear()
        self.web_view.setHtml("")  # Clear the graph by clearing the web view content


        if not user_message:
            self.update_chatgpt_ans_textBrowser(f"Please enter a task in the task field.", is_user=False)
            return  # Stop further execution if the task is empty
        # print("Sending message:", self.task_LineEdit.toPlainText())  # Debugging statement
        # Emit the user's message in chatgpt_ans first

        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_config_file()

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

        # if not self.ChatMode_checkbox.isChecked() and self.data_pathLineEdit.isEnabled() and not self.data_pathLineEdit.toPlainText().strip():
        #     self.update_chatgpt_ans_textBrowser(f"Please load the data to be used.", is_user=False)
        #     return  # Stop further execution if data path is required but empty

        if not self.data_pathLineEdit.isEnabled() and not self.data_pathLineEdit.toPlainText().strip():
            self.update_chatgpt_ans_textBrowser(f"Please load the data to be used.", is_user=False)
            return  # Stop further execution if data path is required but empty

        # Add initial processing message
        # self.update_chatgpt_ans_textBrowser("Initializing analysis...", is_user=False)

        # if self.ChatMode_checkbox.isChecked():
        #     self.update_chatgpt_ans_textBrowser("Generating response...", is_user=False)
        #     self.chatgpt_direct_answer(user_message)
        # else:
        #     # Check if GPT-5 is selected to show appropriate message
        #     current_model = self.modelNameComboBox.currentText()
        #     if current_model == 'gpt-5':
        #         self.update_chatgpt_ans_textBrowser("Analyzing the task (may take some time while GPT-5 is reasoning)...", is_user=False)
        #     else:
        #         self.update_chatgpt_ans_textBrowser("Analyzing the task...", is_user=False)
        #     self.run_script()

        # Check if GPT-5 or GPT-5.1 (with reasoning effort) is selected to show appropriate message
        current_model = self.modelNameComboBox.currentText()
        reasoning_effort = self.reasoningEffortComboBox.currentText() if self.reasoningEffortComboBox.isVisible() else None

        if current_model == 'gpt-5':
            self.update_chatgpt_ans_textBrowser("Analyzing the task (may take some time while GPT-5 is reasoning)...", is_user=False)
        elif current_model == 'gpt-5.1' and reasoning_effort and reasoning_effort != 'none':
            self.update_chatgpt_ans_textBrowser("Analyzing the task (may take some time while GPT-5.1 is reasoning)...", is_user=False)
        else:
            self.update_chatgpt_ans_textBrowser("Analyzing the task...", is_user=False)
        self.run_script()

    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": "GIS Copilot",
                "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": "GIS Copilot",
                "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)

    def chatgpt_direct_answer(self, user_message):
        """Method to interact with GPT-4 and display the result in output_text_edit_2."""
        # Update API key in the config file first
        # self.update_OpenAI_key()

        # Retrieve the API key from the config
        self.OpenAI_key = self.get_openai_key()  # This retrieves the latest key from the config

        # Emit the message from task_LineEdit first
        # user_message = self.task_LineEdit.toPlainText()
        self.OpenAI_key = self.get_openai_key()  # Retrieve the API key from the line edit
        self.model_name = self.modelNameComboBox.currentText()
        
        # Clear the output_text_edit before starting streaming
        self.output_text_edit.clear()
        
        # if user_message.strip():  # Check if the input is not empty
        # self.update_output(f"User: {user_message}")  # Display the user message in output_text_edit_2

        # Start the GPT-4 request in a separate thread
        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
        
        # Connect streaming chunks FIRST for real-time display in output_text_edit
        self.gpt_thread.streaming_chunk.connect(self.handle_streaming_chunk)
        
        # Don't connect output_line to update_output for chat mode to avoid conflicts
        # self.gpt_thread.output_line.connect(self.update_output)
        
        self.gpt_thread.chatgpt_update.connect(
            lambda reply: self.update_chatgpt_ans_textBrowser(f"{reply}", is_user=False))
        self.gpt_thread.finished_signal.connect(lambda: self.update_chatgpt_ans_textBrowser("Done", is_user=False))

        self.gpt_thread.start()

    def open_directory_dialog(self):
        """Open a dialog for the user to select a workspace directory."""
        directory = QFileDialog.getExistingDirectory(self, "Select Workspace Directory")
        if directory:
            # Set the selected directory to the PlainTextLineEdit
            # self.workspace_directoryLineEdit.setPlainText(directory)
            self.workspace_directoryLineEdit2.setText(directory)

    def run_script(self):
        # Clear the output_text_edit before starting
        self.output_text_edit.clear()
        
        # self.update_OpenAI_key()
        # Retrieve the API key from the config
        self.OpenAI_key = self.get_openai_key()
        is_review = self.review_checkbox.isChecked()
        # use_rag = self.use_rag_checkbox.isChecked()
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        script_path = os.path.join(current_script_dir, "SpatialAnalysisAgent", "SpatialAnalysisAgent_MyScript.py")

        self.model_name = self.modelNameComboBox.currentText()
        self.OpenAI_key = self.get_openai_key()  # Retrieve the API key from the line edit
        
        # Check if this is a local model that doesn't require OpenAI key
        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(self.model_name, 'openai')
            
            if provider_name == 'ollama':
                # Local model doesn't need OpenAI key
                pass
            elif not self.OpenAI_key:
                self.update_chatgpt_ans_textBrowser(f"Enter your OpenAI API key or GIBD API key", is_user=False)
                return
        except ImportError:
            # Fallback: require OpenAI key for all models
            if not self.OpenAI_key:
                self.update_chatgpt_ans_textBrowser(f"Enter your OpenAI API key or GIBD API key", is_user=False)
                return

        self.task = self.task_LineEdit.toPlainText()
        self.current_task = self.task  # Store task for feedback
        self.data_path = self.data_pathLineEdit.toPlainText()
        # self.workspace_directory = self.workspace_directoryLineEdit.toPlainText()
        self.workspace_directory = self.workspace_directoryLineEdit2.text()

        # # 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.data_path not in self.data_path_history:
            self.data_path_history.append(self.data_path)
            self.data_path_completer.model().setStringList(self.data_path_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']:
            self.reasoning_effort_value = self.reasoningEffortComboBox.currentText()
            
        self.thread = ScriptThread(script_path, self.task, self.data_path, self.workspace_directory, self.OpenAI_key,
                                   self.model_name, is_review, self.reasoning_effort_value)

        self.thread.output_line.connect(self.update_output)
        self.thread.streaming_chunk.connect(self.handle_streaming_chunk)  # Connect streaming
        self.thread.graph_ready.connect(self.update_graph)
        self.thread.report_ready.connect(self.update_report)
        self.thread.chatgpt_update.connect(self.update_chatgpt_ans_textBrowser)
        # self.thread.extracted_code_ready.connect(self.update_code_editor)
        # self.thread.reviewed_code_ready.connect(self.update_code_editor)
        # self.thread.debugged_code_ready.connect(self.update_code_editor)
        self.thread.generated_code_ready.connect(self.update_code_editor)
        self.thread.script_finished.connect(self.thread_finished)
        self.thread.start()

        # Disable the send_button
        self.run_button.setEnabled(False)
        self.clear_textboxesBtn.setEnabled(False)
        self.task_LineEdit.setEnabled(False)
        self.data_pathLineEdit.setEnabled(False)
        self.loadData.setEnabled(False)

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

    def update_chatgpt_ans_textBrowser(self, message, is_user=False):
        # # Check if this is a CODE_READY message for immediate CodeEditor update
        # if message.startswith("CODE_READY:"):
        #     code_content = message[len("CODE_READY:"):]
        #     self.update_code_editor(code_content)
        #     return  # Don't add this to conversation history

        # 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 stop_script(self):
        if self.thread:
            self.thread.terminate()
            self.update_chatgpt_ans_textBrowser(f"Script terminated")
            # print("Script terminated")
            # Re-enable the send_button
        self.run_button.setEnabled(True)
        self.clear_textboxesBtn.setEnabled(True)
        self.task_LineEdit.setEnabled(True)
        self.data_pathLineEdit.setEnabled(True)
        self.loadData.setEnabled(True)

    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)

        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

        # URL processing already done above, no need to process again
        
        # Check if this is a processing status message (ends with "..." or contains "Executing the code")
        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)

    @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}")
            # if self.ChatMode_checkbox.isChecked():
            #     # Clear the input field after sending the message when switch is checked
            #     self.task_LineEdit.clear()

    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):

        clean_line = self.strip_ansi_sequences(line)

        if self.is_task_breakdown:
            # Check if the current line marks the end of the task breakdown
            if line.strip() == "_":
                self.is_task_breakdown = False
                # Process the accumulated task breakdown lines
                task_breakdown_text = "\n".join(self.task_breakdown_lines)
                # Create clickable "thoughts" link instead of showing full text
                thoughts_link = self.format_ai_thoughts_link(task_breakdown_text)
                self.update_chatgpt_ans_textBrowser(f"Analysis result: {thoughts_link}")
                self.task_breakdown_lines = []  # Reset the accumulator
            else:
                # Accumulate the line
                self.task_breakdown_lines.append(clean_line)
        elif self.is_data_attributes:
            # Check if we've reached the end of data overview output
            # Look for the next section which starts with "AI IS SELECTING THE APPROPRIATE TOOL(S)"
            if line.strip() =="__":
                self.is_data_attributes = False
                # Process the accumulated data overview lines
                data_attributes_text = "\n".join(self.data_attributes_lines)
                # Create clickable "data overview" link instead of showing full text
                data_link = self.format_data_attributes_link(data_attributes_text)
                self.update_chatgpt_ans_textBrowser(f"Analysis result: {data_link}")
                self.data_attributes_lines = []  # Reset the accumulator
            else:
                # Accumulate the line
                self.data_attributes_lines.append(clean_line)
        else:
            # 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

            if "GRAPH_SAVED:" in line:
                html_graph_path = line.split("GRAPH_SAVED:")[1].strip()
                self.update_graph(html_graph_path)
                self.update_chatgpt_ans_textBrowser(
                    "Geoprocessing workflow is ready (see Geoprocessing Workflow tab).")  # Emit the message to chatgpt_ans

            elif "Output:" in line:  # Check for "Output" flag
                generated_output = line.split("Output:")[1].strip()
                if generated_output:
                    self.update_report(generated_output)
                    self.update_chatgpt_ans_textBrowser(f"{generated_output}")  # Emit Output

            elif "List of selected tool IDs:" in line:
                tool_IDs = line.split("List of selected tool IDs:")[1].strip()
                if tool_IDs:
                    # self.update_chatgpt_ans_textBrowser("Selecting tools...", is_user=False)
                    # Format tool IDs as clickable links
                    linked_tools = self.format_tool_ids_as_links(tool_IDs)
                    self.update_chatgpt_ans_textBrowser(f"Selected tool(s): {linked_tools}")

            elif "TASK_BREAKDOWN:" in line:
                # Start accumulating task breakdown lines
                # self.update_chatgpt_ans_textBrowser("Breaking down task into steps...", is_user=False)
                self.is_task_breakdown = True
                task_breakdown_line = line.split("TASK_BREAKDOWN:")[1].strip()
                self.task_breakdown_lines = [task_breakdown_line]

            elif "data overview:" in line:
                    # Extract the data directly from this line and create the link immediately
                    data_content = line.split("data overview: ")[1].strip()
                    # Create clickable "data overview" link instead of showing full text
                    data_link = self.format_data_attributes_link(data_content)
                    self.update_chatgpt_ans_textBrowser(f"Analysis result: {data_link}")
                    # return  # Don't add to output_text_edit
                
            elif "AI IS SELECTING THE APPROPRIATE TOOL(S) ..." in line:
                self.update_chatgpt_ans_textBrowser("Selecting appropriate tools...", is_user=False)
                
            elif "Fine tuned query:" in line:
                self.update_chatgpt_ans_textBrowser("Task analysis complete. Tuning query...", is_user=False)

            # elif "TOOL SELECT PROMPT" in line:
            #     self.update_chatgpt_ans_textBrowser("Creating tool selection prompt...", is_user=False)

            # elif "SELECTED TOOLS:" in line:
            #     self.update_chatgpt_ans_textBrowser("Tools selected successfully...", is_user=False)
                
            elif "---------- AI IS GENERATING THE GEOPROCESSING WORKFLOW FOR THE TASK ----------" in line:
                self.update_chatgpt_ans_textBrowser("Generating geoprocessing workflow...", is_user=False)
                
            # elif "OPERATION PROMPT:" in line:
            #     self.update_chatgpt_ans_textBrowser("Creating operation prompt...", is_user=False)
                
            elif "---------- AI IS GENERATING THE OPERATION CODE ----------" in line:
                current_model = self.modelNameComboBox.currentText()
                if current_model == 'gpt-5':
                    self.update_chatgpt_ans_textBrowser(
                        "Generating operation code (may take some time while GPT-5 is reasoning)...", is_user=False)
                else:
                    self.update_chatgpt_ans_textBrowser("Generating operation code...", is_user=False)

                
            # elif "OPERATION CODE GENERATED SUCCESSFULLY" in line:
            #     self.update_chatgpt_ans_textBrowser("Code generation completed.", is_user=False)


                
            elif "----AI IS REVIEWING THE GENERATED CODE" in line:
                self.update_chatgpt_ans_textBrowser("Reviewing generated code...", is_user=False)

            # elif "OPERATION CODE GENERATED AND REVIEWED SUCCESSFULLY" in line:
            #     self.update_chatgpt_ans_textBrowser("Code generation and review completed. View at Generated Code tab",
            #                                         is_user=False)
            #     return  # Don't add status message to output_text_edit

                
            elif "AI IS EXAMINING THE DATA ..." in line:
                self.update_chatgpt_ans_textBrowser("Examining the data ...", is_user=False)
                # return  # Don't add status message to output_text_edit
                
            # elif "MODEL CONFIGURATION DEBUG INFO" in line:
            #     self.update_chatgpt_ans_textBrowser("Configuring AI model...", is_user=False)
                
            elif "STARTING ANALYSIS..." in line:
                self.update_chatgpt_ans_textBrowser("Starting detailed analysis...", is_user=False)
                return  # Don't add status message to output_text_edit
                
            elif "Generating workflow" in line:
                self.update_chatgpt_ans_textBrowser("Generating workflow...", is_user=False)
                return  # Don't add status message to output_text_edit

            elif "Generating code" in line or "Creating code" in line:
                self.update_chatgpt_ans_textBrowser("Generating code...", is_user=False)
                return  # Don't add status message to output_text_edit
                
            elif "Processing data" in line or "Analyzing data" in line:
                self.update_chatgpt_ans_textBrowser("Processing data...", is_user=False)
                return  # Don't add status message to output_text_edit

            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)
                # return  # Don't add status message to output_text_edit

            elif "-------------- Running code (trial #" in line:
                # Extract trial information and show enhanced status message
                import re
                trial_match = re.search(r'trial # (\d+)/(\d+)', line)
                if trial_match:
                    current_trial = trial_match.group(1)
                    total_trials = trial_match.group(2)
                    self.update_chatgpt_ans_textBrowser(f"Executing the code... (Trial {current_trial} / {total_trials})", is_user=False)
                else:
                    self.update_chatgpt_ans_textBrowser("Executing the code...", is_user=False)
                return  # Don't add status message to output_text_edit

            # elif "Running code2" in line or "Running code (trial #" in line:
            #     self.update_chatgpt_ans_textBrowser("Executing the code...", is_user=False)
            #     if getattr(self, "latest_generated_code", ""):
            #         self.CodeEditor.setPlainText(self.latest_generated_code)
            #     else:
            #         # fallback: keep whatever is already there or show a gentle note
            #         pass
            #     return  # Don't add status message to output_text_edit

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


            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)
                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


            elif "CODE_READY_URLENCODED2:" in line:
                try:
                    import urllib.parse
                    encoded = line.split("CODE_READY_URLENCODED2:", 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("Final code generated (see Generated Code tab).", 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

            elif "CODE_DEBUGGED:" in line:
                try:
                    # Handle non-URL encoded debug code for backward compatibility
                    debug_code = line.split("CODE_DEBUGGED:", 1)[1].strip()
                    # cache + show
                    self.latest_generated_code = debug_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 debugging completed (see Generated Code tab).", is_user=False)
                except Exception as e:
                    self.update_chatgpt_ans_textBrowser(f"Failed to process debugged code: {e}", is_user=False)
                return  # Don't add code pattern to output_text_edit





        # The rest of your 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())



    # @pyqtSlot(bool)
    def thread_finished(self, success):

        if success:
            # self.output_text_edit.append("The script ran successfully.")
            # self.output_text_edit.insertPlainText("The script ran successfully2.")
            self.update_chatgpt_ans_textBrowser(f"Done")
            self.run_button.setEnabled(True)
            self.clear_textboxesBtn.setEnabled(True)
            self.task_LineEdit.setEnabled(True)
            self.data_pathLineEdit.setEnabled(True)
            self.loadData.setEnabled(True)

        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"The script finished with errors.")
            self.run_button.setEnabled(True)
            self.clear_textboxesBtn.setEnabled(True)
            self.task_LineEdit.setEnabled(True)
            self.data_pathLineEdit.setEnabled(True)
            self.loadData.setEnabled(True)

        # Ensure the thread is stopped and cleaned up
        self.thread.quit()  # This will stop the event loop in the thread
        self.thread.wait()  # This will block until the thread has finished executing

        # Re-enable the send_button    #Not working
        self.run_button.setEnabled(True)
        self.task_LineEdit.setEnabled(True)
        self.data_pathLineEdit.setEnabled(True)
        self.loadData.setEnabled(True)
        self.clear_textboxesBtn.setEnabled(True)
        # self.update_chatgpt_ans_textBrowser("--------------------------------------------------------------")

    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 clear_textboxes(self):
        self.output_text_edit.clear()
        self.task_LineEdit.clear()
        self.chatgpt_ans_textBrowser.clear()
        self.conversation_history = []

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

    def handle_streaming_chunk(self, chunk):
        """Handle streaming chunks from GPT and display them in real-time in output_text_edit"""
        # Insert the chunk at the end of the output_text_edit
        self.output_text_edit.insertPlainText(chunk)
        
        # Move cursor to end and scroll to bottom for live streaming effect
        self.output_text_edit.moveCursor(QTextCursor.End)
        self.output_text_edit.repaint()
        self.output_text_edit.verticalScrollBar().setValue(
            self.output_text_edit.verticalScrollBar().maximum()
        )

    def get_openai_key(self):
        api_key = self.OpenAI_key_LineEdit.text()
        # if not api_key:
        # raise ValueError("API key is empty. Please enter a valid OpenAI API key.")
        # self.update_chatgpt_ans_textBrowser(f"Please enter a valid OpenAI API keyYYY.")
        return api_key


    def add_documentation_file(self):
        try:
            # Popup to select the tool category (QGIS Processing Tool or Customized Tool)
            tool_categories = ["QGIS Processing Tool", "Customized Tool"]
            tool_choice, ok = QInputDialog.getItem(
                None, 'Select Tool Category', 'Choose the category of the tool:', tool_categories, 0, False
            )
            # If user made a choice and confirmed it
            if ok and tool_choice:
                if tool_choice == "QGIS Processing Tool":
                    # Set destination for QGIS Processing Tool
                    destination_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "SpatialAnalysisAgent",
                                                   "Tools_Documentation", "QGIS_Tools")
                elif tool_choice == "Customized Tool":
                    # Set destination for Customized Tool
                    destination_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "SpatialAnalysisAgent",
                                                   "Tools_Documentation", "Customized_Tools")
                # 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)'
                )
                # Initialize variables for 'apply to all' options
                apply_to_all_replace = False
                apply_to_all_skip = False

                # 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))

                        # Check if the file already exists
                        if os.path.exists(new_file_path):
                            reply = QMessageBox.question(
                                None, 'File Exists',
                                f'The file "{os.path.basename(file_path)}" already exists. Do you want to replace it?',
                                QMessageBox.Yes | QMessageBox.No, QMessageBox.No
                            )
                            # If user chooses 'No', skip the file
                            if reply == QMessageBox.No:
                                continue  # Skip to the next file
                        # 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)}')

    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 SpatialAnalysisAgentDockWidget(iface)


class ScriptThread(QThread):
    output_line = pyqtSignal(str)
    chatgpt_update = pyqtSignal(str)
    streaming_chunk = pyqtSignal(str)  # New signal for streaming chunks
    graph_ready = pyqtSignal(str)
    report_ready = pyqtSignal(str)
    # extracted_code_ready = pyqtSignal(str)
    generated_code_ready = pyqtSignal(str)
    # reviewed_code_ready = pyqtSignal(str)  # Signal for reviewed code
    debugged_code_ready = pyqtSignal(str)  # Signal for debugged code
    script_finished = pyqtSignal(bool)

    def __init__(self, script_path, task, data_path, workspace_directory, OpenAI_key, model_name, is_review, reasoning_effort_value):
        super().__init__()
        self.script_path = script_path
        self.task = task
        self.data_path = data_path
        self.workspace_directory = workspace_directory
        self.OpenAI_key = OpenAI_key
        self.model_name = model_name
        self.is_review = is_review
        self.reasoning_effort_value = reasoning_effort_value
        self.latest_generated_code = ""  # cache for last generated code
        self._is_running = True  # Flag to control the running state

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

        try:
            # Ensure that the updated configuration is read by reloading the config
            config_path = os.path.join(os.path.dirname(self.script_path), 'SpatialAnalysisAgent', '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,
                'data_path': self.data_path,
                'workspace_directory': self.workspace_directory,
                # 'OpenAI_key': self.OpenAI_key,
                'model_name': self.model_name,
                'is_review': self.is_review,
                'reasoning_effort_value': self.reasoning_effort_value,
                'check_running': self.check_running,
                '_is_running': self._is_running,
                'output_signal': self.chatgpt_update,  # Pass the chatgpt_update signal
                'streaming_callback': self.streaming_chunk.emit  # Pass streaming callback

            }

            # Override print function to flush outputs
            def print_flush(*args, **kwargs):
                print(*args, **kwargs, flush=True)

            local_vars['print'] = print_flush

            # 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

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

            exec(script_content, exec_globals, exec_locals)

            # Emit signals for different code states based on what's available
            if 'generated_code' in exec_locals:
                self.generated_code_ready.emit(exec_locals['generated_code'])

            else:
                # Handle the case where 'generated_code' is not found
                self.output_line.emit("Error: 'generated_code' not found after script execution.")

            # # Check for reviewed code and emit if available
            # if 'reviewed_code' in exec_locals:
            #     self.reviewed_code_ready.emit(exec_locals['reviewed_code'])
            #
            # # Check for debugged code and emit if available
            # if 'debugged_code' in exec_locals:
            #     self.debugged_code_ready.emit(exec_locals['debugged_code'])

            self.script_finished.emit(True)

        except Exception as e:
            # Handle exceptions
            traceback_str = traceback.format_exc()
            self.output_line.emit(f"Error: {e}\n{traceback_str}")
            # self.chatgpt_update.emit(f"An error occurred: {str(e)}\n{traceback_str}")
            self.chatgpt_update.emit(f"An error occurred: {str(e)}")
            self.script_finished.emit(False)
        finally:
            sys.stdout = original_stdout
            sys.stderr = original_stderr

    def emit_captured_output(self, stdout_capture, stderr_capture):
        stdout_capture.flush()
        stderr_capture.flush()
        captured_stdout = stdout_capture.getvalue()
        captured_stderr = stderr_capture.getvalue()
        stdout_capture.truncate(0)
        stderr_capture.truncate(0)
        stdout_capture.seek(0)
        stderr_capture.seek(0)

        capturing_output = False
        captured_output_lines = []

        if captured_stdout:
            # self.output_line.emit(captured_stdout)
            for line in captured_stdout.splitlines(keepends=True):
                if line.endswith('\n'):
                    self.output_line.emit(line.rstrip())
                else:
                    self.output_line.emit(line)

                # for line in captured_stdout.splitlines():
                if "GRAPH_SAVED:" in line:
                    html_graph_path = line.split("GRAPH_SAVED:")[1].strip()
                    self.update_graph(html_graph_path)
                    self.chatgpt_update.emit(f"Geoprocessing Workflow is ready.")  # Emit the message to chatgpt_ans

                elif "Output:" in line:  # Check for "Output" flag
                    generated_output = line.split("Output:")[1].strip()
                    # Check if generated output is not empty before emitting
                    if generated_output:
                        self.update_report(generated_output)
                        self.chatgpt_update.emit(f"{generated_output}")  # Emit Output
                elif "List of selected tool IDs:" in line:
                    tool_IDs = line.split("List of selected tool IDs:")[1].strip()
                    if tool_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_tool_ids_as_links'):
                                main_widget = main_widget.parent()
                            
                            if main_widget and hasattr(main_widget, 'format_tool_ids_as_links'):
                                linked_tools = main_widget.format_tool_ids_as_links(tool_IDs)
                                self.chatgpt_update.emit(f"Selected tool(s): {linked_tools}")
                            else:
                                self.chatgpt_update.emit(f"Selected tool(s): {tool_IDs}")
                        except:
                            self.chatgpt_update.emit(f"Selected tool(s): {tool_IDs}")


                elif "TASK_BREAKDOWN" in line:
                    task_breakdown = line.split("TASK_BREAKDOWN")[1].strip()
                    if task_breakdown:
                        self.chatgpt_update.emit((f"{task_breakdown}"))
                # else:
                #     self.output_line.emit(line)

        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}")

    def stop(self):
        self._is_running = False

    def check_running(self):
        return self._is_running


class GPTRequestThread(QThread):
    output_line = pyqtSignal(str)
    chatgpt_update = pyqtSignal(str)
    streaming_chunk = pyqtSignal(str)  # New signal for streaming chunks
    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 config.ini."""
        current_script_dir = os.path.dirname(os.path.abspath(__file__))
        config_path = os.path.join(current_script_dir, 'SpatialAnalysisAgent', '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:
            # Import the unified model provider
            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 create_unified_client
            
            # Use the unified client that handles routing to different providers
            client, provider = create_unified_client(self.model_name)
            
            # Create streaming response using the provider
            response = provider.generate_completion(
                client, 
                self.model_name,
                [{"role": "user", "content": self.prompt}],
                stream=True
            )
            
            full_response = ""
            for chunk in response:
                if chunk.choices[0].delta.content is not None:
                    content = chunk.choices[0].delta.content
                    full_response += content
                    # Emit each chunk for streaming display
                    self.streaming_chunk.emit(content)
            
            # Emit the complete response at the end
            self.chatgpt_update.emit(full_response)
            
        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  # Initialize python_block as False

        # Define the syntax highlighting rules
        self.highlighting_rules = []

        # Keywords
        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 SpatialAnalysisAgent")
        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/SpatialAnalysisAgent'>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/SpatialAnalysisAgent"
        url = f"https://api.github.com/repos/{username}/SpatialAnalysisAgent"

        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}/SpatialAnalysisAgent"  # Target the user's fork
        FOLDER_IN_REPO = "SpatialAnalysisAgent/Tools_Documentation"  # Folder inside the repo
        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()}")

    def prompt_pull_request(self, username):
        pr_url = f"https://github.com/{username}/SpatialAnalysisAgent/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 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)


