## -*- 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 configparser
import importlib
import os
import shutil
import sys
import urllib

import iface
import qgis
import requests
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
from qgis.PyQt.QtCore import QSettings
import subprocess
import re
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtCore import pyqtSignal
import configparser
import os
import sys
import subprocess
from io import StringIO
import time
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
from PyQt5.QtCore import QThread, pyqtSignal, QUrl, QObject, pyqtSlot, QPropertyAnimation, QPoint, QRect, QSettings
from PyQt5 import QtWidgets, uic
from PyQt5.QtWidgets import QDialog, QHBoxLayout
import os
import sys
import subprocess
import traceback
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
import asyncio
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 check_and_install_libraries , check_missing_libraries, read_libraries_from_file, install_libraries

class LibraryCheckThread(QThread):
    finished_checking = pyqtSignal(list)

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

    def run(self):
        # Perform the library check in this thread
        missing_packages = check_missing_libraries(read_libraries_from_file(self.filename))
        self.finished_checking.emit(missing_packages)


class VersionCheckThread(QThread):
    version_check_completed = pyqtSignal(bool)  # Emits True if update is needed

    def run(self):
        needs_update = self.check_openai_version()
        self.version_check_completed.emit(needs_update)

    def check_openai_version(self):
        try:
            import pkg_resources
            import requests

            # Get the installed version
            installed_version = pkg_resources.get_distribution("openai").version

            # Get the latest version from PyPI
            response = requests.get("https://pypi.org/pypi/openai/json", timeout=5)
            latest_version = response.json()["info"]["version"]

            # Compare versions
            if installed_version != latest_version:
                return True
            else:
                return False
        except Exception as e:
            print(f"Error checking openai version: {e}")
            return False

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)

        self.is_task_breakdown = False
        self.task_breakdown_lines = []

        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)


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


    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 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)
        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):
        if 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):
        if missing_packages:
            message = "The following Python packages are required to use the plugin:\n\n"
            message += "\n".join(missing_packages)
            message += "\n\nWould you like to install them now? After installation, please restart QGIS."

            reply = QMessageBox.question(self, 'Missing Dependencies', message,
                                         QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                install_libraries(missing_packages)

    def check_libraries_once(self):

        """Check if libraries were already installed, otherwise run the check."""
        settings = QSettings('YourOrganization', 'YourApplication')
        libraries_checked = settings.value('libraries_checked', False, type=bool)

        if not libraries_checked:
            # First time: Libraries have not been checked
            print("Checking and installing required libraries...")
            from .install_packages.check_packages import check_and_install_libraries
            # Call your existing method to check and install libraries
            required_packages = os.path.join(os.path.dirname(__file__), 'install_packages', 'requirements.txt')
            check_and_install_libraries(required_packages)

            # Mark the libraries as checked and installed
            settings.setValue('libraries_checked', True)
        else:
            # Libraries have already been checked
            print("Libraries have already been checked and installed.")

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

        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 self.ChatMode_checkbox.isChecked():
            self.chatgpt_direct_answer(user_message)
        else:
            self.run_script()

    def chatgpt_direct_answer(self, user_message):
        """Method to interact with GPT-4 and display the result in output_text_edit_2."""
        # 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()
        # 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
        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):
        # self.update_OpenAI_key()
        # Retrieve the API key from the config
        self.OpenAI_key = self.get_openai_key()
        is_review = self.review_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.OpenAI_key = self.get_openai_key()  # Retrieve the API key from the line edit
        if not self.OpenAI_key:
            self.update_chatgpt_ans_textBrowser(f"Please enter a valid OpenAI API key.", is_user=False)
            return
        self.model_name = self.modelNameComboBox.currentText()


        self.task = self.task_LineEdit.toPlainText()
        self.data_path = self.data_pathLineEdit.toPlainText()
        # self.workspace_directory = self.workspace_directoryLineEdit.toPlainText()
        self.workspace_directory = self.workspace_directoryLineEdit2.text()


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


        self.thread = ScriptThread(script_path, self.task, self.data_path, self.workspace_directory, self.OpenAI_key, self.model_name, is_review)

        self.thread.output_line.connect(self.update_output)
        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.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.CodeEditor.setPlainText(code)

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

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



            # Process URLs in the message
        message = url_pattern.sub(replace_urls, message)
            # color = 'green'

        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="color: {color_message};">{message}</span>
                
            </div>
            '''
        # if is_user:
        #     html = f'<div style="text-align: left; padding: 10px; margin: 5px; border: 2px solid blue; border-radius: 10px;">{text}</div>'
        # else:
        #     html = f'<div style="text-align: left; padding: 10px; margin: 5px; border: 2px solid green; border-radius: 10px;">{text}</div>'

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

        self.chatgpt_ans_textBrowser.setTextCursor(cursor)

    @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)
                self.update_chatgpt_ans_textBrowser(task_breakdown_text)
                self.task_breakdown_lines = []  # Reset the accumulator
            else:
                # Accumulate the line
                self.task_breakdown_lines.append(clean_line)
        else:
            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.")  # 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(f"Selected tool(s): {tool_IDs}")

            elif "TASK_BREAKDOWN:" in line:
                # Start accumulating task breakdown lines
                self.is_task_breakdown = True
                task_breakdown_line = line.split("TASK_BREAKDOWN:")[1].strip()
                self.task_breakdown_lines = [task_breakdown_line]

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




        # clean_line = self.strip_ansi_sequences(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.update_chatgpt_ans_textBrowser("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.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.tool_filename_ready.emit(tool_filename)  # Emit the tool filename
        #         # self.chatgpt_update.emit(f"AI: Selected tool(s): {tool_filename}")
        #         self.update_chatgpt_ans_textBrowser(f"Selected tool(s): {tool_IDs}")
        #
        # elif "TASK_BREAKDOWN:" in line:
        #     task_breakdown = line.split("TASK_BREAKDOWN:")[1].strip()
        #     if task_breakdown:
        #         self.update_chatgpt_ans_textBrowser((f"{task_breakdown}"))
        #
        #
        #
        #
        # if "```python" in clean_line or "```" in clean_line:
        #     self.output_text_edit.insertPlainText(line)
        # else:
        #
        #     self.output_text_edit.insertPlainText(clean_line)
        #     # Conditionally add newline only if it is not already present
        # if not clean_line.endswith('\n'):
        #     self.output_text_edit.insertPlainText('\n')
        # # self.output_text_edit.insertPlainText('\n')  # Add a newline after each line
        # self.output_text_edit.moveCursor(QTextCursor.End)  # Ensure cursor is at the end
        # self.output_text_edit.repaint()
        # # Move the scroll bar to the bottom to avoid unwanted gaps or large spaces
        # 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 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)}')

# 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)
    graph_ready = pyqtSignal(str)
    report_ready = pyqtSignal(str)
    generated_code_ready = pyqtSignal(str)
    script_finished = pyqtSignal(bool)


    def __init__(self, script_path, task, data_path, workspace_directory, OpenAI_key, model_name, is_review):
        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._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,
                'check_running': self.check_running,
                '_is_running': self._is_running,
                'output_signal': self.chatgpt_update,  # Pass the chatgpt_update signal
            }


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

            # Capture the `code` variable directly from the execution environment
            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.")

            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.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:
                        # self.tool_filename_ready.emit(tool_filename)  # Emit the tool filename
                        # self.chatgpt_update.emit(f"AI: Selected tool(s): {tool_filename}")
                        self.chatgpt_update.emit(f"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)
    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:
            # self.update_config_file()
            from openai import OpenAI
            client = OpenAI(api_key=self.OpenAI_key)
            response = client.chat.completions.create(
                model= self.model_name,#"gpt-4",
                messages=[
                    {"role": "user", "content": self.prompt},
                ]
            )
            reply = response.choices[0].message.content.strip()
            # self.output_line.emit(f"AI: {reply}")
            self.chatgpt_update.emit(reply)
        except Exception as e:
            self.output_line.emit(f"Error: {str(e)}")
        finally:
            self.finished_signal.emit()


class PythonHighlighter(QSyntaxHighlighter):
    def __init__(self, document, always_highlight =False):
        super(PythonHighlighter, self).__init__(document)
        self.always_highlight = always_highlight
        self.python_block = False  # 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)

